|
|
|
|
@@ -98,14 +98,18 @@ describe('verify-work.md — auto-transition after UAT passes with 0 issues', ()
|
|
|
|
|
test('human_needed verification is promoted to passed only after successful human UAT', () => {
|
|
|
|
|
const content = fs.readFileSync(VERIFY_WORK, 'utf-8');
|
|
|
|
|
const statusIdx = content.indexOf('VERIFICATION_STATUS=$(msd_run query verification.status "$PHASE_DIR"');
|
|
|
|
|
const humanNeededIdx = content.indexOf('if [ "$VERIFICATION_STATUS_VALUE" = "human_needed" ]; then');
|
|
|
|
|
const setPassedIdx = content.indexOf('msd_run query frontmatter.set "$VERIFICATION_FILE" --field status --value passed');
|
|
|
|
|
const staleIdx = content.indexOf('If `PHASE_VERIFICATION_STATUS` is `stale`');
|
|
|
|
|
const canonIdx = content.indexOf('CANON=$(msd_run query verification.canonicalize-uat "$PHASE_DIR"');
|
|
|
|
|
const predicateIdx = content.indexOf('PHASE_COMPLETE=$(msd_run phase uat-passed "{phase}" --require-verification)');
|
|
|
|
|
|
|
|
|
|
assert.ok(statusIdx !== -1, 'verify-work.md must inspect canonical verification status');
|
|
|
|
|
assert.ok(humanNeededIdx > statusIdx, 'status=passed promotion must be restricted to human_needed');
|
|
|
|
|
assert.ok(setPassedIdx > humanNeededIdx, 'human_needed verification must be promoted after status check');
|
|
|
|
|
assert.ok(setPassedIdx < predicateIdx, 'verification must be canonicalized before the required predicate runs');
|
|
|
|
|
assert.ok(canonIdx > statusIdx, 'the promotion must go through the shared canonicalize-uat seam, after the status read');
|
|
|
|
|
assert.ok(staleIdx < canonIdx, 'a stale report must be re-verified BEFORE canonicalization, or a passed UAT can never be recorded');
|
|
|
|
|
assert.ok(canonIdx < predicateIdx, 'verification must be canonicalized before the required predicate runs');
|
|
|
|
|
assert.ok(
|
|
|
|
|
!content.includes('frontmatter.set "$VERIFICATION_FILE" --field status --value passed'),
|
|
|
|
|
'the workflow must not hand-roll the status flip — verification.canonicalize-uat owns it'
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('stale verification blocks before phase transition', () => {
|
|
|
|
|
@@ -136,47 +140,33 @@ describe('verify-work.md — auto-transition after UAT passes with 0 issues', ()
|
|
|
|
|
// phase-close uses in its --uat-only form (the verification-status blocker
|
|
|
|
|
// is exactly what the flip removes, so the full predicate could never pass
|
|
|
|
|
// at pre-check time); the flagged call stays the later transition gate.
|
|
|
|
|
// The flip itself — gated on the uat-only row predicate, refused for any
|
|
|
|
|
// status other than human_needed, fail-closed on missing evidence — is owned
|
|
|
|
|
// by `verification.canonicalize-uat` and tested behaviorally in
|
|
|
|
|
// tests/uat-evidence.test.cjs. What stays pinned here is the workflow's side
|
|
|
|
|
// of that contract: it calls the seam, and it reports a refusal.
|
|
|
|
|
describe('verify-work.md — canonicalize flip is gated by the UAT predicate (#4663)', () => {
|
|
|
|
|
test('canonicalize flips to passed only when the uat-passed predicate reports passed (#4663)', () => {
|
|
|
|
|
test('the flagged predicate remains the later transition gate (#4663)', () => {
|
|
|
|
|
const content = fs.readFileSync(VERIFY_WORK, 'utf-8');
|
|
|
|
|
const humanNeededIdx = content.indexOf('if [ "$VERIFICATION_STATUS_VALUE" = "human_needed" ]; then');
|
|
|
|
|
const precheckIdx = content.indexOf('UAT_PRECHECK=$(msd_run phase uat-passed "{phase}" --uat-only 2>/dev/null)');
|
|
|
|
|
const flipGuardIdx = content.indexOf('if [ "$UAT_PRECHECK_PASSED" = "true" ]; then');
|
|
|
|
|
const setPassedIdx = content.indexOf('msd_run query frontmatter.set "$VERIFICATION_FILE" --field status --value passed');
|
|
|
|
|
|
|
|
|
|
assert.ok(precheckIdx !== -1, 'the canonicalize block must run the uat-passed predicate before flipping');
|
|
|
|
|
assert.ok(humanNeededIdx !== -1 && precheckIdx > humanNeededIdx, 'the pre-check must sit inside the human_needed branch');
|
|
|
|
|
assert.ok(content.includes(".passed // false"), 'the verdict must be extracted from the typed report with a false default');
|
|
|
|
|
assert.ok(flipGuardIdx !== -1 && flipGuardIdx > precheckIdx, 'the flip must be guarded on the extracted passed verdict');
|
|
|
|
|
assert.ok(setPassedIdx > flipGuardIdx, 'frontmatter.set must sit INSIDE the passed==true guard');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('the canonicalize pre-check runs uat-passed without --require-verification (#4663)', () => {
|
|
|
|
|
const content = fs.readFileSync(VERIFY_WORK, 'utf-8');
|
|
|
|
|
const precheckIdx = content.indexOf('UAT_PRECHECK=$(msd_run phase uat-passed "{phase}" --uat-only 2>/dev/null)');
|
|
|
|
|
const canonIdx = content.indexOf('CANON=$(msd_run query verification.canonicalize-uat "$PHASE_DIR"');
|
|
|
|
|
const flaggedIdx = content.indexOf('PHASE_COMPLETE=$(msd_run phase uat-passed "{phase}" --require-verification)');
|
|
|
|
|
|
|
|
|
|
assert.ok(precheckIdx !== -1, 'the --uat-only pre-check must exist');
|
|
|
|
|
assert.ok(
|
|
|
|
|
!content.slice(precheckIdx, precheckIdx + 120).includes('--require-verification'),
|
|
|
|
|
'the pre-check is the unflagged predicate - requiring verification there would evaluate the very report being written'
|
|
|
|
|
);
|
|
|
|
|
assert.ok(flaggedIdx !== -1 && flaggedIdx > precheckIdx, 'the flagged predicate remains the later transition gate');
|
|
|
|
|
assert.ok(canonIdx !== -1, 'the canonicalize call must exist');
|
|
|
|
|
assert.ok(flaggedIdx !== -1 && flaggedIdx > canonIdx, 'the flagged predicate remains the later transition gate');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('refused canonicalization says the verification stays human_needed (#4663)', () => {
|
|
|
|
|
const content = fs.readFileSync(VERIFY_WORK, 'utf-8');
|
|
|
|
|
assert.match(content, /"\$CANON_REASON" = "uat_not_passed"/, 'the refusal must branch on the typed reason');
|
|
|
|
|
assert.match(content, /stays human_needed/, 'the refusal message must say verification stays human_needed');
|
|
|
|
|
assert.match(content, /blockers \| length/, 'the refusal must carry the blocking-row count');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('an indeterminate pre-check must not flip the report (fail closed) (#4663)', () => {
|
|
|
|
|
test('an indeterminate canonicalize call reads as no reason, never as a flip (#4663)', () => {
|
|
|
|
|
const content = fs.readFileSync(VERIFY_WORK, 'utf-8');
|
|
|
|
|
// jq -r '.passed // false' with an `|| echo "false"` fallback: empty or
|
|
|
|
|
// failed msd_run output must yield no-flip, never a flip.
|
|
|
|
|
assert.ok(
|
|
|
|
|
content.includes(`jq -r '.passed // false' 2>/dev/null || echo "false"`),
|
|
|
|
|
'the extraction must default to false on empty/failed output'
|
|
|
|
|
content.includes(`jq -r '.reason // empty' 2>/dev/null || echo ""`),
|
|
|
|
|
'the extraction must default to empty on empty/failed output'
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|