feat: DONE gate in update_job_status — reject if verify_result null or EMPTY without verify_only
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
994f28f103
commit
5cd792a8fe
2 changed files with 73 additions and 0 deletions
39
__tests__/update-job-status-gate.test.ts
Normal file
39
__tests__/update-job-status-gate.test.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { checkVerifyGate } from '../src/tools/update-job-status.js'
|
||||||
|
|
||||||
|
describe('checkVerifyGate', () => {
|
||||||
|
it('rejects when verify_result is null — agent must verify first', () => {
|
||||||
|
const r = checkVerifyGate(null, false)
|
||||||
|
expect(r.allowed).toBe(false)
|
||||||
|
if (!r.allowed) expect(r.error).toMatch(/verify_task_against_plan/i)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects when verify_result is EMPTY and task is not verify_only', () => {
|
||||||
|
const r = checkVerifyGate('EMPTY', false)
|
||||||
|
expect(r.allowed).toBe(false)
|
||||||
|
if (!r.allowed) {
|
||||||
|
expect(r.error).toMatch(/EMPTY/i)
|
||||||
|
expect(r.error).toMatch(/verify_only/i)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows when verify_result is EMPTY and task IS verify_only', () => {
|
||||||
|
const r = checkVerifyGate('EMPTY', true)
|
||||||
|
expect(r.allowed).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows when verify_result is ALIGNED', () => {
|
||||||
|
const r = checkVerifyGate('ALIGNED', false)
|
||||||
|
expect(r.allowed).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows when verify_result is PARTIAL', () => {
|
||||||
|
const r = checkVerifyGate('PARTIAL', false)
|
||||||
|
expect(r.allowed).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows when verify_result is DIVERGENT', () => {
|
||||||
|
const r = checkVerifyGate('DIVERGENT', false)
|
||||||
|
expect(r.allowed).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -91,6 +91,27 @@ export async function prepareDoneUpdate(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function checkVerifyGate(
|
||||||
|
verifyResult: string | null,
|
||||||
|
verifyOnly: boolean,
|
||||||
|
): { allowed: true } | { allowed: false; error: string } {
|
||||||
|
if (verifyResult === null) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
error: 'Roep eerst verify_task_against_plan aan voordat je DONE markeert.',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (verifyResult === 'EMPTY' && !verifyOnly) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
error:
|
||||||
|
'Plan-vs-implementatie verify gaf EMPTY. Geen wijzigingen gedetecteerd. ' +
|
||||||
|
'Markeer de task als verify_only of pas de implementatie aan.',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { allowed: true }
|
||||||
|
}
|
||||||
|
|
||||||
const DB_STATUS_MAP = {
|
const DB_STATUS_MAP = {
|
||||||
running: 'RUNNING',
|
running: 'RUNNING',
|
||||||
done: 'DONE',
|
done: 'DONE',
|
||||||
|
|
@ -140,6 +161,8 @@ export function registerUpdateJobStatusTool(server: McpServer) {
|
||||||
'Report progress on a claimed ClaudeJob. Allowed transitions from CLAIMED/RUNNING: ' +
|
'Report progress on a claimed ClaudeJob. Allowed transitions from CLAIMED/RUNNING: ' +
|
||||||
'running (start), done (finished), failed (error). ' +
|
'running (start), done (finished), failed (error). ' +
|
||||||
'The Bearer token must match the token that claimed the job. ' +
|
'The Bearer token must match the token that claimed the job. ' +
|
||||||
|
'Before marking done: call verify_task_against_plan first — done is rejected when ' +
|
||||||
|
'verify_result is null or EMPTY (unless task.verify_only is true). ' +
|
||||||
'Automatically emits an SSE event so the Scrum4Me UI updates in real time.',
|
'Automatically emits an SSE event so the Scrum4Me UI updates in real time.',
|
||||||
inputSchema,
|
inputSchema,
|
||||||
},
|
},
|
||||||
|
|
@ -157,6 +180,8 @@ export function registerUpdateJobStatusTool(server: McpServer) {
|
||||||
user_id: true,
|
user_id: true,
|
||||||
product_id: true,
|
product_id: true,
|
||||||
task_id: true,
|
task_id: true,
|
||||||
|
verify_result: true,
|
||||||
|
task: { select: { verify_only: true } },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -176,6 +201,12 @@ export function registerUpdateJobStatusTool(server: McpServer) {
|
||||||
let skipWorktreeCleanup = false
|
let skipWorktreeCleanup = false
|
||||||
|
|
||||||
if (status === 'done') {
|
if (status === 'done') {
|
||||||
|
const gate = checkVerifyGate(
|
||||||
|
job.verify_result ?? null,
|
||||||
|
job.task?.verify_only ?? false,
|
||||||
|
)
|
||||||
|
if (!gate.allowed) return toolError(gate.error)
|
||||||
|
|
||||||
const plan = await prepareDoneUpdate(job_id, branch)
|
const plan = await prepareDoneUpdate(job_id, branch)
|
||||||
actualStatus = plan.dbStatus === 'DONE' ? 'done' : 'failed'
|
actualStatus = plan.dbStatus === 'DONE' ? 'done' : 'failed'
|
||||||
pushedAt = plan.pushedAt
|
pushedAt = plan.pushedAt
|
||||||
|
|
@ -223,6 +254,7 @@ export function registerUpdateJobStatusTool(server: McpServer) {
|
||||||
branch: true,
|
branch: true,
|
||||||
pushed_at: true,
|
pushed_at: true,
|
||||||
pr_url: true,
|
pr_url: true,
|
||||||
|
verify_result: true,
|
||||||
summary: true,
|
summary: true,
|
||||||
error: true,
|
error: true,
|
||||||
started_at: true,
|
started_at: true,
|
||||||
|
|
@ -247,6 +279,7 @@ export function registerUpdateJobStatusTool(server: McpServer) {
|
||||||
branch: updated.branch ?? undefined,
|
branch: updated.branch ?? undefined,
|
||||||
pushed_at: updated.pushed_at?.toISOString() ?? undefined,
|
pushed_at: updated.pushed_at?.toISOString() ?? undefined,
|
||||||
pr_url: updated.pr_url ?? undefined,
|
pr_url: updated.pr_url ?? undefined,
|
||||||
|
verify_result: updated.verify_result?.toLowerCase() ?? undefined,
|
||||||
summary: updated.summary ?? undefined,
|
summary: updated.summary ?? undefined,
|
||||||
error: updated.error ?? undefined,
|
error: updated.error ?? undefined,
|
||||||
}),
|
}),
|
||||||
|
|
@ -268,6 +301,7 @@ export function registerUpdateJobStatusTool(server: McpServer) {
|
||||||
branch: updated.branch,
|
branch: updated.branch,
|
||||||
pushed_at: updated.pushed_at?.toISOString() ?? null,
|
pushed_at: updated.pushed_at?.toISOString() ?? null,
|
||||||
pr_url: updated.pr_url ?? null,
|
pr_url: updated.pr_url ?? null,
|
||||||
|
verify_result: updated.verify_result?.toLowerCase() ?? null,
|
||||||
summary: updated.summary,
|
summary: updated.summary,
|
||||||
error: updated.error,
|
error: updated.error,
|
||||||
started_at: updated.started_at?.toISOString() ?? null,
|
started_at: updated.started_at?.toISOString() ?? null,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue