diff --git a/.osoji.toml b/.osoji.toml index c4a76cde..1818cfa4 100644 --- a/.osoji.toml +++ b/.osoji.toml @@ -1,2 +1,6 @@ [push] endpoint = "https://api.osojicode.ai" + +[audit] +# Archived investigation docs: near-zero audit value, dominant doc-analysis cost. +exclude = ["docs/archive/**"] diff --git a/AGENTS.md b/AGENTS.md index ba607445..03f2707d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## Project Structure & Module Organization - `src/` contains the TypeScript CLI and server core that power the Debug MCP runtime. -- `packages/` hosts workspace modules (`@debugmcp/shared`, `adapter-mock`, `adapter-python`, `adapter-javascript`, `adapter-rust`, `adapter-go`, `adapter-java`, `adapter-dotnet`, `mcp-debugger`) with their own `src/` trees. +- `packages/` hosts workspace modules (`@debugmcp/shared`, `adapter-mock`, `adapter-python`, `adapter-ruby`, `adapter-javascript`, `adapter-rust`, `adapter-go`, `adapter-java`, `adapter-dotnet`, `mcp-debugger`) with their own `src/` trees. - `tests/` is grouped by scope: `core/`, `adapters/*/`, `e2e/`, plus shared utilities in `tests/test-utils/` and fixtures under `tests/fixtures/`. - `docs/` covers design notes; `examples/` hosts adapter recipes; `scripts/` stores CI helpers. - Build artifacts land in `dist/`; recorded assets and Docker helpers live in `assets/` and `docker/`. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0e6d4ff3..761337a1 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -4,13 +4,14 @@ mcp-debugger is a Model Context Protocol (MCP) server that bridges MCP clients ( ## Monorepo Structure -The project uses pnpm workspaces with 9 packages: +The project uses pnpm workspaces with 10 packages: ``` packages/ shared/ Core interfaces and types (IDebugAdapter, IAdapterFactory) adapter-python/ Python debugging via debugpy adapter-javascript/ JavaScript/Node.js debugging via js-debug + adapter-ruby/ Ruby debugging via rdbg adapter-rust/ Rust debugging via CodeLLDB adapter-go/ Go debugging via Delve adapter-java/ Java debugging via JDI bridge diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b26f0f43..6268da7e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -281,6 +281,7 @@ mcp-debugger/ ├── packages/ # Monorepo workspace packages │ ├── shared/ # Shared interfaces, types, and utilities │ ├── adapter-python/ # Python debug adapter (debugpy) +│ ├── adapter-ruby/ # Ruby debug adapter (rdbg/debug gem) │ ├── adapter-javascript/# JavaScript/Node.js adapter (js-debug) │ ├── adapter-rust/ # Rust adapter (CodeLLDB) │ ├── adapter-go/ # Go adapter (Delve) @@ -312,7 +313,7 @@ mcp-debugger/ - **DAP Proxy**: Handles communication with debug adapters via DAP protocol - **Adapter Registry**: Dynamically loads and manages language-specific adapters - **Adapter Policies**: Language-specific behavior via policy pattern -- **MCP Tools**: Implements the 20 MCP protocol tools +- **MCP Tools**: Implements the 21 MCP protocol tools ## 🏃 Running the Demo diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 33692ec1..e370ca80 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -31,7 +31,7 @@ export interface IDebugAdapter extends EventEmitter { dispose(): Promise; // Environment validation - validateEnvironment(): Promise; + validateEnvironment(executablePath?: string): Promise; resolveExecutablePath(preferredPath?: string): Promise; // DAP operations @@ -61,9 +61,9 @@ Each supported language implements the IDebugAdapter interface: The **[AdapterRegistry](../../src/adapters/adapter-registry.ts)** manages available adapters through two mechanisms: explicit registration and dynamic loading. Factories can be pre-registered at startup, or loaded on demand via `AdapterLoader` when `enableDynamicLoading` is enabled (or `MCP_CONTAINER=true`): ```typescript -// Explicit registration -registry.register('python', new PythonAdapterFactory()); -registry.register('mock', new MockAdapterFactory()); +// Explicit registration (register is async and returns Promise) +await registry.register('python', new PythonAdapterFactory()); +await registry.register('mock', new MockAdapterFactory()); // Dynamic loading happens automatically when create() is called // for a language without a pre-registered factory @@ -227,7 +227,7 @@ The `stopped` event means PAUSED, not terminated: ```typescript // ❌ Wrong: Confusing stopped with terminated if (event.event === 'stopped') { - this.state = AdapterState.TERMINATED; // NO! + this.state = AdapterState.DISCONNECTED; // NO! } // ✅ Correct: Stopped = paused for debugging diff --git a/docs/architecture/adapter-api-reference.md b/docs/architecture/adapter-api-reference.md index f638e03c..838c3402 100644 --- a/docs/architecture/adapter-api-reference.md +++ b/docs/architecture/adapter-api-reference.md @@ -35,7 +35,7 @@ State - `getCurrentThreadId(): number | null` — Active thread (if any) Environment validation -- `validateEnvironment(): Promise` — Check runtime prerequisites +- `validateEnvironment(executablePath?: string): Promise` — Check runtime prerequisites (optional `executablePath` validates a specific user-configured interpreter instead of an auto-detected one) - `getRequiredDependencies(): DependencyInfo[]` — Declare dependencies (name/version/required) Executable management @@ -59,6 +59,7 @@ Attach support (optional) - `supportsDetach?(): boolean` — Whether the adapter supports detaching without terminating the debuggee - `transformAttachConfig?(config: GenericAttachConfig): LanguageSpecificAttachConfig` — Transforms generic attach config to language-specific format - `getDefaultAttachConfig?(): Partial` — Gets default attach configuration for this language +- `usesDirectConnectForAttach?(): boolean` — when true, attach connects directly to an already-listening DAP server (e.g. rdbg started with `--open`) instead of spawning an adapter process; `ProxyManager` skips `buildAdapterCommand` and the adapter policy returns a `'connect'` spawn config from the attach host/port Debug configuration - `transformLaunchConfig(config: GenericLaunchConfig): Promise` (async to permit build/compilation steps before launch) diff --git a/docs/architecture/adapter-development-guide.md b/docs/architecture/adapter-development-guide.md index fe754814..c067c5e9 100644 --- a/docs/architecture/adapter-development-guide.md +++ b/docs/architecture/adapter-development-guide.md @@ -42,7 +42,7 @@ packages/adapter-/ Naming conventions: - Package name: `@debugmcp/adapter-` - Factory class: `AdapterFactory` (e.g., `GoAdapterFactory`) -- File names: kebab-case (e.g., `go-debug-adapter.ts`, not `GoDebugAdapter.ts`) +- File names: kebab-case is the recommended convention for new adapters (used by 7 of the 8 shipped adapters), e.g. `go-debug-adapter.ts`. The .NET adapter uses PascalCase (`DotnetAdapterFactory.ts`, `DotnetDebugAdapter.ts`) for historical reasons. --- @@ -133,7 +133,7 @@ Based on `packages/adapter-go/tsconfig.json`: ### 4. Implement `IAdapterFactory` -The factory creates adapter instances, provides metadata, and validates the environment. Implement the `IAdapterFactory` interface from `@debugmcp/shared` — do not extend a base class. +The factory creates adapter instances, provides metadata, and validates the environment. Either implement `IAdapterFactory` directly (as the Go reference and most adapters do) or extend the abstract `BaseAdapterFactory` (exported from `@debugmcp/shared`), which provides default `getMetadata()` and a default `validate()` (as `JavascriptAdapterFactory` does). **Interface** (3 required methods): ```typescript @@ -273,10 +273,13 @@ export { GoDebugAdapter } from './go-debug-adapter.js'; export { GoAdapterFactory } from './go-adapter-factory.js'; export * from './utils/go-utils.js'; -// Optional default export (not used by the dynamic loader, but included by some adapters) +import { GoAdapterFactory as _GoAdapterFactory } from './go-adapter-factory.js'; + +// Convenience default export — NOT used by the mcp-debugger dynamic loader, +// which imports the named {Language}AdapterFactory export only export default { name: 'go', - factory: (await import('./go-adapter-factory.js')).GoAdapterFactory + factory: _GoAdapterFactory }; ``` @@ -360,13 +363,11 @@ The policy implements the `AdapterPolicy` interface (from `adapter-policy.ts`). See `packages/shared/src/interfaces/adapter-policy-go.ts` for a minimal, clean policy example. -### Wiring the policy (3 locations) - -1. **DAP proxy** — `src/proxy/dap-proxy-worker.ts` → `selectAdapterPolicy()` method: add a new `else if` branch matching your adapter command. +### Wiring the policy (2 locations) -2. **Session manager** — `src/session/session-manager-data.ts` → `selectPolicy()` method: add a new `case` branch for your `DebugLanguage` value. +1. **Policy map (required)** — add a `case` for your `DebugLanguage` in `getPolicyForLanguage()` in `packages/shared/src/interfaces/adapter-policy-map.ts`. This is consumed by both the DAP proxy and session manager. (The `else if` command-shape chain in `dap-proxy-worker.ts` `selectAdapterPolicy()` is a legacy fallback; add a branch there only if you must support sessions that arrive without a language field.) -3. **Export** — Add the policy to `packages/shared/src/index.ts` so both locations can import it. +2. **Export** — Add the policy to `packages/shared/src/index.ts` so `adapter-policy-map.ts` can import it. --- @@ -477,8 +478,7 @@ The loader: - [ ] `DebugLanguage` enum updated in `packages/shared/src/models/index.ts` - [ ] Adapter policy created in `packages/shared/src/interfaces/adapter-policy-.ts` - [ ] Policy exported from `packages/shared/src/index.ts` -- [ ] Policy wired into `selectAdapterPolicy()` in `src/proxy/dap-proxy-worker.ts` -- [ ] Policy wired into `selectPolicy()` in `src/session/session-manager-data.ts` +- [ ] Policy `case` added to `getPolicyForLanguage()` in `packages/shared/src/interfaces/adapter-policy-map.ts` - [ ] Registered in root `package.json` optionalDependencies - [ ] Added to known adapters list in `src/adapters/adapter-loader.ts` - [ ] Vitest alias added in `vitest.config.ts` diff --git a/docs/architecture/adapter-pattern-design.md b/docs/architecture/adapter-pattern-design.md index 99232a54..c7bd2b87 100644 --- a/docs/architecture/adapter-pattern-design.md +++ b/docs/architecture/adapter-pattern-design.md @@ -365,7 +365,7 @@ sequenceDiagram "name": "create_debug_session", "inputSchema": { "properties": { - "language": { "enum": ["python", "javascript", "rust", "go", "java", "dotnet", "mock"] }, + "language": { "enum": ["python", "ruby", "javascript", "rust", "go", "java", "dotnet", "mock"] }, "executablePath": { "type": "string" } } } diff --git a/docs/architecture/adapter-policy-pattern.md b/docs/architecture/adapter-policy-pattern.md index 07c8adc6..92a97921 100644 --- a/docs/architecture/adapter-policy-pattern.md +++ b/docs/architecture/adapter-policy-pattern.md @@ -14,13 +14,13 @@ This document explains the AdapterPolicy pattern and how it relates to the main - **Purpose**: Complete debug adapter implementation - **Scope**: Handles all DAP protocol communication and language runtime management - **Location**: `packages/adapter-/` -- **Examples**: `JavascriptDebugAdapter`, `PythonDebugAdapter`, `MockDebugAdapter`, `RustDebugAdapter`, `GoDebugAdapter` +- **Examples**: `JavascriptDebugAdapter`, `PythonDebugAdapter`, `MockDebugAdapter`, `RustDebugAdapter`, `GoDebugAdapter`, `RubyDebugAdapter`, `JavaDebugAdapter`, `DotnetDebugAdapter` ### AdapterPolicy (Supporting Pattern) - **Purpose**: Language-specific policies for session management - **Scope**: Lightweight behaviors used by SessionManager and DAP proxy layer - **Location**: `packages/shared/src/interfaces/adapter-policy-*.ts` -- **All policies**: `DefaultAdapterPolicy`, `PythonAdapterPolicy`, `JsDebugAdapterPolicy`, `RustAdapterPolicy`, `GoAdapterPolicy`, `JavaAdapterPolicy`, `DotnetAdapterPolicy`, `MockAdapterPolicy` +- **All policies**: `DefaultAdapterPolicy`, `PythonAdapterPolicy`, `JsDebugAdapterPolicy`, `RubyAdapterPolicy`, `RustAdapterPolicy`, `GoAdapterPolicy`, `JavaAdapterPolicy`, `DotnetAdapterPolicy`, `MockAdapterPolicy` ## AdapterPolicy Interface @@ -136,6 +136,7 @@ Client Request → Server → SessionManager | `DefaultAdapterPolicy` | `adapter-policy.ts` | `'default'` | `'default'` | No | No | | `PythonAdapterPolicy` | `adapter-policy-python.ts` | `'python'` | `'debugpy'` | No | No | | `JsDebugAdapterPolicy` | `adapter-policy-js.ts` | `'js-debug'` | `'pwa-node'` | Yes | Yes | +| `RubyAdapterPolicy` | `adapter-policy-ruby.ts` | `'ruby'` | `'rdbg'` | No | No | | `RustAdapterPolicy` | `adapter-policy-rust.ts` | `'rust'` | `'lldb'` | No | No | | `GoAdapterPolicy` | `adapter-policy-go.ts` | `'go'` | `'dlv-dap'` | No | No | | `JavaAdapterPolicy` | `adapter-policy-java.ts` | `'java'` | `'java'` | No | No | @@ -158,10 +159,10 @@ Client Request → Server → SessionManager - `resolveExecutablePath()` checks `PYTHON_PATH` env var, then defaults to `'python'` (Windows) or `'python3'` (Unix) - `validateExecutable()` spawns Python to detect Windows Store aliases - `extractLocalVariables()` filters out `special variables`, `function variables`, dunder variables, and `_pydev*` internals -- `getLocalScopeName()` returns `['Locals']` +- `getLocalScopeName()` returns `['Locals', 'Local']` **RustAdapterPolicy**: -- `resolveExecutablePath()` prefers an explicitly provided path, then `CARGO_PATH` env var, otherwise returns `undefined` to let downstream adapter discovery decide. Despite the name, this does not resolve the vendored CodeLLDB path; actual adapter spawn path selection happens in `getAdapterSpawnConfig()`. +- `resolveExecutablePath()` prefers an explicitly provided path, otherwise returns `undefined` to let downstream adapter discovery (codelldb-resolver.ts, which checks `CODELLDB_PATH`) decide. Despite the name, this does not resolve the vendored CodeLLDB path; actual adapter spawn path selection happens in `getAdapterSpawnConfig()`. - `validateExecutable()` checks filesystem existence of the candidate executable, then spawns it with `--version` and verifies stdout contains `codelldb` - `getAdapterSpawnConfig()` resolves a vendored CodeLLDB binary based on platform/arch - `getLocalScopeName()` returns `['Local', 'Locals']` @@ -203,24 +204,7 @@ Session management classes use a `selectPolicy()` method to get the appropriate ```typescript export class SessionManagerData extends SessionManagerCore { protected selectPolicy(language: string | DebugLanguage): AdapterPolicy { - switch (language) { - case DebugLanguage.PYTHON: - return PythonAdapterPolicy; - case DebugLanguage.JAVASCRIPT: - return JsDebugAdapterPolicy; - case DebugLanguage.RUST: - return RustAdapterPolicy; - case DebugLanguage.GO: - return GoAdapterPolicy; - case DebugLanguage.JAVA: - return JavaAdapterPolicy; - case DebugLanguage.DOTNET: - return DotnetAdapterPolicy; - case DebugLanguage.MOCK: - return MockAdapterPolicy; - default: - return DefaultAdapterPolicy; - } + return getPolicyForLanguage(language); } async getStackTrace(sessionId: string): Promise { @@ -237,6 +221,11 @@ export class SessionManagerData extends SessionManagerCore { } ``` +The language → policy switch itself lives in `getPolicyForLanguage()` +(`packages/shared/src/interfaces/adapter-policy-map.ts`), which is the single +source of truth shared by `SessionManager`, `SessionStore`, and the DAP proxy +worker so the mapping cannot drift between processes. + ## Migration from Hardcoded Conditionals ### Before (Hardcoded) diff --git a/docs/architecture/api-reference.md b/docs/architecture/api-reference.md index 429e05cb..70b29852 100644 --- a/docs/architecture/api-reference.md +++ b/docs/architecture/api-reference.md @@ -60,9 +60,11 @@ Gets the currently active thread ID during debugging. ### Environment Validation Methods -#### `validateEnvironment(): Promise` +#### `validateEnvironment(executablePath?: string): Promise` Comprehensive environment check for debugging readiness. +**Parameters**: `executablePath` (optional) — a user-specified interpreter/executable path to validate. + **Returns**: ```typescript { @@ -180,6 +182,11 @@ Gets default attach configuration for this language. **Returns**: Default attach configuration with language-specific defaults +#### `usesDirectConnectForAttach?(): boolean` +Whether attach connects directly to an already-listening DAP server (e.g. `rdbg` started with `--open`) instead of spawning an adapter process. When `true`, no adapter command is built for attach sessions; the adapter policy returns a `'connect'` spawn config from the attach host/port. + +**Returns**: `true` if attach uses direct connection + ### Launch Barrier (Optional) #### `createLaunchBarrier?(command: string, args?: unknown): AdapterLaunchBarrier | undefined` @@ -347,10 +354,10 @@ Gets variable scopes for a stack frame. #### `getVariables(sessionId: string, variablesReference: number): Promise` Gets variables in a scope. -#### `evaluateExpression(sessionId: string, expression: string, frameId?: number, context?: string): Promise` +#### `evaluateExpression(sessionId: string, expression: string, frameId?: number, timeoutMs?: number): Promise` Evaluates an expression in the current context. Returns a structured `EvaluateResult` with `result`, `type`, `variablesReference`, and optional error text. -**Note**: The `context` parameter is accepted by the API but the DAP `evaluate` request is always sent with `context: 'variables'` internally, regardless of the value passed. +**Note**: The DAP `evaluate` `context` is chosen by the adapter policy's `getEvaluateContext()` (defaults to `'variables'`; the Ruby policy uses `'repl'`). There is no client-supplied context parameter. #### `attachToProcess(sessionId: string, attachConfig: AttachConfig): Promise` Attaches the debugger to a running process. @@ -358,6 +365,9 @@ Attaches the debugger to a running process. #### `detachFromProcess(sessionId: string, terminateProcess?: boolean): Promise` Detaches the debugger from an attached process. +#### `redefineClasses(sessionId: string, classesDir: string, sinceTimestamp?: number, timeoutMs?: number): Promise` +Java only. Hot-swaps changed classes into a running JVM via a custom DAP `redefineClasses` request. `sinceTimestamp` (ms) limits the scan to `.class` files modified after that time (0/omitted = all); `timeoutMs` overrides the DAP request timeout. Exposed as the `redefine_classes` MCP tool. + #### `listThreads(sessionId: string): Promise>` Lists all threads in the debug session. @@ -435,6 +445,27 @@ Checks if a language has a registered adapter. #### `getSupportedLanguages(): string[]` Lists all registered languages. +#### `unregister(language: string): boolean` +Removes an adapter factory and disposes any active adapters for the language. Returns `false` if no factory was registered. + +#### `getAdapterInfo(language: string): AdapterInfo | undefined` +Returns metadata for a registered adapter, or `undefined`. + +#### `getAllAdapterInfo(): Map` +Returns metadata for all registered adapters. + +#### `async listLanguages(): Promise` +Lists all known languages from static registration and dynamic discovery (used by server.ts for language advertisement). + +#### `async listAvailableAdapters(): Promise` +Lists detailed adapter metadata (known adapters plus install status). + +#### `disposeAll(): Promise` +Disposes all created adapters, clears factories, and resets the registry. + +#### `getActiveAdapterCount(): number` +Returns the total number of active adapter instances across all languages. + ## Event System ### Adapter Events @@ -481,6 +512,7 @@ Common sequences: ```typescript enum DebugLanguage { PYTHON = 'python', + RUBY = 'ruby', JAVASCRIPT = 'javascript', RUST = 'rust', GO = 'go', diff --git a/docs/architecture/component-design.md b/docs/architecture/component-design.md index 8117b20b..8679f4a1 100644 --- a/docs/architecture/component-design.md +++ b/docs/architecture/component-design.md @@ -66,9 +66,11 @@ class SessionManager { async stepOut(sessionId: string): Promise async continue(sessionId: string): Promise async pause(sessionId: string): Promise - async evaluateExpression(sessionId: string, expression: string, frameId?: number, context?: string): Promise - async attachToProcess(sessionId: string, attachConfig: {...}): Promise + async evaluateExpression(sessionId: string, expression: string, frameId?: number, timeoutMs?: number): Promise + async attachToProcess(sessionId: string, attachConfig: { port?: number; host?: string; processId?: number | string; timeout?: number; sourcePaths?: string[]; stopOnEntry?: boolean; justMyCode?: boolean; verifyTimeout?: number; }): Promise async detachFromProcess(sessionId: string, terminateProcess?: boolean): Promise + async listThreads(sessionId: string): Promise> + async redefineClasses(sessionId: string, classesDir: string, sinceTimestamp?: number, timeoutMs?: number): Promise // Adapter registry (from SessionManagerCore) public adapterRegistry: IAdapterRegistry @@ -356,6 +358,9 @@ SessionStore provides centralized storage and management for debug sessions with ```typescript class SessionStore { + // Policy + selectPolicy(language: DebugLanguage): AdapterPolicy + // Creation createSession(params: CreateSessionParams): DebugSessionInfo @@ -364,9 +369,16 @@ class SessionStore { getOrThrow(sessionId: string): ManagedSession getAll(): DebugSessionInfo[] getAllManaged(): ManagedSession[] + has(sessionId: string): boolean // Updates + set(sessionId: string, session: ManagedSession): void + update(sessionId: string, updates: Partial): void updateState(sessionId: string, state: SessionState): void + + // Removal + remove(sessionId: string): boolean + clear(): void // Metadata size(): number @@ -395,6 +407,8 @@ Centralized error messages ensure consistency and provide helpful troubleshootin - DAP request timeouts - Proxy initialization failures - Step operation timeouts + - Pause grace-window feedback + - Attach verification failures - Adapter readiness timeouts ### Example Implementation @@ -448,16 +462,13 @@ The dependency injection system enables comprehensive testing by abstracting all logger: ILogger; proxyManagerFactory: IProxyManagerFactory; sessionStoreFactory: ISessionStoreFactory; - debugTargetLauncher: IDebugTargetLauncher; environment: IEnvironment; adapterRegistry: IAdapterRegistry; } ``` - The top-level `Dependencies` container (in `src/container/dependencies.ts`) also includes launcher services that are used by the proxy and session layers: - - `processLauncher: IProcessLauncher` -- general-purpose process spawning + The top-level `Dependencies` container (in `src/container/dependencies.ts`) has this field set: `fileSystem`, `processManager`, `networkManager`, `logger`, `environment`, `proxyProcessLauncher`, `proxyManagerFactory`, `sessionStoreFactory`, `adapterRegistry`. The launcher service used by the proxy and session layers is: - `proxyProcessLauncher: IProxyProcessLauncher` -- spawning proxy worker child processes - - `debugTargetLauncher: IDebugTargetLauncher` -- spawning the debug target (uses `processLauncher` and `networkManager`) ### Benefits diff --git a/docs/architecture/dual-pattern-decision-guide.md b/docs/architecture/dual-pattern-decision-guide.md index 2f8223c1..81db99c2 100644 --- a/docs/architecture/dual-pattern-decision-guide.md +++ b/docs/architecture/dual-pattern-decision-guide.md @@ -236,11 +236,8 @@ export const NewLanguagePolicy: AdapterPolicy = { }; ``` -### Step 3: Add to selectPolicy() and update DebugLanguage enum -Add your language to the `DebugLanguage` enum in `@debugmcp/shared`, then add your policy to **all three** `selectPolicy()` locations: -- `src/session/session-manager-data.ts` (session-level data operations) -- `src/proxy/dap-proxy-worker.ts` (proxy-level adapter behavior via `selectAdapterPolicy()`) -- `src/session/session-store.ts` (session persistence policy selection) +### Step 3: Add to getPolicyForLanguage() and update DebugLanguage enum +Add your language to the `DebugLanguage` enum in `@debugmcp/shared`, then add a case (plus its import) to `getPolicyForLanguage()` in `packages/shared/src/interfaces/adapter-policy-map.ts`. This is the single source of truth for language-to-policy mapping — `session-manager-data.ts`, `session-store.ts`, and `dap-proxy-worker.ts` (`selectAdapterPolicy()`) all delegate to it, so there is no need to edit them separately: ```typescript case DebugLanguage.NEWLANG: diff --git a/docs/architecture/dynamic-loading-architecture.md b/docs/architecture/dynamic-loading-architecture.md index 352b6164..09e57912 100644 --- a/docs/architecture/dynamic-loading-architecture.md +++ b/docs/architecture/dynamic-loading-architecture.md @@ -1,6 +1,6 @@ # Dynamic Loading Architecture -Status: v0.19.0 +Status: v0.23.0 Scope: Adapter discovery, lazy loading, caching, error handling, and container considerations ## Overview @@ -59,7 +59,7 @@ cache.set(language, factory); - Registers the loaded factory and immediately uses it - Provides: - `getSupportedLanguages()` for currently registered factories - - `listLanguages()` returns registered languages plus the hardcoded known-adapter catalog entries (not true filesystem or package discovery) when dynamic loading is enabled (not just installed adapters) + - `listLanguages()` returns statically registered languages unioned with the adapters the loader reports as installed (each hardcoded catalog entry is probed via a real dynamic import to set install status). Known-but-uninstalled catalog entries are excluded unless statically registered. - `listAvailableAdapters()` for installed metadata (name, package, description) - Enforces instance limits and auto-dispose timers @@ -146,7 +146,7 @@ sequenceDiagram - Preloading vs Lazy: - Prefer lazy for most cases to keep cold-start minimal - Calling `listLanguages()` early probes discoverability/availability, but actual registry registration and adapter initialization still occur on `create(...)` or explicit `loadAdapter(...)` paths. To truly preload, construct sessions or call `loadAdapter()` on startup if your environment benefits from it. - - **Container mode exception**: When `MCP_CONTAINER=true`, startup pre-registers known adapters (mock, python, javascript, rust, go, java) via `tryRegister` calls in `dependencies.ts`, so the first `create(...)` call does not incur a dynamic import cost. Note that dotnet is not pre-registered in container mode. + - **Container mode exception**: When `MCP_CONTAINER=true`, startup pre-registers known adapters (mock, python, javascript, ruby, rust, go, java) via `tryRegister` calls in `dependencies.ts`, so the first `create(...)` call does not incur a dynamic import cost. Note that dotnet is not pre-registered in container mode. ## Container Considerations @@ -185,7 +185,7 @@ sequenceDiagram - Run `DEBUG=mcp:*` in your client environment if supported - Verify adapter presence: - `npm ls @debugmcp/adapter-*` - - Call `list_supported_languages` tool to see what the server reports. Note that this tool may include known but uninstalled adapters (marked with `installed: false`) when dynamic loading is enabled, since it merges the hardcoded known-adapter catalog with actually registered factories. + - Call `list_supported_languages` tool to see what the server reports. Note that this tool merges the hardcoded known-adapter catalog with actually registered factories, and probes each catalog entry with a real dynamic import to report accurate `installed: true`/`false` status. - Use the diagnostic client: - `node scripts/diagnose-stdio-client.mjs` verifies connect → list → create → close flow diff --git a/docs/architecture/javascript-adapter.md b/docs/architecture/javascript-adapter.md index 526752f5..a2525c1c 100644 --- a/docs/architecture/javascript-adapter.md +++ b/docs/architecture/javascript-adapter.md @@ -10,7 +10,7 @@ Goals: ## Modular by default -The MCP Debugger ships without auto-installing optional adapters. The adapter loader has a hardcoded known-adapter registry for all seven languages (mock, python, javascript, rust, go, java, dotnet), but not all are built by default. JavaScript is available as an optional adapter in a separate package: +The MCP Debugger ships without auto-installing optional adapters. The adapter loader has a hardcoded known-adapter registry for all eight languages (mock, python, javascript, ruby, rust, go, java, dotnet), but not all are built by default. JavaScript is available as an optional adapter in a separate package: - Package name: `@debugmcp/adapter-javascript` - Factory export: `JavascriptAdapterFactory` @@ -58,7 +58,7 @@ The shared model defines: The display name ("JavaScript/TypeScript") and default executable (`node`) are defined in the adapter implementation (`packages/adapter-javascript/`) and its factory metadata, not in the shared model itself. The shared model only carries the language enum value. Unit tests were updated to reflect the addition: -- `tests/core/unit/session/models.test.ts` now expects seven languages (python, javascript, rust, go, java, dotnet, mock) and verifies inclusion of `javascript`. +- `tests/core/unit/session/models.test.ts` now expects eight languages (python, javascript, ruby, rust, go, java, dotnet, mock) and verifies inclusion of `javascript`. ## Verification steps @@ -104,7 +104,6 @@ Adapter exports include: - Utility re-exports include: - `resolveNodeExecutable` -- resolves the Node runtime path in a cross-platform, deterministic manner. - `detectTsRunners` -- detects available TypeScript runners (ts-node, tsx, etc.) in the environment. - - `transformConfig` -- transforms generic launch config into js-debug-specific configuration. The `packages/adapter-javascript/package.json` manifest includes: - `"exports": { ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" } }` -- ESM import with type declarations diff --git a/docs/architecture/js-debug-vendoring.md b/docs/architecture/js-debug-vendoring.md index b95c6f25..3ef19b73 100644 --- a/docs/architecture/js-debug-vendoring.md +++ b/docs/architecture/js-debug-vendoring.md @@ -100,7 +100,7 @@ try { await fsp.copyFile(VENDOR_FILE, VENDOR_FILE_CJS); } catch { /* optional CJ // NEW: Search-and-copy support JS sidecars from anywhere in the extracted tree const supportTargets = new Set(['bootloader.js', 'hash.js', 'watchdog.js']); -const supportFiles = await findAllByBasename(path.dirname(found.abs), supportTargets); +const supportFiles = await findAllByBasename(extractDir, supportTargets); // Copy any found support files to the vendor root, preserving basename for (const supportSrc of supportFiles) { diff --git a/docs/architecture/system-overview.md b/docs/architecture/system-overview.md index 945b3a7c..19aa382e 100644 --- a/docs/architecture/system-overview.md +++ b/docs/architecture/system-overview.md @@ -72,7 +72,6 @@ graph TB logger: ILogger; proxyManagerFactory: IProxyManagerFactory; sessionStoreFactory: ISessionStoreFactory; - debugTargetLauncher: IDebugTargetLauncher; environment: IEnvironment; adapterRegistry: IAdapterRegistry; } diff --git a/docs/architecture/testing-architecture.md b/docs/architecture/testing-architecture.md index c406b48e..ed057e09 100644 --- a/docs/architecture/testing-architecture.md +++ b/docs/architecture/testing-architecture.md @@ -148,7 +148,7 @@ Cleanup: `afterAll` closes the MCP client and kills the server process. `afterEa ### STDIO Smoke Test Matrix -Nine per-language STDIO smoke tests: Python, JavaScript, Rust, Go, Java (launch), Java (attach), Java (evaluate), Java (inner class), .NET. Each follows the standard lifecycle: +Sixteen per-language STDIO smoke tests: Python, Python (attach), JavaScript, JavaScript (attach), Rust, Go, Java (launch), Java (attach), Java (evaluate), Java (inner class), Java (pause), Java (event race), Java (redefine), .NET, Ruby, Ruby (attach). Each follows the standard lifecycle: 1. Create session → set breakpoint → start debugging 2. Inspect: stack trace, scopes, variables @@ -165,7 +165,7 @@ Two SSE test files test the SSE HTTP transport: Python over SSE (`mcp-server-smo **File:** `tests/e2e/comprehensive-mcp-tools.test.ts` -Tests all 20 MCP tools across 7 languages (Python, JavaScript, Mock, Rust, Go, Java, Dotnet) where the toolchain is available. Produces a PASS/FAIL/SKIP matrix report with per-tool per-language status and timing. Toolchain detection uses `hasCommand()` checks (e.g., `rustc --version`, `go version`). +Tests all 20 MCP tools across 8 languages (Python, JavaScript, Mock, Rust, Go, Java, Dotnet, Ruby) where the toolchain is available. Produces a PASS/FAIL/SKIP matrix report with per-tool per-language status and timing. Toolchain detection uses `hasCommand()` checks (e.g., `rustc --version`, `go version`). ### Docker E2E @@ -252,7 +252,7 @@ Protocol-level correctness checks. `breakpoint-messages/` contains Python script - CLI entry points (`cli-entry.ts`) — process-level stdio handling, not unit-testable - Proxy entry point (`dap-proxy-entry.ts`) — runs as a separate process - Mock adapter process (`mock-adapter-process.ts`) — tested via E2E, not importable -- Module-init side-effects (`batteries-included.ts`) — only import statements +- Module-init side-effects (`batteries-included.ts`) — static adapter imports plus a module-level side effect that registers each adapter factory into a `globalThis[GLOBAL_KEY]` registry with language-based deduplication - Barrel/index exports — prevent duplicate coverage counting - Factory pattern files with minimal logic diff --git a/docs/development/dap-sequence-reference.md b/docs/development/dap-sequence-reference.md index 5614b66c..c23f4c1c 100644 --- a/docs/development/dap-sequence-reference.md +++ b/docs/development/dap-sequence-reference.md @@ -122,7 +122,7 @@ This sequence (paused → continued → exited → terminated) is the typical fl * **User stops the program manually:** If the user presses a "Stop" button, the client might send a `terminate` request or `disconnect` request to the adapter. The adapter will then end the program (e.g., kill the process if it launched it, or detach if attached). The adapter still should send a `terminated` event to signal the session is ending (even if the stop was user-initiated). If the program was killed, an `exited` event may or may not be sent depending on if the adapter was able to capture an exit code (sometimes a forced kill might not have a meaningful exit code, but typically it would be treated as exit code 0 or a specific code). The sequence in that scenario: (User stop request) → adapter possibly sends `terminated` immediately (and kills process) → maybe an `exited` if there is an exit code to report. The key is that `terminated` is always emitted once debugging stops. - **Note on mcp-debugger event handling:** In this project there are two layers of event processing. Raw DAP events are forwarded via `handleDapEvent()`: most events (`terminated`, `exited`, `continued`) are forwarded with empty args, but `stopped` is forwarded with its full args (`threadId`, `reason`, `body`). Separately, proxy status messages (`adapter_exited`, `dap_connection_closed`, `terminated`) are normalized by `handleStatusMessage()` into a unified local `exit` event with `[code ?? 1, signal || undefined]`. Note that exit code uses `??` (nullish coalescing), so exit code `0` is preserved as `0` -- only `null` or `undefined` codes fall back to `1`. Signal uses `||`, so an empty string signal also falls back to `undefined`. This means a proxy status `terminated` becomes an `exit` event, while a raw DAP `terminated` event remains a `terminated` event. + **Note on mcp-debugger event handling:** In this project there are two layers of event processing. Raw DAP events are forwarded via `handleDapEvent()`: most events (`terminated`, `exited`, `continued`) are forwarded with empty args, but `stopped` is forwarded with its full args (`threadId`, `reason`, `body`). Separately, proxy status messages (`adapter_exited`, `dap_connection_closed`, `terminated`) are normalized into a local `exit` event, but the two layers disagree on how they do it: the imperative `handleStatusMessage()` (guarded by an `exitEmitted` flag) emits `[code ?? 1, signal || undefined]`, preserving exit code `0`; the functional dap-core `handleDapEvent`/`handleProxyMessage()` path emits `[code || 1, signal || undefined]`, which folds exit code `0` back to `1`. Both paths run for a status message (only raw `dapEvent` messages skip the functional-core emit), so a code-`0` status can produce two `exit` emissions with different codes -- exit code `0` is not reliably preserved end-to-end. Signal uses `||` in both paths, so an empty string signal also falls back to `undefined`. This means a proxy status `terminated` becomes an `exit` event, while a raw DAP `terminated` event remains a `terminated` event. * **Exception causes program to end:** If the debuggee crashes or encounters an unhandled exception that terminates it, the adapter might first send a `stopped` event with reason `exception` (if it breaks on the exception). If the user doesn’t intervene and the program truly crashes/exits, the adapter will then send the `exited` and `terminated`. In some configurations, adapters are set to break on all uncaught exceptions – giving a chance to inspect – which would be a `stopped` event, and if the user then continues, the program may immediately exit, leading to the exit/terminated events as usual. diff --git a/docs/development/debugging-guide.md b/docs/development/debugging-guide.md index b3173f9d..b7e49b13 100644 --- a/docs/development/debugging-guide.md +++ b/docs/development/debugging-guide.md @@ -4,7 +4,7 @@ This guide covers how to debug the MCP Debug Server itself during development. Y ## Overview -> **Warning**: `console.log`, `console.error`, and all other `console` methods are silenced at process startup (in both STDIO and SSE modes) to protect stdio/IPC transports from being corrupted by unexpected output. Any `console.*` calls you add to server or proxy code will produce no output. Use `this.logger.debug(...)` (or another Winston logger method) for in-process logging, or write directly to a file (e.g., `fs.appendFileSync('/tmp/debug.log', ...)`) for low-level startup diagnostics. +> **Warning**: `console.log`, `console.error`, and all other `console` methods are silenced at process startup (regardless of transport mode — STDIO, SSE, and HTTP) to protect stdio/IPC transports from being corrupted by unexpected output. Any `console.*` calls you add to server or proxy code will produce no output. Use `this.logger.debug(...)` (or another Winston logger method) for in-process logging, or write directly to a file (e.g., `fs.appendFileSync('/tmp/debug.log', ...)`) for low-level startup diagnostics. Debugging a debug server presents unique challenges: - Multiple processes (server, proxy, debug adapter) @@ -374,12 +374,12 @@ case 'debug_diagnostics': ### 3. Health Checks -The SSE command handler (`src/cli/sse-command.ts`) already exposes a `GET /health` endpoint on the same port as the SSE server. No separate server is needed: +Note: SSE transport is deprecated in favor of `mcp-debugger http` (the recommended production transport); the HTTP transport exposes the same health endpoint. Both the HTTP command handler (`src/cli/http-command.ts`) and the SSE command handler (`src/cli/sse-command.ts`) expose a `GET /health` endpoint on the same port as the server. No separate server is needed: ```bash # Query the built-in health endpoint (default port 3001) curl http://localhost:3001/health -# Returns: { "status": "ok", "mode": "sse", "connections": N, "sessions": [...] } +# Returns: { "status": "ok", "mode": "http", "connections": N, "sessions": [...] } ``` ## Debugging Checklists diff --git a/docs/development/setup-guide.md b/docs/development/setup-guide.md index e8ccfe9c..e6696ff2 100644 --- a/docs/development/setup-guide.md +++ b/docs/development/setup-guide.md @@ -106,6 +106,7 @@ mcp-debugger/ │ ├── adapter-go/ # Go adapter (Delve) │ ├── adapter-java/ # Java debug adapter (JDI) │ ├── adapter-dotnet/ # .NET debug adapter (netcoredbg) +│ ├── adapter-ruby/ # Ruby debug adapter (rdbg) │ ├── adapter-mock/ # Mock adapter for testing │ └── mcp-debugger/ # Self-contained CLI bundle (npx distribution) ├── src/ # Core server source code @@ -162,7 +163,15 @@ npm run lint:fix node dist/index.js stdio ``` -#### SSE Mode +#### HTTP Mode (Streamable HTTP, recommended) + +```bash +node dist/index.js http -p 3001 +``` + +#### SSE Mode (deprecated) + +SSE transport is deprecated -- use the `http` subcommand instead. ```bash node dist/index.js sse -p 3001 @@ -171,10 +180,10 @@ node dist/index.js sse -p 3001 #### With Debug Logging ```bash -node dist/index.js sse -p 3001 --log-level debug --log-file ./logs/debug.log +node dist/index.js http -p 3001 --log-level debug --log-file ./logs/debug.log ``` -Note: Console output is unconditionally silenced at process startup for all transport modes (STDIO and SSE) to prevent any stray output from corrupting protocol communication. Use `--log-file` to capture logs. +Note: Console output is unconditionally silenced at process startup for all transport modes (STDIO, HTTP, and SSE) to prevent any stray output from corrupting protocol communication. Use `--log-file` to capture logs. ## VS Code Setup diff --git a/docs/docker-support.md b/docs/docker-support.md index b233b013..400354d4 100644 --- a/docs/docker-support.md +++ b/docs/docker-support.md @@ -76,7 +76,8 @@ Here's the recommended configuration for your MCP settings file: "get_scopes", "evaluate_expression", "get_source_context", - "list_threads" + "list_threads", + "redefine_classes" ], "disabled": false, "timeout": 60 diff --git a/docs/error-handling-guide.md b/docs/error-handling-guide.md index 5c35bb02..217a8a30 100644 --- a/docs/error-handling-guide.md +++ b/docs/error-handling-guide.md @@ -73,8 +73,8 @@ if (!session.proxyManager?.isRunning()) { The error handling uses a mixed strategy across three layers: 1. **Implementation Layer (Backend)** - Uses typed `McpError` subclasses (e.g., `SessionNotFoundError`, `ProxyNotRunningError`) for infrastructure failures (unknown session, terminated session, proxy not running). These are defined in `src/errors/debug-errors.ts`. Operation-level failures (e.g., session not paused, missing thread ID) return structured `DebugResult` objects with `success: false` rather than throwing. -2. **Server Layer** - The MCP server (`src/server.ts`) may also throw `McpError` directly (e.g., for invalid parameters before reaching the session manager). The MCP SDK serializes all `McpError` instances into protocol-level error responses. `DebugResult` failures are returned as successful tool responses with `success: false`. -3. **Client Layer** - Receives either a protocol-level MCP error (from typed backend errors or server-level `McpError`) or a structured failure result with `success: false`, depending on which layer the failure originated in +2. **Server Layer** - The MCP server (`src/server.ts`) throws `McpError` directly only for invalid parameters before reaching the session manager (e.g., missing `sessionId`) or for unexpected non-`Error` throws. The typed session-lifecycle errors from the implementation layer (`SessionNotFoundError`, `SessionTerminatedError`, `ProxyNotRunningError`) are caught in each tool handler and converted into a successful tool response whose JSON payload has `success: false, error: ` -- they are not propagated as protocol-level `McpError`. `DebugResult` failures are likewise returned as successful tool responses with `success: false`. +3. **Client Layer** - Receives either a protocol-level MCP error (only from invalid-parameter checks or unexpected non-`Error` throws) or a structured failure result with `success: false` (for typed session errors and `DebugResult` failures), depending on which layer the failure originated in ```typescript // Implementation (throws typed error for infrastructure failures) @@ -86,15 +86,23 @@ async continue(sessionId: string): Promise { // ... continue logic } -// Server (automatic serialization by MCP) +// Server (catches typed session errors per-tool) try { - await this.sessionManager.continue(sessionId); + const continueResult = await this.continueExecution(sessionId); + result = { content: [{ type: 'text', text: JSON.stringify({ success: continueResult }) }] }; } catch (error) { - // MCP framework serializes error.message automatically - throw error; + if (error instanceof SessionTerminatedError || + error instanceof SessionNotFoundError || + error instanceof ProxyNotRunningError) { + result = { content: [{ type: 'text', text: JSON.stringify({ success: false, error: error.message }) }] }; + } else if (error instanceof Error) { + result = { content: [{ type: 'text', text: JSON.stringify({ success: false, error: error.message }) }] }; + } else { + throw error; + } } -// Client receives: "Session not found: test-session" +// Client receives a successful tool response: { success: false, error: "Session not found: test-session" } ``` ## Testing Patterns diff --git a/docs/patterns/dependency-injection.md b/docs/patterns/dependency-injection.md index d214b9c9..9d3ca9a9 100644 --- a/docs/patterns/dependency-injection.md +++ b/docs/patterns/dependency-injection.md @@ -27,7 +27,7 @@ High-level modules depend on abstractions, not concrete implementations. **Location**: `src/session/session-manager-core.ts` -> **Note**: `SessionManager` (in `session-manager.ts`) extends `SessionManagerOperations`, which extends `SessionManagerCore`. The dependency injection and core logic live in `SessionManagerCore`. `SessionManager` implements `handleAutoContinue(sessionId)` which calls `this.continue(sessionId)` to auto-continue past entry breakpoints. +> **Note**: `SessionManager` (in `session-manager.ts`) extends `SessionManagerOperations`, which extends `SessionManagerData`, which extends `SessionManagerCore`. The dependency injection and core logic live in `SessionManagerCore`. `SessionManager` implements `handleAutoContinue(sessionId)` which calls `this.continue(sessionId)` to auto-continue past entry breakpoints. ```typescript // Define dependencies interface @@ -37,7 +37,6 @@ export interface SessionManagerDependencies { logger: ILogger; proxyManagerFactory: IProxyManagerFactory; sessionStoreFactory: ISessionStoreFactory; - debugTargetLauncher: IDebugTargetLauncher; environment: IEnvironment; adapterRegistry: IAdapterRegistry; } @@ -53,7 +52,6 @@ constructor( this.environment = dependencies.environment; this.proxyManagerFactory = dependencies.proxyManagerFactory; this.sessionStoreFactory = dependencies.sessionStoreFactory; - this.debugTargetLauncher = dependencies.debugTargetLauncher; this.adapterRegistry = dependencies.adapterRegistry; // Use injected dependencies @@ -118,7 +116,7 @@ This factory pattern allows SessionManager to create ProxyManager instances with ### Core External Dependencies -**Location**: `packages/shared/src/interfaces/external-dependencies.ts` (defines `IFileSystem`, `IProcessManager`, `INetworkManager`, `ILogger`, `IEnvironment`) and `packages/shared/src/interfaces/process-interfaces.ts` (defines `IProcessLauncher`, `IProxyProcessLauncher`) +**Location**: `packages/shared/src/interfaces/external-dependencies.ts` (defines `IFileSystem`, `IProcessManager`, `INetworkManager`, `ILogger`, `IEnvironment`) and `packages/shared/src/interfaces/process-interfaces.ts` (defines `IProxyProcessLauncher` and related IProcess/IProxyProcess types) ```typescript // File system operations @@ -139,12 +137,12 @@ export interface IProcessManager { exec(command: string): Promise<{ stdout: string; stderr: string }>; } -// Process launching (used by AdapterDependencies — note this is a different interface) -// IProcessLauncher is in process-interfaces.ts and is what adapters receive. +// Process launching (note this is a different interface from IProcessManager) +// IProxyProcessLauncher is in process-interfaces.ts and is consumed by +// ProxyManagerFactory/ProxyManager — adapters receive AdapterDependencies +// (fileSystem, logger, environment, networkManager?) instead +// (see "Process-Specific Interfaces" below for its full definition). // IProcessManager is in external-dependencies.ts and is the lower-level system abstraction. -export interface IProcessLauncher { - launch(command: string, args: string[], options?: IProcessOptions): IProcess; -} // Network operations export interface INetworkManager { @@ -199,9 +197,7 @@ export function createProductionDependencies(config: ContainerConfig = {}): Depe const networkManager = new NetworkManagerImpl(); // Process launchers - const processLauncher = new ProcessLauncherImpl(processManager); const proxyProcessLauncher = new ProxyProcessLauncherImpl(processManager); - const debugTargetLauncher = new DebugTargetLauncherImpl(processLauncher, networkManager); // Factories const proxyManagerFactory = new ProxyManagerFactory(proxyProcessLauncher, fileSystem, logger); @@ -216,7 +212,7 @@ export function createProductionDependencies(config: ContainerConfig = {}): Depe return { fileSystem, processManager, networkManager, logger, environment, - processLauncher, proxyProcessLauncher, debugTargetLauncher, + proxyProcessLauncher, proxyManagerFactory, sessionStoreFactory, adapterRegistry }; } @@ -229,17 +225,15 @@ export function createProductionDependencies(config: ContainerConfig = {}): Depe ```typescript // Returns a Dependencies object (defined in tests/test-utils/helpers/test-dependencies.ts) // containing: fileSystem, processManager, networkManager, logger, -// processLauncher, proxyProcessLauncher, debugTargetLauncher, +// proxyProcessLauncher, // proxyManagerFactory, sessionStoreFactory -export async function createMockDependencies(): Promise { +export function createMockDependencies(): Dependencies { const logger = createMockLogger(); const fileSystem = createMockFileSystem(); const processManager = createMockProcessManager(); const networkManager = createMockNetworkManager(); - const processLauncher = new FakeProcessLauncher(); - const proxyProcessLauncher = new FakeProxyProcessLauncher(); - const debugTargetLauncher = new FakeDebugTargetLauncher(); + const proxyProcessLauncher = createMockProxyProcessLauncher(); const proxyManagerFactory = new MockProxyManagerFactory(); proxyManagerFactory.createFn = () => new MockProxyManager(); @@ -247,24 +241,14 @@ export async function createMockDependencies(): Promise { return { fileSystem, processManager, networkManager, logger, - processLauncher, proxyProcessLauncher, debugTargetLauncher, + proxyProcessLauncher, proxyManagerFactory, sessionStoreFactory }; } -// There is also a synchronous helper for SessionManager-specific tests: -export function createMockSessionManagerDependencies(): SessionManagerDependencies { - return { - fileSystem: createMockFileSystem(), - networkManager: createMockNetworkManager(), - logger: createMockLogger(), - proxyManagerFactory: new MockProxyManagerFactory(), - sessionStoreFactory: new MockSessionStoreFactory(), - debugTargetLauncher: createMockDebugTargetLauncher(), - environment: createMockEnvironment(), - adapterRegistry: createMockAdapterRegistry() - }; -} +// A separate SessionManager-specific mock helper (also named createMockDependencies) +// lives in tests/core/unit/session/session-manager-test-utils.ts and returns +// SessionManagerDependencies. export function createMockFileSystem(): IFileSystem { return { diff --git a/docs/patterns/error-handling.md b/docs/patterns/error-handling.md index f3c7049c..2009cf52 100644 --- a/docs/patterns/error-handling.md +++ b/docs/patterns/error-handling.md @@ -22,6 +22,10 @@ export const ErrorMessages = { `Debug adapter did not respond to '${command}' request within ${timeout}s. ` + `This typically means the debug adapter has crashed or lost connection. ` + `Try restarting your debug session. If the problem persists, check the debug adapter logs.`, + + dapRequestTimeoutHint: () => + `If the operation is expected to take this long, retry with a larger 'timeout' (ms) argument. ` + + `Note the operation may still be running in the debuggee.`, proxyInitTimeout: (timeout: number) => `Debug proxy initialization did not complete within ${timeout}s. ` + @@ -33,6 +37,16 @@ export const ErrorMessages = { `(e.g. stepping over a long-running call). The session remains 'running' and will ` + `become 'paused' when the step completes. Check the session state, or call ` + `pause_execution to interrupt.`, + + pausePending: (graceSeconds: number) => + `Pause requested; no 'stopped' event within ${graceSeconds}s ` + + `(the program may be blocked in native code or a syscall). The session will report ` + + `'paused' once the stop lands. Check the session state to confirm.`, + + attachVerifyFailed: (timeoutMs: number, lastFailure: string) => + `Attach did not become debuggable: no threads reported within ${timeoutMs}ms ` + + `(last failure: ${lastFailure}). If the target is just slow to become debuggable ` + + `(e.g. a busy or warming JVM), retry with a larger 'verifyTimeout' (ms) on attach_to_process.`, adapterReadyTimeout: (timeout: number) => `Timed out waiting for debug adapter to be ready after ${timeout}s. ` + @@ -331,14 +345,34 @@ this.logger.error(`[Component] Error description`, { ```typescript async stop(): Promise { if (!this.proxyProcess) { - return; // Already stopped, no error + // No proxy process, but still dispose adapter to release instance slot + this.cleanup(); + return; } this.logger.info(`[ProxyManager] Stopping proxy for session ${this.sessionId}`); + // Give in-flight DAP requests a bounded window to settle before we stop + // processing messages and cancel them (issue #122 follow-up). + await this.drainPendingDapRequests(this.stopDrainTimeoutMs); + + // The proxy may have exited while we drained; re-check before touching + // the process handle. + const process = this.proxyProcess; + if (!process) { + this.isStopped = true; + this.cleanup(); + return; + } + + // Mark as shutting down to stop processing new messages, then clean up + // (cancels whatever is still pending after the drain) + this.isStopped = true; + this.cleanup(); + // Send terminate command try { - this.sendCommand({ cmd: 'terminate' }); + process.send({ cmd: 'terminate', sessionId: this.sessionId }); } catch (error) { this.logger.error(`[ProxyManager] Error sending terminate command:`, error); // Continue with force kill @@ -348,11 +382,11 @@ async stop(): Promise { return new Promise((resolve) => { const timeout = setTimeout(() => { this.logger.warn(`[ProxyManager] Timeout waiting for proxy exit. Force killing.`); - this.proxyProcess?.kill('SIGKILL'); + process.kill('SIGKILL'); resolve(); }, 5000); - this.proxyProcess?.once('exit', () => { + process.once('exit', () => { clearTimeout(timeout); resolve(); }); diff --git a/docs/quickstart.md b/docs/quickstart.md index 2ab343ce..ee66e964 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -4,8 +4,8 @@ This guide will help you get started with `mcp-debugger` quickly, using real exa ## Prerequisites -- **Node.js** (v18+) and npm installed -- **Python** (3.7+) with `debugpy` installed +- **Node.js** (v22+) and npm installed +- **Python** (3.8+) with `debugpy` installed - **MCP Client** (Claude Desktop, or custom implementation) ## Installation diff --git a/docs/rust-debugging.md b/docs/rust-debugging.md index c1dcc26c..c11d80a1 100644 --- a/docs/rust-debugging.md +++ b/docs/rust-debugging.md @@ -40,7 +40,7 @@ SKIP_ADAPTER_VENDOR=true pnpm install ### Useful environment flags -- `CODELLDB_VERSION`: override the release tag (the runtime resolver defaults to `1.11.8` when reading vendored version metadata fails) +- `CODELLDB_VERSION`: override the release tag (downloaded by the vendor script, which defaults to `1.11.8`) - `CODELLDB_FORCE_REBUILD=true`: ignore cached binaries and re-download - `CODELLDB_PLATFORMS=win32-x64,linux-x64,linux-arm64,darwin-x64,darwin-arm64`: vendor specific platforms (comma-separated) - `CODELLDB_VENDOR_ALL=false`: opt out of the "vendor every platform" default and fall back to host-only downloads @@ -161,7 +161,8 @@ Our test suite uses `tests/e2e/rust-example-utils.ts` to make sure every rust sm ``` 2. **Resolve the correct binary path for `start_debugging`.** Breakpoints should always reference the `.rs` source file (absolute paths avoid MCP resolution issues), but `start_debugging.scriptPath` must point to the compiled artifact: - Windows GNU: `examples/rust//target/x86_64-pc-windows-gnu/debug/.exe` - - Windows MSVC fallback: `examples/rust//target/debug/.exe` + - Windows MSVC: `examples/rust//target/x86_64-pc-windows-msvc/debug/.exe` + - Windows generic fallback: `examples/rust//target/debug/.exe` - Unix-like hosts: `examples/rust//target/debug/` Tokio/async builds use exactly the same rule—the helper’s `prepareRustExample('async_example')` just compiles a different crate and returns `{ sourcePath, binaryPath }` so the smoke tests can reuse those paths. diff --git a/docs/stack-trace-filtering.md b/docs/stack-trace-filtering.md index 49a707ea..77128b19 100644 --- a/docs/stack-trace-filtering.md +++ b/docs/stack-trace-filtering.md @@ -8,6 +8,7 @@ The MCP Debugger Server now supports language-specific stack trace filtering to - **Go**: Filters out `/runtime/` and `/testing/` frames by default (implements `filterStackFrames`) - **Java**: Filters out JDK internal frames by default (implements `filterStackFrames`) - **.NET/C#**: Filters out `System.*` and `Microsoft.*` runtime frames and sourceless frames by default +- **Ruby**: Filters out `` and `/gems/` frames by default (implements `filterStackFrames`) - **Python**: No filtering applied (shows all frames) - **Configurable**: Use `includeInternals: true` to see all frames diff --git a/docs/tool-reference.md b/docs/tool-reference.md index 5e8406cf..74264e5e 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -35,7 +35,7 @@ Creates a new debugging session. **Parameters:** - `language` (string, required): The programming language to debug. Languages are discovered dynamically from installed adapters. The default fallback languages (when dynamic discovery is unavailable) are `"python"` and `"mock"`. When all adapters are available, the full list is: `"python"`, `"ruby"`, `"javascript"`, `"rust"`, `"go"`, `"java"`, `"dotnet"`, `"mock"`. The actual list depends on which `@debugmcp/adapter-*` packages are discoverable at runtime. -- `name` (string, optional): A descriptive name for the debug session. Defaults to `"session-<8 chars>"` (e.g., `"session-a4d1acc8"`). +- `name` (string, optional): A descriptive name for the debug session. Defaults to `"-debug-"` (e.g., `"python-debug-1711500000000"`), built from the session language and `Date.now()`. - `executablePath` (string, optional): Path to the language interpreter/executable (e.g., Python interpreter path). **Response:** @@ -129,6 +129,7 @@ Sets a breakpoint in a source file. - `file` (string, required): Path to the source file (absolute or relative to project root). - `line` (number, required): Line number where to set breakpoint (1-indexed). - `condition` (string, optional): Conditional expression for the breakpoint *(not verified to work)*. +- `suspendPolicy` (string, optional): Suspend policy when the breakpoint is hit — `"all"` suspends all threads (default), `"thread"` suspends only the event thread. Only supported by the Java/JDI adapter. **Response:** ```json @@ -557,6 +558,7 @@ Evaluates an expression in the context of the current debug session. - `sessionId` (string, required): The ID of the debug session. - `expression` (string, required): The expression to evaluate. - `frameId` (number, optional): Stack frame ID for context. If not provided, automatically uses the current (top) frame. +- `timeout` (number, optional): Maximum time in milliseconds to wait for the evaluation to complete (default: 30000, max: 600000). On expiry the request fails but the expression may keep executing in the debuggee. **Response:** ```json @@ -713,6 +715,7 @@ Hot-swap changed Java classes into a running JVM using JDI `VirtualMachine.redef - `sessionId` (string, required): The debug session ID (must be an active Java session) - `classesDir` (string, required): Absolute path to compiled classes directory (e.g., `build/classes/java/main/`) - `sinceTimestamp` (number, optional): Unix timestamp in milliseconds. Only redefine `.class` files modified after this time. `0` or omitted = scan all files. +- `timeout` (number, optional): Maximum time in milliseconds to wait for the redefinition to complete (default: 30000, max: 600000). Increase when hot-swapping many classes at once. **Response:** ```json diff --git a/docs/usage.md b/docs/usage.md index bf3123ab..5e2f3c72 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -65,19 +65,28 @@ Here's a real example of debugging a Python script with a bug: ```python # swap_vars.py +# A simple script that swaps two variables, with an intentional bug for debugging. + def swap_variables(a, b): print(f"Initial values: a = {a}, b = {b}") + # Intentionally buggy swap logic for demonstration + # Correct logic would use a temporary variable: temp = a; a = b; b = temp + # Or Python's tuple assignment: a, b = b, a + a = b # Bug: 'a' loses its original value here b = a # Bug: 'b' gets the new value of 'a' (which is original 'b') + print(f"Swapped values: a = {a}, b = {b}") return a, b def main(): x = 10 y = 20 + print("Starting variable swap demo...") swapped_x, swapped_y = swap_variables(x, y) + # Verification if swapped_x == 20 and swapped_y == 10: print("Swap successful!") else: @@ -114,16 +123,16 @@ Set a breakpoint where the bug occurs: { "sessionId": "a4d1acc8-84a8-44fe-a13e-28628c5b33c7", "file": "swap_vars.py", - "line": 4 + "line": 10 } // Response: { "success": true, "breakpointId": "28e06119-619e-43c0-b029-339cec2615df", "file": "C:\\path\\to\\swap_vars.py", - "line": 4, + "line": 10, "verified": false, - "message": "Breakpoint set at C:\\path\\to\\swap_vars.py:4" + "message": "Breakpoint set at C:\\path\\to\\swap_vars.py:10" } ``` @@ -164,21 +173,21 @@ Set a breakpoint where the bug occurs: "id": 3, "name": "swap_variables", "file": "C:\\path\\to\\swap_vars.py", - "line": 4, + "line": 10, "column": 1 }, { "id": 4, "name": "main", "file": "C:\\path\\to\\swap_vars.py", - "line": 13, + "line": 21, "column": 1 }, { "id": 2, "name": "", "file": "C:\\path\\to\\swap_vars.py", - "line": 21, + "line": 30, "column": 1 } ], @@ -372,7 +381,7 @@ You can also evaluate arbitrary expressions in the current debug context: ## Fully Implemented Features -All 20 tools are fully implemented, including: +All 21 tools are fully implemented, including: - **pause_execution**: Sends a DAP pause request and returns immediately; paused state is updated asynchronously. The session normally must be in the `running` state, but calling pause on an already paused session succeeds as a no-op. - **evaluate_expression**: Evaluates arbitrary expressions in the current debug context. When `frameId` is not specified, the server infers it by fetching the stack trace and using the topmost frame -- this works reliably only when a single frame exists or the top frame is the desired context. Callers should provide `frameId` explicitly when debugging code with multiple stack frames. Expressions with side effects are allowed (can modify program state). @@ -387,4 +396,4 @@ All 20 tools are fully implemented, including: --- -*Last updated: 2026-03-21 - All 20 tools including list_threads, pause_execution, and evaluate_expression are fully implemented (v0.19.0)* +*Last updated: 2026-03-21 - All 21 tools including list_threads, pause_execution, and evaluate_expression are fully implemented (v0.23.0)* diff --git a/examples/java/EventRaceTest.java b/examples/java/EventRaceTest.java index 218bf745..86ea9d09 100644 --- a/examples/java/EventRaceTest.java +++ b/examples/java/EventRaceTest.java @@ -31,7 +31,7 @@ public static void main(String[] args) throws Exception { // Now reference LateLoadedHelper — triggers class loading // If a breakpoint was set in LateLoadedHelper, a ClassPrepareEvent fires - String msg = LateLoadedHelper.greet("World"); // line 33 + String msg = LateLoadedHelper.greet("World"); // line 34 System.out.println(msg); System.out.println("EventRaceTest done."); diff --git a/examples/java/InfiniteWait.java b/examples/java/InfiniteWait.java index c22b5531..d62e3374 100644 --- a/examples/java/InfiniteWait.java +++ b/examples/java/InfiniteWait.java @@ -25,11 +25,11 @@ public static void main(String[] args) throws Exception { // Sleep to allow time for debugger attach and class loading. // With suspend=y, JDI bridge sets breakpoints via ClassPrepareRequest; // after VM resume + class load, deferred breakpoints resolve automatically. - Thread.sleep(2000); // line 27 — pause for breakpoint setup - int x = 42; // line 28 - int y = 58; // line 29 - int sum = compute(x, y); // line 30 — calls compute - String msg = format("Sum", sum); // line 31 — calls format + Thread.sleep(2000); // line 28 — pause for breakpoint setup + int x = 42; // line 29 + int y = 58; // line 30 + int sum = compute(x, y); // line 31 — calls compute + String msg = format("Sum", sum); // line 32 — calls format System.out.println(msg); } } diff --git a/examples/java/RedefineTarget.java b/examples/java/RedefineTarget.java index 71f91981..05f0ce96 100644 --- a/examples/java/RedefineTarget.java +++ b/examples/java/RedefineTarget.java @@ -19,7 +19,7 @@ public static void main(String[] args) throws Exception { System.out.println("val1 = " + val1); // After hot-reload, getValue() should return 99 - int val2 = getValue(); // line 23 — second breakpoint target + int val2 = getValue(); // line 22 — second breakpoint target System.out.println("val2 = " + val2); System.out.println("RedefineTarget done."); diff --git a/examples/javascript/test_complete_js_debug.js b/examples/javascript/test_complete_js_debug.js index b0c7c9c2..ed1ec1f5 100644 --- a/examples/javascript/test_complete_js_debug.js +++ b/examples/javascript/test_complete_js_debug.js @@ -22,7 +22,7 @@ function deepFunction(level) { return deepFunction(level - 1); } else { const localVar = "Bottom of stack"; - console.log("At bottom of stack"); // Line 24 - good for breakpoint + console.log("At bottom of stack"); // Line 25 - good for breakpoint return localVar; } } diff --git a/mcp_debugger_launcher/README.md b/mcp_debugger_launcher/README.md index a4159a91..11ef2fa0 100644 --- a/mcp_debugger_launcher/README.md +++ b/mcp_debugger_launcher/README.md @@ -84,6 +84,7 @@ debug-mcp-server --help - **sse**: Server-Sent Events mode for HTTP-based communication - Default port: 3001 - Custom port: Use `--port` option + - > **Deprecated:** SSE transport is deprecated in the debug-mcp-server and will be removed in a future release; the server prints a deprecation warning at startup. This launcher currently forwards only `stdio` and `sse`, so for HTTP-based transport invoke the server's `http` subcommand directly (`mcp-debugger http -p `) until the launcher adds an `http` mode. ## Troubleshooting diff --git a/mcp_debugger_launcher/mcp_debugger_launcher/cli.py b/mcp_debugger_launcher/mcp_debugger_launcher/cli.py index 3f7e0b6c..937679f2 100644 --- a/mcp_debugger_launcher/mcp_debugger_launcher/cli.py +++ b/mcp_debugger_launcher/mcp_debugger_launcher/cli.py @@ -105,7 +105,7 @@ def main(mode: str, port: Optional[int], docker: bool, npm: bool, # Check for conflicting options if docker and npm: print("❌ Error: Cannot specify both --docker and --npm", file=sys.stderr) - return 1 + sys.exit(1) # Detect available runtimes print("🚀 debug-mcp-server Launcher") @@ -119,34 +119,34 @@ def main(mode: str, port: Optional[int], docker: bool, npm: bool, if not runtimes["docker"]["available"]: print("\n❌ Error: Docker is not installed or not available", file=sys.stderr) print_installation_help() - return 1 + sys.exit(1) if "daemon not running" in (runtimes["docker"]["version"] or ""): print("\n❌ Error: Docker daemon is not running", file=sys.stderr) print(" Please start Docker Desktop and try again.", file=sys.stderr) - return 1 + sys.exit(1) runtime = "docker" elif npm: if not runtimes["nodejs"]["available"]: print("\n❌ Error: Node.js is not installed", file=sys.stderr) print_installation_help() - return 1 + sys.exit(1) if not runtimes["nodejs"]["npx_available"]: print("\n❌ Error: npx is not available", file=sys.stderr) print(" This usually comes with Node.js. Try reinstalling Node.js.", file=sys.stderr) - return 1 + sys.exit(1) runtime = "npx" else: # Auto-detect if verbose: print_runtime_status(runtimes, verbose=True) - + runtime = RuntimeDetector.get_recommended_runtime(runtimes) - + if not runtime: print("\n❌ No suitable runtime found!", file=sys.stderr) print_runtime_status(runtimes, verbose=False) print_installation_help() - return 1 + sys.exit(1) # Display what we're going to do print(f"\n🎯 Mode: {mode.upper()}") @@ -165,18 +165,18 @@ def main(mode: str, port: Optional[int], docker: bool, npm: bool, if mode == "sse" and port: cmd.extend(["--port", str(port)]) print(f"\n🔍 Would execute: {' '.join(cmd)}") - return 0 - + sys.exit(0) + # Check if we need to provide installation instructions if not runtimes["nodejs"]["package_accessible"]: print("\n📦 Package not installed locally. npx will download it automatically.") print(" This may take a moment on first run...\n") - + print("\n" + "─" * 40) print("Starting debug-mcp-server...") print("─" * 40 + "\n") - - return launcher.launch_with_npx(mode, port) + + sys.exit(launcher.launch_with_npx(mode, port)) elif runtime == "docker": if dry_run: @@ -188,15 +188,15 @@ def main(mode: str, port: Optional[int], docker: bool, npm: bool, if mode == "sse" and port: cmd.extend(["--port", str(port)]) print(f"\n🔍 Would execute: {' '.join(cmd)}") - return 0 - + sys.exit(0) + print("\n" + "─" * 40) print("Starting debug-mcp-server...") print("─" * 40 + "\n") - - return launcher.launch_with_docker(mode, port) - - return 0 + + sys.exit(launcher.launch_with_docker(mode, port)) + + sys.exit(0) if __name__ == "__main__": sys.exit(main()) diff --git a/mcp_debugger_launcher/mcp_debugger_launcher/launcher.py b/mcp_debugger_launcher/mcp_debugger_launcher/launcher.py index 30ef8c53..3b1f20c8 100644 --- a/mcp_debugger_launcher/mcp_debugger_launcher/launcher.py +++ b/mcp_debugger_launcher/mcp_debugger_launcher/launcher.py @@ -61,15 +61,12 @@ def launch_with_npx(self, mode: str = "stdio", port: Optional[int] = None) -> in except FileNotFoundError: self.log("npx command not found. Node.js may not be installed.", error=True) return 1 - except subprocess.CalledProcessError as e: - self.log(f"Process failed with error: {e}", error=True) - return e.returncode except KeyboardInterrupt: self.log("\nShutting down...") return 0 finally: self._cleanup() - + def launch_with_docker(self, mode: str = "stdio", port: Optional[int] = None) -> int: """Launch the server using Docker.""" cmd = ["docker", "run", "-it", "--rm"] diff --git a/packages/adapter-java/java/JdiDapServer.java b/packages/adapter-java/java/JdiDapServer.java index 4af21084..8147c8ee 100644 --- a/packages/adapter-java/java/JdiDapServer.java +++ b/packages/adapter-java/java/JdiDapServer.java @@ -1949,6 +1949,11 @@ private RuntimeException error(String msg) { private Value parseExpression() { return parseOr(); } + // Known limitation: this evaluator fuses parsing and evaluation, so the + // right-hand operand of || and && is always evaluated to consume its + // tokens even when Java's short-circuit semantics would skip it. Any + // side effects (e.g. method calls) in a short-circuited RHS still occur, + // unlike real Java. The final boolean result returned is still correct. private Value parseOr() { Value left = parseAnd(); while (match(TT.OR)) { @@ -1963,6 +1968,8 @@ private Value parseOr() { return left; } + // Known limitation: see parseOr() above -- the RHS of && is always + // evaluated (for side effects too) even when short-circuited. private Value parseAnd() { Value left = parseEquality(); while (match(TT.AND)) { @@ -2089,6 +2096,11 @@ private Value parsePrimary() { String text = t.text; // Strip L/l suffix text = text.substring(0, text.length() - 1); + if (text.startsWith("0x") || text.startsWith("0X")) { + return vm.mirrorOf(Long.parseUnsignedLong(text.substring(2), 16)); + } else if (text.startsWith("0b") || text.startsWith("0B")) { + return vm.mirrorOf(Long.parseUnsignedLong(text.substring(2), 2)); + } return vm.mirrorOf(Long.parseLong(text)); } case FLOAT: { diff --git a/packages/adapter-javascript/docs/README.md b/packages/adapter-javascript/docs/README.md index 36aae2e3..cd829bfd 100644 --- a/packages/adapter-javascript/docs/README.md +++ b/packages/adapter-javascript/docs/README.md @@ -6,14 +6,14 @@ Key points - ESM TypeScript project with dist/ output and type declarations - Exports `JavascriptAdapterFactory` as the entry point for dynamic loading - Full `JavascriptDebugAdapter` implementation (~760 lines) with comprehensive DAP integration -- Real utilities: `detectTsRunners`, `transformConfig`, TypeScript detection +- Real utilities: `detectTsRunners`, `determineOutFiles`, `isESMProject`, `hasTsConfigPaths`, TypeScript detection - Vendor folder for js-debug (bundled `vsDebugServer.js` with `.cjs` twin and sidecars) - Uses .js suffix on relative TS imports to match ESM resolution Status and scope - This is a fully implemented adapter supporting JavaScript and TypeScript debugging - Environment validation includes Node.js detection, vendor file verification, and optional TypeScript runner detection -- `DebugLanguage.JAVASCRIPT` is a full member of the enum (7 languages: Python, JavaScript, Rust, Go, Java, Dotnet, Mock) +- `DebugLanguage.JAVASCRIPT` is a full member of the enum (8 languages: Python, JavaScript, Ruby, Rust, Go, Java, Dotnet, Mock) Build and test - Build: pnpm -w -F @debugmcp/adapter-javascript run build @@ -32,7 +32,7 @@ Structure - src/javascript-adapter-factory.ts extends the shared BaseAdapterFactory - src/javascript-debug-adapter.ts provides full DAP integration (~760 lines) - src/utils/typescript-detector.ts — TypeScript detection and runner discovery (`detectTsRunners`) -- src/utils/config-transformer.ts — Launch configuration transformation (`transformConfig`) +- src/utils/config-transformer.ts — Launch configuration helpers (`determineOutFiles`, `isESMProject`, `hasTsConfigPaths`) - src/types/* — TypeScript types for adapter configuration Notes @@ -83,7 +83,7 @@ Expected outputs - vendor/js-debug/vsDebugServer.cjs (CommonJS twin) - vendor/js-debug/bootloader.js (required sidecar) - vendor/js-debug/hash.js (required sidecar) -- vendor/js-debug/watchdog.js (required sidecar) +- vendor/js-debug/watchdog.js (optional sidecar — copied if present) - vendor/js-debug/package.json (forces `type: 'commonjs'`) - vendor/js-debug/vsDebugServer.js.sha256 - vendor/js-debug/manifest.json (metadata: source, repo, version, asset, sha256, fetchedAt) diff --git a/packages/adapter-mock/README.md b/packages/adapter-mock/README.md index c65537fb..aa1f804f 100644 --- a/packages/adapter-mock/README.md +++ b/packages/adapter-mock/README.md @@ -33,6 +33,6 @@ import type { MockAdapterConfig } from '@debugmcp/adapter-mock'; ## Notes - Primary path resolution for the mock-adapter-process uses import.meta.url when running from the compiled package. -- A fallback path is provided for monorepo-root execution: packages/adapter-mock/dist/mock-adapter-process.js -- A third fallback is available for CJS bundle execution (resolving from the bundle's directory) +- A second fallback resolves the CJS bundle variant (mock-adapter-process.cjs) in the same directory as the compiled file, for the npx bundle scenario. +- A third fallback (used only when import.meta.url resolution throws) resolves from the current working directory: packages/adapter-mock/dist/mock-adapter-process.js, for monorepo-root execution. - The mock-adapter-process supports a `--host` flag to specify the listening address (e.g., `--host=127.0.0.1`; defaults to `localhost`) diff --git a/packages/adapter-mock/src/mock-debug-adapter.ts b/packages/adapter-mock/src/mock-debug-adapter.ts index 24f7dbb5..dd19ae8c 100644 --- a/packages/adapter-mock/src/mock-debug-adapter.ts +++ b/packages/adapter-mock/src/mock-debug-adapter.ts @@ -328,9 +328,9 @@ export class MockDebugAdapter extends EventEmitter implements IDebugAdapter { command: string, args?: unknown ): Promise { - // This will be handled by ProxyManager - // Mock adapter just validates the request is appropriate - + // Actual DAP communication is handled by ProxyManager. This mock performs + // no validation -- it only logs the request and returns an empty response. + this.dependencies.logger?.debug(`[MockDebugAdapter] DAP request: ${command}`, args); // ProxyManager will handle actual communication diff --git a/packages/adapter-ruby/src/utils/ruby-utils.ts b/packages/adapter-ruby/src/utils/ruby-utils.ts index 4d473b4b..2f1f8632 100644 --- a/packages/adapter-ruby/src/utils/ruby-utils.ts +++ b/packages/adapter-ruby/src/utils/ruby-utils.ts @@ -204,7 +204,8 @@ export async function getRubyVersion(rubyPath: string): Promise { child.stderr?.on('data', (data) => { output += data.toString(); }); child.on('error', () => resolve(null)); - child.on('exit', (code) => { + // 'close' (not 'exit') so stdio is fully drained before we read `output` + child.on('close', (code) => { if (code !== 0 || output.length === 0) { resolve(null); return; @@ -232,7 +233,8 @@ export async function getRdbgVersion(rdbgPath: string): Promise { child.stderr?.on('data', (data) => { output += data.toString(); }); child.on('error', () => resolve(null)); - child.on('exit', (code) => { + // 'close' (not 'exit') so stdio is fully drained before we read `output` + child.on('close', (code) => { if (code !== 0 || output.length === 0) { resolve(null); return; diff --git a/packages/adapter-rust/src/rust-debug-adapter.ts b/packages/adapter-rust/src/rust-debug-adapter.ts index 391350db..a33f6bf1 100644 --- a/packages/adapter-rust/src/rust-debug-adapter.ts +++ b/packages/adapter-rust/src/rust-debug-adapter.ts @@ -886,9 +886,9 @@ export class RustDebugAdapter extends EventEmitter implements IDebugAdapter { } else if (rustConfig.cargo.test) { binaryName = rustConfig.cargo.test; } else { - // Try to find default binary - binaryName = 'main'; // Will need to be resolved from Cargo.toml - this.dependencies.logger?.warn('[RustDebugAdapter] No binary specified, defaulting to "main"'); + // Resolve default binary from Cargo.toml (mirrors the .rs branch above) + const { getDefaultBinary } = await import('./utils/cargo-utils.js'); + binaryName = await getDefaultBinary(rustConfig.cwd || process.cwd()); } const extension = this.platform === 'win32' ? '.exe' : ''; diff --git a/packages/mcp-debugger/README.md b/packages/mcp-debugger/README.md index 54f76a68..5c720c78 100644 --- a/packages/mcp-debugger/README.md +++ b/packages/mcp-debugger/README.md @@ -24,10 +24,18 @@ mcp-debugger stdio ``` ### SSE mode + +> **Deprecated:** SSE transport is deprecated and will be removed in a future release. Use `mcp-debugger http --port 3001` instead. + ```bash mcp-debugger sse --port 3001 ``` +### HTTP mode (recommended) +```bash +mcp-debugger http --port 3001 +``` + ## Batteries-Included Adapters All language adapters are bundled into the CLI package. No separate installation is needed. The following adapters are included: @@ -57,8 +65,8 @@ Analyzes a Rust executable to determine whether it was built with the GNU or MSV - `--log-level ` - Set log level (error, warn, info, debug) - `--log-file ` - Log to file instead of console -### SSE-only options -- `-p, --port ` - Port for SSE mode (default: 3001) +### SSE and HTTP options +- `-p, --port ` - Port for SSE or HTTP mode (default: 3001) ## Documentation diff --git a/packages/shared/README.md b/packages/shared/README.md index 9250deed..e83b237e 100644 --- a/packages/shared/README.md +++ b/packages/shared/README.md @@ -97,6 +97,7 @@ Everything below is exported from the package root (`import { ... } from '@debug | `CommandHandling` | type | How the adapter handles launch commands | | `DefaultAdapterPolicy` | class | Lightweight default/placeholder policy | | `PythonAdapterPolicy` | class | Python/debugpy policy | +| `RubyAdapterPolicy` | class | Ruby/rdbg adapter policy | | `JsDebugAdapterPolicy` | class | JavaScript/js-debug policy | | `RustAdapterPolicy` | class | Rust/CodeLLDB policy | | `RustAdapterPolicyInterface` | type | Rust policy interface (for mocking) | @@ -104,6 +105,7 @@ Everything below is exported from the package root (`import { ... } from '@debug | `JavaAdapterPolicy` | class | Java/JDI bridge policy | | `DotnetAdapterPolicy` | class | .NET/netcoredbg policy | | `MockAdapterPolicy` | class | Mock adapter policy for testing | +| `getPolicyForLanguage` | function | Dispatch: returns the `AdapterPolicy` for a given `DebugLanguage` | ### DAP Client Behavior @@ -119,7 +121,7 @@ Everything below is exported from the package root (`import { ... } from '@debug | Export | Kind | Description | |--------|------|-------------| -| `DebugLanguage` | enum | Supported languages (Python, JavaScript, Rust, Go, Java, Dotnet, Mock) | +| `DebugLanguage` | enum | Supported languages (Python, Ruby, JavaScript, Rust, Go, Java, Dotnet, Mock) | | `SessionState` | enum | Session states (CREATED → READY → RUNNING ⇄ PAUSED → STOPPED) | | `SessionLifecycleState` | enum | Coarse lifecycle (CREATED → ACTIVE → TERMINATED) | | `ExecutionState` | enum | Fine-grained execution state | @@ -175,6 +177,17 @@ Everything below is exported from the package root (`import { ... } from '@debug |--------|--------|-------------| | `DebugProtocol` | `@vscode/debugprotocol` | VSCode Debug Adapter Protocol type namespace | +### Logging-Safety Utilities + +| Export | Kind | Description | +|--------|------|-------------| +| `sanitizeEnvForLogging` | function | Sanitize a child-process environment map before logging | +| `sanitizePayloadForLogging` | function | Sanitize an arbitrary payload before logging | +| `sanitizeStderr` | function | Sanitize a stderr chunk before it reaches logs or tool errors | +| `sanitizeStderrTail` | function | Sanitize the trailing tail of stderr output before it reaches logs or tool errors | + +These helpers ensure unsanitized child-process output and environment data never reach logs or tool error responses. + ## Package Structure ``` diff --git a/packages/shared/src/interfaces/adapter-policy-js.ts b/packages/shared/src/interfaces/adapter-policy-js.ts index 13df8862..86131918 100644 --- a/packages/shared/src/interfaces/adapter-policy-js.ts +++ b/packages/shared/src/interfaces/adapter-policy-js.ts @@ -5,6 +5,7 @@ * generic DAP flow in core code. */ import type { DebugProtocol } from '@vscode/debugprotocol'; +import * as path from 'path'; import type { AdapterPolicy, AdapterSpecificState, CommandHandling } from './adapter-policy.js'; import { SessionState } from '@debugmcp/shared'; import type { StackFrame, Variable } from '../models/index.js'; @@ -376,8 +377,6 @@ export const JsDebugAdapterPolicy: AdapterPolicy = { } else { // LAUNCH flow (default for MCP). Use adapter-configured policy; do not add parent attach-by-port afterward try { - // Dynamic import to avoid circular dependency issues - const path = await import('path'); if (typeof baseLaunchConfig.program !== 'string' || !baseLaunchConfig.program.length) { baseLaunchConfig.program = scriptPath; } diff --git a/scripts/setup/windows-rust-debug.ps1 b/scripts/setup/windows-rust-debug.ps1 index e76ee55c..d837636b 100644 --- a/scripts/setup/windows-rust-debug.ps1 +++ b/scripts/setup/windows-rust-debug.ps1 @@ -8,11 +8,12 @@ bundled Rust examples, and optionally runs the Rust smoke test suite. .PARAMETER UpdateUserPath - When supplied, the script permanently appends rustup's self-contained GNU bin - directory (which hosts dlltool.exe and ld.exe) to the current user's PATH and - sets the DLLTOOL user environment variable. Without this switch, the script - only amends the PATH for the current PowerShell session and prints manual - instructions for the user. + When supplied, the script permanently appends the directory containing the + preferred dlltool.exe -- the MSYS2 MinGW bin directory when the MSYS2/MinGW-w64 + toolchain could be provisioned, otherwise rustup's self-contained GNU bin + directory -- to the current user's PATH, and sets the DLLTOOL user environment + variable. Without this switch, the script only amends the PATH for the current + PowerShell session and prints manual instructions for the user. .PARAMETER SkipBuild Skip building the Rust examples. Useful when just validating dependencies. diff --git a/scripts/sync-to-wsl.sh b/scripts/sync-to-wsl.sh index 81dc3b7f..7d09f1f4 100644 --- a/scripts/sync-to-wsl.sh +++ b/scripts/sync-to-wsl.sh @@ -114,23 +114,15 @@ echo -e "${GREEN}Fixing file permissions...${NC}" # Make shell scripts executable chmod +x scripts/*.sh 2>/dev/null || true -# 8. Check if package-lock.json exists, if not we need to generate it -if [ ! -f "package-lock.json" ]; then - echo -e "${YELLOW}package-lock.json not found (excluded from sync for speed)${NC}" - if [ "$NO_INSTALL" = false ]; then - echo -e "${GREEN}Will be regenerated during npm install${NC}" - fi -fi - -# 9. Install dependencies (unless --no-install) +# 8. Install dependencies (unless --no-install) if [ "$NO_INSTALL" = false ]; then - echo -e "${GREEN}Installing npm dependencies...${NC}" + echo -e "${GREEN}Installing dependencies via pnpm...${NC}" pnpm install --frozen-lockfile else - echo -e "${YELLOW}Skipping npm install (--no-install flag set)${NC}" + echo -e "${YELLOW}Skipping pnpm install (--no-install flag set)${NC}" fi -# 10. Build project (unless --no-build) +# 9. Build project (unless --no-build) if [ "$NO_BUILD" = false ]; then echo -e "${GREEN}Building project...${NC}" npm run build diff --git a/scripts/test-mcp-integration.sh b/scripts/test-mcp-integration.sh index 608a0f05..d0d89c55 100755 --- a/scripts/test-mcp-integration.sh +++ b/scripts/test-mcp-integration.sh @@ -76,6 +76,7 @@ if echo "$output" | python3 -m json.tool > /dev/null 2>&1; then else echo -e "${YELLOW}⚠ WARNING${NC}" echo " Auto-detection may not be working, explicit stdio argument required" + TESTS_FAILED=$((TESTS_FAILED + 1)) fi # Test 5: Claude CLI integration diff --git a/tests/README.md b/tests/README.md index 833613d7..2bf0e3f5 100644 --- a/tests/README.md +++ b/tests/README.md @@ -94,7 +94,7 @@ tests/ │ └── utils/ # Type guards, session migration │ ├── e2e/ # End-to-end tests -│ ├── mcp-server-smoke-*.ts # Per-language smoke tests (python, javascript, rust, go, java, dotnet) +│ ├── mcp-server-smoke-*.ts # Per-language smoke tests (python, javascript, ruby, rust, go, java, dotnet) │ ├── docker/ # Docker container tests (python, javascript, rust, entrypoint) │ └── npx/ # NPX distribution tests (python, javascript) │ diff --git a/tests/core/unit/server/server-inspection-tools.test.ts b/tests/core/unit/server/server-inspection-tools.test.ts index 75f81c53..9bf782be 100644 --- a/tests/core/unit/server/server-inspection-tools.test.ts +++ b/tests/core/unit/server/server-inspection-tools.test.ts @@ -171,7 +171,8 @@ describe('Server Inspection Tools Tests', () => { }; } - // The server now returns a success response with error message instead of throwing + // An unknown session makes validateSession throw McpError('Session not found'); the try/catch above + // normalizes that thrown error into the success:false shape asserted below. const content = JSON.parse(result.content[0].text); expect(content.success).toBe(false); expect(content.error).toContain('Session not found: test-session'); diff --git a/tests/core/unit/server/server-language-discovery.test.ts b/tests/core/unit/server/server-language-discovery.test.ts index 0f19c898..3c31f592 100644 --- a/tests/core/unit/server/server-language-discovery.test.ts +++ b/tests/core/unit/server/server-language-discovery.test.ts @@ -422,7 +422,7 @@ describe('Server Language Discovery Tests', () => { expect(result.content[0].type).toBe('text'); const content = JSON.parse(result.content[0].text); - // Success should be false since mock doesn't actually start debugging + // startDebugging is mocked to resolve successfully; assert only that the response carries a success field. expect(content.success).toBeDefined(); }); @@ -452,7 +452,7 @@ describe('Server Language Discovery Tests', () => { expect(result.content[0].type).toBe('text'); const content = JSON.parse(result.content[0].text); - // Success should be false since mock doesn't actually start debugging + // startDebugging is mocked to resolve successfully; assert only that the response carries a success field. expect(content.success).toBeDefined(); }); }); diff --git a/tests/core/unit/session/session-manager-test-utils.ts b/tests/core/unit/session/session-manager-test-utils.ts index 0e12689a..d9c714c4 100644 --- a/tests/core/unit/session/session-manager-test-utils.ts +++ b/tests/core/unit/session/session-manager-test-utils.ts @@ -16,25 +16,6 @@ import { createMockFileSystem, createMockLogger } from '../../../test-utils/help import { IAdapterRegistry } from '@debugmcp/shared'; import { createMockAdapterRegistry as createCentralizedMockAdapterRegistry } from '../../../test-utils/mocks/mock-adapter-registry.js'; -// Mock modules that SessionManager may import transitively during tests -vi.mock('./dist/implementations/index.js', () => ({ - FileSystemImpl: vi.fn(), - ProcessManagerImpl: vi.fn(), - NetworkManagerImpl: vi.fn(), - ProxyProcessLauncherImpl: vi.fn(), -})); - -vi.mock('./dist/proxy/proxy-manager.js', () => ({ - ProxyManager: vi.fn().mockImplementation(function() { return ({ - on: vi.fn(), - start: vi.fn().mockResolvedValue(undefined), - stop: vi.fn().mockResolvedValue(undefined), - sendDapRequest: vi.fn().mockResolvedValue({ success: true }), - isRunning: vi.fn().mockReturnValue(false), - getCurrentThreadId: vi.fn().mockReturnValue(null), - }); }), -})); - /** * Create a mock environment for testing */ diff --git a/tests/e2e/README.md b/tests/e2e/README.md index b9e1b156..39fb2879 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -156,7 +156,7 @@ npx vitest run tests/e2e/npx/ # NPX smoke tests The smoke tests provide comprehensive coverage of: 1. **Transport Methods**: stdio, SSE, JavaScript-SSE, containerized stdio -2. **Language Adapters**: All 7 adapters (Python, JavaScript, Rust, Go, Java, .NET/C#, Mock) +2. **Language Adapters**: All 8 adapters (Python, JavaScript, Rust, Go, Java, .NET/C#, Ruby, Mock) 3. **Path Resolution**: Different working directories, path translation, absolute vs relative paths 4. **Environment Handling**: Container environment variables, volume mounts 5. **Error Scenarios**: Proper cleanup on failure, detailed error logging diff --git a/tests/e2e/comprehensive-mcp-tools.test.ts b/tests/e2e/comprehensive-mcp-tools.test.ts index 3bd965e4..f6c0b4eb 100644 --- a/tests/e2e/comprehensive-mcp-tools.test.ts +++ b/tests/e2e/comprehensive-mcp-tools.test.ts @@ -413,7 +413,7 @@ describe(`Comprehensive MCP Debugger Test — 20 Tools × ${LANGUAGES.length} La if (lang.language !== 'mock') { // Real language: full debug workflow - it(`Tools 6-15,19: full debug workflow (${lang.language})`, async () => { + it(`Tools 6-15,20: full debug workflow (${lang.language})`, async () => { // Create fresh session const createRes = await mcpClient!.callTool({ name: 'create_debug_session', @@ -569,7 +569,7 @@ describe(`Comprehensive MCP Debugger Test — 20 Tools × ${LANGUAGES.length} La const stepRes = await callToolSafely(mcpClient!, 'step_out', { sessionId: currentSessionId }); await new Promise(r => setTimeout(r, 2000)); // step_out may fail if we're at the top frame — that's acceptable - const ok = stepRes.success !== false || (stepRes.message && !stepRes.message.includes('error')); + const ok = stepRes.success !== false || (!!stepRes.message && !stepRes.message.includes('error')); record('step_out', lang.language, ok ? 'PASS' : 'FAIL', `success=${stepRes.success}, msg=${(stepRes.message ?? '').slice(0, 100)}`, Date.now() - t0); diff --git a/tests/e2e/docker/docker-smoke-javascript.test.ts b/tests/e2e/docker/docker-smoke-javascript.test.ts index 6097e662..472fd885 100644 --- a/tests/e2e/docker/docker-smoke-javascript.test.ts +++ b/tests/e2e/docker/docker-smoke-javascript.test.ts @@ -26,6 +26,13 @@ describe.skipIf(SKIP_DOCKER)('Docker: JavaScript Debugging Smoke Tests', () => { let cleanup: (() => Promise) | null = null; let sessionId: string | null = null; let containerName: string | null = null; + let anyFailed = false; + + afterEach((ctx) => { + if (ctx.task.result?.state === 'fail') { + anyFailed = true; + } + }); beforeAll(async () => { console.log('[Docker JS] Building Docker image...'); @@ -62,7 +69,7 @@ describe.skipIf(SKIP_DOCKER)('Docker: JavaScript Debugging Smoke Tests', () => { } // Print Docker logs for debugging if test failed - if (containerName && process.env.VITEST_FAILED) { + if (containerName && anyFailed) { console.log('[Docker JS] Container logs:'); const logs = await getDockerLogs(containerName); console.log(logs); diff --git a/tests/e2e/docker/docker-smoke-python.test.ts b/tests/e2e/docker/docker-smoke-python.test.ts index 383320cc..fab1b513 100644 --- a/tests/e2e/docker/docker-smoke-python.test.ts +++ b/tests/e2e/docker/docker-smoke-python.test.ts @@ -23,6 +23,13 @@ describe.skipIf(SKIP_DOCKER)('Docker: Python Debugging Smoke Tests', () => { let cleanup: (() => Promise) | null = null; let sessionId: string | null = null; let containerName: string | null = null; + let anyFailed = false; + + afterEach((ctx) => { + if (ctx.task.result?.state === 'fail') { + anyFailed = true; + } + }); beforeAll(async () => { console.log('[Docker Python] Building Docker image...'); @@ -59,7 +66,7 @@ describe.skipIf(SKIP_DOCKER)('Docker: Python Debugging Smoke Tests', () => { } // Print Docker logs for debugging if test failed - if (containerName && process.env.VITEST_FAILED) { + if (containerName && anyFailed) { console.log('[Docker Python] Container logs:'); const logs = await getDockerLogs(containerName); console.log(logs); diff --git a/tests/e2e/mcp-server-smoke-java-redefine.test.ts b/tests/e2e/mcp-server-smoke-java-redefine.test.ts index 3f4a97ea..f9a65d3f 100644 --- a/tests/e2e/mcp-server-smoke-java-redefine.test.ts +++ b/tests/e2e/mcp-server-smoke-java-redefine.test.ts @@ -7,7 +7,7 @@ * 3. Hitting a breakpoint, verifying getValue() == 42 * 4. Recompiling with RedefineTargetV2 (getValue() returns 99) * 5. Calling redefine_classes to hot-swap the class - * 6. Continuing, hitting second breakpoint, verifying getValue() == 99 + * 6. Evaluating getValue() at the same breakpoint after hot-swap, verifying result == 99 * * Prerequisites: JDK installed (java + javac on PATH) */ diff --git a/tests/e2e/mcp-server-smoke-sse.test.ts b/tests/e2e/mcp-server-smoke-sse.test.ts index 0148abc5..a1bce3fe 100644 --- a/tests/e2e/mcp-server-smoke-sse.test.ts +++ b/tests/e2e/mcp-server-smoke-sse.test.ts @@ -60,7 +60,7 @@ describe('MCP Server E2E SSE Smoke Test', () => { }); // If still not killed, use SIGKILL - if (proc && !proc.killed) { + if (proc && proc.exitCode === null) { console.log('[SSE Smoke Test] Graceful shutdown failed, using SIGKILL...'); proc.kill('SIGKILL'); } else { diff --git a/tests/e2e/npx/README.md b/tests/e2e/npx/README.md index 11ae0f19..319182ca 100644 --- a/tests/e2e/npx/README.md +++ b/tests/e2e/npx/README.md @@ -4,7 +4,7 @@ Tests for the packaged npm distribution artifact intended for npx/npm-exec usage ## What These Tests Do -1. **Build the project** - Runs `pnpm build` +1. **Verify build artifacts** - The tests do not build. `ensureWorkspaceBuilt()` checks that the root and mcp-debugger package `dist` output already exist (the `pretest:e2e:npx` hook runs `pnpm build` once before the suite) and throws `Workspace build output missing` if either is absent. 2. **Create npm package** - Runs `npm pack` to create tarball 3. **Install globally** - Installs the package from tarball 4. **Test via global install** - Runs the MCP server using global installation and direct Node.js invocation (not npx) diff --git a/tests/smoke/QUIRKS_SUMMARY.md b/tests/smoke/QUIRKS_SUMMARY.md index 54afc0a4..c480eef0 100644 --- a/tests/smoke/QUIRKS_SUMMARY.md +++ b/tests/smoke/QUIRKS_SUMMARY.md @@ -98,7 +98,7 @@ const newVarsResult = await callToolSafely(mcpClient!, 'get_variables', { #### Python Path Resolution ```typescript // Always use absolute paths for Python: -const scriptPath = path.resolve(ROOT, 'test-scripts', 'test_python_debug.py'); +const scriptPath = path.resolve(ROOT, 'examples', 'python', 'test_python_debug.py'); ``` #### Quirk-Aware Assertions diff --git a/tests/stress/cross-transport-parity.test.ts b/tests/stress/cross-transport-parity.test.ts index e1a3b32d..d54adb20 100644 --- a/tests/stress/cross-transport-parity.test.ts +++ b/tests/stress/cross-transport-parity.test.ts @@ -81,6 +81,19 @@ class TransportTester { clearTimeout(timeout); reject(err); }); + + // If the child exits before becoming ready (e.g. EADDRINUSE on a port + // collision), surface the real failure immediately instead of waiting + // out the full startup timeout. + let stderrBuf = ''; + this.sseServer.stderr?.on('data', (d) => { stderrBuf += d.toString(); }); + this.sseServer.on('exit', (code) => { + if (code !== null && code !== 0) { + clearInterval(checkReady); + clearTimeout(timeout); + reject(new Error(`SSE server exited (code ${code}) before ready: ${stderrBuf.trim()}`)); + } + }); }); } diff --git a/tests/test-utils/helpers/test-coverage-summary.js b/tests/test-utils/helpers/test-coverage-summary.js index d40dfb84..e34c1388 100644 --- a/tests/test-utils/helpers/test-coverage-summary.js +++ b/tests/test-utils/helpers/test-coverage-summary.js @@ -113,7 +113,7 @@ async function testCoverageSummary() { fs.unlinkSync(jsonFile); } - process.exit(exitCode); + process.exit(Math.max(childExitCode ?? 0, exitCode)); } catch (error) { console.error('Error reading results:', error.message); diff --git a/tests/test-utils/helpers/test-results-analyzer.js b/tests/test-utils/helpers/test-results-analyzer.js index 59daa91e..6e66668d 100644 --- a/tests/test-utils/helpers/test-results-analyzer.js +++ b/tests/test-utils/helpers/test-results-analyzer.js @@ -73,7 +73,7 @@ class TestResultsAnalyzer { // Coverage summary if available if (results.coverageMap) { console.log('\nCoverage Summary:'); - console.log(' (Run with --level=detailed for coverage breakdown)'); + console.log(' (Detailed coverage breakdown not implemented)'); } } diff --git a/tests/unit/implementations/process-manager-impl.test.ts b/tests/unit/implementations/process-manager-impl.test.ts index bcf97c7c..514678b2 100644 --- a/tests/unit/implementations/process-manager-impl.test.ts +++ b/tests/unit/implementations/process-manager-impl.test.ts @@ -1,5 +1,7 @@ /** - * Tests edge case handling for different return types from the promisified exec function. + * Tests ProcessManagerImpl: spawn delegation to child_process.spawn (command/args/options, + * default-args, and spawn-error propagation) plus exec return-type edge-case handling for the + * promisified exec function. */ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';