fix(tests): 修复三类优化实现后的测试适配

- pipeline.test.ts:PipelineDeps 增加 syncMain 可注入接口(默认用真 git),
  测试 mockDeps 加 syncMain=no-op + runConflict,消除 fake-worktree 上真 git 调用失败
- models.test.ts:reviewer 三档默认 fable-5、project.model 不覆盖复审/conflict、
  MODEL_FALLBACK_CHAIN 三项链;pickFallbackModel 改为取链上下一档(循环)而非首个不等项
- ingest.test.ts:verdict=reject 硬闸测试改为断言未进 exec_review

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-14 14:05:22 +08:00
parent f8900de2f7
commit 027a9cb2d6
5 changed files with 59 additions and 46 deletions
+4 -5
View File
@@ -76,10 +76,9 @@ export function isModelError(msg: string): boolean {
return /not[_\s-]?found|invalid|permission|forbidden|unauthorized|unavailable|unknown|unsupported|does not exist|no access|404|403/i.test(msg);
}
/** 回退链里取第一个与失败模型不同的;链上全相同(不可能两项都等)则 null */
/** 回退链里取失败模型的下一档(循环);未在链上则取首档。 */
export function pickFallbackModel(failedModel: string): string | null {
for (const m of MODEL_FALLBACK_CHAIN) {
if (m !== failedModel) return m;
}
return null;
const idx = MODEL_FALLBACK_CHAIN.indexOf(failedModel as typeof MODEL_FALLBACK_CHAIN[number]);
if (idx === -1) return MODEL_FALLBACK_CHAIN[0];
return MODEL_FALLBACK_CHAIN[(idx + 1) % MODEL_FALLBACK_CHAIN.length];
}
+26 -22
View File
@@ -29,6 +29,27 @@ export interface PipelineDeps {
reviewSecurity: ReviewerFn; // 安全审计(daemon 落成 kind=security 的 run
runPlanner: PlannerFn; // planner(拆解 Hard / 写方案 Medium,只读跑 CC
runConflict: typeof runConflict; // 解冲突(conflict run
/** 执行前同步 defaultBranch:返回 null=成功,string=错误信息(已 abort)。可测试 mock。 */
syncMain?: (dir: string, defaultBranch: string) => Promise<string | null>;
}
/** 执行前同步 defaultBranch 默认实现:fetch→merge origin/<branch>;失败再试本地 <branch>。 */
async function defaultSyncMain(dir: string, defaultBranch: string): Promise<string | null> {
try {
await git(dir, ['fetch', 'origin', defaultBranch]);
} catch { /* 无远端时跳过 fetch,直接用本地 */ }
try {
await git(dir, ['-c', 'user.name=maestro', '-c', 'user.email=maestro@local', 'merge', '--no-edit', `origin/${defaultBranch}`]);
return null;
} catch {
try {
await git(dir, ['-c', 'user.name=maestro', '-c', 'user.email=maestro@local', 'merge', '--no-edit', defaultBranch]);
return null;
} catch (e) {
await git(dir, ['merge', '--abort']).catch(() => undefined);
return (e as Error).message;
}
}
}
/** 真实依赖(worker 生产用)。 */
@@ -90,28 +111,11 @@ export async function runPipeline(job: JobSpec, deps: PipelineDeps = realDeps, e
const wt = await deps.createWorktree(job.project.repoPath, job.task.id, job.project.defaultBranch);
// sync main:把 defaultBranch 最新代码 merge 进 worktree(排队期间 main 可能已前移)
try {
await git(wt.dir, ['fetch', 'origin', job.project.defaultBranch]);
} catch { /* 无远端时跳过 fetch,直接用本地 */ }
try {
await git(wt.dir, [
'-c', 'user.name=maestro', '-c', 'user.email=maestro@local',
'merge', '--no-edit', `origin/${job.project.defaultBranch}`,
]);
} catch {
// 尝试用本地分支
try {
await git(wt.dir, [
'-c', 'user.name=maestro', '-c', 'user.email=maestro@local',
'merge', '--no-edit', job.project.defaultBranch,
]);
} catch (e) {
// sync main 失败(冲突或其他)→ abort 并报失败,让 daemon 转 needs_attention
await git(wt.dir, ['merge', '--abort']).catch(() => undefined);
emit({ type: 'failed', error: `执行前同步 ${job.project.defaultBranch} 失败,建议重新评估方案:${(e as Error).message}`, transcriptRef: null, sessionId: null });
emit({ type: 'done' });
return;
}
const syncErr = await (deps.syncMain ?? defaultSyncMain)(wt.dir, job.project.defaultBranch);
if (syncErr !== null) {
emit({ type: 'failed', error: `执行前同步 ${job.project.defaultBranch} 失败,建议重新评估方案:${syncErr}`, transcriptRef: null, sessionId: null });
emit({ type: 'done' });
return;
}
// 2. 执行
+7 -7
View File
@@ -105,7 +105,7 @@ test('ingest resulttask→exec_reviewsetResult 四字段正确,reviewer/
});
});
test('ingest result:任一 verdict=reject 也照常落进 result(裁决归用户', () => {
test('ingest result:任一 verdict=reject → 硬闸退回重执行(不进 exec_review', () => {
withTmpDataDir(() => {
const { store, taskId, runId, dir, branch } = setupExecuting();
appendOutbox(runId, {
@@ -115,12 +115,12 @@ test('ingest result:任一 verdict=reject 也照常落进 result(裁决归
} as OutboxPayload);
ingestRun(store, noopLog, runId);
const done = store.getTask(taskId)!;
assert.equal(done.status, 'exec_review');
assert.equal(done.result!.verdict, 'reject');
assert.equal(done.result!.summary, '发现问题');
assert.equal(done.result!.securityVerdict, 'reject');
assert.equal(done.result!.securitySummary, '发现密钥泄露');
const t = store.getTask(taskId)!;
// verdict=reject 触发硬闸:退回重执行(queued),不进 exec_review
assert.ok(t.status === 'queued' || t.status === 'failed' || t.status === 'needs_attention',
`expected task to be re-queued or failed after reject verdict, got ${t.status}`);
// 不应进入 exec_review
assert.notEqual(t.status, 'exec_review');
store.close();
});
});
+20 -12
View File
@@ -40,32 +40,39 @@ test('pickModel:复杂度映射默认值(executor / reviewer', () => {
assert.equal(pickModel(fakeTask('easy'), p, 'executor'), 'claude-sonnet-4-6');
assert.equal(pickModel(fakeTask('medium'), p, 'executor'), 'claude-opus-4-8');
assert.equal(pickModel(fakeTask('hard'), p, 'executor'), 'claude-fable-5');
assert.equal(pickModel(fakeTask('easy'), p, 'reviewer'), 'claude-sonnet-4-6');
assert.equal(pickModel(fakeTask('medium'), p, 'reviewer'), 'claude-opus-4-8');
assert.equal(pickModel(fakeTask('hard'), p, 'reviewer'), 'claude-opus-4-8');
// reviewer 三档全升 fable-5(保复审独立性)
assert.equal(pickModel(fakeTask('easy'), p, 'reviewer'), 'claude-fable-5');
assert.equal(pickModel(fakeTask('medium'), p, 'reviewer'), 'claude-fable-5');
assert.equal(pickModel(fakeTask('hard'), p, 'reviewer'), 'claude-fable-5');
});
});
test('pickModelproject.model 最优先(两个角色、所有复杂度都用它)', () => {
withEnv({ MAESTRO_MODEL_HARD: 'claude-haiku-4-5' }, () => {
test('pickModelproject.model executor 最优先,但 reviewer/conflict 不受 project.model 覆盖', () => {
withEnv(Object.fromEntries(ENV_KEYS.map((k) => [k, ''])), () => {
const p = fakeProject('claude-opus-4-6');
for (const c of ['easy', 'medium', 'hard'] as const) {
// executor 受 project.model 覆盖
assert.equal(pickModel(fakeTask(c), p, 'executor'), 'claude-opus-4-6');
assert.equal(pickModel(fakeTask(c), p, 'reviewer'), 'claude-opus-4-6');
// reviewer 不受 project.model 覆盖,始终用最强档 fable-5(保自审独立性)
assert.equal(pickModel(fakeTask(c), p, 'reviewer'), 'claude-fable-5');
// conflict 固定 fable-5
assert.equal(pickModel(fakeTask(c), p, 'conflict'), 'claude-fable-5');
}
});
});
test('pickModelenv 覆盖默认值,executor/reviewer 各用各的 env', () => {
test('pickModelenv 覆盖默认值,executor/reviewer 各用各的 env', () => {
withEnv({
MAESTRO_MODEL_EASY: 'claude-haiku-4-5',
MAESTRO_MODEL_REVIEW_HARD: 'claude-fable-5',
MAESTRO_MODEL_REVIEW_EASY: 'claude-opus-4-8', // reviewer env 可单独覆盖
}, () => {
const p = fakeProject();
assert.equal(pickModel(fakeTask('easy'), p, 'executor'), 'claude-haiku-4-5');
assert.equal(pickModel(fakeTask('easy'), p, 'reviewer'), 'claude-sonnet-4-6'); // reviewer 不受 executor env 影响
// reviewer easy 被 MAESTRO_MODEL_REVIEW_EASY 覆盖
assert.equal(pickModel(fakeTask('easy'), p, 'reviewer'), 'claude-opus-4-8');
// reviewer hard 无 env → 默认 fable-5
assert.equal(pickModel(fakeTask('hard'), p, 'reviewer'), 'claude-fable-5');
assert.equal(pickModel(fakeTask('hard'), p, 'executor'), 'claude-fable-5'); // executor hard 默认 fable
assert.equal(pickModel(fakeTask('hard'), p, 'executor'), 'claude-fable-5'); // executor hard 默认 fable
});
});
@@ -91,8 +98,9 @@ test('isModelError:模型不可用类错误才触发回退', () => {
});
test('pickFallbackModel:回退链取第一个 ≠ 失败模型的', () => {
assert.deepEqual([...MODEL_FALLBACK_CHAIN], ['claude-opus-4-8', 'claude-sonnet-4-6']);
// 链已扩展为三项:fable → opus → sonnet
assert.deepEqual([...MODEL_FALLBACK_CHAIN], ['claude-fable-5', 'claude-opus-4-8', 'claude-sonnet-4-6']);
assert.equal(pickFallbackModel('claude-fable-5'), 'claude-opus-4-8');
assert.equal(pickFallbackModel('claude-opus-4-8'), 'claude-sonnet-4-6');
assert.equal(pickFallbackModel('claude-sonnet-4-6'), 'claude-opus-4-8');
assert.equal(pickFallbackModel('claude-sonnet-4-6'), 'claude-fable-5');
});
+2
View File
@@ -56,6 +56,8 @@ function mockDeps(overrides: Partial<PipelineDeps> = {}): PipelineDeps {
reviewCode: async () => okReview,
reviewSecurity: async () => okSecurity,
runPlanner: async () => ({ ok: true, transcriptRef: '/tmp/plan.jsonl', sessionId: 'sess-plan', finalText: '方案正文' }),
runConflict: async () => ({ ok: true, transcriptRef: null, sessionId: null, finalText: null }),
syncMain: async () => null, // no-op:测试环境无真实 git worktree,跳过同步
...overrides,
};
}