Skip to content

refactor: inject process handles so tests never mutate the global process (#183)#185

Merged
debugmcpdev merged 3 commits into
mainfrom
feat/183-process-handle-injection
Jul 14, 2026
Merged

refactor: inject process handles so tests never mutate the global process (#183)#185
debugmcpdev merged 3 commits into
mainfrom
feat/183-process-handle-injection

Conversation

@debugmcpdev

Copy link
Copy Markdown
Collaborator

Fixes #183.

What

The proper fix behind the #159 CI flake: production code no longer hardwires the global process, so tests no longer have to mutate it to drive the code under test. With the offenders gone, the #184 leak guard is promoted from lenient to enforcing: CI now runs with LEAK_GUARD_STRICT=1.

New abstraction

  • ProcessLike (src/interfaces/process-interfaces.ts) — minimal current-process surface (on/removeListener/listeners/rawListeners/listenerCount/exit/send?/connected/env/argv/uptime/stdin/stdout). Hand-rolled rather than Pick<NodeJS.Process, ...>: the exact Node types (tty stream intersections, never-returning exit, per-event listener overloads) make fakes unimplementable. Structurally satisfied by both the global process and the new EventEmitter-backed FakeCurrentProcess test fake (tests/test-utils/mocks/fake-current-process.ts).

Injection sites (all default to the global process — production behavior unchanged, entry files untouched)

  • ProxyRunner (ProxyRunnerOptions.proc): all ~20 touchpoints — IPC mode detection, heartbeat interval, 10s init timeout, message/disconnect/error listeners, readline stdio, and the setupGlobalErrorHandlers method. The worker exit hook is threaded as (code) => this.proc.exit(code) (identical to the worker's own default in prod).
  • detectExecutionMode(proc?), createProductionDependencies(proc?) (messageSender IPC/stdout channel).
  • CLI commands (stdio, http, sse, error-handlers): proc?: ProcessLike in each dependencies object; signal registration, env gates, exit diagnostics, and the exitProcess default route through it. Stdin watchdog gets env from the handle.
  • GenericAdapterManager: constructor-injected platform (win32 tree-kill branch, Ruby launch orphans the debuggee ruby.exe on Windows: worker destroys the DAP socket before sending disconnect, and tree-kill only fires on the slow path #156 heritage).
  • Narrow seams where full ProcessLike would be ceremony: SignalFn for pid-liveness (jvm-orphan-reaper, logger stale-log cleanup); platform params on resolveExecutablePath and buildRdbgInvocation.

Deleted

  • The standalone setupGlobalErrorHandlers() in dap-proxy-dependencies.ts — dead in production (only its own tests called it) and behaviorally divergent from the ProxyRunner method prod uses (it exited on unhandledRejection; the real one doesn't). Docs updated.
  • The CI flake: vitest worker fork exits unexpectedly on windows-latest, failing a fully green suite #159-era test safety nets (top-level process.exit spy nets in dap-proxy-core/worker tests, listener-baseline belt-and-braces) — unreachable now that every runner/worker gets an injected handle.

Deliberate exceptions

  • proxy-bootstrap.js untouched (plain-JS pre-TS orphan-prevention layer).
  • environment-impl.test.ts keeps its process.cwd spy, documented as the sanctioned boundary test — ProcessEnvironment IS the process abstraction; injecting a fake there is tautological.
  • packages/adapter-* platform-redefinition tests (~60 sites, cannot trip the leak guard) deferred to a follow-up issue to keep this reviewable.

Verification

  • Key checkpoint: after the ProxyRunner rewrite, the unmodified old tests still passed (default = global process, byte-identical)
  • LEAK_GUARD_STRICT=1 pnpm run test:ci-coverage (same command as CI, both projects) — green, 0 leak messages
  • LEAK_GUARD_STRICT=1 FLAKE_RUNS=3 pnpm run test:flake — 3/3 shuffled runs green
  • E2E smoke (python 5 tests, javascript 3 tests) — real proxy spawn over the default path
  • Acceptance greps: no process.on = reassignment, no delete process.send/defineProperty(process, ...), no vi.spyOn(process, 'exit'|'kill'|'on') in migrated suites; dap-proxy-core.ts touches the global only in its two = process defaults
  • npm run lint clean

New convenience script: pnpm run test:strict (strict guard locally; default local runs stay lenient).

🤖 Generated with Claude Code

cynarlab and others added 3 commits July 13, 2026 23:08
…dencies (#183)

- Add ProcessLike interface (src/interfaces/process-interfaces.ts): minimal
  current-process surface, structurally satisfied by both the global process
  and EventEmitter-backed fakes
- ProxyRunner: options.proc (default global process); all ~20 process
  touchpoints routed through this.proc; worker exit hook threaded as
  (code) => this.proc.exit(code) (identical to the worker default in prod)
- detectExecutionMode takes a narrow injectable handle
- createProductionDependencies(proc): messageSender IPC/stdout channel injectable
- Delete dead standalone setupGlobalErrorHandlers (only tests called it;
  diverged from the ProxyRunner method prod uses); update error-handling docs
- New FakeCurrentProcess test fake; migrate dap-proxy-core, dap-proxy-dependencies,
  and dap-proxy-worker tests off all global-process mutation; remove the #159
  process.exit spy safety nets (no longer reachable)

Verified: proxy suites green under LEAK_GUARD_STRICT=1; production entry
(dap-proxy-entry.ts) unchanged - defaults preserve byte-identical behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…f utilities; enforce strict leak guard in CI (#183)

- stdio/http/sse commands + error-handlers: proc?: ProcessLike dependency;
  signal registration, env gates, exit diagnostics, and the exitProcess
  default all route through the injected handle
- stdin-watchdog: stdin option widened to NodeJS.ReadableStream; callers
  pass env from the handle
- jvm-orphan-reaper + logger stale-log cleanup: injectable SignalFn seam
  for pid-liveness instead of global process.kill spies
- adapter-policy resolveExecutablePath + buildRdbgInvocation: optional
  platform parameter (defaults to process.platform)
- Test migrations: no CLI/utils test reassigns process.on, spies
  process.exit/kill, or redefines process.platform anymore; the one
  sanctioned boundary test (ProcessEnvironment cwd delegation) is documented
- CI: LEAK_GUARD_STRICT=1 in ci.yml and flake-hunt.yml; new test:strict script

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…er (#183)

Caught by the acceptance grep: the killProcessTree shutdown tests redefined
process.platform globally. The win32 tree-kill branch (#156) now reads an
injected platform (default: real platform); tests construct managers with
explicit win32/linux platforms instead.

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

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.40260% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/proxy/dap-proxy-core.ts 93.75% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@debugmcpdev debugmcpdev merged commit 163cc03 into main Jul 14, 2026
10 checks passed
@debugmcpdev debugmcpdev deleted the feat/183-process-handle-injection branch July 14, 2026 08:10
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.

Inject a process handle into ProxyRunner, setupGlobalErrorHandlers, and stdio-command signal registration

2 participants