git-vault.test.js 3.5 KB
/**
 * 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);
  });
});