* fix: require fresh phase verification before transition * no-mistakes(review): Fix canonical verification closeout gates * no-mistakes(review): Fix verify-work frontmatter promotion command * no-mistakes(review): Fix stale verification gates * no-mistakes(review): Fix canonical verification routing gates * no-mistakes(review): Fix verification dependency and runtime routing gates * no-mistakes(review): Block stale verification bypasses * fix: handle large init manager outputs in verification workflows * chore: update changeset pr number * fix(verify-work): use fresh verification.status for stale gate The stale check after UAT used phase_completion.verification_status from session-start INIT while human_needed promotion already queried fresh verification.status. Align the stale gate with the canonical query so mid-session verification refresh is not ignored. * fix(init): skip roadmap-checked phases when selecting next_phase Roadmap-only phases without a disk directory were still promoted to next_phase when their checkbox was already checked. Exclude checkboxComplete phases so progress routing does not point at work the roadmap already marks done. * fix: gaps_found not overridden by stale, transition uses canonical verification - verification.cts: check gaps_found before stale so gap-closure routing is not masked by a newer summary mtime - phase.cts: remove redundant findStaleVerificationSummary — readVerificationStatus already handles stale detection - transition.md: replace raw grep on file content with verification.status query to avoid false-positive blocks from body text matching * ci: retrigger tests after rebase * fix(transition): replace gsd_run advisory check with awk frontmatter extraction The runtime launcher is not defined until the update_roadmap_and_state step bash block (~line 165). The early verify_completion block used gsd_run to query verification.status, which violated the runtime-launcher-parity test: 'preamble appears AFTER the first gsd_run reference'. Replace the gsd_run call with an awk-based frontmatter extractor that reads only the status: field between the two --- fences. This avoids both the preamble-ordering constraint and the original false-positive grep bug where body text like 'previous_status: gaps_found' would match a full-text regex. The phase.complete gate at update_roadmap_and_state is the canonical enforcement point; this early check is advisory only. Also update workflow-size-baseline.json for the updated transition.md size. Fixes: runtime-launcher-parity test (B) Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com> * fix: re-check verification under planning lock in phase complete Move readVerificationStatus into withPlanningLock so stale verification cannot slip through when a SUMMARY.md is written between the gate and the roadmap/state mutation. Return the blocked status from the lock callback and emit the error after release to avoid leaving .lock behind. * fix(transition): gate on canonical verification.status including stale Replace awk frontmatter read with verification.status query so transition blocks when summaries are newer than VERIFICATION.md, matching phase.complete and other workflows (autonomous, progress, verify-work). * Fix workflow verification gates for yolo transition and stale routing Require VERIFY_STATUS passed before yolo/interactive transition advance. Route stale verification recovery to verify-work, matching canonical projection. * fix(transition): use verification.status query for stale-aware advisory check The awk-based check read raw frontmatter status: passed, which misses the stale case where summaries are newer than the VERIFICATION.md file even though the frontmatter still says passed. The stale status is computed from file modification times, not stored in frontmatter. Move the preamble to the verify_completion bash block (the first block with a gsd_run call) so gsd_run query verification.status can be used for the advisory check. This gives the full readVerificationStatus logic including mtime-based staleness detection, matching the enforcement gate at phase.complete. Capture full JSON (VERIFY_JSON) so next_action can be included in the advisory output alongside the status. Also update workflow-size-baseline.json for the updated transition.md size. Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com> * ci: trigger test matrix for 525b946 Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com> * fix(transition): restore awk frontmatter extraction for pre-shim verification check The gsd_run launcher shim is not defined until line ~163 of transition.md, so the verification debt check at line ~80 cannot use gsd_run. Restore the awk-based frontmatter extraction that correctly reads status without needing the runtime, and restore the shim at its proper location before phase.complete. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(#1522): clarify transition verification gate wording * fix(#1522): update transition workflow size baseline * fix(#1522): update workflow-size-baseline after rebase onto next Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com> * fix(#1522): guard findStaleVerificationSummary FS calls + thread opts.fs seam (review) Address review blocker B1 on #1548: findStaleVerificationSummary ran fs.readdirSync and two fs.statSync calls unguarded between readVerificationStatus's try/catch sections, so a TOCTOU race (a SUMMARY listed by scanPhasePlans then removed before statSync) or any FS error threw uncaught into callers NOT under the planning lock (init.manager / init.progress / uat-predicate). Wrap the body in try/catch degrading to 'not stale', and thread the injectable opts.fs seam (add statSync to FsLike, pass fsImpl from the caller) for parity with readVerificationStatus's no-throw contract and testability. Also adds the Verification Module glossary entry to CONTEXT.md (review B3). --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
501 lines
20 KiB
JavaScript
501 lines
20 KiB
JavaScript
// allow-test-rule: source-text-is-the-product
|
|
// Reads .md/.json/.yml product files whose deployed text IS what the
|
|
// runtime loads — testing text content tests the deployed contract.
|
|
// Migrated from pending-migration-to-typed-ir per #455.
|
|
|
|
/**
|
|
* GSD Milestone Summary + Audit Tests
|
|
*
|
|
* Validates the milestone-summary command, milestone-audit module (#2158),
|
|
* workflow audit gates (complete-milestone, verify-work), and STATE.md template.
|
|
* Also tests artifact discovery logic.
|
|
*/
|
|
|
|
const { test, describe, beforeEach, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { cleanup } = require('./helpers.cjs');
|
|
|
|
const repoRoot = path.resolve(__dirname, '..');
|
|
const commandPath = path.join(repoRoot, 'commands', 'gsd', 'milestone-summary.md');
|
|
const workflowPath = path.join(repoRoot, 'gsd-core', 'workflows', 'milestone-summary.md');
|
|
|
|
function extractStep(content, stepName) {
|
|
const start = content.indexOf(`<step name="${stepName}">`);
|
|
assert.ok(start !== -1, `${stepName} step must exist`);
|
|
const end = content.indexOf('</step>', start);
|
|
assert.ok(end !== -1, `${stepName} step must close`);
|
|
return content.slice(start, end);
|
|
}
|
|
|
|
describe('milestone-summary command', () => {
|
|
test('command file exists', () => {
|
|
assert.ok(fs.existsSync(commandPath), 'commands/gsd/milestone-summary.md should exist');
|
|
});
|
|
|
|
test('command has correct frontmatter name', () => {
|
|
const content = fs.readFileSync(commandPath, 'utf-8');
|
|
assert.ok(content.includes('name: gsd:milestone-summary'), 'should have correct command name');
|
|
});
|
|
|
|
test('command references workflow in execution_context', () => {
|
|
const content = fs.readFileSync(commandPath, 'utf-8');
|
|
assert.ok(
|
|
content.includes('workflows/milestone-summary.md'),
|
|
'should reference the milestone-summary workflow'
|
|
);
|
|
});
|
|
|
|
test('command accepts optional version argument', () => {
|
|
const content = fs.readFileSync(commandPath, 'utf-8');
|
|
assert.ok(content.includes('argument-hint'), 'should have argument-hint');
|
|
assert.ok(content.includes('[version]'), 'version should be optional (bracketed)');
|
|
});
|
|
});
|
|
|
|
describe('milestone-summary workflow', () => {
|
|
test('workflow file exists', () => {
|
|
assert.ok(fs.existsSync(workflowPath), 'workflows/milestone-summary.md should exist');
|
|
});
|
|
|
|
test('workflow reads milestone artifacts', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf-8');
|
|
const requiredArtifacts = [
|
|
'ROADMAP.md',
|
|
'REQUIREMENTS.md',
|
|
'PROJECT.md',
|
|
'SUMMARY.md',
|
|
'VERIFICATION.md',
|
|
'CONTEXT.md',
|
|
'RETROSPECTIVE.md',
|
|
];
|
|
for (const artifact of requiredArtifacts) {
|
|
assert.ok(
|
|
content.includes(artifact),
|
|
`workflow should reference ${artifact}`
|
|
);
|
|
}
|
|
});
|
|
|
|
test('workflow writes to reports directory', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf-8');
|
|
assert.ok(
|
|
content.includes('.planning/reports/MILESTONE_SUMMARY'),
|
|
'should write summary to .planning/reports/'
|
|
);
|
|
});
|
|
|
|
test('workflow has interactive Q&A mode', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf-8');
|
|
assert.ok(
|
|
content.includes('Interactive Mode') || content.includes('ask anything'),
|
|
'should offer interactive Q&A after summary'
|
|
);
|
|
});
|
|
|
|
test('workflow handles both archived and current milestones', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf-8');
|
|
assert.ok(content.includes('Archived milestone'), 'should handle archived milestones');
|
|
assert.ok(content.includes('Current') || content.includes('in-progress'), 'should handle current milestones');
|
|
});
|
|
|
|
test('workflow generates all 7 summary sections', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf-8');
|
|
const sections = [
|
|
'Project Overview',
|
|
'Architecture',
|
|
'Phases Delivered',
|
|
'Requirements Coverage',
|
|
'Key Decisions',
|
|
'Tech Debt',
|
|
'Getting Started',
|
|
];
|
|
for (const section of sections) {
|
|
assert.ok(
|
|
content.includes(section),
|
|
`summary should include "${section}" section`
|
|
);
|
|
}
|
|
});
|
|
|
|
test('workflow updates STATE.md', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf-8');
|
|
assert.ok(
|
|
content.includes('state record-session') || content.includes('state.record-session'),
|
|
'should update STATE.md via state record-session (CJS or gsd-sdk query)'
|
|
);
|
|
});
|
|
|
|
test('workflow has overwrite guard for existing summaries', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf-8');
|
|
assert.ok(
|
|
content.includes('already exists'),
|
|
'should check for existing summary before overwriting'
|
|
);
|
|
});
|
|
|
|
test('workflow handles empty phase directories gracefully', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf-8');
|
|
assert.ok(
|
|
content.includes('no phase directories') || content.includes('No phases'),
|
|
'should handle case where no phases exist'
|
|
);
|
|
});
|
|
|
|
test('workflow checks both audit file locations for archived milestones', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf-8');
|
|
assert.ok(
|
|
content.includes('.planning/milestones/v${VERSION}-MILESTONE-AUDIT.md'),
|
|
'should check milestones/ directory for archived audit file'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('milestone-summary command structure', () => {
|
|
test('command has success_criteria section', () => {
|
|
const content = fs.readFileSync(commandPath, 'utf-8');
|
|
assert.ok(
|
|
content.includes('<success_criteria>'),
|
|
'should have success_criteria section (follows complete-milestone pattern)'
|
|
);
|
|
});
|
|
|
|
test('command context lists RESEARCH.md', () => {
|
|
const content = fs.readFileSync(commandPath, 'utf-8');
|
|
assert.ok(
|
|
content.includes('RESEARCH.md'),
|
|
'should list RESEARCH.md in context block'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('milestone-summary artifact path resolution', () => {
|
|
test('archived milestone paths point to milestones/ directory', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf-8');
|
|
// Archived roadmap path should be under milestones/
|
|
assert.ok(
|
|
content.includes('.planning/milestones/v${VERSION}-ROADMAP.md'),
|
|
'archived ROADMAP path should be under .planning/milestones/'
|
|
);
|
|
assert.ok(
|
|
content.includes('.planning/milestones/v${VERSION}-REQUIREMENTS.md'),
|
|
'archived REQUIREMENTS path should be under .planning/milestones/'
|
|
);
|
|
assert.ok(
|
|
content.includes('.planning/milestones/v${VERSION}-MILESTONE-AUDIT.md'),
|
|
'archived AUDIT path should be under .planning/milestones/'
|
|
);
|
|
});
|
|
|
|
test('current milestone paths point to .planning/ root', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf-8');
|
|
// Current milestone should read from .planning/ root
|
|
const lines = content.split('\n');
|
|
const currentSection = lines.slice(
|
|
lines.findIndex(l => l.includes('Current/in-progress')),
|
|
lines.findIndex(l => l.includes('Current/in-progress')) + 10
|
|
).join('\n');
|
|
assert.ok(
|
|
currentSection.includes('ROADMAP_PATH=".planning/ROADMAP.md"'),
|
|
'current ROADMAP path should be at .planning/ root'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('milestone-summary fixture-based artifact discovery', () => {
|
|
const os = require('os');
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-ms-test-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('discovers artifacts in archived milestone structure', () => {
|
|
// Create archived milestone structure
|
|
const milestonesDir = path.join(tmpDir, '.planning', 'milestones');
|
|
fs.mkdirSync(milestonesDir, { recursive: true });
|
|
fs.writeFileSync(path.join(milestonesDir, 'v1.0-ROADMAP.md'), '# Roadmap v1.0');
|
|
fs.writeFileSync(path.join(milestonesDir, 'v1.0-REQUIREMENTS.md'), '# Reqs v1.0');
|
|
fs.writeFileSync(path.join(milestonesDir, 'v1.0-MILESTONE-AUDIT.md'), '# Audit v1.0');
|
|
|
|
// Verify all 3 archived files are discoverable
|
|
const files = fs.readdirSync(milestonesDir);
|
|
assert.ok(files.includes('v1.0-ROADMAP.md'), 'archived ROADMAP should exist');
|
|
assert.ok(files.includes('v1.0-REQUIREMENTS.md'), 'archived REQUIREMENTS should exist');
|
|
assert.ok(files.includes('v1.0-MILESTONE-AUDIT.md'), 'archived AUDIT should exist');
|
|
});
|
|
|
|
test('discovers phase artifacts across multiple phases', () => {
|
|
// Create phase structure with varying artifact completeness
|
|
const phase1 = path.join(tmpDir, '.planning', 'phases', '01-setup');
|
|
const phase2 = path.join(tmpDir, '.planning', 'phases', '02-core');
|
|
const phase3 = path.join(tmpDir, '.planning', 'phases', '03-ui');
|
|
fs.mkdirSync(phase1, { recursive: true });
|
|
fs.mkdirSync(phase2, { recursive: true });
|
|
fs.mkdirSync(phase3, { recursive: true });
|
|
|
|
// Phase 1: all artifacts
|
|
fs.writeFileSync(path.join(phase1, '01-SUMMARY.md'), 'one_liner: Setup');
|
|
fs.writeFileSync(path.join(phase1, '01-CONTEXT.md'), '<decisions>D-01</decisions>');
|
|
fs.writeFileSync(path.join(phase1, '01-VERIFICATION.md'), 'status: passed');
|
|
fs.writeFileSync(path.join(phase1, '01-RESEARCH.md'), '# Research');
|
|
|
|
// Phase 2: partial artifacts (no RESEARCH, no VERIFICATION)
|
|
fs.writeFileSync(path.join(phase2, '02-SUMMARY.md'), 'one_liner: Core');
|
|
fs.writeFileSync(path.join(phase2, '02-CONTEXT.md'), '<decisions>D-02</decisions>');
|
|
|
|
// Phase 3: only SUMMARY
|
|
fs.writeFileSync(path.join(phase3, '03-SUMMARY.md'), 'one_liner: UI');
|
|
|
|
// Verify discovery
|
|
const phasesDir = path.join(tmpDir, '.planning', 'phases');
|
|
const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true })
|
|
.filter(e => e.isDirectory())
|
|
.map(e => e.name);
|
|
assert.strictEqual(phaseDirs.length, 3, 'should find 3 phase directories');
|
|
|
|
// Phase 1 has all 4 artifact types
|
|
const p1Files = fs.readdirSync(phase1);
|
|
assert.strictEqual(p1Files.length, 4, 'phase 1 should have 4 artifacts');
|
|
|
|
// Phase 2 has 2 artifact types
|
|
const p2Files = fs.readdirSync(phase2);
|
|
assert.strictEqual(p2Files.length, 2, 'phase 2 should have 2 artifacts');
|
|
|
|
// Phase 3 has 1 artifact type
|
|
const p3Files = fs.readdirSync(phase3);
|
|
assert.strictEqual(p3Files.length, 1, 'phase 3 should have 1 artifact');
|
|
});
|
|
|
|
test('handles empty .planning directory without error', () => {
|
|
const planningDir = path.join(tmpDir, '.planning');
|
|
fs.mkdirSync(planningDir, { recursive: true });
|
|
|
|
// No milestones, no phases — just empty .planning/
|
|
const contents = fs.readdirSync(planningDir);
|
|
assert.strictEqual(contents.length, 0, 'empty .planning/ should have no contents');
|
|
|
|
// Should not throw when checking for milestones dir
|
|
const milestonesExists = fs.existsSync(path.join(planningDir, 'milestones'));
|
|
assert.strictEqual(milestonesExists, false, 'milestones/ should not exist');
|
|
|
|
const phasesExists = fs.existsSync(path.join(planningDir, 'phases'));
|
|
assert.strictEqual(phasesExists, false, 'phases/ should not exist');
|
|
});
|
|
|
|
test('output path pattern produces valid filenames', () => {
|
|
const versions = ['1.0', '1.1', '2.0', '0.1'];
|
|
for (const v of versions) {
|
|
const filename = `MILESTONE_SUMMARY-v${v}.md`;
|
|
assert.ok(
|
|
/^MILESTONE_SUMMARY-v\d+\.\d+\.md$/.test(filename),
|
|
`"${filename}" should be a valid milestone summary filename`
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('milestone-summary git stats resilience', () => {
|
|
test('workflow has fallback methods when tag does not exist', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf-8');
|
|
assert.ok(
|
|
content.includes('Method 1') && content.includes('Method 2'),
|
|
'should have multiple fallback methods for git stats'
|
|
);
|
|
});
|
|
|
|
test('workflow can skip stats gracefully', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf-8');
|
|
assert.ok(
|
|
content.includes('Skip stats') || content.includes('statistics unavailable'),
|
|
'should handle case where git stats cannot be gathered'
|
|
);
|
|
});
|
|
|
|
test('command has type: prompt in frontmatter', () => {
|
|
const content = fs.readFileSync(commandPath, 'utf-8');
|
|
assert.ok(
|
|
content.includes('type: prompt'),
|
|
'should have type: prompt for consistency with complete-milestone.md'
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// audit.cjs module (#2158)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('audit.cjs module (#2158)', () => {
|
|
const { createTempProject: createTP, cleanup: cleanTP } = require('./helpers.cjs');
|
|
let tmpDir;
|
|
|
|
beforeEach(() => { tmpDir = createTP('audit-test'); });
|
|
afterEach(() => { cleanTP(tmpDir); });
|
|
|
|
test('auditOpenArtifacts returns structured result with counts', () => {
|
|
const { auditOpenArtifacts } = require('../gsd-core/bin/lib/audit.cjs');
|
|
const result = auditOpenArtifacts(tmpDir);
|
|
assert.ok(typeof result === 'object');
|
|
assert.ok(typeof result.counts === 'object');
|
|
assert.ok(typeof result.counts.total === 'number');
|
|
assert.ok(typeof result.has_open_items === 'boolean');
|
|
});
|
|
|
|
test('auditOpenArtifacts handles missing planning directories gracefully', () => {
|
|
const { auditOpenArtifacts } = require('../gsd-core/bin/lib/audit.cjs');
|
|
const result = auditOpenArtifacts(tmpDir);
|
|
assert.strictEqual(result.counts.total, 0);
|
|
assert.strictEqual(result.has_open_items, false);
|
|
});
|
|
|
|
test('auditOpenArtifacts detects open debug sessions', () => {
|
|
const { auditOpenArtifacts } = require('../gsd-core/bin/lib/audit.cjs');
|
|
const debugDir = path.join(tmpDir, '.planning', 'debug');
|
|
fs.mkdirSync(debugDir, { recursive: true });
|
|
fs.writeFileSync(path.join(debugDir, 'test-bug.md'), [
|
|
'---', 'status: investigating', 'trigger: login fails', 'updated: 2026-04-10', '---',
|
|
'# Debug: test-bug',
|
|
].join('\n'));
|
|
|
|
const result = auditOpenArtifacts(tmpDir);
|
|
assert.strictEqual(result.counts.debug_sessions, 1);
|
|
assert.ok(result.has_open_items);
|
|
});
|
|
|
|
test('auditOpenArtifacts ignores resolved debug sessions', () => {
|
|
const { auditOpenArtifacts } = require('../gsd-core/bin/lib/audit.cjs');
|
|
const resolvedDir = path.join(tmpDir, '.planning', 'debug', 'resolved');
|
|
fs.mkdirSync(resolvedDir, { recursive: true });
|
|
fs.writeFileSync(path.join(resolvedDir, 'old-bug.md'), ['---', 'status: resolved', '---', '# Resolved'].join('\n'));
|
|
|
|
const result = auditOpenArtifacts(tmpDir);
|
|
assert.strictEqual(result.counts.debug_sessions, 0);
|
|
});
|
|
|
|
test('formatAuditReport returns string with header', () => {
|
|
const { auditOpenArtifacts, formatAuditReport } = require('../gsd-core/bin/lib/audit.cjs');
|
|
const report = formatAuditReport(auditOpenArtifacts(tmpDir));
|
|
assert.ok(typeof report === 'string');
|
|
assert.ok(report.includes('Artifact Audit') || report.includes('artifact audit') || report.includes('All artifact'));
|
|
});
|
|
|
|
test('formatAuditReport shows all clear when no open items', () => {
|
|
const { auditOpenArtifacts, formatAuditReport } = require('../gsd-core/bin/lib/audit.cjs');
|
|
const report = formatAuditReport(auditOpenArtifacts(tmpDir));
|
|
assert.ok(report.includes('clear') || report.includes('0 items') || report.includes('no open'));
|
|
});
|
|
});
|
|
|
|
describe('complete-milestone workflow has pre-close audit gate (#2158)', () => {
|
|
const completeMilestoneContent = fs.readFileSync(
|
|
path.join(__dirname, '..', 'gsd-core', 'workflows', 'complete-milestone.md'),
|
|
'utf8',
|
|
);
|
|
|
|
test('complete-milestone has pre_close_artifact_audit step', () => {
|
|
assert.ok(
|
|
completeMilestoneContent.includes('pre_close_artifact_audit') ||
|
|
completeMilestoneContent.includes('audit-open'),
|
|
);
|
|
});
|
|
|
|
test('complete-milestone surfaces deferred items to STATE.md', () => {
|
|
assert.ok(completeMilestoneContent.includes('Deferred Items'));
|
|
});
|
|
|
|
test('complete-milestone has security note for audit output', () => {
|
|
assert.ok(
|
|
completeMilestoneContent.includes('sanitiz') || completeMilestoneContent.includes('SECURITY'),
|
|
);
|
|
});
|
|
|
|
test('complete-milestone distinguishes verified and override closeout (#1527)', () => {
|
|
assert.match(completeMilestoneContent, /all_phases_verified/);
|
|
assert.match(completeMilestoneContent, /closeout_type/);
|
|
assert.match(completeMilestoneContent, /verified_closeout/);
|
|
assert.match(completeMilestoneContent, /override_closeout/);
|
|
assert.match(completeMilestoneContent, /Known verification overrides/);
|
|
});
|
|
|
|
test('verified closeout uses init.manager canonical verification projection (#1522)', () => {
|
|
const readinessStep = extractStep(completeMilestoneContent, 'verify_readiness');
|
|
|
|
assert.match(readinessStep, /INIT_MANAGER=\$\(gsd_run query init\.manager\)/);
|
|
assert.ok(
|
|
readinessStep.includes('if [[ "$INIT_MANAGER" == @file:* ]]; then INIT_MANAGER=$(cat "${INIT_MANAGER#@file:}"); fi'),
|
|
'complete-milestone readiness must dereference large init.manager payloads before jq',
|
|
);
|
|
assert.match(readinessStep, /select\(\(\.number \| tostring \| test\("\^999/);
|
|
assert.match(readinessStep, /\| not\)\)/);
|
|
assert.match(readinessStep, /phase_complete === true/);
|
|
assert.match(readinessStep, /verification_status === 'passed'/);
|
|
assert.match(readinessStep, /If not all_phases_verified/);
|
|
assert.match(readinessStep, /verified_closeout must not proceed/);
|
|
assert.doesNotMatch(readinessStep, /ROADMAP=\$\(gsd_run query roadmap\.analyze\)/);
|
|
assert.doesNotMatch(readinessStep, /disk_status === 'complete'/);
|
|
});
|
|
});
|
|
|
|
describe('verify-work workflow has phase artifact check (#2157)', () => {
|
|
const verifyWorkContent = fs.readFileSync(
|
|
path.join(__dirname, '..', 'gsd-core', 'workflows', 'verify-work.md'),
|
|
'utf8',
|
|
);
|
|
|
|
test('verify-work has scan_phase_artifacts step', () => {
|
|
assert.ok(
|
|
verifyWorkContent.includes('scan_phase_artifacts') || verifyWorkContent.includes('audit-open'),
|
|
);
|
|
});
|
|
|
|
test('verify-work prompts user on open UAT gaps', () => {
|
|
assert.ok(verifyWorkContent.includes('gaps') && verifyWorkContent.includes('Proceed'));
|
|
});
|
|
});
|
|
|
|
describe('state.md template has Deferred Items section (#2158)', () => {
|
|
const stateTemplate = fs.readFileSync(
|
|
path.join(__dirname, '..', 'gsd-core', 'templates', 'state.md'),
|
|
'utf8',
|
|
);
|
|
|
|
test('state.md template includes Deferred Items section', () => {
|
|
assert.ok(stateTemplate.includes('Deferred Items'));
|
|
});
|
|
});
|
|
|
|
describe('audit-open CLI command — ReferenceError regression (#2236)', () => {
|
|
const { createTempProject: createTP, cleanup: cleanTP, runGsdTools: run } = require('./helpers.cjs');
|
|
let tmpDir;
|
|
|
|
beforeEach(() => { tmpDir = createTP('audit-open-cli-test'); });
|
|
afterEach(() => { cleanTP(tmpDir); });
|
|
|
|
test('audit-open exits without error on an empty project', () => {
|
|
const result = run(['audit-open'], tmpDir);
|
|
assert.ok(result.success, `audit-open crashed: ${result.error}`);
|
|
});
|
|
|
|
test('audit-open --json exits without error and returns valid JSON', () => {
|
|
const result = run(['audit-open', '--json'], tmpDir);
|
|
assert.ok(result.success, `audit-open --json crashed: ${result.error}`);
|
|
let parsed;
|
|
assert.doesNotThrow(() => { parsed = JSON.parse(result.output); });
|
|
assert.ok(typeof parsed === 'object');
|
|
assert.ok(typeof parsed.counts === 'object');
|
|
});
|
|
|
|
test('audit-open error is not ReferenceError: output is not defined', () => {
|
|
const result = run(['audit-open'], tmpDir);
|
|
assert.ok(
|
|
!String(result.error).includes('output is not defined'),
|
|
`ReferenceError regression: ${result.error}`,
|
|
);
|
|
});
|
|
});
|