/** * gitVault 单测 * * 需要系统装了 git。如果没装则 skip 所有用例。 */ const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); const pathResolver = require('../../services/devloop/path-resolver'); const gitVault = require('../../services/devloop/git-vault'); const docStore = require('../../services/devloop/doc-store'); let gitAvailable = false; try { execSync('git --version', { stdio: 'ignore' }); gitAvailable = true; } catch { /* skip */ } (gitAvailable ? describe : describe.skip)('gitVault', () => { let pid, root; beforeEach(() => { pathResolver._resetForTesting(); gitVault._resetMutexesForTesting(); const r = pathResolver.allocateProjectRoot({ company_id: 'c-test', mode: 'sandbox' }); pid = r.pid; root = r.root_path; }); afterEach(() => { fs.rmSync(root, { recursive: true, force: true }); }); it('init creates .git + initial commit', async () => { const r = await gitVault.init(pid); expect(r.ok).toBe(true); expect(fs.existsSync(path.join(root, '.git'))).toBe(true); expect(fs.existsSync(path.join(root, '.gitignore'))).toBe(true); const logs = await gitVault.log(pid); expect(logs.length).toBeGreaterThanOrEqual(1); expect(logs[0].summary).toMatch(/^init: project p-/); }); it('init is idempotent', async () => { await gitVault.init(pid); const r = await gitVault.init(pid); expect(r.already_exists).toBe(true); }); it('commit with content creates new sha', async () => { await gitVault.init(pid); // 写一个真实的文件再 commit (PRD/HTML 校验严格,这里直接 raw) fs.writeFileSync(path.join(root, 'README.md'), 'hello\n'); const r = await gitVault.commit(pid, { kind: 'chore', summary: 'add README', author: { name: 'PM Agent', email: 'pm@devloop.local' }, }); expect(r.ok).toBe(true); expect(r.skipped).toBeFalsy(); expect(r.sha).toMatch(/^[a-f0-9]{40}$/); // author 被记录 const logs = await gitVault.log(pid, { limit: 1 }); expect(logs[0].author_name).toBe('PM Agent'); expect(logs[0].author_email).toBe('pm@devloop.local'); }); it('commit with nothing-to-commit returns skipped', async () => { await gitVault.init(pid); const r = await gitVault.commit(pid, { kind: 'chore', summary: 'empty commit attempt', }); expect(r.ok).toBe(true); expect(r.skipped).toBe(true); }); it('tag + listTags', async () => { await gitVault.init(pid); await gitVault.tag(pid, 'shipped-v1'); const tags = await gitVault.listTags(pid); expect(tags).toContain('shipped-v1'); }); it('status detects dirty tree', async () => { await gitVault.init(pid); let s = await gitVault.status(pid); expect(s.dirty).toBe(false); fs.writeFileSync(path.join(root, 'untracked.txt'), 'x'); s = await gitVault.status(pid); expect(s.dirty).toBe(true); }); it('resetWorkingTree clears dirty', async () => { await gitVault.init(pid); fs.writeFileSync(path.join(root, 'dirt.txt'), 'x'); expect((await gitVault.status(pid)).dirty).toBe(true); await gitVault.resetWorkingTree(pid); expect((await gitVault.status(pid)).dirty).toBe(false); }); it('archive produces zip', async () => { await gitVault.init(pid); fs.writeFileSync(path.join(root, 'kept.txt'), 'x'); await gitVault.commit(pid, { kind: 'chore', summary: 'add file' }); const r = await gitVault.archive(pid); expect(fs.existsSync(r.path)).toBe(true); fs.rmSync(r.path); }); });