import { test } from 'node:test'; import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { Store } from '../src/store/index.js'; import { acceptAndMerge } from '../src/executor/exec-merge.js'; import type { Task } from '../src/model/types.js'; const noopLog = { info() {}, error() {} }; function git(cwd: string, args: string[]): string { return execFileSync('git', args, { cwd, encoding: 'utf8' }); } /** 独立 dataDir + 真实 git repo(main 含 1 commit),t.after 收尾。 */ function setup(t: { after: (fn: () => void) => void }): string { const dataDir = mkdtempSync(join(tmpdir(), 'maestro-em-data-')); const prev = process.env.MAESTRO_DATA_DIR; process.env.MAESTRO_DATA_DIR = dataDir; const repo = mkdtempSync(join(tmpdir(), 'maestro-em-repo-')); git(repo, ['init', '-b', 'main']); git(repo, ['config', 'user.name', 'em']); git(repo, ['config', 'user.email', 'em@t']); writeFileSync(join(repo, 'README.md'), '# demo\n'); git(repo, ['add', '-A']); git(repo, ['commit', '-qm', 'init']); t.after(() => { if (prev === undefined) delete process.env.MAESTRO_DATA_DIR; else process.env.MAESTRO_DATA_DIR = prev; rmSync(dataDir, { recursive: true, force: true }); rmSync(repo, { recursive: true, force: true }); }); return repo; } /** 在 store 里把任务推到 exec_review 且带 result(双 verdict=approve)。 */ function taskAtExecReview(s: Store, projectId: string, branch: string): Task { const t = s.createTask({ projectId, title: 'feat', complexity: 'easy' }); s.setOperations(t.id, 'do'); s.transition(t.id, 'queued'); s.transition(t.id, 'executing'); s.setResult(t.id, { branch, worktree: null, diffSummary: '', commits: [], prUrl: null, summary: 'ok', verdict: 'approve', securitySummary: 'ok', securityVerdict: 'approve', mergeTaskId: null, }); s.transition(t.id, 'exec_review'); return s.getTask(t.id)!; } test('acceptAndMerge:可合并 → 任务 done + result.prUrl=merged:', async (t) => { const repo = setup(t); const s = new Store(':memory:'); const p = s.createProject({ name: 'em', repoPath: repo }); const branch = 'maestro/tsk_ok'; git(repo, ['checkout', '-q', '-b', branch]); writeFileSync(join(repo, 'feat.txt'), 'F\n'); git(repo, ['add', '-A']); git(repo, ['commit', '-qm', 'maestro(tsk_ok): feat']); git(repo, ['switch', '-q', '--detach']); // 释放 main,让合并走临时 worktree const task = taskAtExecReview(s, p.id, branch); const outcome = await acceptAndMerge(s, noopLog, task, 'test'); assert.equal(outcome.ok, true, outcome.error); assert.ok(outcome.mergeCommit); const after = s.getTask(task.id)!; assert.equal(after.status, 'done', '合并成功 → done'); assert.match(after.result!.prUrl!, /^merged:/); s.close(); }); test('acceptAndMerge:冲突 → ok:false + 建补救任务 + 原任务留 exec_review', async (t) => { const repo = setup(t); const s = new Store(':memory:'); const p = s.createProject({ name: 'em', repoPath: repo }); // main 与 branch 改同一文件同一行 → 冲突 writeFileSync(join(repo, 'shared.txt'), 'base\n'); git(repo, ['add', '-A']); git(repo, ['commit', '-qm', 'base shared']); const branch = 'maestro/tsk_conf'; git(repo, ['checkout', '-q', '-b', branch]); writeFileSync(join(repo, 'shared.txt'), 'BRANCH\n'); git(repo, ['add', '-A']); git(repo, ['commit', '-qm', 'maestro(tsk_conf): branch side']); git(repo, ['checkout', '-q', 'main']); writeFileSync(join(repo, 'shared.txt'), 'MAIN\n'); git(repo, ['add', '-A']); git(repo, ['commit', '-qm', 'main side']); git(repo, ['switch', '-q', '--detach']); const task = taskAtExecReview(s, p.id, branch); const outcome = await acceptAndMerge(s, noopLog, task, 'test'); assert.equal(outcome.ok, false, '冲突应失败'); assert.ok(outcome.remediationTaskId, '应建补救任务'); assert.equal(s.getTask(task.id)!.status, 'exec_review', '原任务留审核闸(不 decide)'); s.close(); });