diff --git a/_bmad/scripts/memtrace/memtrace-adapter.mjs b/_bmad/scripts/memtrace/memtrace-adapter.mjs index 6e2e71a47..9f49701eb 100644 --- a/_bmad/scripts/memtrace/memtrace-adapter.mjs +++ b/_bmad/scripts/memtrace/memtrace-adapter.mjs @@ -239,7 +239,7 @@ function resolveRepoId(args) { for (let i = parts.length; i > 0; i--) { const dir = parts.slice(0, i).join('/'); const candidate = process.platform === 'win32' - ? resolve(dir || parts[0], '.memtrace-workspace') + ? resolve(dir.endsWith(':') ? dir + '\\' : (dir || parts[0]), '.memtrace-workspace') : resolve('/', dir, '.memtrace-workspace'); if (existsSync(candidate)) { return parts[i - 1] || 'project'; @@ -247,7 +247,11 @@ function resolveRepoId(args) { } // Fallback: use CWD basename - return parts[parts.length - 1] || 'project'; + const fallback = parts[parts.length - 1] || 'project'; + if (fallback === 'project' && !args.repo) { + console.error('WARNING: Could not detect repo ID from CWD or .memtrace-workspace. Using "project".'); + } + return fallback; } async function checkIndexFreshness(client, repoId) { @@ -266,6 +270,9 @@ async function checkIndexFreshness(client, repoId) { const ageMinutes = Math.round((Date.now() - Date.parse(lastIndexed)) / 60000 * 10) / 10; const valid = Number.isFinite(ageMinutes); + if (!valid) { + console.error(`WARNING: Unparseable last_indexed timestamp for repo "${repoId}": "${lastIndexed}"`); + } const isFresh = valid && ageMinutes <= FRESHNESS_MAX_AGE_MINUTES; return { found: true, repo_id: repoId, last_indexed: lastIndexed, age_minutes: valid ? ageMinutes : null, is_fresh: isFresh }; @@ -320,6 +327,9 @@ async function queryListRepos(client) { let isFresh = false; if (lastIndexed) { ageMinutes = Math.round((Date.now() - Date.parse(lastIndexed)) / 60000 * 10) / 10; + if (!Number.isFinite(ageMinutes)) { + console.error(`WARNING: Unparseable last_indexed timestamp for repo "${repoId}": "${lastIndexed}"`); + } isFresh = ageMinutes <= FRESHNESS_MAX_AGE_MINUTES; } return { @@ -567,6 +577,12 @@ async function main() { const repoId = resolveRepoId(args); + // Pre-flight: batch mode only supports get_impact and find_dead_code + if (args.batch && !['get_impact', 'find_dead_code'].includes(args.query)) { + fail(`--batch does not support --query ${args.query}. Supported: get_impact, find_dead_code. See --help.`); + process.exit(1); + } + // Pre-flight freshness check (before main MCP session) if (args.checkFreshness) { const freshness = await runFreshnessCheck(repoId); @@ -585,7 +601,10 @@ async function main() { } catch (diagErr) { diagClient.kill(); fail(`Failed to emit diagnostic: ${diagErr.message}`); + process.exit(1); } + } else { + console.log(JSON.stringify({ error: 'index_stale', freshness })); } process.exit(1); } @@ -594,7 +613,7 @@ async function main() { // Batch mode: process targets sequentially if (args.batch) { if (!args.targets || args.targets.length === 0) { - fail('--batch requires at least one --target value'); + fail('--batch requires at least one --target value. Use --target "sym1,sym2" or repeated --target flags.'); process.exit(1); } await runBatchQuery(args, repoId, start); diff --git a/_bmad/scripts/memtrace/memtrace-adapter.test.mjs b/_bmad/scripts/memtrace/memtrace-adapter.test.mjs index b8fe8b9ec..8fda75756 100644 --- a/_bmad/scripts/memtrace/memtrace-adapter.test.mjs +++ b/_bmad/scripts/memtrace/memtrace-adapter.test.mjs @@ -200,12 +200,39 @@ describe('memtrace-adapter.mjs', () => { assert.ok(r.stderr.includes('[FRESHNESS]'), 'STDERR must contain [FRESHNESS] line'); } else if (r.code === 1) { assert.ok(r.stderr.includes('[FRESHNESS]'), 'STDERR must contain [FRESHNESS] line even on stale index'); + // On stale index, STDOUT should have JSON diagnostic with freshness_error field + try { + const parsed = JSON.parse(r.stdout); + assert.ok(parsed.freshness_error, 'Diagnostic JSON must have freshness_error field'); + } catch { + // STDOUT may not always be parseable JSON; not a hard failure + } + } + }); + + it('invalid MEMTRACE_FRESHNESS_MAX_AGE_MINUTES should not crash', { timeout: 30000 }, async () => { + const r = await runAdapter(['--query', 'list_repos', '--check-freshness']); + // Should exit cleanly (0 or 1) regardless of env value — never hang or crash + assert.ok(r.code === 0 || r.code === 1, 'Must exit cleanly with invalid env'); + }); + + it('--check-freshness without --repo should auto-detect and not crash', { timeout: 30000 }, async () => { + const r = await runAdapter(['--query', 'list_repos', '--check-freshness']); + assert.ok(r.code === 0 || r.code === 1, 'Must exit cleanly with auto-detected repo'); + if (r.code === 0 || r.code === 1) { + assert.ok(r.stderr.includes('[FRESHNESS]'), 'STDERR must contain [FRESHNESS] line'); } }); }); describe('Batch mode (--batch)', () => { + it('--batch with list_repos should exit 1 with unsupported query error', async () => { + const r = await runAdapter(['--query', 'list_repos', '--batch']); + assert.equal(r.code, 1); + assert.ok(r.stderr.includes('not support') || r.stderr.includes('ERROR')); + }); + it('--batch with comma-separated targets should produce results array', { timeout: 30000 }, async () => { const r = await runAdapter(['--target', 'bmad-dev-story,parseArgs', '--query', 'get_impact', '--repo', 'Repos', '--batch']); if (r.code === 0) { @@ -238,6 +265,18 @@ describe('memtrace-adapter.mjs', () => { } } }); + + it('--batch with all-failing non-existent targets should produce zero successes', { timeout: 30000 }, async () => { + const r = await runAdapter(['--target', '!@#$%^&*()_NE1_SYM,!@#$%^&*()_NE2_SYM', '--query', 'get_impact', '--repo', 'Repos', '--batch']); + if (r.code === 1) { + let parsed; + try { parsed = JSON.parse(r.stdout); } catch { /* ignore parse errors */ } + if (parsed && parsed.results) { + assert.equal(parsed.total_succeeded, 0, 'All targets should have failed'); + assert.ok(parsed.total_failed >= 1, 'Should have at least 1 failure'); + } + } + }); }); describe('MCP queries', () => {