BMAD-METHOD/_bmad/scripts/memtrace/qa-memtrace.test.mjs

150 lines
6.4 KiB
JavaScript

#!/usr/bin/env node
import { strict as assert } from 'assert';
import { writeFileSync, unlinkSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { execFileSync } from 'child_process';
import { fileURLToPath } from 'url';
const SCRIPT_PATH = join(fileURLToPath(new URL('.', import.meta.url)), 'qa-memtrace.mjs');
const TMP = tmpdir();
function runScript(br, tc, threshold) {
const brPath = join(TMP, `br-${Date.now()}.json`);
const tcPath = join(TMP, `tc-${Date.now()}.json`);
try {
writeFileSync(brPath, JSON.stringify(br), 'utf8');
writeFileSync(tcPath, JSON.stringify(tc), 'utf8');
const args = ['--blast-radius', brPath, '--test-coverage', tcPath];
if (threshold !== undefined) args.push('--threshold', String(threshold));
const stdout = execFileSync(process.execPath, [SCRIPT_PATH, ...args], { encoding: 'utf8', timeout: 5000 });
return { exitCode: 0, output: JSON.parse(stdout.trim()) };
} catch (e) {
const raw = e.stdout;
const stdout = (Buffer.isBuffer(raw) ? raw.toString('utf8') : (raw || '')).trim();
let parsed;
try { parsed = JSON.parse(stdout); }
catch { parsed = { status: 'error', error: stdout }; }
return { exitCode: e.status || 1, output: parsed };
} finally {
try { unlinkSync(brPath); } catch {}
try { unlinkSync(tcPath); } catch {}
}
}
function makeBr(symbols) {
return { target: 'test', risk_level: 'Low', affected_symbols: symbols, total_count: symbols.length };
}
function makeTc(modules) {
return { modules, coverage_summary: { total_modules: modules.length } };
}
let passed = 0;
let failed = 0;
function test(name, fn) {
try { fn(); passed++; console.log(` V ${name}`); }
catch (e) { failed++; console.log(` X ${name}: ${e.message}`); }
}
console.log('qa-memtrace.mjs Test Suite\n');
test('all nodes covered', () => {
const br = makeBr([{name:'foo',file:'src/a.ts',depth:1}]);
const tc = makeTc([{module:'src/a.ts',test_files:['test/a.test.ts'],symbols_covered:['foo'],coverage:'Yes'}]);
const r = runScript(br, tc);
assert.equal(r.exitCode, 0);
assert.equal(r.output.passed, true);
assert.equal(r.output.covered_nodes, 1);
});
test('some nodes uncovered', () => {
const br = makeBr([{name:'foo',file:'src/a.ts',depth:1},{name:'bar',file:'src/b.ts',depth:2}]);
const tc = makeTc([{module:'src/a.ts',test_files:['test/a.test.ts'],symbols_covered:['foo'],coverage:'Yes'},{module:'src/b.ts',test_files:[],symbols_covered:[],coverage:'None'}]);
const r = runScript(br, tc);
assert.equal(r.exitCode, 1);
assert.equal(r.output.passed, false);
assert.equal(r.output.uncovered_nodes, 1);
});
test('threshold met (50% with threshold 50)', () => {
const br = makeBr([{name:'foo',file:'src/a.ts',depth:1},{name:'bar',file:'src/b.ts',depth:2}]);
const tc = makeTc([{module:'src/a.ts',test_files:['test/a.test.ts'],symbols_covered:['foo'],coverage:'Yes'},{module:'src/b.ts',test_files:[],symbols_covered:[],coverage:'None'}]);
const r = runScript(br, tc, 50);
assert.equal(r.exitCode, 0);
assert.equal(r.output.passed, true);
assert.equal(r.output.coverage_percentage, 50);
});
test('coverage below threshold (50% with threshold 80)', () => {
const br = makeBr([{name:'foo',file:'src/a.ts',depth:1},{name:'bar',file:'src/b.ts',depth:2}]);
const tc = makeTc([{module:'src/a.ts',test_files:['test/a.test.ts'],symbols_covered:['foo'],coverage:'Yes'},{module:'src/b.ts',test_files:[],symbols_covered:[],coverage:'None'}]);
const r = runScript(br, tc, 80);
assert.equal(r.exitCode, 1);
assert.equal(r.output.passed, false);
});
test('empty blast radius', () => {
const br = makeBr([]);
const tc = makeTc([]);
const r = runScript(br, tc);
assert.equal(r.exitCode, 0);
assert.equal(r.output.passed, true);
assert.ok(r.output.note.includes('Empty blast radius'));
});
test('no test coverage at all', () => {
const br = makeBr([{name:'foo',file:'src/a.ts',depth:1},{name:'bar',file:'src/b.ts',depth:2}]);
const tc = makeTc([{module:'src/a.ts',test_files:[],symbols_covered:[],coverage:'None'},{module:'src/b.ts',test_files:[],symbols_covered:[],coverage:'None'}]);
const r = runScript(br, tc);
assert.equal(r.exitCode, 1);
assert.equal(r.output.passed, false);
assert.equal(r.output.uncovered_nodes, 2);
});
test('partial coverage handling', () => {
const br = makeBr([{name:'foo',file:'src/a.ts',depth:1},{name:'helper',file:'src/a.ts',depth:2},{name:'bar',file:'src/b.ts',depth:1}]);
const tc = makeTc([{module:'src/a.ts',test_files:['test/a.test.ts'],symbols_covered:['foo','helper'],coverage:'Partial:1'},{module:'src/b.ts',test_files:[],symbols_covered:[],coverage:'None'}]);
const r = runScript(br, tc);
assert.equal(r.exitCode, 1);
assert.equal(r.output.covered_nodes, 1);
assert.equal(r.output.uncovered_nodes, 2);
});
test('threshold 0 flag-only mode', () => {
const br = makeBr([{name:'foo',file:'src/a.ts',depth:1}]);
const tc = makeTc([{module:'src/a.ts',test_files:[],symbols_covered:[],coverage:'None'}]);
const r = runScript(br, tc, 0);
assert.equal(r.exitCode, 0);
assert.equal(r.output.passed, true);
});
test('threshold 100 strict mode', () => {
const br = makeBr([{name:'foo',file:'src/a.ts',depth:1},{name:'bar',file:'src/b.ts',depth:2}]);
const tc = makeTc([{module:'src/a.ts',test_files:['test/a.test.ts'],symbols_covered:['foo'],coverage:'Yes'},{module:'src/b.ts',test_files:[],symbols_covered:[],coverage:'None'}]);
const r = runScript(br, tc, 100);
assert.equal(r.exitCode, 1);
assert.equal(r.output.passed, false);
});
test('missing --test-coverage arg → exit 1 and emit error', () => {
const br = makeBr([{name:'foo',file:'src/a.ts',depth:1}]);
const brPath = join(TMP, `br-err-${Date.now()}.json`);
writeFileSync(brPath, JSON.stringify(br), 'utf8');
try {
execFileSync(process.execPath, [SCRIPT_PATH, '--blast-radius', brPath], { encoding: 'utf8', timeout: 5000 });
assert.fail('should have thrown');
} catch (e) {
assert.ok(e.status === 1 || e.status === null, `expected 1 or null, got ${e.status}`);
const stderr = Buffer.isBuffer(e.stderr) ? e.stderr.toString('utf8') : (e.stderr || '');
assert.ok(stderr.includes('Missing') || (e.stdout || '').includes('TIMEOUT'),
`expected error about missing arg, got stderr: ${stderr}, stdout: ${e.stdout}`);
} finally {
try { unlinkSync(brPath); } catch {}
}
});
console.log(`\nResults: ${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);