feat(cli) wire CLI under Aspire and end three dead-config items#54
Merged
Conversation
Bundles the three critical dead-config fixes surfaced by PR #53's Bicep deploy-prep audit: 1. PinballWizard.Cli.Program.cs now calls builder.AddServiceDefaults() and gates Cosmos/OPDB/Cosmos-backed-politeness registrations on ConnectionStrings:cosmos OR Cosmos:AccountEndpoint presence. Standalone CLI runs without Cosmos behave exactly as before; Aspire-orchestrated runs (or production with Cosmos:AccountEndpoint set from Bicep outputs) automatically wire the persistence layer + OPDB + per-source politeness overrides. 2. AddCosmosPersistence accepts an externally-registered CosmosClient. All registrations switched to TryAddSingleton so an Aspire-injected client (built from the connection string for the local emulator) is preserved. CosmosOptions.AccountEndpoint is now string? — required only when the Managed-Identity fallback is in play; the fallback throws InvalidOperationException with a clear remediation message if both registrations are absent. 3. OPDB CLI dispatch via --source opdb. Special-cased rather than wrapped in an ISourceScraper adapter because OPDB writes to IMachineRepository instead of yielding ScrapedItems. Returns exit code 2 with a clear message when OPDB / Cosmos aren't configured. OPDB added to ScraperOrchestrator.SourceAliases so the contract test recognises the alias. The --source flag description explicitly warns that 'all' does NOT include 'opdb' (run --source opdb explicitly). 4. IPerSourcePolitenessResolver + DefaultPerSourcePolitenessResolver + IngestionSourcePolitenessResolver. New abstraction in the polite- scraping namespace that returns the effective PolitenessOptions for a request URL. The Cosmos-backed implementation reads IngestionSource.PolitenessOverrides on first lookup, caches the resulting host -> effective-options map, and degrades safely to defaults when Cosmos is unreachable so a transient outage never blocks scraping. ApplyOverrides is public static for direct test access. PolitenessGate consumes the resolver per-request for effective delay + 429-streak limit; the previously-cached _options field is removed. 5. CosmosOptions / OpdbOptions expose AccountEndpointKey / CosmosConnectionName / BaseUrlKey constants so Program.cs can presence-check the configuration without duplicating the section strings (avoids exactly the dead-config drift the project's pre-PR self-audit was created to catch). Three test files updated for the new PolitenessGate ctor (each constructs DefaultPerSourcePolitenessResolver from the existing politenessOptions). +10 new tests in IngestionSourcePolitenessResolverTests pinning ApplyOverrides math, host-keyed lookup, graceful degradation on repository failure, and load-once caching. Tests: 486 -> 496. Build clean, zero warnings. README gains a 'Local development with .NET Aspire' section and the --source flag table is updated to enumerate every manufacturer scraper plus opdb. Pre-existing staleness in other README sections is left for a future docs PR. Pre-push self-audit: /local-review (0 critical / 5 minor — 3 fixed, 2 deferred-with-justification) plus 7-item mechanical checklist (all pass).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Bundled fix for the three critical dead-config items surfaced by PR #53's Bicep deploy-prep audit. All three were the exact pattern that motivated
feedback_pre_pr_self_audit.md: production code that exists but is never called, so a deploy alone gets nothing useful at runtime.Three logical pieces, one PR (single "make the runtime deploy-ready" theme; tightly coupled wiring):
1. Aspire wiring + Cosmos refactor
PinballWizard.Cli/Program.csnow callsbuilder.AddServiceDefaults()(OTel + service discovery + standard HTTP resilience + health checks). Cosmos / OPDB / Cosmos-backed-politeness registrations are gated onConnectionStrings:cosmos(Aspire) ORCosmos:AccountEndpoint(Managed-Identity) being present. Standalone CLI runs without Cosmos behave exactly as before; Aspire-orchestrated runs (or production withCosmos:AccountEndpointfrom Bicep outputs) auto-wire everything.AddCosmosPersistenceaccepts an externally-registeredCosmosClient— all registrations switched toTryAddSingletonso Aspire'sAddAzureCosmosClient("cosmos")registration is preserved.CosmosOptions.AccountEndpointis nowstring?(was[Required]); the fallback throwsInvalidOperationExceptionwith a clear remediation message if both registrations are absent.2. OPDB CLI dispatch via
--source opdbSpecial-cased in
Program.csrather than wrapped in anISourceScraperadapter — OPDB writes toIMachineRepositoryinstead of yieldingScrapedItems. Returns exit code 2 with a clear message when OPDB / Cosmos aren't configured.OPDBis added toScraperOrchestrator.SourceAliasessoSourceAliasContractTestsrecognises the alias. The--sourceflag description explicitly warns thatalldoes NOT includeopdb(run--source opdbexplicitly).3.
IPerSourcePolitenessResolver+PolitenessGaterewireIPerSourcePolitenessResolveris the new abstraction returning the effectivePolitenessOptionsfor a request URL.DefaultPerSourcePolitenessResolveralways returns the global defaults (registered viaTryAddSingletoninAddPoliteScraping)IngestionSourcePolitenessResolverreadsIngestionSource.PolitenessOverridesfromIIngestionSourceRepositoryon first lookup, caches the resulting host -> effective-options map, and degrades safely to defaults when Cosmos is unreachable so a transient outage never blocks scraping. Wired byAddCosmosBackedPolitenessOverrides.ApplyOverridesispublic staticfor direct test access.PolitenessGateconsumes the resolver per-request for effective delay + 429-streak limit; the previously-cached_optionsfield is removed.Configuration-key constants
CosmosOptions/OpdbOptionsexposeAccountEndpointKey/CosmosConnectionName/BaseUrlKeyconstants soProgram.cscan presence-check the configuration without duplicating the section strings — avoids exactly the dead-config drift the project's pre-PR self-audit was created to catch.Test Plan
dotnet build PinballWizard.slnx-> 7 / 7 projects, 0 warnings, 0 errorsdotnet test PinballWizard.slnx-> 496 / 496 passing (was 486)ApplyOverrides_NullOverrides_ReturnsDefaultsUnchangedApplyOverrides_AllNullFields_ReturnsValueEqualToDefaultsApplyOverrides_RequestDelayOverride_AppliesOnly/Max429StreakOverride/UserAgentSuffix/RobotsTxtPathOverrideResolveAsync_KnownHost_ReturnsEffectiveOptionsWithOverridesResolveAsync_UnknownHost_ReturnsDefaultsResolveAsync_RepositoryThrows_FallsBackToDefaults(load-bearing — pins the resilience invariant)ResolveAsync_CalledTwice_LoadsRepositoryOncePolitenessGatector (PolitenessGateTests / OpdbSyncServiceTests / OpdbClientTests) — each constructs aDefaultPerSourcePolitenessResolverfrom the existingOptions.Create(politenessOptions)and passes that to the gateOut of Scope
AddProject<Projects.PinballWizard_Cli>requires arg-handling design (per-invocation args vs per-launch args). UX nicety, not a critical fix; future PR.machinesandingestion_sourcescontainers with their partition keys toinfra/modules/shared.bicep. This PR's runtime path usesCosmosBootstrapper.EnsureCreatedAsyncto create containers on first connect (works with both the local emulator and a deployed account); declaring them in Bicep is a defence-in-depth follow-up.ingestion_sources— until containers exist somewhere (deployed Cosmos OR local emulator running), the seed has nowhere to land. Future PR after the first deploy.IngestionSourcePolitenessResolver— flagged minor by the local review. The_initLocksemaphore is a textbook init-once pattern; aTask.WhenAlltwo-parallel-resolves test would be defensive but the cost-of-deferring is low.Lease—ReportResponseAsynccurrently re-resolves rather than reusing the options fromAcquireForRequestAsync. ~50ns per call extra; not measurable against actual network latency. Defer.Checklist
docs/adr/— N/A (this PR implements the existing ADR 0007 design that definedPolitenessOverrides; no new architecture)README.mdand/ordocs/are updated in the same PR — README gains aLocal development with .NET Aspiresection and the--sourcetable is updated for opdb~/.claude/projects/c--projects-PinballWizard/memory/is now stale, it has been updated or removed in the same PR — handoff memory updated separatelyTODO/FIXME/ commented-out code committed<NoWarn>without a comment explaining why and the removal criterionPre-push self-audit
Step 0 —
/local-review(qualitative)/local-reviewand addressed every critical finding before push--sourcedescription now explicitly warnsallexcludesopdb(was a silent UX surprise)Program.csblock comment adds a one-line note that gating is by presence not validation (threat-model accepts env-var redirection as strictly weaker than the RCE the attacker would already need)CosmosOptions.AccountEndpointKey/CosmosConnectionName/OpdbOptions.BaseUrlKeyconstants prevent dead-config drift on section renames (the exact failure modefeedback_pre_pr_self_audit.mdis meant to catch)ResolveAsyncinReportResponseAsync(~50ns/call; not measurable)Step 1 — Mechanical checklist
*Optionsproperty has at least one real getter call insrc/—CosmosOptions.AccountEndpointis read byAddCosmosPersistencefallback;AccountEndpointKey/CosmosConnectionName/BaseUrlKeyconstants are read byProgram.csgatingApplyOverridesmatches every documentedPolitenessOverridesfield semanticcatch { }— the onlycatchin new code (IngestionSourcePolitenessResolver.cs:84) iscatch (Exception ex) when (ex is not OperationCanceledException)ISourceScraper? — N/A (OPDB is special-cased rather than adapted)git log -1 --format='%an <%ae>'shows personal noreply, not work email