Skip to content

refactor(availability): launch gate on the shared probe; typed IAdapterRegistry surface (#435, parts 3-4) - #443

Merged
debugmcpdev merged 2 commits into
mainfrom
feat/435-gate-typed-registry
Aug 23, 2026
Merged

refactor(availability): launch gate on the shared probe; typed IAdapterRegistry surface (#435, parts 3-4)#443
debugmcpdev merged 2 commits into
mainfrom
feat/435-gate-typed-registry

Conversation

@debugmcpdev

Copy link
Copy Markdown
Collaborator

Parts 3–4 of #435, completing the issue (part 1: #437; part 2: #442).

Part 3 — launch gate on the shared probe

checkLaunchToolchain (the gate behind create_debug_session and start_debugging, issue #360) was the last hand-rolled getFactory→validate→fail-open path. It is now a thin wrapper over probeLanguageEntry with a synthetic entry — installed: true plus an empty disabledSet provably neutralize the probe's notInstalled/disabled short-circuits, so the gate keeps gating on the toolchain only (disabled languages are already refused upstream at create_debug_session). The gate's availability answer and reason text are now literally computeModeAvailability's launch mode, so it can no longer drift from list_supported_languages the way the server/doctor loops used to before part 1.

The full fail-open contract was pinned with direct unit tests before the rewrite (missing registry, throwing getFactory, missing validate, throwing validate, load errors, the empty-errors fallback sentence, one-validate-per-cache-window), and a drift-fence matrix asserts checkLaunchToolchain(...) ≡ probeLanguageEntry(synthetic).modes.launch across every registry shape — the gate-side analogue of the doctor parity test. The existing launch-gate and #360 carve-out suites pass unmodified.

Part 4 — typed registry surface + loader-error surfacing

IAdapterRegistry gains the members the server was reaching through as unknown as duck-typing: required listLanguages / listAvailableAdapters / getFactory / getFactoryMetadata (plus register aligned to its actual Promise<void>), and optional getFactoryResult. All four cast sites are gone (server.ts ×3, session-manager-operations.ts ×1; runtime typeof guards stay for partial test doubles) — a rename on the concrete AdapterRegistry now breaks the build instead of silently degrading every language to fail-open. Doctor's duplicate DoctorRegistry structural type becomes a Pick of the interface.

getFactoryResult closes the swallowed-loader-error hole: AdapterRegistry.getFactory's catch { return undefined } collapsed a corrupt package, a broken export, and dynamic-loading-disabled into one undefined, so probe.factoryLoadError could never fire. Now getFactory delegates to getFactoryResult (fail-open contract unchanged, one code path), the availability probe prefers getFactoryResult when the registry offers it, and doctor reports the loader's real error message instead of "(the registry returned no factory)". The two remaining discovery-fallback swallows log warnings. New shared types: AdapterManifestEntry (the loader-manifest shape, finally disambiguated from the factory-declared AdapterMetadata) and FactoryLoadResult.

Test fixtures: the central IAdapterRegistry mock provides fail-open-neutral defaults for the required members (mirroring the pre-typed fallback paths); the WithErrors/WithLanguages builders override the discovery members consistently. AdapterRegistry.getFactory/getFactoryResult get their first direct unit tests (registered / cached / loader-throws / dynamic-disabled).

Deferred (noted on #435 at close)

Consolidating the three near-identical doctor fake-registry builders; doctor wording for dynamicLoadingDisabled (production always enables dynamic loading); switching the #360 carve-out site to getFactoryMetadata; extending the isAdapterRegistry runtime guard to the new members.

Verified locally: full build, lint, 222-file/4048-test unit suite, and a real mcp-debugger doctor --json run (the real registry now feeds getFactoryResult through the shared probe).

Closes #435.

🤖 Generated with Claude Code

…erRegistry surface (#435, parts 3-4)

Part 3 - checkLaunchToolchain is now a thin wrapper over probeLanguageEntry
with a synthetic entry (installed: true, empty disabledSet, which provably
neutralize the probe's notInstalled/disabled short-circuits - disabled
languages are refused upstream at create_debug_session). The gate's answer
and reason text are now literally computeModeAvailability's launch mode, so
the gate can no longer drift from list_supported_languages the way the
server/doctor loops used to. Direct unit tests pin the full fail-open
contract, and a drift-fence matrix asserts gate === probe.modes.launch
across every registry shape (the gate-side analogue of the doctor parity
test).

Part 4 - IAdapterRegistry gains the typed discovery surface the server was
reaching through `as unknown as` casts: required listLanguages /
listAvailableAdapters / getFactory / getFactoryMetadata (plus register
aligned to Promise<void>), and optional getFactoryResult. All four cast
sites (server.ts x3, session-manager-operations.ts) are removed; a rename
on the concrete AdapterRegistry now breaks the build instead of silently
degrading every language to fail-open. Doctor's duplicate DoctorRegistry
structural type becomes a Pick of the interface.

getFactoryResult fixes the swallowed-loader-error hole: AdapterRegistry's
`catch { return undefined }` collapsed a missing package, a broken export,
and dynamic-loading-disabled into one undefined. getFactory now delegates
to getFactoryResult (contract unchanged, one code path), the availability
probe prefers getFactoryResult when present, and doctor finally reports the
loader's real error message instead of "(the registry returned no
factory)". The two remaining discovery-fallback swallows now log warnings.
New shared types: AdapterManifestEntry (the loader manifest shape,
disambiguated from the factory-declared AdapterMetadata), FactoryLoadResult.

Test fixtures: the central IAdapterRegistry mock now provides fail-open-
neutral defaults for the required members (mirroring the pre-typed fallback
paths); the WithErrors/WithLanguages builders override the discovery
members consistently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.14815% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/container/dependencies.ts 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Review findings on parts 3-4, fixed:

- AdapterRegistry no longer creates a per-instance winston logger (which
  piped every HTTP-session registry into the process-lifetime shared
  transport with no detach path - the issue-#404 leak class, and it
  bypassed --log-level/--log-file). AdapterRegistryConfig gains an
  injected `logger` sink, wired from the container logger in
  createProductionDependencies; absent -> warnings dropped.
- the drift-fence matrix was vacuous: its describe-scope vi.fn fixtures
  were wiped by the global afterEach vi.resetAllMocks before any fence
  test ran, so both sides always failed open and true===true passed.
  Registries are now built per-test via thunks and each shape carries an
  expectAvailable pin, so silent vacuity cannot recur.
- checkLaunchToolchain warns with the real load failure carried in
  probe.factoryLoadError instead of discarding the breadcrumb the
  getFactoryResult plumbing exists to deliver; its fallback sentence moves
  to ErrorMessages.modeUnavailableReason.launchFallback (rule 5), and the
  docstring's "cannot drift" claim is rescoped to the toolchain axis.
- the probe's getFactoryResult option type derives from the shared
  FactoryLoadResult (Omit + widened factory) so a field rename there is a
  build break, not silent drift; the resolution is optional-chained so an
  untyped registry resolving undefined lands in the honest no-factory
  branch instead of a bogus corrupt-adapter diagnosis.
- listAvailableAdapters survives a registered factory whose getMetadata
  throws (attach falls back to 'none' with a warn) - one bad plain-JS
  factory no longer kills every doctor verdict.
- the attach-'none' gate warn-logs when a registry double lacks
  getFactoryMetadata instead of silently self-disabling; the coverage
  suite's registry double gains the member so its ~20 attachToProcess
  tests actually exercise the gate.
- isAdapterRegistry now checks the four new required members (a legacy
  three-method registry is no longer certified), with first guard tests.
- the parity fence's fake registry offers getFactoryResult, so the fence
  covers the branch production actually runs; the central mock keeps it
  deliberately absent (a default would shadow per-test getFactory
  overrides) and its discovery defaults are implementations that survive
  mock resets.
- DoctorDependencies.adapterRegistry is typed DoctorRegistry (cast gone;
  runtime guard kept as defense against override casts).

Not addressed (deliberate): per-launch getMetadata warns for broken
factories are undeduped - they only fire for already-broken adapters and
aid diagnosis.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@debugmcpdev
debugmcpdev merged commit 66885f1 into main Aug 23, 2026
10 checks passed
@debugmcpdev
debugmcpdev deleted the feat/435-gate-typed-registry branch August 23, 2026 04:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

doctor: share the per-language availability loop with list_supported_languages, and move presentation knowledge into adapters

2 participants