/** * triager-tools 单测 */ const fs = require('fs'); const path = require('path'); const os = require('os'); const TEST_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'devloop-tr-')); process.env.HOME = TEST_HOME; const pathResolver = require('../../../services/devloop/path-resolver'); const docStore = require('../../../services/devloop/doc-store'); const tools = require('../../../src/tools/devloop/triager-tools'); function setupProject() { pathResolver._resetForTesting(); const root = fs.mkdtempSync(path.join(os.tmpdir(), 'tr-proj-')); const pid = 'p-triagertest'; const reg = { [pid]: { root_path: root, mode: 'linked-new', company_id: 'c-test' }, }; fs.mkdirSync(path.join(TEST_HOME, '.devloop'), { recursive: true }); fs.writeFileSync( path.join(TEST_HOME, '.devloop', 'linked-projects.json'), JSON.stringify(reg) ); pathResolver._resetForTesting(); return { pid, root }; } describe('triager-tools', () => { let env, ctx; beforeEach(() => { env = setupProject(); ctx = { pid: env.pid, agent: 'triager' }; }); afterEach(() => { fs.rmSync(env.root, { recursive: true, force: true }); }); describe('extract_evidence', () => { it('verifies a quote present in a file', async () => { const target = path.join(env.root, 'unit-x.js'); fs.writeFileSync(target, 'const VAL = 42;\nfunction foo() { return VAL; }\n'); const r = await tools.extract_evidence.execute({ evidence: ['unit-x.js: function foo() { return VAL'], }, ctx); expect(r.ok).toBe(true); expect(r.results[0].ok).toBe(true); }); it('rejects malformed evidence', async () => { const r = await tools.extract_evidence.execute({ evidence: ['no-colon-no-quote'], }, ctx); expect(r.ok).toBe(false); expect(r.failed[0].reason).toMatch(/malformed/); }); it('reports quote not found', async () => { const target = path.join(env.root, 'file.js'); fs.writeFileSync(target, 'function bar() {}'); const r = await tools.extract_evidence.execute({ evidence: ['file.js: never-going-to-find-this'], }, ctx); expect(r.ok).toBe(false); expect(r.failed[0].reason).toMatch(/not found/); }); }); describe('category_classifier', () => { it('flags infra_flake on EADDRINUSE in process log', async () => { const r = await tools.category_classifier.execute({ result_json: { failures: [] }, process_log: 'Error: listen EADDRINUSE 0.0.0.0:3000', }, ctx); expect(r.category).toBe('infra_flake'); }); it('flags impl_bug on 500 vs 4xx', async () => { const r = await tools.category_classifier.execute({ result_json: { failures: [{ got: { status: 500 }, expected: { status: 401 } }] }, process_log: '', }, ctx); expect(r.category).toBe('impl_bug'); }); it('falls back to contract_mismatch with low confidence', async () => { const r = await tools.category_classifier.execute({ result_json: { failures: [] }, process_log: '', sdd_text: 'this sdd describes everything', }, ctx); expect(r.category).toBe('contract_mismatch'); expect(r.confidence).toBeLessThanOrEqual(0.5); }); }); describe('diff_contract_vs_impl', () => { it('reports divergence when endpoint not in code', async () => { const unitId = 'u-x'; const unitDir = path.join(env.root, 'units', unitId); fs.mkdirSync(path.join(unitDir, 'code'), { recursive: true }); fs.writeFileSync( path.join(unitDir, 'contract.json'), JSON.stringify({ exports: { http: [{ method: 'POST', path: '/api/login' }] }, }) ); fs.writeFileSync( path.join(unitDir, 'code', 'server.js'), 'function nothing(){}' ); const r = await tools.diff_contract_vs_impl.execute({ unit_id: unitId }, ctx); expect(r.matches).toBe(false); expect(r.divergences.some(d => d.includes('/api/login'))).toBe(true); }); it('matches when endpoint appears in code', async () => { const unitId = 'u-y'; const unitDir = path.join(env.root, 'units', unitId); fs.mkdirSync(path.join(unitDir, 'code'), { recursive: true }); fs.writeFileSync( path.join(unitDir, 'contract.json'), JSON.stringify({ exports: { http: [{ method: 'GET', path: '/api/ping' }] }, }) ); fs.writeFileSync( path.join(unitDir, 'code', 'server.js'), `app.get('/api/ping', (q,r) => r.send('pong'))` ); const r = await tools.diff_contract_vs_impl.execute({ unit_id: unitId }, ctx); expect(r.matches).toBe(true); }); }); describe('find_similar_bug', () => { it('returns empty when no bugs.ndjson yet', async () => { const r = await tools.find_similar_bug.execute({ signature: { unit_id: 'u-none', test_name: 't' }, }, ctx); expect(r.ok).toBe(true); expect(r.total).toBe(0); }); }); });