feat(gate): verify_required levels — ALIGNED/ALIGNED_OR_PARTIAL/ANY (#16)
Sluit story 'Verify-gate uitbreiden' in PBI 'Agent verify-flow hardening' af.
The previous gate weighed only EMPTY: any PARTIAL or DIVERGENT verify
slipped through. The Insights batch (2 May 2026) showed why that's
weak — agent-jobs claiming DONE while only delivering helpers, not
the requested UI components, with verify=DIVERGENT/PARTIAL accepted.
New decision matrix:
null → reject (run verify_task_against_plan)
EMPTY + !verify_only → reject
EMPTY + verify_only → allowed
ALIGNED → always allowed
PARTIAL/DIVERGENT
required=ALIGNED → reject (strict task)
required=ALIGNED_OR_PARTIAL (default) → allowed only if summary
≥20 chars (acknowledge drift)
required=ANY → allowed (refactor escape hatch)
`update_job_status('done')` now reads `task.verify_required` from the DB
(field added in Scrum4Me PR #53) and passes it + `summary` to the gate.
Tool description updated with the new rules.
Vendor submodule synced to pick up the schema enum.
Tests: 129/129 (was 120 + 9 new combinatorial gate tests).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0bcca15235
commit
1fe6ccf609
4 changed files with 131 additions and 33 deletions
|
|
@ -1,39 +1,79 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { checkVerifyGate } from '../src/tools/update-job-status.js'
|
||||
|
||||
const LONG_SUMMARY = 'Refactor touched extra files for type narrowing.'
|
||||
|
||||
describe('checkVerifyGate', () => {
|
||||
it('rejects when verify_result is null — agent must verify first', () => {
|
||||
it('rejects when verify_result is null', () => {
|
||||
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', () => {
|
||||
it('rejects EMPTY when 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)
|
||||
}
|
||||
if (!r.allowed) expect(r.error).toMatch(/EMPTY/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 EMPTY when task is verify_only', () => {
|
||||
expect(checkVerifyGate('EMPTY', true).allowed).toBe(true)
|
||||
})
|
||||
|
||||
it('allows when verify_result is ALIGNED', () => {
|
||||
const r = checkVerifyGate('ALIGNED', false)
|
||||
expect(r.allowed).toBe(true)
|
||||
it('always allows ALIGNED', () => {
|
||||
expect(checkVerifyGate('ALIGNED', false, 'ALIGNED').allowed).toBe(true)
|
||||
expect(checkVerifyGate('ALIGNED', false, 'ALIGNED_OR_PARTIAL').allowed).toBe(true)
|
||||
expect(checkVerifyGate('ALIGNED', false, 'ANY').allowed).toBe(true)
|
||||
})
|
||||
|
||||
it('allows when verify_result is PARTIAL', () => {
|
||||
describe('verify_required=ALIGNED (strict)', () => {
|
||||
it('rejects PARTIAL', () => {
|
||||
const r = checkVerifyGate('PARTIAL', false, 'ALIGNED', LONG_SUMMARY)
|
||||
expect(r.allowed).toBe(false)
|
||||
if (!r.allowed) expect(r.error).toMatch(/ALIGNED/)
|
||||
})
|
||||
it('rejects DIVERGENT', () => {
|
||||
const r = checkVerifyGate('DIVERGENT', false, 'ALIGNED', LONG_SUMMARY)
|
||||
expect(r.allowed).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('verify_required=ALIGNED_OR_PARTIAL (default — needs summary on drift)', () => {
|
||||
it('rejects PARTIAL without summary', () => {
|
||||
const r = checkVerifyGate('PARTIAL', false, 'ALIGNED_OR_PARTIAL', undefined)
|
||||
expect(r.allowed).toBe(false)
|
||||
if (!r.allowed) expect(r.error).toMatch(/summary/i)
|
||||
})
|
||||
it('rejects PARTIAL with too-short summary', () => {
|
||||
const r = checkVerifyGate('PARTIAL', false, 'ALIGNED_OR_PARTIAL', 'short')
|
||||
expect(r.allowed).toBe(false)
|
||||
})
|
||||
it('allows PARTIAL with long summary', () => {
|
||||
expect(checkVerifyGate('PARTIAL', false, 'ALIGNED_OR_PARTIAL', LONG_SUMMARY).allowed).toBe(true)
|
||||
})
|
||||
it('rejects DIVERGENT without summary', () => {
|
||||
expect(checkVerifyGate('DIVERGENT', false, 'ALIGNED_OR_PARTIAL', undefined).allowed).toBe(false)
|
||||
})
|
||||
it('allows DIVERGENT with long summary', () => {
|
||||
expect(checkVerifyGate('DIVERGENT', false, 'ALIGNED_OR_PARTIAL', LONG_SUMMARY).allowed).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('verify_required=ANY (refactor escape hatch)', () => {
|
||||
it('allows PARTIAL without summary', () => {
|
||||
expect(checkVerifyGate('PARTIAL', false, 'ANY').allowed).toBe(true)
|
||||
})
|
||||
it('allows DIVERGENT without summary', () => {
|
||||
expect(checkVerifyGate('DIVERGENT', false, 'ANY').allowed).toBe(true)
|
||||
})
|
||||
it('still rejects EMPTY (verify_only takes precedence)', () => {
|
||||
expect(checkVerifyGate('EMPTY', false, 'ANY').allowed).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('default verify_required=ALIGNED_OR_PARTIAL when omitted', () => {
|
||||
// No third arg → falls back to ALIGNED_OR_PARTIAL → PARTIAL needs summary
|
||||
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)
|
||||
expect(r.allowed).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue