fix(ingest): autoApproveExec 传错 id 致自动合并不触发(线上实测捕获)

condition 误把 taskId 传给 autoApproveExecOn(projectId) → getProject(taskId)=null
→ 永远 false,自动合并从不触发(任务卡 exec_review)。
重构为 shouldAutoApproveExec(store, task, codeV, secV),内部读 task.projectId 自取项目,
杜绝传错 id;补单测覆盖。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-24 23:51:07 +08:00
parent a9c51021aa
commit 4c09186f36
2 changed files with 39 additions and 14 deletions
+20 -13
View File
@@ -41,9 +41,18 @@ function shouldAutoApprovePlan(
return subtasks.every((s) => s.complexity === 'easy'); return subtasks.every((s) => s.complexity === 'easy');
} }
/** exec 自动放行开关:项目 auto-approved 且 autoApproveExec 打开(默认关)。 */ /**
function autoApproveExecOn(store: Store, projectId: string): boolean { * 是否对该执行结果自动放行(跳过 exec_review 自动合并):双复审均 approve +
const project = store.getProject(projectId); * 项目 auto-approved + autoApproveExec 开。读 task.projectId 自取项目(杜绝传错 id)。默认关。
*/
export function shouldAutoApproveExec(
store: Store,
task: { projectId: string },
codeVerdict: string | null,
securityVerdict: string | null,
): boolean {
if (codeVerdict !== 'approve' || securityVerdict !== 'approve') return false;
const project = store.getProject(task.projectId);
return !!project && project.autonomy === 'auto-approved' && !!project.autoApproveExec; return !!project && project.autonomy === 'auto-approved' && !!project.autoApproveExec;
} }
@@ -212,16 +221,14 @@ function applyRecord(store: Store, log: IngestLogger, taskId: string, runId: str
// 自动放行(默认关):auto-approved + autoApproveExec + 双复审均 approve → 自动合并。 // 自动放行(默认关):auto-approved + autoApproveExec + 双复审均 approve → 自动合并。
// 合并是 git 异步操作,fire-and-forget 不阻塞 tick;冲突则补救任务 + 留 exec_review 人审。 // 合并是 git 异步操作,fire-and-forget 不阻塞 tick;冲突则补救任务 + 留 exec_review 人审。
if (rec.code.verdict === 'approve' && rec.security.verdict === 'approve' && autoApproveExecOn(store, taskId)) { const reviewed = store.getTask(taskId);
const reviewed = store.getTask(taskId); if (reviewed && shouldAutoApproveExec(store, reviewed, rec.code.verdict, rec.security.verdict)) {
if (reviewed) { void acceptAndMerge(store, log, reviewed, 'auto-approve-exec')
void acceptAndMerge(store, log, reviewed, 'auto-approve-exec') .then((o) => {
.then((o) => { if (o.ok) log.info(`task=${taskId} 双复审 approve + 自动放行 → 已自动合并(${o.mergeCommit}`);
if (o.ok) log.info(`task=${taskId} 双复审 approve + 自动放行 → 已自动合并(${o.mergeCommit}`); else log.info(`task=${taskId} 自动合并未成(${o.error})→ 留 exec_review 人审${o.remediationTaskId ? `(补救任务 ${o.remediationTaskId}` : ''}`);
else log.info(`task=${taskId} 自动合并未成(${o.error})→ 留 exec_review 人审${o.remediationTaskId ? `(补救任务 ${o.remediationTaskId}` : ''}`); })
}) .catch((e) => log.error(`task=${taskId} 自动合并异常:${(e as Error).message}`));
.catch((e) => log.error(`task=${taskId} 自动合并异常:${(e as Error).message}`));
}
} }
return; return;
} }
+19 -1
View File
@@ -4,7 +4,7 @@ import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
import { Store } from '../src/store/index.js'; import { Store } from '../src/store/index.js';
import { ingestRun } from '../src/daemon/ingest.js'; import { ingestRun, shouldAutoApproveExec } from '../src/daemon/ingest.js';
import { appendOutbox, type OutboxPayload } from '../src/executor/protocol.js'; import { appendOutbox, type OutboxPayload } from '../src/executor/protocol.js';
import { branchFor, worktreeDirFor } from '../src/executor/worktree.js'; import { branchFor, worktreeDirFor } from '../src/executor/worktree.js';
@@ -304,6 +304,24 @@ test('autoApprovePlan:含 medium 子任务 → 不放行(改动面/复杂度
}); });
}); });
test('shouldAutoApproveExec:双 approve + auto-approved + 开关 → true;任一不满足 → false', () => {
const s = new Store(':memory:');
const p = s.createProject({ name: 'aae', repoPath: '/tmp/aae-' + Math.random(), autonomy: 'auto-approved' });
const t = s.createTask({ projectId: p.id, title: 'x', complexity: 'easy' });
// 开关默认关 → false
assert.equal(shouldAutoApproveExec(s, t, 'approve', 'approve'), false, '开关关');
s.patchProject(p.id, { autoApproveExec: true });
// 现在满足三条件 → true(函数读 task.projectId 自取项目,传错 id 不可能)
assert.equal(shouldAutoApproveExec(s, t, 'approve', 'approve'), true);
// verdict 任一非 approve → false
assert.equal(shouldAutoApproveExec(s, t, 'approve', 'reject'), false);
assert.equal(shouldAutoApproveExec(s, t, null, 'approve'), false);
// 非 auto-approved → false
s.patchProject(p.id, { autonomy: 'auto-easy' });
assert.equal(shouldAutoApproveExec(s, t, 'approve', 'approve'), false);
s.close();
});
test('ingest planner failed:走 failPlanAttempt(退避,任务留 speccing,不进 failed', () => { test('ingest planner failed:走 failPlanAttempt(退避,任务留 speccing,不进 failed', () => {
withTmpDataDir(() => { withTmpDataDir(() => {
const { store, taskId, runId } = setupPlanning('medium'); const { store, taskId, runId } = setupPlanning('medium');