feat(adapter): implement MCP query adapter with get_impact, list_repos, and find_dead_code (Stories 3.1-3.2)
- Add memtrace-adapter.mjs: standalone Node.js MCP client over stdio with 10000ms timeout, MEMTRACE_MCP_ERROR_TIMEOUT token, structured JSON output matching qa-memtrace.mjs contract - Support --query get_impact (blast radius), --query find_dead_code (dead code detection with repo_id + file_path scoping), --query list_repos (index freshness) - Add 14 automated tests: 9 CLI arg handling + 5 MCP integration - Update dev-story SKILL.md step 5 to use adapter for blast radius and dead-code queries, with empty-results skip for pitfall validation - Update quick-dev step-03-implement and step-oneshot workflows - Extend code-review acceptance auditors with adapter usage verification and dead-code adapter check - Fix raw memtrace_get_impact reference in SKILL.md (review patch)
This commit is contained in:
parent
7a9d66364a
commit
ffb430ed99
|
|
@ -37,6 +37,12 @@ failed_layers: '' # set at runtime: comma-separated list of layers that failed o
|
||||||
> - **If the spec file contains a blast radius report AND a Test Coverage Justification BUT no "Mathematical Quality Gate Output"**: this is a Phase 1-level story using only textual justification. Flag as `patch` (not `decision_needed`): "Phase 1 story — consider upgrading to mathematical quality gate via qa-memtrace.mjs."
|
> - **If the spec file contains a blast radius report AND a Test Coverage Justification BUT no "Mathematical Quality Gate Output"**: this is a Phase 1-level story using only textual justification. Flag as `patch` (not `decision_needed`): "Phase 1 story — consider upgrading to mathematical quality gate via qa-memtrace.mjs."
|
||||||
> - **If the spec file contains only a blast radius report (no test justification, no mathematical gate)**: raise a `decision_needed` finding: "Missing both Test Coverage Justification and Mathematical Quality Gate Output — Phase 2 story must include qa-memtrace.mjs execution results."
|
> - **If the spec file contains only a blast radius report (no test justification, no mathematical gate)**: raise a `decision_needed` finding: "Missing both Test Coverage Justification and Mathematical Quality Gate Output — Phase 2 story must include qa-memtrace.mjs execution results."
|
||||||
>
|
>
|
||||||
|
> **Quality Gate — Adapter Usage Verification:** Check whether the spec/story file records indicate use of the `memtrace-adapter.mjs` wrapper (rather than raw MCP tool calls) for blast radius and availability queries. Apply these rules:
|
||||||
|
> - **If the spec/story file's Dev Agent Record or diff commentary references `memtrace-adapter.mjs`** for blast radius queries (`--query get_impact`) and availability checks (`--query list_repos`) → pass: adapter usage confirmed.
|
||||||
|
> - **If the spec/story file shows `memtrace_get_impact` or `list_indexed_repositories` being called directly** (without the adapter wrapper) for the blast radius step → raise a `patch` finding: "Direct MCP call detected — blast radius step should use `memtrace-adapter.mjs` instead of raw `memtrace_get_impact` or `list_indexed_repositories` for consistent timeout handling and error token emission."
|
||||||
|
> - **If the blast radius step is absent or the story doesn't involve code modification (new-file-only stories)** → skip this gate.
|
||||||
|
> - **If the spec/story file involves dead-code queries (`find_dead_code`)**: check whether the adapter was used (`--query find_dead_code`) rather than raw `memtrace_find_dead_code` MCP calls. If `memtrace_find_dead_code` was called directly without the adapter → raise a `patch` finding: "Direct MCP call detected — dead-code query should use `memtrace-adapter.mjs --query find_dead_code` instead of raw `memtrace_find_dead_code` for consistent timeout handling and error token emission."
|
||||||
|
>
|
||||||
> **Quality Gate — Dead Code Pitfall Validation:** Check whether the spec/story file includes a "Dead Code Pitfall Validation Report" section (JSON output from `validate-dead-code.mjs`). Apply these rules:
|
> **Quality Gate — Dead Code Pitfall Validation:** Check whether the spec/story file includes a "Dead Code Pitfall Validation Report" section (JSON output from `validate-dead-code.mjs`). Apply these rules:
|
||||||
> - **If the spec file contains "Dead Code Pitfall Validation Report"**: verify that the `suspects` list entries were addressed in the implementation (check if corresponding tasks exist in Tasks/Subtasks, or if deleted files match suspect entries). If SUSPECT entries were not addressed → raise a `decision_needed` finding per unaddressed suspect: "SUSPECT dead-code entry not addressed: [name] in [file] — pitfall validation flagged this as truly dead code but no removal task was completed."
|
> - **If the spec file contains "Dead Code Pitfall Validation Report"**: verify that the `suspects` list entries were addressed in the implementation (check if corresponding tasks exist in Tasks/Subtasks, or if deleted files match suspect entries). If SUSPECT entries were not addressed → raise a `decision_needed` finding per unaddressed suspect: "SUSPECT dead-code entry not addressed: [name] in [file] — pitfall validation flagged this as truly dead code but no removal task was completed."
|
||||||
> - **If the story involves dead-code removal (find_dead_code, dead-code in tasks) BUT no "Dead Code Pitfall Validation Report" exists in the spec file** → raise a `patch` finding: "Missing Dead Code Pitfall Validation Report — story involved dead-code removal but no pitfall validation was performed via validate-dead-code.mjs."
|
> - **If the story involves dead-code removal (find_dead_code, dead-code in tasks) BUT no "Dead Code Pitfall Validation Report" exists in the spec file** → raise a `patch` finding: "Missing Dead Code Pitfall Validation Report — story involved dead-code removal but no pitfall validation was performed via validate-dead-code.mjs."
|
||||||
|
|
|
||||||
|
|
@ -301,15 +301,15 @@ Activation is complete. Begin the workflow below.
|
||||||
<action>Extract target symbols and files from the story's Dev Notes and Tasks/Subtasks sections. Identify every file that this story will modify or create.</action>
|
<action>Extract target symbols and files from the story's Dev Notes and Tasks/Subtasks sections. Identify every file that this story will modify or create.</action>
|
||||||
<action if="no explicit targets found in story Dev Notes">ASK user: "Which symbols or files are you modifying? The story doesn't specify explicit targets for blast radius analysis."</action>
|
<action if="no explicit targets found in story Dev Notes">ASK user: "Which symbols or files are you modifying? The story doesn't specify explicit targets for blast radius analysis."</action>
|
||||||
|
|
||||||
<!-- Memtrace availability check -->
|
<!-- Memtrace availability check via adapter -->
|
||||||
<action>Verify Memtrace MCP tools are available. Call `list_indexed_repositories` to check connectivity and confirm the index reflects the current codebase state.</action>
|
<action>Verify Memtrace MCP tools are available by calling the adapter: `node _bmad/scripts/memtrace/memtrace-adapter.mjs --query list_repos`. If exit 0 — Memtrace is reachable; parse the STDOUT JSON for repository list and index freshness timestamps. If exit 1 — Memtrace is unavailable.</action>
|
||||||
<check if="Memtrace MCP tools are NOT available or return errors">
|
<check if="Memtrace adapter exit code is 1 (MCP server not reachable or timeout)">
|
||||||
<action>HALT: "Memtrace MCP server is not available. Structural blast radius verification cannot be performed. Please start the Memtrace server or explicitly override this safety check."</action>
|
<action>HALT: "Memtrace MCP server is not available. Structural blast radius verification cannot be performed. Please start the Memtrace server or explicitly override this safety check."</action>
|
||||||
</check>
|
</check>
|
||||||
|
|
||||||
<!-- Calculate blast radius -->
|
<!-- Calculate blast radius via adapter -->
|
||||||
<action>For each target symbol, call `memtrace_get_impact` with the symbol as the target. Process targets SEQUENTIALLY using `for...of` with `await` — NEVER use `Promise.all`.</action>
|
<action>For each target symbol, call the memtrace-adapter: `node _bmad/scripts/memtrace/memtrace-adapter.mjs --target <symbol> --query get_impact`. Process targets SEQUENTIALLY using `for...of` with `await` — NEVER use `Promise.all`. If the adapter exits with code 1 → HALT with timeout/unavailable message.</action>
|
||||||
<action>Collate the results: extract `risk_level`, `affected_symbols`, `affected_files` from each response.</action>
|
<action>Parse the adapter's STDOUT JSON for each target. Extract `risk_level`, `affected_symbols` (array of {name, file, depth}), and `affected_files` from the JSON output.</action>
|
||||||
|
|
||||||
<!-- Summarize to stay under token budget -->
|
<!-- Summarize to stay under token budget -->
|
||||||
<action>Apply hierarchical summarization to keep the final report under 2000 tokens:
|
<action>Apply hierarchical summarization to keep the final report under 2000 tokens:
|
||||||
|
|
@ -376,7 +376,7 @@ Activation is complete. Begin the workflow below.
|
||||||
- `module-C`: Partial coverage — `test/module-c.test.ts` covers 3 of 5 impacted functions
|
- `module-C`: Partial coverage — `test/module-c.test.ts` covers 3 of 5 impacted functions
|
||||||
</action>
|
</action>
|
||||||
<action>Enforce the combined token budget: the blast radius report + test coverage justification together must not exceed 2000 tokens. If the combined output exceeds this limit, prioritize: (1) uncovered modules, (2) high-risk modules, (3) covered modules. Keep the table concise (one line per module).</action>
|
<action>Enforce the combined token budget: the blast radius report + test coverage justification together must not exceed 2000 tokens. If the combined output exceeds this limit, prioritize: (1) uncovered modules, (2) high-risk modules, (3) covered modules. Keep the table concise (one line per module).</action>
|
||||||
<action>If the blast radius report has zero affected modules (empty result from `memtrace_get_impact`), skip the Test Coverage Justification and append a note: "No affected modules to map — blast radius is empty."</action>
|
<action>If the blast radius report has zero affected modules (empty result from `memtrace-adapter.mjs --query get_impact`), skip the Test Coverage Justification and append a note: "No affected modules to map — blast radius is empty."</action>
|
||||||
<action>Ask the user for their coverage threshold: "Review threshold: block if N% of modules are uncovered? (0 = never block, 100 = block if any uncovered)":</action>
|
<action>Ask the user for their coverage threshold: "Review threshold: block if N% of modules are uncovered? (0 = never block, 100 = block if any uncovered)":</action>
|
||||||
<check if="user provides a percentage threshold">
|
<check if="user provides a percentage threshold">
|
||||||
<action>Store the threshold. During the code-review phase, the Acceptance Auditor will block changes exceeding this threshold.</action>
|
<action>Store the threshold. During the code-review phase, the Acceptance Auditor will block changes exceeding this threshold.</action>
|
||||||
|
|
@ -426,7 +426,11 @@ Activation is complete. Begin the workflow below.
|
||||||
<critical>If the story involves dead-code removal, validate candidates against pitfalls-catalog.json before proceeding.</critical>
|
<critical>If the story involves dead-code removal, validate candidates against pitfalls-catalog.json before proceeding.</critical>
|
||||||
|
|
||||||
<check if="story involves dead-code removal (look for find_dead_code usage in story context or tasks)">
|
<check if="story involves dead-code removal (look for find_dead_code usage in story context or tasks)">
|
||||||
<action>Call `memtrace_find_dead_code` via MCP to retrieve dead-code candidates for the target module(s). Process sequentially using `for...of` with `await`.</action>
|
<action>For each target module, call the memtrace-adapter: `node _bmad/scripts/memtrace/memtrace-adapter.mjs --target <module_path> --query find_dead_code [--repo <repo_id>]`. Process sequentially using `for...of` with `await`. Parse the adapter's STDOUT JSON for the `symbols` array.</action>
|
||||||
|
<check if="symbols array is empty (total_count === 0)">
|
||||||
|
<output>✅ No dead-code candidates found in target module. Pitfall validation skipped.</output>
|
||||||
|
<goto anchor="dead_code_done" />
|
||||||
|
</check>
|
||||||
<action>Serialize the dead-code candidates to a temporary JSON file in the system temp directory.</action>
|
<action>Serialize the dead-code candidates to a temporary JSON file in the system temp directory.</action>
|
||||||
<action>Execute: `node _bmad/scripts/memtrace/validate-dead-code.mjs --candidates <temp-file>`</action>
|
<action>Execute: `node _bmad/scripts/memtrace/validate-dead-code.mjs --candidates <temp-file>`</action>
|
||||||
<action>Read the script's STDOUT (JSON output) and capture the exit code.</action>
|
<action>Read the script's STDOUT (JSON output) and capture the exit code.</action>
|
||||||
|
|
@ -477,6 +481,8 @@ Activation is complete. Begin the workflow below.
|
||||||
<action>Clean up temporary JSON files.</action>
|
<action>Clean up temporary JSON files.</action>
|
||||||
</check>
|
</check>
|
||||||
|
|
||||||
|
<anchor id="dead_code_done" />
|
||||||
|
|
||||||
<check if="story does NOT involve dead-code removal">
|
<check if="story does NOT involve dead-code removal">
|
||||||
<action>Skip this substep entirely.</action>
|
<action>Skip this substep entirely.</action>
|
||||||
</check>
|
</check>
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,9 @@ Verify `{spec_file}` resolves to a non-empty path and the file exists on disk. I
|
||||||
|
|
||||||
1. **Identify targets**: Extract the symbols and files being modified from `{spec_file}` — the `## Code Map` section lists target files and their roles.
|
1. **Identify targets**: Extract the symbols and files being modified from `{spec_file}` — the `## Code Map` section lists target files and their roles.
|
||||||
|
|
||||||
2. **Verify Memtrace availability**: Check if Memtrace MCP tools are reachable (call `list_indexed_repositories` or equivalent). If unavailable, HALT: "Memtrace MCP server is not available. Structural blast radius verification cannot be performed. Please start the Memtrace server or explicitly override this safety check."
|
2. **Verify Memtrace availability**: Check if the Memtrace MCP server is reachable by calling the adapter: `node _bmad/scripts/memtrace/memtrace-adapter.mjs --query list_repos`. If exit 0 — server is reachable (parse STDOUT JSON for repository list). If exit 1 — HALT: "Memtrace MCP server is not available. Structural blast radius verification cannot be performed. Please start the Memtrace server or explicitly override this safety check."
|
||||||
|
|
||||||
3. **Calculate blast radius**: For each target symbol, call `memtrace_get_impact`. Process targets SEQUENTIALLY using `for...of` — NEVER use `Promise.all`. Extract `risk_level`, `affected_symbols`, and `affected_files`.
|
3. **Calculate blast radius**: For each target symbol, call the memtrace-adapter: `node _bmad/scripts/memtrace/memtrace-adapter.mjs --target <symbol> --query get_impact`. Process targets SEQUENTIALLY using `for...of` — NEVER use `Promise.all`. If adapter exits with code 1 → HALT (timeout or unavailable). Parse the STDOUT JSON extracting `risk_level`, `affected_symbols`, and `affected_files`.
|
||||||
|
|
||||||
4. **Summarize for token budget**: Keep the final report under 2000 tokens:
|
4. **Summarize for token budget**: Keep the final report under 2000 tokens:
|
||||||
- Collapse depth > 3 into module-level counts only
|
- Collapse depth > 3 into module-level counts only
|
||||||
|
|
@ -91,7 +91,7 @@ Verify `{spec_file}` resolves to a non-empty path and the file exists on disk. I
|
||||||
- The qa-memtrace.mjs exit code is the FINAL authority. Exit 1 is a HARD BLOCK on implementation.
|
- The qa-memtrace.mjs exit code is the FINAL authority. Exit 1 is a HARD BLOCK on implementation.
|
||||||
|
|
||||||
5c. **Dead Code Pitfall Validation**: If the story involves dead-code removal (find_dead_code usage in context or tasks):
|
5c. **Dead Code Pitfall Validation**: If the story involves dead-code removal (find_dead_code usage in context or tasks):
|
||||||
- Call `memtrace_find_dead_code` via MCP for the target module(s). Process sequentially — NEVER use `Promise.all`.
|
- Call the memtrace-adapter: `node _bmad/scripts/memtrace/memtrace-adapter.mjs --target <module_path> --query find_dead_code [--repo <repo_id>]`. Process sequentially — NEVER use `Promise.all`. Parse the adapter's STDOUT JSON for the `symbols` array.
|
||||||
- Serialize candidates to a temp JSON file.
|
- Serialize candidates to a temp JSON file.
|
||||||
- Run: `node _bmad/scripts/memtrace/validate-dead-code.mjs --candidates <temp-file>`
|
- Run: `node _bmad/scripts/memtrace/validate-dead-code.mjs --candidates <temp-file>`
|
||||||
- If exit 0: log output to `{spec_file}` completion notes as "Dead Code Pitfall Validation Report". Present SUSPECT entries for manual review. FALSE_POS and GHOST are ignored.
|
- If exit 0: log output to `{spec_file}` completion notes as "Dead Code Pitfall Validation Report". Present SUSPECT entries for manual review. FALSE_POS and GHOST are ignored.
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
||||||
|
|
||||||
1. **Identify targets**: Derive the symbols/files being modified from the clarified user intent. If ambiguous, ask the user for explicit targets.
|
1. **Identify targets**: Derive the symbols/files being modified from the clarified user intent. If ambiguous, ask the user for explicit targets.
|
||||||
|
|
||||||
2. **Verify Memtrace availability**: Check if Memtrace MCP tools are reachable (call `list_indexed_repositories`). If unavailable, HALT: "Memtrace MCP server is not available. Structural blast radius verification cannot be performed. Please start the Memtrace server or explicitly override this safety check."
|
2. **Verify Memtrace availability**: Check if the Memtrace MCP server is reachable by calling the adapter: `node _bmad/scripts/memtrace/memtrace-adapter.mjs --query list_repos`. If exit 0 — server is reachable (parse STDOUT JSON for repository list). If exit 1 — HALT: "Memtrace MCP server is not available. Structural blast radius verification cannot be performed. Please start the Memtrace server or explicitly override this safety check."
|
||||||
|
|
||||||
3. **Calculate blast radius**: For each target symbol, call `memtrace_get_impact`. Process targets SEQUENTIALLY — NEVER use `Promise.all`. Extract `risk_level`, `affected_symbols`, and `affected_files`.
|
3. **Calculate blast radius**: For each target symbol, call the memtrace-adapter: `node _bmad/scripts/memtrace/memtrace-adapter.mjs --target <symbol> --query get_impact`. Process targets SEQUENTIALLY — NEVER use `Promise.all`. If adapter exits with code 1 → HALT (timeout or unavailable). Parse the STDOUT JSON extracting `risk_level`, `affected_symbols`, and `affected_files`.
|
||||||
|
|
||||||
4. **Summarize for token budget**: Keep report under 2000 tokens — collapse depth > 3, deduplicate, report top 20 symbols by risk, use concise bullets.
|
4. **Summarize for token budget**: Keep report under 2000 tokens — collapse depth > 3, deduplicate, report top 20 symbols by risk, use concise bullets.
|
||||||
|
|
||||||
|
|
@ -75,7 +75,7 @@ deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
||||||
- The qa-memtrace.mjs exit code is the FINAL authority. Exit 1 is a HARD BLOCK on implementation.
|
- The qa-memtrace.mjs exit code is the FINAL authority. Exit 1 is a HARD BLOCK on implementation.
|
||||||
|
|
||||||
5c. **Dead Code Pitfall Validation**: If the story involves dead-code removal (find_dead_code usage):
|
5c. **Dead Code Pitfall Validation**: If the story involves dead-code removal (find_dead_code usage):
|
||||||
- Call `memtrace_find_dead_code` via MCP. Process sequentially — NEVER use `Promise.all`.
|
- Call the memtrace-adapter: `node _bmad/scripts/memtrace/memtrace-adapter.mjs --target <module_path> --query find_dead_code [--repo <repo_id>]`. Process sequentially — NEVER use `Promise.all`. Parse the adapter's STDOUT JSON for the `symbols` array.
|
||||||
- Serialize candidates to a temp JSON file.
|
- Serialize candidates to a temp JSON file.
|
||||||
- Run: `node _bmad/scripts/memtrace/validate-dead-code.mjs --candidates <temp-file>`
|
- Run: `node _bmad/scripts/memtrace/validate-dead-code.mjs --candidates <temp-file>`
|
||||||
- If exit 0: log output to `{spec_file}` completion notes as "Dead Code Pitfall Validation Report". Present SUSPECT entries for manual review. Ignore FALSE_POS and GHOST.
|
- If exit 0: log output to `{spec_file}` completion notes as "Dead Code Pitfall Validation Report". Present SUSPECT entries for manual review. Ignore FALSE_POS and GHOST.
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,12 @@ failed_layers: '' # set at runtime: comma-separated list of layers that failed o
|
||||||
> - **If the spec file contains a blast radius report AND a Test Coverage Justification BUT no "Mathematical Quality Gate Output"**: this is a Phase 1-level story using only textual justification. Flag as `patch` (not `decision_needed`): "Phase 1 story — consider upgrading to mathematical quality gate via qa-memtrace.mjs."
|
> - **If the spec file contains a blast radius report AND a Test Coverage Justification BUT no "Mathematical Quality Gate Output"**: this is a Phase 1-level story using only textual justification. Flag as `patch` (not `decision_needed`): "Phase 1 story — consider upgrading to mathematical quality gate via qa-memtrace.mjs."
|
||||||
> - **If the spec file contains only a blast radius report (no test justification, no mathematical gate)**: raise a `decision_needed` finding: "Missing both Test Coverage Justification and Mathematical Quality Gate Output — Phase 2 story must include qa-memtrace.mjs execution results."
|
> - **If the spec file contains only a blast radius report (no test justification, no mathematical gate)**: raise a `decision_needed` finding: "Missing both Test Coverage Justification and Mathematical Quality Gate Output — Phase 2 story must include qa-memtrace.mjs execution results."
|
||||||
>
|
>
|
||||||
|
> **Quality Gate — Adapter Usage Verification:** Check whether the spec/story file records indicate use of the `memtrace-adapter.mjs` wrapper (rather than raw MCP tool calls) for blast radius and availability queries. Apply these rules:
|
||||||
|
> - **If the spec/story file's Dev Agent Record or diff commentary references `memtrace-adapter.mjs`** for blast radius queries (`--query get_impact`) and availability checks (`--query list_repos`) → pass: adapter usage confirmed.
|
||||||
|
> - **If the spec/story file shows `memtrace_get_impact` or `list_indexed_repositories` being called directly** (without the adapter wrapper) for the blast radius step → raise a `patch` finding: "Direct MCP call detected — blast radius step should use `memtrace-adapter.mjs` instead of raw `memtrace_get_impact` or `list_indexed_repositories` for consistent timeout handling and error token emission."
|
||||||
|
> - **If the blast radius step is absent or the story doesn't involve code modification (new-file-only stories)** → skip this gate.
|
||||||
|
> - **If the spec/story file involves dead-code queries (`find_dead_code`)**: check whether the adapter was used (`--query find_dead_code`) rather than raw `memtrace_find_dead_code` MCP calls. If `memtrace_find_dead_code` was called directly without the adapter → raise a `patch` finding: "Direct MCP call detected — dead-code query should use `memtrace-adapter.mjs --query find_dead_code` instead of raw `memtrace_find_dead_code` for consistent timeout handling and error token emission."
|
||||||
|
>
|
||||||
> **Quality Gate — Dead Code Pitfall Validation:** Check whether the spec/story file includes a "Dead Code Pitfall Validation Report" section (JSON output from `validate-dead-code.mjs`). Apply these rules:
|
> **Quality Gate — Dead Code Pitfall Validation:** Check whether the spec/story file includes a "Dead Code Pitfall Validation Report" section (JSON output from `validate-dead-code.mjs`). Apply these rules:
|
||||||
> - **If the spec file contains "Dead Code Pitfall Validation Report"**: verify that the `suspects` list entries were addressed in the implementation (check if corresponding tasks exist in Tasks/Subtasks, or if deleted files match suspect entries). If SUSPECT entries were not addressed → raise a `decision_needed` finding per unaddressed suspect: "SUSPECT dead-code entry not addressed: [name] in [file] — pitfall validation flagged this as truly dead code but no removal task was completed."
|
> - **If the spec file contains "Dead Code Pitfall Validation Report"**: verify that the `suspects` list entries were addressed in the implementation (check if corresponding tasks exist in Tasks/Subtasks, or if deleted files match suspect entries). If SUSPECT entries were not addressed → raise a `decision_needed` finding per unaddressed suspect: "SUSPECT dead-code entry not addressed: [name] in [file] — pitfall validation flagged this as truly dead code but no removal task was completed."
|
||||||
> - **If the story involves dead-code removal (find_dead_code, dead-code in tasks) BUT no "Dead Code Pitfall Validation Report" exists in the spec file** → raise a `patch` finding: "Missing Dead Code Pitfall Validation Report — story involved dead-code removal but no pitfall validation was performed via validate-dead-code.mjs."
|
> - **If the story involves dead-code removal (find_dead_code, dead-code in tasks) BUT no "Dead Code Pitfall Validation Report" exists in the spec file** → raise a `patch` finding: "Missing Dead Code Pitfall Validation Report — story involved dead-code removal but no pitfall validation was performed via validate-dead-code.mjs."
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,328 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
|
const TIMEOUT_MS = parseInt(process.env.MEMTRACE_TIMEOUT_MS || '10000', 10);
|
||||||
|
const TIMEOUT_TOKEN = 'MEMTRACE_MCP_ERROR_TIMEOUT';
|
||||||
|
|
||||||
|
function parseArgs() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
||||||
|
console.log(`Usage: node memtrace-adapter.mjs --target <symbol> --query <type> [--repo <repo_id>]
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
--target <symbol> Symbol name or file path to query (required for get_impact, find_dead_code)
|
||||||
|
--query <type> Query type: get_impact, find_dead_code, list_repos (required)
|
||||||
|
--repo <repo_id> Repository ID (optional — auto-detected from .memtrace-workspace if omitted)
|
||||||
|
|
||||||
|
Query types:
|
||||||
|
get_impact Fetch structural blast radius for a target symbol
|
||||||
|
find_dead_code Find dead code in a target module (stub — full impl in Story 3.2)
|
||||||
|
list_repos List indexed repositories with freshness timestamps
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
node memtrace-adapter.mjs --target "validateToken" --query get_impact
|
||||||
|
node memtrace-adapter.mjs --query list_repos
|
||||||
|
node memtrace-adapter.mjs --target "src/auth" --query find_dead_code
|
||||||
|
node memtrace-adapter.mjs --help`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = { target: null, query: null, repo: null };
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
if (args[i] === '--target' && i + 1 < args.length) {
|
||||||
|
result.target = args[++i];
|
||||||
|
} else if (args[i] === '--query' && i + 1 < args.length) {
|
||||||
|
result.query = args[++i];
|
||||||
|
} else if (args[i] === '--repo' && i + 1 < args.length) {
|
||||||
|
result.repo = args[++i];
|
||||||
|
} else {
|
||||||
|
fail(`Unknown argument: ${args[i]}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.query) {
|
||||||
|
fail('Missing required argument: --query');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const validQueries = ['get_impact', 'find_dead_code', 'list_repos'];
|
||||||
|
if (!validQueries.includes(result.query)) {
|
||||||
|
fail(`Invalid query type: "${result.query}". Valid: ${validQueries.join(', ')}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((result.query === 'get_impact' || result.query === 'find_dead_code')) {
|
||||||
|
if (result.target === null) {
|
||||||
|
fail(`Missing required argument: --target is required for --query ${result.query}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (result.target.trim() === '') {
|
||||||
|
fail('--target must be a non-empty string');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fail(msg) {
|
||||||
|
console.error(`ERROR: ${msg}`);
|
||||||
|
console.log(TIMEOUT_TOKEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
class McpClient {
|
||||||
|
constructor() {
|
||||||
|
this.child = null;
|
||||||
|
this.stdoutBuffer = '';
|
||||||
|
this.requestId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
spawn() {
|
||||||
|
return new Promise((resolvePromise, reject) => {
|
||||||
|
try {
|
||||||
|
this.child = spawn('memtrace', ['mcp'], {
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
shell: process.platform === 'win32',
|
||||||
|
windowsHide: true
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
reject(new Error(`Failed to spawn memtrace: ${err.message}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onError = (err) => {
|
||||||
|
cleanup();
|
||||||
|
const msg = err.code === 'ENOENT'
|
||||||
|
? `memtrace binary not found on PATH. Ensure memtrace is installed (npm install -g memtrace) and available.`
|
||||||
|
: `memtrace spawn error: ${err.message}`;
|
||||||
|
reject(new Error(msg));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onExit = (code, signal) => {
|
||||||
|
cleanup();
|
||||||
|
if (signal) {
|
||||||
|
reject(new Error(`memtrace process terminated by signal ${signal}`));
|
||||||
|
} else if (code !== 0) {
|
||||||
|
reject(new Error(`memtrace process exited with code ${code}`));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
if (this.child) {
|
||||||
|
this.child.removeListener('error', onError);
|
||||||
|
this.child.removeListener('exit', onExit);
|
||||||
|
this.child.stderr.removeListener('data', onStderr);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStderr = () => {
|
||||||
|
// MCP servers may log diagnostics to stderr — capture but don't reject
|
||||||
|
};
|
||||||
|
|
||||||
|
this.child.on('error', onError);
|
||||||
|
this.child.on('exit', onExit);
|
||||||
|
this.child.stderr.on('data', onStderr);
|
||||||
|
|
||||||
|
resolvePromise();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendRequest(method, params = {}) {
|
||||||
|
const id = ++this.requestId;
|
||||||
|
const request = JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n';
|
||||||
|
return new Promise((resolvePromise, reject) => {
|
||||||
|
const listener = (data) => {
|
||||||
|
this.stdoutBuffer += data.toString();
|
||||||
|
const lines = this.stdoutBuffer.split('\n');
|
||||||
|
this.stdoutBuffer = lines.pop() || '';
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
try {
|
||||||
|
const response = JSON.parse(line);
|
||||||
|
if (response.id === id) {
|
||||||
|
this.child.stdout.removeListener('data', listener);
|
||||||
|
if (response.error) {
|
||||||
|
reject(new Error(`MCP error: ${response.error.message || JSON.stringify(response.error)}`));
|
||||||
|
} else {
|
||||||
|
resolvePromise(response.result);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Partial JSON in buffer — wait for more data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.child.stdout.on('data', listener);
|
||||||
|
this.child.stdin.write(request);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async handshake() {
|
||||||
|
const capabilities = await this.sendRequest('initialize', {
|
||||||
|
protocolVersion: '2024-11-05',
|
||||||
|
capabilities: {},
|
||||||
|
clientInfo: { name: 'bmad-memtrace-adapter', version: '1.0.0' }
|
||||||
|
});
|
||||||
|
// Send initialized notification (fire-and-forget, no response expected)
|
||||||
|
this.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized', params: {} }) + '\n');
|
||||||
|
return capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
async callTool(name, args) {
|
||||||
|
return this.sendRequest('tools/call', { name, arguments: args });
|
||||||
|
}
|
||||||
|
|
||||||
|
async shutdown() {
|
||||||
|
try {
|
||||||
|
await this.sendRequest('shutdown', {});
|
||||||
|
} catch (err) {
|
||||||
|
// Shutdown errors are non-fatal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kill() {
|
||||||
|
if (this.child) {
|
||||||
|
try { this.child.stdin.end(); } catch (e) {}
|
||||||
|
try { this.child.kill('SIGTERM'); } catch (e) {}
|
||||||
|
// SIGTERM is sufficient on modern systems; no need for SIGKILL timer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRepoId(args) {
|
||||||
|
if (args.repo) return args.repo;
|
||||||
|
|
||||||
|
// Try to auto-detect from .memtrace-workspace
|
||||||
|
let cwd = process.cwd();
|
||||||
|
const parts = cwd.split(/[\\/]/);
|
||||||
|
for (let i = parts.length; i > 0; i--) {
|
||||||
|
const dir = parts.slice(0, i).join('/');
|
||||||
|
if (existsSync(resolve('/', ...(process.platform === 'win32' ? [dir.split(':')[0] + ':', ...dir.split(':')[1]?.split('/').filter(Boolean) || []] : []), '.memtrace-workspace'))) {
|
||||||
|
// Found workspace anchor — use basename
|
||||||
|
return parts[i - 1] || parts[parts.length - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: use CWD basename
|
||||||
|
return parts[parts.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function queryGetImpact(client, target, repoId) {
|
||||||
|
const result = await client.callTool('get_impact', { target, repo_id: repoId });
|
||||||
|
return {
|
||||||
|
target,
|
||||||
|
risk_level: result.risk || 'Low',
|
||||||
|
affected_symbols: (result.affected_symbols || []).map(s => ({
|
||||||
|
name: s.name,
|
||||||
|
file: s.file || '',
|
||||||
|
depth: s.depth || 1
|
||||||
|
})),
|
||||||
|
affected_files: result.affected_files || [],
|
||||||
|
total_count: result.total_affected || result.affected_symbols?.length || 0,
|
||||||
|
elapsed_ms: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function queryFindDeadCode(client, target, repoId) {
|
||||||
|
const result = await client.callTool('find_dead_code', {
|
||||||
|
repo_id: repoId,
|
||||||
|
file_path: target
|
||||||
|
});
|
||||||
|
const symbols = (result?.symbols || []).map(s => ({
|
||||||
|
name: s.name || '<unknown>',
|
||||||
|
kind: s.kind || 'Function',
|
||||||
|
file: s.file || '',
|
||||||
|
line: s.line || 0
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
query: 'find_dead_code',
|
||||||
|
target,
|
||||||
|
symbols,
|
||||||
|
total_count: symbols.length,
|
||||||
|
elapsed_ms: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function queryListRepos(client) {
|
||||||
|
const result = await client.callTool('list_indexed_repositories', {});
|
||||||
|
const repos = Array.isArray(result?.repos) ? result.repos : [];
|
||||||
|
return {
|
||||||
|
query: 'list_repos',
|
||||||
|
repositories: repos.map(r => ({
|
||||||
|
repo_id: r.repo_id,
|
||||||
|
last_indexed: r.last_indexed_at || r.last_indexed || null,
|
||||||
|
total_nodes: r.total_nodes || r.nodes || 0
|
||||||
|
})),
|
||||||
|
elapsed_ms: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function withTimeout(promise, ms) {
|
||||||
|
let timer;
|
||||||
|
const timeout = new Promise((_, reject) => {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
reject(new TimeoutError(`Query timed out after ${ms}ms`));
|
||||||
|
}, ms);
|
||||||
|
});
|
||||||
|
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimeoutError extends Error {
|
||||||
|
constructor(message) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'TimeoutError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const args = parseArgs();
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
const repoId = resolveRepoId(args);
|
||||||
|
const client = new McpClient();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.spawn();
|
||||||
|
await withTimeout(client.handshake(), TIMEOUT_MS);
|
||||||
|
|
||||||
|
let queryFn;
|
||||||
|
if (args.query === 'get_impact') {
|
||||||
|
queryFn = queryGetImpact(client, args.target, repoId);
|
||||||
|
} else if (args.query === 'find_dead_code') {
|
||||||
|
queryFn = queryFindDeadCode(client, args.target, repoId);
|
||||||
|
} else if (args.query === 'list_repos') {
|
||||||
|
queryFn = queryListRepos(client);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unhandled query type: ${args.query}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = await withTimeout(queryFn, TIMEOUT_MS);
|
||||||
|
result.elapsed_ms = Date.now() - start;
|
||||||
|
|
||||||
|
await withTimeout(client.shutdown(), 5000);
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
process.exit(0);
|
||||||
|
} catch (err) {
|
||||||
|
client.kill();
|
||||||
|
const elapsed = Date.now() - start;
|
||||||
|
|
||||||
|
if (err instanceof TimeoutError) {
|
||||||
|
console.log(TIMEOUT_TOKEN);
|
||||||
|
console.error(`ERROR: Query timed out after ${elapsed}ms`);
|
||||||
|
} else {
|
||||||
|
fail(err.message);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
import { describe, it } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { execFile } from 'node:child_process';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
|
const ADAPTER = resolve(import.meta.dirname, 'memtrace-adapter.mjs');
|
||||||
|
|
||||||
|
function runAdapter(args) {
|
||||||
|
return new Promise((resolvePromise) => {
|
||||||
|
execFile(process.execPath, [ADAPTER, ...args], {
|
||||||
|
env: { ...process.env, NODE_NO_WARNINGS: '1' },
|
||||||
|
timeout: 30000,
|
||||||
|
windowsHide: true
|
||||||
|
}, (error, stdout, stderr) => {
|
||||||
|
resolvePromise({
|
||||||
|
code: error?.code === 'ETIMEDOUT' ? null : (error?.code || 0),
|
||||||
|
signal: error?.signal || null,
|
||||||
|
stdout: stdout || '',
|
||||||
|
stderr: stderr || '',
|
||||||
|
error
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('memtrace-adapter.mjs', () => {
|
||||||
|
|
||||||
|
describe('CLI argument handling', () => {
|
||||||
|
it('should output usage with --help and exit 0', async () => {
|
||||||
|
const r = await runAdapter(['--help']);
|
||||||
|
assert.equal(r.code, 0);
|
||||||
|
assert.ok(r.stdout.includes('Usage:'));
|
||||||
|
assert.ok(r.stdout.includes('--query'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should output usage with -h and exit 0', async () => {
|
||||||
|
const r = await runAdapter(['-h']);
|
||||||
|
assert.equal(r.code, 0);
|
||||||
|
assert.ok(r.stdout.includes('Usage:'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should output usage with no args and exit 0', async () => {
|
||||||
|
const r = await runAdapter([]);
|
||||||
|
assert.equal(r.code, 0);
|
||||||
|
assert.ok(r.stdout.includes('Usage:'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exit 1 when missing --target for get_impact', async () => {
|
||||||
|
const r = await runAdapter(['--query', 'get_impact']);
|
||||||
|
assert.equal(r.code, 1);
|
||||||
|
assert.ok(r.stderr.includes('--target'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exit 1 when missing --target for find_dead_code', async () => {
|
||||||
|
const r = await runAdapter(['--query', 'find_dead_code']);
|
||||||
|
assert.equal(r.code, 1);
|
||||||
|
assert.ok(r.stderr.includes('--target'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exit 1 when --target is empty string', async () => {
|
||||||
|
const r = await runAdapter(['--target', '', '--query', 'get_impact']);
|
||||||
|
assert.equal(r.code, 1);
|
||||||
|
assert.ok(r.stderr.includes('non-empty'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exit 1 for unknown --query type', async () => {
|
||||||
|
const r = await runAdapter(['--target', 'foo', '--query', 'invalid_query']);
|
||||||
|
assert.equal(r.code, 1);
|
||||||
|
assert.ok(r.stderr.includes('Invalid query'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exit 1 when missing --query', async () => {
|
||||||
|
const r = await runAdapter(['--target', 'foo']);
|
||||||
|
assert.equal(r.code, 1);
|
||||||
|
assert.ok(r.stderr.includes('--query'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exit 1 for unknown argument', async () => {
|
||||||
|
const r = await runAdapter(['--unknown']);
|
||||||
|
assert.equal(r.code, 1);
|
||||||
|
assert.ok(r.stderr.includes('Unknown argument'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MCP queries', () => {
|
||||||
|
|
||||||
|
it('should list repositories and return valid JSON with repos array', { timeout: 20000 }, async () => {
|
||||||
|
const r = await runAdapter(['--query', 'list_repos']);
|
||||||
|
assert.equal(r.code, 0);
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(r.stdout);
|
||||||
|
} catch (e) {
|
||||||
|
assert.fail(`STDOUT is not valid JSON: ${r.stdout.slice(0, 200)}`);
|
||||||
|
}
|
||||||
|
assert.equal(parsed.query, 'list_repos');
|
||||||
|
assert.ok(Array.isArray(parsed.repositories));
|
||||||
|
assert.ok(typeof parsed.elapsed_ms === 'number');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should query get_impact and return structured JSON on exit 0 (or error with MEMTRACE_MCP_ERROR_TIMEOUT on exit 1)', { timeout: 30000 }, async () => {
|
||||||
|
const r = await runAdapter(['--target', 'bmad-dev-story', '--query', 'get_impact', '--repo', 'Repos']);
|
||||||
|
// Should either succeed with data or fail gracefully
|
||||||
|
if (r.code === 0) {
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(r.stdout);
|
||||||
|
} catch (e) {
|
||||||
|
assert.fail(`STDOUT is not valid JSON: ${r.stdout.slice(0, 200)}`);
|
||||||
|
}
|
||||||
|
assert.ok(typeof parsed.target === 'string');
|
||||||
|
assert.ok(typeof parsed.risk_level === 'string');
|
||||||
|
assert.ok(Array.isArray(parsed.affected_symbols));
|
||||||
|
assert.ok(typeof parsed.total_count === 'number');
|
||||||
|
assert.ok(typeof parsed.elapsed_ms === 'number');
|
||||||
|
} else {
|
||||||
|
// On failure, must emit MEMTRACE_MCP_ERROR_TIMEOUT
|
||||||
|
assert.ok(r.stdout.includes('MEMTRACE_MCP_ERROR_TIMEOUT'),
|
||||||
|
`Expected MEMTRACE_MCP_ERROR_TIMEOUT in stdout. Exit code: ${r.code}, stderr: ${r.stderr.slice(0, 200)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should query find_dead_code with --target and --repo and return structured JSON', { timeout: 30000 }, async () => {
|
||||||
|
const r = await runAdapter(['--target', 'src', '--query', 'find_dead_code', '--repo', 'Repos']);
|
||||||
|
if (r.code === 0) {
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(r.stdout);
|
||||||
|
} catch (e) {
|
||||||
|
assert.fail(`STDOUT is not valid JSON: ${r.stdout.slice(0, 200)}`);
|
||||||
|
}
|
||||||
|
assert.equal(parsed.query, 'find_dead_code');
|
||||||
|
assert.equal(typeof parsed.target, 'string');
|
||||||
|
assert.ok(Array.isArray(parsed.symbols));
|
||||||
|
assert.equal(parsed.total_count, parsed.symbols.length);
|
||||||
|
assert.equal(typeof parsed.elapsed_ms, 'number');
|
||||||
|
assert.equal(parsed.note, undefined, 'Stub note must be removed');
|
||||||
|
if (parsed.symbols.length > 0) {
|
||||||
|
assert.ok(parsed.symbols.every(s => typeof s.name === 'string' && typeof s.file === 'string'),
|
||||||
|
'Each symbol must have name and file fields');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert.ok(r.stdout.includes('MEMTRACE_MCP_ERROR_TIMEOUT'),
|
||||||
|
`Expected MEMTRACE_MCP_ERROR_TIMEOUT. Exit code: ${r.code}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should query find_dead_code without --repo and auto-detect repo', { timeout: 30000 }, async () => {
|
||||||
|
const r = await runAdapter(['--target', 'src', '--query', 'find_dead_code']);
|
||||||
|
if (r.code === 0) {
|
||||||
|
let parsed = JSON.parse(r.stdout);
|
||||||
|
assert.equal(parsed.query, 'find_dead_code');
|
||||||
|
assert.ok(Array.isArray(parsed.symbols));
|
||||||
|
assert.equal(parsed.total_count, parsed.symbols.length);
|
||||||
|
if (parsed.symbols.length > 0) {
|
||||||
|
assert.ok(parsed.symbols.every(s => typeof s.name === 'string' && typeof s.file === 'string'),
|
||||||
|
'Each symbol must have name and file fields');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert.ok(r.stdout.includes('MEMTRACE_MCP_ERROR_TIMEOUT'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit MEMTRACE_MCP_ERROR_TIMEOUT and exit 1 on MCP error', { timeout: 20000 }, async () => {
|
||||||
|
// Query for a non-existent target to trigger an MCP error
|
||||||
|
const r = await runAdapter(['--target', '!@#$%^&*()_NONEXISTENT_SYMBOL_12345', '--query', 'get_impact', '--repo', 'Repos']);
|
||||||
|
// Should always exit 0 or 1 — never hang
|
||||||
|
if (r.code === 1) {
|
||||||
|
assert.ok(r.stdout.includes('MEMTRACE_MCP_ERROR_TIMEOUT'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue