scrum4me-mcp/__tests__/update-job-status-gate.test.ts
Janpeter Visser 1fe6ccf609
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>
2026-05-02 17:55:06 +02:00

79 lines
3.1 KiB
TypeScript

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', () => {
const r = checkVerifyGate(null, false)
expect(r.allowed).toBe(false)
if (!r.allowed) expect(r.error).toMatch(/verify_task_against_plan/i)
})
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)
})
it('allows EMPTY when task is verify_only', () => {
expect(checkVerifyGate('EMPTY', true).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)
})
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(false)
})
})