Some checks failed
Validate Branch Name / PR mergeability (pull_request) Successful in 24s
Changeset Required / PR mergeability (pull_request) Successful in 10s
Default Flip Documentation / PR mergeability (pull_request) Successful in 9s
Dependabot Auto-Merge / auto-merge (pull_request) Has been skipped
Docs Required / PR mergeability (pull_request) Successful in 10s
Mutation Testing / PR mergeability (pull_request) Successful in 10s
Security Scan / PR mergeability (pull_request) Successful in 10s
Tests / PR mergeability (pull_request) Successful in 10s
Tests / Base branch health (pull_request) Successful in 11s
Tests / Detect test scope (pull_request) Successful in 16s
PR Target Validator / validate-target (pull_request_target) Successful in 14s
Validate Branch Name / check-branch (pull_request) Successful in 9s
Changeset Required / changeset-lint (pull_request) Successful in 27s
Default Flip Documentation / default-flip-documentation (pull_request) Successful in 27s
Docs Required / docs-lint (pull_request) Successful in 6m35s
Mutation Testing / Detect changed covered modules (pull_request) Successful in 20s
Security Scan / security (pull_request) Successful in 6m31s
Tests / lint-tests (pull_request) Failing after 1m56s
Tests / plugin-validate (pull_request) Successful in 1m2s
Tests / test (ubuntu-latest, 24, shard 1/3) (pull_request) Failing after 20s
Tests / test (ubuntu-latest, 24, shard 2/3) (pull_request) Failing after 19s
Tests / test (ubuntu-latest, 24, shard 3/3) (pull_request) Failing after 18s
Tests / test (ubuntu-latest, 24) (pull_request) Failing after 19s
Tests / test (inert CI) (pull_request) Has been skipped
Tests / QA loop walk (smell ratchet) (pull_request) Failing after 20s
Mutation Testing / Stryker (${{ matrix.name }}) (pull_request) Has been skipped
Tests / Coverage gate (merged shards) (pull_request) Has been skipped
Tests / Publish emitted-baseline artifact (pull_request) Has been skipped
Mutation Testing / Stryker mutation score (changed files only) (pull_request) Has been skipped
Branch Cleanup / Delete merged PR branch (pull_request) Failing after 10s
Branch Cleanup / Weekly orphaned branch sweep (pull_request) Has been skipped
Tests / conformance test (macos-latest, 24) (pull_request) Has been cancelled
Tests / conformance test (windows-latest, 24, shard 1/3) (pull_request) Has been cancelled
Tests / conformance test (windows-latest, 24, shard 2/3) (pull_request) Has been cancelled
Tests / conformance test (windows-latest, 24, shard 3/3) (pull_request) Has been cancelled
Tests / Required tests (pull_request) Has been cancelled
verify-work now calls verification.canonicalize-uat instead of hand-rolling the status flip, and progress's Route V.stale re-verifies in place. The flip's behavior is covered in tests/uat-evidence.test.cjs; these tests keep the workflow's side of the contract. Refreshes the compact-content benchmark baseline for the moved bytes.
301 lines
15 KiB
JavaScript
301 lines
15 KiB
JavaScript
// allow-test-rule: source-text-is-the-product
|
|
// Workflow .md / agent .md / command .md / reference .md files — their text
|
|
// IS what the runtime loads. Testing text content tests the deployed contract.
|
|
// Per CONTRIBUTING.md exception matrix.
|
|
'use strict';
|
|
|
|
|
|
/**
|
|
* verify-work auto-transition tests (#2018)
|
|
*
|
|
* Validates that verify-work.md calls the transition workflow to mark the
|
|
* phase complete in ROADMAP.md and STATE.md when UAT passes with 0 issues.
|
|
*/
|
|
|
|
const { test, describe } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const VERIFY_WORK = path.join(__dirname, '..', 'msd-core', 'workflows', 'verify-work.md');
|
|
|
|
describe('verify-work.md — auto-transition after UAT passes with 0 issues', () => {
|
|
test('workflow reads transition.md when issues == 0 and security gate cleared', () => {
|
|
const content = fs.readFileSync(VERIFY_WORK, 'utf-8');
|
|
assert.ok(
|
|
content.includes('transition.md'),
|
|
'verify-work.md must reference transition.md for phase completion when issues == 0'
|
|
);
|
|
});
|
|
|
|
test('transition call appears after complete_session section', () => {
|
|
const content = fs.readFileSync(VERIFY_WORK, 'utf-8');
|
|
const completeSessionIdx = content.indexOf('complete_session');
|
|
const transitionIdx = content.indexOf('transition.md');
|
|
assert.ok(
|
|
completeSessionIdx !== -1,
|
|
'verify-work.md must contain a complete_session section'
|
|
);
|
|
assert.ok(
|
|
transitionIdx !== -1,
|
|
'verify-work.md must reference transition.md'
|
|
);
|
|
assert.ok(
|
|
transitionIdx > completeSessionIdx,
|
|
'transition.md reference must appear after the complete_session section'
|
|
);
|
|
});
|
|
|
|
test('security gate check gates the transition (no auto-transition when security pending)', () => {
|
|
const content = fs.readFileSync(VERIFY_WORK, 'utf-8');
|
|
// The capability-resolved security check must appear before the transition reference.
|
|
const securityHookIdx = content.indexOf('loop render-hooks verify:post');
|
|
const transitionIdx = content.indexOf('transition.md');
|
|
assert.ok(
|
|
securityHookIdx !== -1,
|
|
'verify-work.md must resolve verify:post capability hooks before transitioning'
|
|
);
|
|
assert.ok(
|
|
securityHookIdx < transitionIdx,
|
|
'verify:post capability hook check must appear before transition.md reference'
|
|
);
|
|
});
|
|
|
|
test('transition is only invoked when security gate is cleared or disabled', () => {
|
|
const content = fs.readFileSync(VERIFY_WORK, 'utf-8');
|
|
// Transition must be guarded by security check:
|
|
// Either no active secure-phase hook exists, or security file exists with 0 open threats.
|
|
const hasGuardedTransition =
|
|
content.includes('transition.md') &&
|
|
(
|
|
content.includes('loop render-hooks verify:post') &&
|
|
content.includes('ref.skill == "secure-phase"') &&
|
|
(content.includes('threats_open') || content.includes('SECURITY_FILE'))
|
|
);
|
|
assert.ok(
|
|
hasGuardedTransition,
|
|
'transition.md invocation must be guarded by security gate checks'
|
|
);
|
|
});
|
|
|
|
test('auto-transition is gated by UAT plus canonical verification predicate', () => {
|
|
const content = fs.readFileSync(VERIFY_WORK, 'utf-8');
|
|
const predicateIdx = content.indexOf('phase uat-passed');
|
|
const requireVerificationIdx = content.indexOf('--require-verification');
|
|
const transitionIdx = content.indexOf('transition.md');
|
|
|
|
assert.ok(predicateIdx !== -1, 'verify-work.md must call phase uat-passed before transition');
|
|
assert.ok(
|
|
requireVerificationIdx > predicateIdx,
|
|
'verify-work.md must require canonical verification in the UAT predicate'
|
|
);
|
|
assert.ok(
|
|
predicateIdx < transitionIdx,
|
|
'UAT-plus-verification predicate must run before transition.md'
|
|
);
|
|
});
|
|
|
|
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 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(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', () => {
|
|
const content = fs.readFileSync(VERIFY_WORK, 'utf-8');
|
|
const staleIdx = content.indexOf('If `PHASE_VERIFICATION_STATUS` is `stale`');
|
|
const predicateIdx = content.indexOf('PHASE_COMPLETE=$(msd_run phase uat-passed "{phase}" --require-verification)');
|
|
const transitionIdx = content.indexOf('transition.md');
|
|
|
|
assert.ok(staleIdx !== -1, 'verify-work.md must stop on stale verification');
|
|
assert.ok(staleIdx < predicateIdx, 'stale verification must be checked before the required predicate');
|
|
assert.ok(staleIdx < transitionIdx, 'stale verification must be checked before transition');
|
|
});
|
|
|
|
test('transition is NOT suggested when security enforcement is enabled and no SECURITY.md exists', () => {
|
|
const content = fs.readFileSync(VERIFY_WORK, 'utf-8');
|
|
// The workflow should suggest /msd-secure-phase when security is enabled but no file exists
|
|
assert.ok(
|
|
content.includes('msd-secure-phase') || content.includes('msd:secure-phase'),
|
|
'verify-work.md must suggest /msd:secure-phase when security gate blocks transition'
|
|
);
|
|
});
|
|
});
|
|
|
|
// ── #4663 — the canonicalize flip requires the UAT predicate, not a vacuous zero ──
|
|
// "zero issues" is not pass evidence: blocked rows are not issues by this same
|
|
// workflow's rule, so a 0-passed / 0-issues / N-blocked session must NOT flip
|
|
// VERIFICATION.md to `passed`. The flip now consumes the SAME predicate the
|
|
// 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('the flagged predicate remains the later transition gate (#4663)', () => {
|
|
const content = fs.readFileSync(VERIFY_WORK, 'utf-8');
|
|
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(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 canonicalize call reads as no reason, never as a flip (#4663)', () => {
|
|
const content = fs.readFileSync(VERIFY_WORK, 'utf-8');
|
|
assert.ok(
|
|
content.includes(`jq -r '.reason // empty' 2>/dev/null || echo ""`),
|
|
'the extraction must default to empty on empty/failed output'
|
|
);
|
|
});
|
|
});
|
|
|
|
|
|
// ────────────────────────────────────────────────────────────────────────
|
|
// Folded from tests/bug-3381-verify-work-workstream.test.cjs — consolidation epic #1969 (B4 #1973)
|
|
// ────────────────────────────────────────────────────────────────────────
|
|
{
|
|
const { describe: __foldDescribe } = require('node:test');
|
|
__foldDescribe("folded:bug-3381-verify-work-workstream (consolidation epic #1969 B4 #1973)", () => {
|
|
// allow-test-rule: source-text-is-the-product — verify-work.md is a runtime workflow contract. (see #3381)
|
|
|
|
'use strict';
|
|
|
|
const { test, describe } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
describe('bug #1716: resume_from_file routes to complete_session when no [pending] tests remain', () => {
|
|
test('resume_from_file step contains guard clause for zero-pending (all-blocked) case', () => {
|
|
const workflow = fs.readFileSync(
|
|
path.join(__dirname, '..', 'msd-core', 'workflows', 'verify-work.md'),
|
|
'utf8',
|
|
);
|
|
|
|
const stepStart = workflow.indexOf('<step name="resume_from_file">');
|
|
assert.ok(stepStart !== -1, 'resume_from_file step must exist');
|
|
|
|
const stepEnd = workflow.indexOf('</step>', stepStart);
|
|
const stepBody = workflow.slice(stepStart, stepEnd);
|
|
|
|
// Guard must appear immediately after the find-pending instruction.
|
|
// Without it, all-blocked sessions (pending_count==0, blocked_count>0)
|
|
// silently terminate and never reach complete_session (#1716).
|
|
const findIdx = stepBody.indexOf("Find first test with `result: [pending]`.");
|
|
const guardIdx = stepBody.indexOf("If no `[pending]` test found → go to `complete_session`.");
|
|
|
|
assert.ok(findIdx !== -1, 'find-pending instruction must be present');
|
|
assert.ok(guardIdx !== -1, 'guard clause for zero-pending case must be present');
|
|
assert.ok(guardIdx > findIdx, 'guard must appear after find-pending instruction');
|
|
|
|
const between = stepBody
|
|
.slice(findIdx + "Find first test with `result: [pending]`.".length, guardIdx)
|
|
.trim();
|
|
assert.strictEqual(between, '', 'guard must be the next non-whitespace line after find-pending');
|
|
});
|
|
});
|
|
|
|
describe('bug #3381: verify-work forwards workstream context', () => {
|
|
test('workflow forwards ${MSD_WS} to workstream-sensitive SDK queries', () => {
|
|
// #2994 fragmentization moved the roadmap.get-phase user-story-format-guard
|
|
// bash block out of verify-work.md into
|
|
// msd-core/workflows/verify-work/steps/mvp-uat-framing.md behind a section
|
|
// marker (`state:phase-mvp-mode`). Read host + every step file combined so
|
|
// this MSD_WS-forwarding guard keeps seeing the moved query.
|
|
const VERIFY_WORK_MD = path.join(__dirname, '..', 'msd-core', 'workflows', 'verify-work.md');
|
|
const VERIFY_WORK_STEPS_DIR = path.join(__dirname, '..', 'msd-core', 'workflows', 'verify-work', 'steps');
|
|
let workflow = fs.readFileSync(VERIFY_WORK_MD, 'utf8');
|
|
if (fs.existsSync(VERIFY_WORK_STEPS_DIR)) {
|
|
for (const entry of fs.readdirSync(VERIFY_WORK_STEPS_DIR).sort()) {
|
|
if (entry.endsWith('.md')) {
|
|
workflow += '\n' + fs.readFileSync(path.join(VERIFY_WORK_STEPS_DIR, entry), 'utf8');
|
|
}
|
|
}
|
|
}
|
|
|
|
// The --ws capture character class was deliberately narrowed from
|
|
// `[^[:space:]]+` to `[A-Za-z0-9._-]+` (workstream slugs are
|
|
// alphanumeric/dot/underscore/hyphen; the old class captured any
|
|
// non-space run, including shell metacharacters). Forwarding itself
|
|
// (MSD_WS reaching every workstream-sensitive query below) is
|
|
// unaffected by the narrower class, so only the literal pattern here
|
|
// is updated — the asserted forwarding property is unchanged.
|
|
assert.match(workflow, /MSD_WS=""/, 'verify-work must initialize MSD_WS');
|
|
assert.match(
|
|
workflow,
|
|
/grep -qE -- '--ws\[\[:space:\]\]\+\[A-Za-z0-9\._-\]\+'/,
|
|
'verify-work must detect --ws in $ARGUMENTS',
|
|
);
|
|
assert.match(
|
|
workflow,
|
|
/grep -oE -- '--ws\[\[:space:\]\]\+\[A-Za-z0-9\._-\]\+'/,
|
|
'verify-work must extract the --ws flag pair from $ARGUMENTS',
|
|
);
|
|
assert.match(
|
|
workflow,
|
|
/PHASE_ARG=\$\(echo "\$ARGUMENTS" \| sed -E 's\/--ws\[\[:space:\]\]\+\[A-Za-z0-9\._-\]\+\/\/g' \| xargs\)/,
|
|
'verify-work must derive PHASE_ARG after removing --ws',
|
|
);
|
|
// After #3797 architectural fix, callsites use msd_run
|
|
assert.match(
|
|
workflow,
|
|
/msd_run query init\.verify-work "\$\{PHASE_ARG\}" \$\{MSD_WS\}/,
|
|
'init.verify-work must receive MSD_WS so phase_dir resolves in workstreams',
|
|
);
|
|
assert.match(
|
|
workflow,
|
|
/msd_run query phase\.mvp-mode "\$\{phase_number\}" \$\{MSD_WS\} --pick active/,
|
|
'phase.mvp-mode must receive MSD_WS so roadmap mode is workstream-scoped',
|
|
);
|
|
assert.match(
|
|
workflow,
|
|
/msd_run query roadmap\.get-phase "\$\{phase_number\}" \$\{MSD_WS\} --pick goal/,
|
|
'roadmap.get-phase must receive MSD_WS so goals are workstream-scoped',
|
|
);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// ── #4682 — the stale stop routes to the verifier, not to itself ─────────────
|
|
// A stale report means covered source files changed after the verifier ran;
|
|
// the only remedy is re-running the verifier. /msd-verify-work never rewrites
|
|
// VERIFICATION.md, so advising it from its own stale block is an advice loop.
|
|
describe('verify-work.md — stale stop routes to the verifier (#4682)', () => {
|
|
test('the stale stop instructs re-running the verifier, not verify-work (#4682)', () => {
|
|
const content = fs.readFileSync(VERIFY_WORK, 'utf-8');
|
|
const staleIdx = content.indexOf('If `PHASE_VERIFICATION_STATUS` is `stale`');
|
|
assert.ok(staleIdx !== -1, 'the stale stop must exist');
|
|
const block = content.slice(staleIdx, staleIdx + 1600);
|
|
|
|
assert.match(block, /msd-verifier/, 'the stale stop must route to the msd-verifier agent');
|
|
assert.match(block, /verification\.status/, 'it must re-check verification.status afterwards');
|
|
assert.doesNotMatch(
|
|
block, /`\/msd:verify-work \{phase\}` — re-run verification/,
|
|
'the self-referential re-run advice must be gone'
|
|
);
|
|
});
|
|
});
|