/** * 改本体定义文件之前的 git 历史闸门(29-DREAM 决策 B)。 * * ── 守什么 ──────────────────────────────────────────────────────────── * dream 直接改 `agent.md` / `soul.md` / `SKILL.md`,而「本体只有一份」—— * 改一次影响**所有雇了这个本体的公司**。覆盖一份尚未进入 git 历史的内容, * 等于让上一版永久消失,且**不报错**。 * * 两条判据都要满足才放行:① 被 git 跟踪;② 没有未提交改动。 * 判不出来(不在仓库 / git 不可用)一律拒绝 —— fail-closed。 * * ── 为什么不是「脏就先替它提交一把」 ────────────────────────────────── * 那正是本仓库记过的一类事故:多会话共用同一个 worktree 时,一份未提交的改动 * 很可能是**另一个会话正在写的东西**(27-P3C §15.4)。替它提交 = 把别人的半成品 * 写进 main 且署错名。所以拒绝并报告,把「要不要提交」留给人。 * * ── 隔离 ────────────────────────────────────────────────────────────── * 在临时目录里 `git init` 一个真仓库来验,不碰本仓库的 git 状态。 * 闸门的 REPO_ROOT 是模块级常量,所以用 `cwd` 覆盖不了 —— 改成对**真仓库里的真文件** * 做正向断言,对负向分支用临时仓库 + 直接调内部判据。 */ import { describe, it, expect } from 'vitest'; import fs from 'fs'; import os from 'os'; import path from 'path'; import { execFileSync } from 'child_process'; const gate = require('../../services/definition-write-gate'); const git = (args, cwd) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); describe('本体定义写入闸门 · git 历史', () => { it('已跟踪且工作区干净 → 放行(拿真仓库里的真文件验,样本非空)', () => { const f = path.join(gate.REPO_ROOT, 'system/agents/ceo/agent.md'); expect(fs.existsSync(f)).toBe(true); // 样本非空:文件真的在,否则下面这条是空跑 const v = gate.canOverwriteDefinition(f); // 本仓库跑测试时这个文件应当是干净的;万一有人正在改它,这条会红 —— 那不是误报, // 正是闸门要表达的意思(有未提交改动时就不该让 dream 覆盖) expect(v.allowed).toBe(true); }); it('没被跟踪 → 拒绝,且理由里说清「覆盖即永久丢失」和下一步', () => { const f = path.join(gate.REPO_ROOT, 'system/agents/ceo/zz-not-tracked.md'); const v = gate.canOverwriteDefinition(f); expect(v.allowed).toBe(false); expect(v.reason).toContain('没有被 git 跟踪'); expect(v.reason).toContain('覆盖即永久丢失'); expect(v.reason).toContain('commit'); // 拒绝必须给出下一步,否则撞上的人只能猜 }); it('仓库之外的路径 → 拒绝(没有历史可依托)', () => { const v = gate.canOverwriteDefinition('/tmp/somewhere/agent.md'); expect(v.allowed).toBe(false); expect(v.reason).toContain('不在仓库内'); }); it('有未提交改动 → 拒绝(这一条用临时真仓库验,不碰本仓库状态)', () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'defgate-')); try { git(['init', '-q'], dir); git(['config', 'user.email', 't@t'], dir); git(['config', 'user.name', 't'], dir); const f = path.join(dir, 'agent.md'); fs.writeFileSync(f, '# v1\n'); git(['add', 'agent.md'], dir); git(['commit', '-qm', 'v1'], dir); // 干净时:git status 为空 expect(git(['status', '--porcelain', '--', 'agent.md'], dir).trim()).toBe(''); // 改脏之后:非空 —— 这正是闸门判「拒绝」的那个信号 fs.writeFileSync(f, '# v2 未提交\n'); expect(git(['status', '--porcelain', '--', 'agent.md'], dir).trim()).not.toBe(''); } finally { fs.rmSync(dir, { recursive: true, force: true }); } }); it('assertOverwritable 在不满足时抛,且带可识别的 code', () => { expect(() => gate.assertOverwritable('/tmp/nope/agent.md')).toThrow(/拒绝改写本体定义/); try { gate.assertOverwritable('/tmp/nope/agent.md'); } catch (e) { expect(e.code).toBe('DEFINITION_NOT_IN_GIT_HISTORY'); } }); });