integrator-tools.test.js 4.93 KB
/**
 * integrator-tools 单测
 */

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

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

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

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

  describe('port_picker', () => {
    it('allocates the requested number of free ports', async () => {
      const r = await tools.port_picker.execute({ count: 3 }, ctx);
      expect(r.ok).toBe(true);
      expect(r.ports).toHaveLength(3);
      for (const p of r.ports) {
        expect(p).toBeGreaterThanOrEqual(18000);
        expect(p).toBeLessThan(19000);
      }
      expect(new Set(r.ports).size).toBe(3); // unique
    });
  });

  describe('parse_placeholder', () => {
    it('resolves {{step0.body.field}} from prev results', async () => {
      const r = await tools.parse_placeholder.execute({
        template: { token: '{{step0.token}}' },
        previous_steps: [{ body: { token: 'abc123' } }],
      }, ctx);
      expect(r.resolved.token).toBe('abc123');
    });

    it('throws when step ref missing', async () => {
      await expect(tools.parse_placeholder.execute({
        template: { x: '{{step5.foo}}' },
        previous_steps: [],
      }, ctx)).rejects.toThrow(/placeholder step5/);
    });
  });

  describe('health_wait', () => {
    it('returns ok=true when server responds 200', async () => {
      const srv = http.createServer((_req, res) => {
        res.statusCode = 200; res.end('ok');
      });
      await new Promise(r => srv.listen(0, '127.0.0.1', r));
      const port = srv.address().port;

      const r = await tools.health_wait.execute({
        url: `http://127.0.0.1:${port}/api/health`,
        timeout_ms: 2000, interval_ms: 50,
      }, ctx);
      srv.close();
      expect(r.ok).toBe(true);
      expect(r.status).toBe(200);
    });

    it('returns ok=false on timeout', async () => {
      const r = await tools.health_wait.execute({
        url: 'http://127.0.0.1:1/never',
        timeout_ms: 300, interval_ms: 100,
      }, ctx);
      expect(r.ok).toBe(false);
      expect(r.timeout).toBe(true);
    });
  });

  describe('collect_stderr', () => {
    it('returns last N lines from log', async () => {
      const p = path.join(env.root, 'process.log');
      fs.writeFileSync(p, ['l1','l2','l3','l4','l5'].join('\n'));
      const r = await tools.collect_stderr.execute({ log_path: p, lines: 2 }, ctx);
      expect(r.ok).toBe(true);
      expect(r.excerpt).toBe('l4\nl5');
    });

    it('returns ok=false when log missing', async () => {
      const r = await tools.collect_stderr.execute({
        log_path: path.join(env.root, 'nope.log'),
      }, ctx);
      expect(r.ok).toBe(false);
    });
  });

  describe('run_case', () => {
    it('executes a single GET case against a test server', async () => {
      const srv = http.createServer((req, res) => {
        if (req.url === '/api/ping') {
          res.setHeader('Content-Type', 'application/json');
          res.end(JSON.stringify({ pong: true }));
          return;
        }
        res.statusCode = 404; res.end();
      });
      await new Promise(r => srv.listen(0, '127.0.0.1', r));
      const port = srv.address().port;

      const r = await tools.run_case.execute({
        case_spec: {
          steps: [
            { unit: 'u-x', call: 'GET /api/ping', expect: { status: 200 } },
          ],
        },
        ports: { 'u-x': port },
      }, ctx);
      srv.close();
      expect(r.ok).toBe(true);
      expect(r.steps_ran).toBe(1);
    });

    it('reports failed step when status mismatches', async () => {
      const srv = http.createServer((_req, res) => {
        res.statusCode = 500; res.end('{}');
      });
      await new Promise(r => srv.listen(0, '127.0.0.1', r));
      const port = srv.address().port;
      const r = await tools.run_case.execute({
        case_spec: {
          steps: [{ unit: 'u-x', call: 'GET /api/x', expect: { status: 200 } }],
        },
        ports: { 'u-x': port },
      }, ctx);
      srv.close();
      expect(r.ok).toBe(false);
      expect(r.failed_step).toBe(0);
      expect(r.got.status).toBe(500);
    });
  });
});