graph-engine-target-dir.test.js 6.37 KB
/**
 * Unit Tests — graph-engine 的公司级目标代码目录(39 号文档 P5)
 *
 * 验的是 `ensureProjectRoot` 在配了 `company.json.target_dir` 时的行为:
 *   T1 没配 target_dir:走老的 sandbox 路径,行为不变(零回归)
 *   T2 配了 target_dir:两个不同 root_run_id 的 run(同一公司)解析到同一个 root_path
 *   T3 target_dir 校验失败(git 树不干净):ensureProjectRoot 优雅降级成 null + timeline 记录,
 *      不静默 fallback 回 sandbox
 *   T4 GRAPH_RUNS_DIR 测试覆盖优先级高于 target_dir(不能让业务配置绕过测试隔离)
 *
 * `ensureProjectRoot` 本身不碰 SQLite(只读写文件系统),所以不需要像
 * graph-boundary.test.js 那样做 GRAPH_DB_PATH 隔离;但会真的往 `data/companies/` 写
 * 东西(company.json 走 companyRootDir,不认测试注入,除非设 GRAPH_RUNS_DIR——T1-T3
 * 就是要验「不设 GRAPH_RUNS_DIR 时的生产行为」,所以只能用真路径 + 用后即删,
 * 手法同 path-resolver.test.js 的 sandbox 用例)。
 */
const fs = require('fs');
const os = require('os');
const path = require('path');

const engine = require('../../services/graph-engine');
const { companyRootDir } = require('../../services/company-paths');

describe('graph-engine 公司级目标代码目录(target_dir)', () => {
  const cleanups = [];

  afterEach(() => {
    delete process.env.GRAPH_RUNS_DIR;
    while (cleanups.length) {
      const p = cleanups.pop();
      try { fs.rmSync(p, { recursive: true, force: true }); } catch { /* best-effort */ }
    }
  });

  function writeCompany(companyId, patch) {
    const dir = companyRootDir(companyId);
    fs.mkdirSync(dir, { recursive: true });
    cleanups.push(dir);
    fs.writeFileSync(path.join(dir, 'company.json'), JSON.stringify({ id: companyId, name: companyId, ...patch }));
    return dir;
  }

  function fakeRun(companyId, rootRunId) {
    return { root_run_id: rootRunId, company_id: companyId };
  }

  it('T1 没配 target_dir:走 sandbox,行为跟改动前一样', () => {
    const cid = 'tdcfg-t1';
    writeCompany(cid, {});
    const run = fakeRun(cid, 'r-t1');
    const rec = engine.ensureProjectRoot({ run });
    expect(rec).not.toBe(null);
    expect(rec.mode).toBe('sandbox');
    expect(rec.root_path).toContain(path.join('data', 'companies', cid, 'projects'));
    expect(fs.existsSync(rec.root_path)).toBe(true);
    cleanups.push(path.join(companyRootDir(cid), '..', 'runs')); // 无害;下面按 run 目录精确清
    cleanups.push(path.dirname(path.dirname(rec.root_path))); // projects/ 目录
    cleanups.push(path.join(companyRootDir(cid), 'runs', run.root_run_id));
  });

  it('T2 配了 target_dir:同一公司两个不同 run 解析到同一个 root_path,且不产生 pid 目录', () => {
    const cid = 'tdcfg-t2';
    const target = fs.mkdtempSync(path.join(os.tmpdir(), 'graph-engine-target-'));
    cleanups.push(target);
    fs.writeFileSync(path.join(target, 'README.md'), '# existing repo, no .git');
    writeCompany(cid, { target_dir: target, target_mode: 'linked-existing' });

    const runA = fakeRun(cid, 'r-t2-a');
    const runB = fakeRun(cid, 'r-t2-b');
    cleanups.push(path.join(companyRootDir(cid), 'runs', runA.root_run_id));
    cleanups.push(path.join(companyRootDir(cid), 'runs', runB.root_run_id));

    const recA = engine.ensureProjectRoot({ run: runA });
    const recB = engine.ensureProjectRoot({ run: runB });

    expect(recA).not.toBe(null);
    expect(recB).not.toBe(null);
    expect(recA.root_path).toBe(target);
    expect(recB.root_path).toBe(target);
    expect(recA.mode).toBe('linked-existing');
    // 没有新的 sandbox pid 目录被造出来
    const projectsDir = path.join(companyRootDir(cid), 'projects');
    expect(fs.existsSync(projectsDir)).toBe(false);
  });

  it('T2b target_mode 缺省时按 linked-existing 处理(目标目录本来就该已经存在)', () => {
    const cid = 'tdcfg-t2b';
    const target = fs.mkdtempSync(path.join(os.tmpdir(), 'graph-engine-target-'));
    cleanups.push(target);
    writeCompany(cid, { target_dir: target }); // 不传 target_mode

    const run = fakeRun(cid, 'r-t2b');
    cleanups.push(path.join(companyRootDir(cid), 'runs', run.root_run_id));

    const rec = engine.ensureProjectRoot({ run });
    expect(rec.root_path).toBe(target);
    expect(rec.mode).toBe('linked-existing');
  });

  it('T3 target_dir 校验失败(git 树不干净):不静默降级回 sandbox,返回 null', () => {
    const { execFileSync } = require('child_process');
    const cid = 'tdcfg-t3';
    const target = fs.mkdtempSync(path.join(os.tmpdir(), 'graph-engine-target-dirty-'));
    cleanups.push(target);
    execFileSync('git', ['init', '-q', '-b', 'main'], { cwd: target });
    execFileSync('git', ['config', 'user.email', 't@l'], { cwd: target });
    execFileSync('git', ['config', 'user.name', 't'], { cwd: target });
    fs.writeFileSync(path.join(target, 'a.txt'), 'one');
    execFileSync('git', ['add', '-A'], { cwd: target });
    execFileSync('git', ['commit', '-q', '-m', 'init'], { cwd: target });
    fs.writeFileSync(path.join(target, 'b.txt'), 'untracked, dirty tree');

    writeCompany(cid, { target_dir: target, target_mode: 'linked-existing' });
    const run = fakeRun(cid, 'r-t3');
    cleanups.push(path.join(companyRootDir(cid), 'runs', run.root_run_id));

    const rec = engine.ensureProjectRoot({ run });
    expect(rec).toBe(null); // 优雅失败,不是编一个假 sandbox 路径

    const projectsDir = path.join(companyRootDir(cid), 'projects');
    expect(fs.existsSync(projectsDir)).toBe(false); // 确认真的没有偷偷退化成 sandbox
  });

  it('T4 GRAPH_RUNS_DIR 测试覆盖优先级高于 target_dir(测试隔离不能被业务配置绕过)', () => {
    const cid = 'tdcfg-t4';
    const target = fs.mkdtempSync(path.join(os.tmpdir(), 'graph-engine-target-override-'));
    cleanups.push(target);
    writeCompany(cid, { target_dir: target, target_mode: 'linked-existing' });

    const runsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'graph-engine-runs-override-'));
    cleanups.push(runsDir);
    process.env.GRAPH_RUNS_DIR = runsDir;

    const run = fakeRun(cid, 'r-t4');
    const rec = engine.ensureProjectRoot({ run });
    expect(rec.mode).toBe('runs-dir-override');
    expect(rec.root_path).toBe(path.join(runsDir, run.root_run_id, 'project'));
    expect(rec.root_path).not.toBe(target);
  });
});