import { test } from 'node:test'; import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { createWorktree, worktreeDiff, removeWorktree, worktreeDirFor, branchFor } from '../src/executor/worktree.js'; function gitSync(cwd: string, args: string[]): string { return execFileSync('git', args, { cwd, encoding: 'utf8' }); } /** /tmp 下建一个带 1 个 commit 的真实 git repo(main 分支) */ function makeRepo(): string { const repo = mkdtempSync(join(tmpdir(), 'maestro-wt-repo-')); gitSync(repo, ['init', '-b', 'main']); gitSync(repo, ['config', 'user.name', 'maestro-test']); gitSync(repo, ['config', 'user.email', 'test@maestro.local']); writeFileSync(join(repo, 'README.md'), '# demo\n'); gitSync(repo, ['add', '-A']); gitSync(repo, ['commit', '-m', 'init']); return repo; } test('worktree:create / diff / remove 在真实 git repo 上工作', async (t) => { // worktree 根目录隔离到临时 MAESTRO_DATA_DIR,不碰 ~/.maestro const dataDir = mkdtempSync(join(tmpdir(), 'maestro-wt-data-')); const prevDataDir = process.env.MAESTRO_DATA_DIR; process.env.MAESTRO_DATA_DIR = dataDir; const repo = makeRepo(); t.after(() => { if (prevDataDir === undefined) delete process.env.MAESTRO_DATA_DIR; else process.env.MAESTRO_DATA_DIR = prevDataDir; rmSync(dataDir, { recursive: true, force: true }); rmSync(repo, { recursive: true, force: true }); }); const taskId = 'tsk_test123'; // 1) 创建:目录与分支符合约定 const wt = await createWorktree(repo, taskId, 'main'); assert.equal(wt.branch, branchFor(taskId)); assert.equal(wt.dir, worktreeDirFor(repo, taskId)); assert.ok(existsSync(join(wt.dir, 'README.md')), 'worktree 应检出 README.md'); const head = gitSync(wt.dir, ['rev-parse', '--abbrev-ref', 'HEAD']).trim(); assert.equal(head, wt.branch); // 2) 空分支 diff:无 commit、无摘要 const empty = await worktreeDiff(repo, wt.dir, wt.branch, 'main'); assert.equal(empty.commits.length, 0); assert.equal(empty.diffSummary, ''); // 3) 在 worktree 提交一笔改动 → diff 能看到 commit 与 --stat 摘要 writeFileSync(join(wt.dir, 'README.md'), '# demo\nhello maestro\n'); gitSync(wt.dir, ['add', '-A']); gitSync(wt.dir, ['commit', '-m', `maestro(${taskId}): tweak readme`]); const diff = await worktreeDiff(repo, wt.dir, wt.branch, 'main'); assert.equal(diff.commits.length, 1); assert.ok(diff.commits[0].includes(taskId), `commit 行应含任务 id:${diff.commits[0]}`); assert.ok(diff.diffSummary.includes('README.md'), `diffSummary 应含文件名:${diff.diffSummary}`); // 4) 已存在同名 worktree/分支时重建(重试场景):从 main 干净重来 const wt2 = await createWorktree(repo, taskId, 'main'); assert.equal(wt2.dir, wt.dir); const diff2 = await worktreeDiff(repo, wt2.dir, wt2.branch, 'main'); assert.equal(diff2.commits.length, 0, '重建后分支应回到 baseBranch,无旧 commit'); // 5) remove:目录与注册项移除,主 repo 不受影响 await removeWorktree(repo, wt2.dir); assert.ok(!existsSync(wt2.dir), 'worktree 目录应被移除'); const list = gitSync(repo, ['worktree', 'list']); assert.ok(!list.includes(taskId), 'git worktree list 不应再含该任务'); assert.ok(existsSync(join(repo, 'README.md')), '主 repo 不受影响'); }); test('worktree:错误透传(不存在的 baseBranch)', async (t) => { const dataDir = mkdtempSync(join(tmpdir(), 'maestro-wt-data2-')); const prevDataDir = process.env.MAESTRO_DATA_DIR; process.env.MAESTRO_DATA_DIR = dataDir; const repo = makeRepo(); t.after(() => { if (prevDataDir === undefined) delete process.env.MAESTRO_DATA_DIR; else process.env.MAESTRO_DATA_DIR = prevDataDir; rmSync(dataDir, { recursive: true, force: true }); rmSync(repo, { recursive: true, force: true }); }); await assert.rejects( () => createWorktree(repo, 'tsk_badbase', 'no-such-branch'), /git worktree add/, ); });