/** * 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); }); });