triager-tools.test.js 4.91 KB
/**
 * 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);
    });
  });
});