tester-tools.test.js 4.83 KB
/**
 * tester-tools 单测
 */

const fs = require('fs');
const path = require('path');
const os = require('os');

const TEST_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'devloop-tt-'));
process.env.HOME = TEST_HOME;

const pathResolver = require('../../../services/devloop/path-resolver');
const tools = require('../../../src/tools/devloop/tester-tools');

function setupProject() {
  pathResolver._resetForTesting();
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'tt-proj-'));
  const pid = 'p-testertest';
  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('tester-tools', () => {
  let env, ctx;
  beforeEach(() => {
    env = setupProject();
    ctx = { pid: env.pid, agent: 'tester' };
  });
  afterEach(() => {
    fs.rmSync(env.root, { recursive: true, force: true });
  });

  describe('hint_builder', () => {
    it('hints on TypeError compare (bcrypt-like)', async () => {
      const r = await tools.hint_builder.execute({
        stack: 'TypeError: Illegal arguments: undefined,string compare',
      }, ctx);
      expect(r.hint).toMatch(/bcrypt\.compare/);
    });

    it('hints on 401 expected vs 500 got', async () => {
      const r = await tools.hint_builder.execute({
        expected: 401, got: 500, stack: '',
      }, ctx);
      expect(r.hint).toMatch(/401/);
    });

    it('parses status code from stack', async () => {
      const r = await tools.hint_builder.execute({
        stack: 'Expected: 200\nReceived: 404',
      }, ctx);
      expect(r.hint).toMatch(/200/);
    });
  });

  describe('parse_vitest', () => {
    it('returns crashed when raw json missing', async () => {
      const r = await tools.parse_vitest.execute({
        raw_json_path: path.join(env.root, 'nope.json'),
      }, ctx);
      expect(r.ok).toBe(false);
      expect(r.crashed).toBe(true);
    });

    it('parses passing vitest output', async () => {
      const rawPath = path.join(env.root, 'raw.json');
      fs.writeFileSync(rawPath, JSON.stringify({
        testResults: [{
          assertionResults: [
            { status: 'passed', fullName: 'a' },
            { status: 'passed', fullName: 'b' },
          ],
        }],
      }));
      const r = await tools.parse_vitest.execute({ raw_json_path: rawPath }, ctx);
      expect(r.ok).toBe(true);
      expect(r.summary.passed).toBe(2);
      expect(r.failures).toHaveLength(0);
    });

    it('extracts failure with hint', async () => {
      const rawPath = path.join(env.root, 'raw2.json');
      fs.writeFileSync(rawPath, JSON.stringify({
        testResults: [{
          assertionResults: [{
            status: 'failed', fullName: 'login returns 401',
            failureMessages: ['Expected: 401\nReceived: 500\nat handler'],
          }],
        }],
      }));
      const r = await tools.parse_vitest.execute({ raw_json_path: rawPath }, ctx);
      expect(r.ok).toBe(false);
      expect(r.summary.failed).toBe(1);
      expect(r.failures[0].hint).toMatch(/401/);
    });

    it('writes result.json when result_out_path given', async () => {
      const rawPath = path.join(env.root, 'raw3.json');
      fs.writeFileSync(rawPath, JSON.stringify({
        testResults: [{ assertionResults: [{ status: 'passed', fullName: 't' }] }],
      }));
      const out = path.join(env.root, 'result.json');
      await tools.parse_vitest.execute({
        raw_json_path: rawPath, result_out_path: out,
      }, ctx);
      expect(fs.existsSync(out)).toBe(true);
    });
  });

  describe('bug_open / bug_list / bug_transition', () => {
    it('opens a bug then lists it', async () => {
      const r = await tools.bug_open.execute({
        title: 'login 500 instead of 401',
        unit_id: 'u-be-auth',
        test_name: 'login wrong password',
        severity: 'high',
      }, ctx);
      expect(r.ok).toBe(true);
      expect(r.bug_id).toBeTruthy();

      const ls = await tools.bug_list.execute({ unit_id: 'u-be-auth' }, ctx);
      expect(ls.total).toBeGreaterThanOrEqual(1);
    });

    it('transitions bug open → fixing', async () => {
      const opened = await tools.bug_open.execute({
        title: 'x', unit_id: 'u-a', test_name: 't',
      }, ctx);
      const r = await tools.bug_transition.execute({
        bug_id: opened.bug_id, to: 'fixing', note: 'coder picked up',
      }, ctx);
      expect(r.ok).toBe(true);
    });

    it('rejects invalid bug transition', async () => {
      const opened = await tools.bug_open.execute({
        title: 'y', unit_id: 'u-b', test_name: 't',
      }, ctx);
      await expect(tools.bug_transition.execute({
        bug_id: opened.bug_id, to: 'verified',
      }, ctx)).rejects.toThrow();
    });
  });
});