* feat(#2982): extend no-source-grep lint to catch var-binding readFileSync.includes() The base lint (scripts/lint-no-source-grep.cjs) only catches readFileSync(...).<text-method>() chained directly. The much more common var-binding form escapes it: const src = fs.readFileSync(p, 'utf8'); // 50 lines later if (src.includes('foo')) {} // ← still grep, lint missed it Scan of the test suite found ~141 files using this pattern. Implementation built TDD per #2982 with structured-IR assertions: scripts/lint-no-source-grep-extras.cjs - detectVarBindingViolations(src) — pure detector, two passes: pass 1 collects vars bound from readFileSync, pass 2 finds any <var>.<includes|startsWith|endsWith|match|search>( on those vars. - detectWrappedAssertOkMatch(src) — flags assert.ok(<expr>.match(...)) which escapes the assert.match rule. - VIOLATION enum exposes stable codes for tests to assert on. scripts/lint-no-source-grep.cjs - Wires the new detectors into the existing per-file check; one additional violation row per file with the first 3 sample tokens. tests/bug-2982-lint-var-binding.test.cjs - 13 tests, all assertions on typed VIOLATION enum / structured records. Covers all 5 text-match methods, multi-var, no-bind, string literal (must NOT trigger), wrapped assert.ok(.match), and assert.match (must NOT double-flag). Migration backlog (#2974 expanded scope): - 42 files annotated `// allow-test-rule: source-text-is-the-product` (legitimate — they read .md/.json/.yml files whose deployed text IS the product) - 3 files annotated `// allow-test-rule: pending-migration-to-typed-ir [#2974]` (read .cjs/.js source — clear migration debt) - 95 files annotated `pending-migration-to-typed-ir [#2974]` with `Per-file review may reclassify as source-text-is-the-product during migration` (mixed — manual review under #2974) After this lands the lint reports 0 violations on main; new violations in PRs surface immediately. Closes #2982 Refs #2974 * test(#2982): fix truncated test name per CR The label ended with a bare '(' from a copy-paste mishap. Now reads 'does NOT flag .matchAll(...) — matchAll is not match, so assert.ok(.matchAll(...)) is not flagged'. * chore(#2982): add changeset fragment for PR #2985 * chore(#2982): add changeset fragment for PR #2985
205 lines
7.9 KiB
JavaScript
205 lines
7.9 KiB
JavaScript
// allow-test-rule: pending-migration-to-typed-ir [#2974]
|
|
// Tracked in #2974 for migration to typed-IR assertions per CONTRIBUTING.md
|
|
// "Prohibited: Raw Text Matching on Test Outputs". Per-file review may
|
|
// reclassify some entries as source-text-is-the-product during migration.
|
|
|
|
/**
|
|
* GSD Tools Tests - /gsd-next safety gates and prior-phase completeness scan
|
|
*
|
|
* Validates that the next workflow includes three hard-stop safety gates
|
|
* (checkpoint, error state, verification), a prior-phase completeness scan
|
|
* replacing the old consecutive-call counter, and a --force bypass flag.
|
|
*
|
|
* Closes: #1732, #2089
|
|
*/
|
|
|
|
const { test, describe } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
describe('/gsd-next safety gates (#1732, #2089)', () => {
|
|
const workflowPath = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'next.md');
|
|
// #2790: next.md command was consolidated into progress.md as the --next flag.
|
|
const commandPath = path.join(__dirname, '..', 'commands', 'gsd', 'progress.md');
|
|
|
|
test('workflow contains safety_gates step', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf8');
|
|
assert.ok(
|
|
content.includes('<step name="safety_gates">'),
|
|
'workflow should have a safety_gates step'
|
|
);
|
|
});
|
|
|
|
test('safety_gates step appears between detect_state and determine_next_action', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf8');
|
|
const detectIdx = content.indexOf('name="detect_state"');
|
|
const gatesIdx = content.indexOf('name="safety_gates"');
|
|
const routeIdx = content.indexOf('name="determine_next_action"');
|
|
assert.ok(detectIdx > -1, 'detect_state step should exist');
|
|
assert.ok(gatesIdx > -1, 'safety_gates step should exist');
|
|
assert.ok(routeIdx > -1, 'determine_next_action step should exist');
|
|
assert.ok(
|
|
detectIdx < gatesIdx && gatesIdx < routeIdx,
|
|
'safety_gates must appear between detect_state and determine_next_action'
|
|
);
|
|
});
|
|
|
|
test('Gate 1: unresolved checkpoint (.continue-here.md)', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf8');
|
|
assert.ok(
|
|
content.includes('.continue-here.md'),
|
|
'Gate 1 should check for .planning/.continue-here.md'
|
|
);
|
|
assert.ok(
|
|
content.includes('Unresolved checkpoint'),
|
|
'Gate 1 should display "Unresolved checkpoint" message'
|
|
);
|
|
});
|
|
|
|
test('Gate 2: error state in STATE.md', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf8');
|
|
assert.ok(
|
|
content.includes('status: error') || content.includes('status: failed'),
|
|
'Gate 2 should check for error/failed status in STATE.md'
|
|
);
|
|
assert.ok(
|
|
content.includes('Project in error state'),
|
|
'Gate 2 should display "Project in error state" message'
|
|
);
|
|
});
|
|
|
|
test('Gate 3: unchecked verification failures', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf8');
|
|
assert.ok(
|
|
content.includes('VERIFICATION.md'),
|
|
'Gate 3 should check VERIFICATION.md'
|
|
);
|
|
assert.ok(
|
|
content.includes('FAIL'),
|
|
'Gate 3 should look for FAIL items'
|
|
);
|
|
assert.ok(
|
|
content.includes('Unchecked verification failures'),
|
|
'Gate 3 should display "Unchecked verification failures" message'
|
|
);
|
|
});
|
|
|
|
test('prior-phase completeness scan replaces consecutive-call counter', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf8');
|
|
assert.ok(
|
|
content.includes('Prior-phase completeness scan'),
|
|
'workflow should have a prior-phase completeness scan section'
|
|
);
|
|
assert.ok(
|
|
!content.includes('.next-call-count'),
|
|
'workflow must not reference the old .next-call-count counter file'
|
|
);
|
|
assert.ok(
|
|
!content.includes('consecutively'),
|
|
'workflow must not reference consecutive call counting'
|
|
);
|
|
});
|
|
|
|
test('completeness scan checks plans without summaries', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf8');
|
|
assert.ok(
|
|
content.includes('Plans without summaries') || content.includes('no SUMMARY.md'),
|
|
'completeness scan should detect plans that ran without producing summaries'
|
|
);
|
|
});
|
|
|
|
test('completeness scan checks verification failures in prior phases', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf8');
|
|
assert.ok(
|
|
content.includes('Verification failures not overridden') ||
|
|
content.includes('VERIFICATION.md with `FAIL`'),
|
|
'completeness scan should detect unoverridden FAIL items in prior phase VERIFICATION.md'
|
|
);
|
|
});
|
|
|
|
test('completeness scan checks CONTEXT.md without plans', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf8');
|
|
assert.ok(
|
|
content.includes('CONTEXT.md without plans') ||
|
|
content.includes('CONTEXT.md but no PLAN.md'),
|
|
'completeness scan should detect phases with discussion but no planning'
|
|
);
|
|
});
|
|
|
|
test('completeness scan offers Continue, Stop, and Force options', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf8');
|
|
assert.ok(content.includes('[C]'), 'completeness scan should offer [C] Continue option');
|
|
assert.ok(content.includes('[S]'), 'completeness scan should offer [S] Stop option');
|
|
assert.ok(content.includes('[F]'), 'completeness scan should offer [F] Force option');
|
|
});
|
|
|
|
test('deferral path creates backlog entry using 999.x scheme', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf8');
|
|
assert.ok(
|
|
content.includes('999.'),
|
|
'deferral should use the 999.x backlog numbering scheme'
|
|
);
|
|
assert.ok(
|
|
content.includes('Backlog') || content.includes('BACKLOG'),
|
|
'deferral should write to the Backlog section of ROADMAP.md'
|
|
);
|
|
});
|
|
|
|
test('clean prior phases route silently with no interruption', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf8');
|
|
assert.ok(
|
|
content.includes('silently') || content.includes('no interruption'),
|
|
'workflow should route without interruption when prior phases are clean'
|
|
);
|
|
});
|
|
|
|
test('--force flag bypasses all gates', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf8');
|
|
assert.ok(
|
|
content.includes('--force'),
|
|
'workflow should document --force flag'
|
|
);
|
|
assert.ok(
|
|
content.includes('skipping safety gates'),
|
|
'workflow should print warning when --force is used'
|
|
);
|
|
});
|
|
|
|
test('command definition documents --next flag with --force AND completeness routing (#2790)', () => {
|
|
// #2790 absorbed standalone /gsd-next into /gsd-progress --next. The
|
|
// consolidated command must preserve BOTH safety-relevant contracts:
|
|
// (a) --force escape hatch for bypassing safety gates
|
|
// (b) the completeness scan / next-workflow routing semantics
|
|
// Earlier OR-based predicates passed when only `--next` was mentioned,
|
|
// letting the completeness contract regress silently.
|
|
const content = fs.readFileSync(commandPath, 'utf8');
|
|
assert.ok(content.includes('--next'),
|
|
'progress.md must document the --next flag (absorbed from standalone next.md command in #2790)');
|
|
assert.ok(content.includes('--force'),
|
|
'progress.md must document the --force escape hatch for --next (#2790 carried over from next.md)');
|
|
const documentsCompleteness =
|
|
/completeness/i.test(content) ||
|
|
/next workflow/i.test(content) ||
|
|
/scans? all prior phases/i.test(content);
|
|
assert.ok(documentsCompleteness,
|
|
'progress.md must document the completeness scan / next-workflow routing for --next (#2790)');
|
|
});
|
|
|
|
test('next workflow documents --force bypass flag', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf8');
|
|
assert.ok(
|
|
content.includes('--force'),
|
|
'next.md workflow must document --force flag for bypassing safety gates'
|
|
);
|
|
});
|
|
|
|
test('gates exit on first hit', () => {
|
|
const content = fs.readFileSync(workflowPath, 'utf8');
|
|
assert.ok(
|
|
content.includes('Exit on first hit'),
|
|
'safety gates should exit on first hit'
|
|
);
|
|
});
|
|
});
|