mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-25 17:25:23 +02:00
* refactor(tests): standardize to node:assert/strict and t.after() per CONTRIBUTING.md
- Replace require('node:assert') with require('node:assert/strict') across
all 73 test files to enforce strict equality (no type coercion)
- Replace try/finally cleanup blocks with t.after() hooks in core.test.cjs
and hooks-opt-in.test.cjs per the test lifecycle standards
- Utility functions in codex-config and security-scan retain try/finally
as that is appropriate for per-function resource guards, not lifecycle hooks
Closes #1674
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* perf(tests): add --test-concurrency=4 to test runner for parallel file execution
Node.js --test-concurrency controls how many test files run as parallel child
processes. Set to 4 by default, configurable via TEST_CONCURRENCY env var.
Fixes tests at a known level rather than inheriting os.availableParallelism()
which varies across CI environments.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): allowlist verify.test.cjs in prompt-injection scanner
tests/verify.test.cjs uses <human>...</human> as GSD phase task-type
XML (meaning "a human should verify this step"), which matches the
scanner's fake-message-boundary pattern for LLM APIs. This is a
false positive — add it to the allowlist alongside the other test files
that legitimately contain injection-adjacent patterns.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
389 lines
16 KiB
JavaScript
389 lines
16 KiB
JavaScript
/**
|
|
* GSD Tools Tests - Health Validation
|
|
*
|
|
* Tests for fix/health-validation-1473c:
|
|
* - W011: STATE/ROADMAP cross-validation (phase divergence detection)
|
|
* - W012: branching_strategy validation
|
|
* - W013: context_window validation
|
|
* - W014: phase_branch_template placeholder validation
|
|
* - W015: milestone_branch_template placeholder validation
|
|
* - stateReplaceFieldWithFallback field-miss warning
|
|
* - Boundary conditions and edge cases
|
|
*/
|
|
|
|
const { test, describe, beforeEach, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
|
|
|
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
|
|
function writeMinimalRoadmap(tmpDir, phases = ['1']) {
|
|
const lines = phases.map(n => `### Phase ${n}: Phase ${n} Description`).join('\n');
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
`# Roadmap\n\n${lines}\n`
|
|
);
|
|
}
|
|
|
|
function writeMinimalStateMd(tmpDir, content) {
|
|
const defaultContent = content || `# Session State\n\n## Current Position\n\nPhase: 1\n`;
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'STATE.md'),
|
|
defaultContent
|
|
);
|
|
}
|
|
|
|
function writeMinimalProjectMd(tmpDir) {
|
|
const sections = ['## What This Is', '## Core Value', '## Requirements'];
|
|
const content = sections.map(s => `${s}\n\nContent here.\n`).join('\n');
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'PROJECT.md'),
|
|
`# Project\n\n${content}`
|
|
);
|
|
}
|
|
|
|
function writeValidConfigJson(tmpDir, overrides = {}) {
|
|
const base = { model_profile: 'balanced', commit_docs: true };
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'config.json'),
|
|
JSON.stringify({ ...base, ...overrides }, null, 2)
|
|
);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 1. W011: STATE/ROADMAP cross-validation
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('W011: STATE/ROADMAP cross-validation', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempProject();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('STATE says current phase but ROADMAP shows it as complete -> warning', () => {
|
|
writeMinimalProjectMd(tmpDir);
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
`# Roadmap\n\n- [x] Phase 3: Database Layer\n\n### Phase 3: Database Layer\n**Goal:** DB setup\n`
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'STATE.md'),
|
|
`# Session State\n\n**Current Phase:** 03\n**Current Phase Name:** Database Layer\n**Status:** In progress\n`
|
|
);
|
|
writeValidConfigJson(tmpDir);
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-database-layer'), { recursive: true });
|
|
|
|
const result = runGsdTools('validate health', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.ok(
|
|
output.warnings.some(w => w.code === 'W011'),
|
|
`Expected W011 in warnings: ${JSON.stringify(output.warnings)}`
|
|
);
|
|
});
|
|
|
|
test('STATE and ROADMAP agree (phase not checked off) -> no W011 warning', () => {
|
|
writeMinimalProjectMd(tmpDir);
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
`# Roadmap\n\n- [ ] Phase 2: API Layer\n\n### Phase 2: API Layer\n**Goal:** Build API\n`
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'STATE.md'),
|
|
`# Session State\n\n**Current Phase:** 2\n**Status:** In progress\n`
|
|
);
|
|
writeValidConfigJson(tmpDir);
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-api-layer'), { recursive: true });
|
|
|
|
const result = runGsdTools('validate health', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.ok(
|
|
!output.warnings.some(w => w.code === 'W011'),
|
|
`Should not have W011: ${JSON.stringify(output.warnings)}`
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 2. W012-W015: Config field validation
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('config field validation', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempProject();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('W012: invalid branching_strategy triggers warning', () => {
|
|
writeMinimalProjectMd(tmpDir);
|
|
writeMinimalRoadmap(tmpDir, ['1']);
|
|
writeMinimalStateMd(tmpDir, '# Session State\n\nPhase 1 in progress.\n');
|
|
writeValidConfigJson(tmpDir, { branching_strategy: 'banana' });
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
|
|
|
|
const result = runGsdTools('validate health', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.ok(
|
|
output.warnings.some(w => w.code === 'W012'),
|
|
`Expected W012 in warnings: ${JSON.stringify(output.warnings)}`
|
|
);
|
|
});
|
|
|
|
test('W013: negative context_window triggers warning', () => {
|
|
writeMinimalProjectMd(tmpDir);
|
|
writeMinimalRoadmap(tmpDir, ['1']);
|
|
writeMinimalStateMd(tmpDir, '# Session State\n\nPhase 1 in progress.\n');
|
|
writeValidConfigJson(tmpDir, { context_window: -500 });
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
|
|
|
|
const result = runGsdTools('validate health', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.ok(
|
|
output.warnings.some(w => w.code === 'W013'),
|
|
`Expected W013 in warnings: ${JSON.stringify(output.warnings)}`
|
|
);
|
|
});
|
|
|
|
test('W014: phase_branch_template missing {phase} triggers warning', () => {
|
|
writeMinimalProjectMd(tmpDir);
|
|
writeMinimalRoadmap(tmpDir, ['1']);
|
|
writeMinimalStateMd(tmpDir, '# Session State\n\nPhase 1 in progress.\n');
|
|
writeValidConfigJson(tmpDir, { phase_branch_template: 'gsd/no-placeholder-{slug}' });
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
|
|
|
|
const result = runGsdTools('validate health', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.ok(
|
|
output.warnings.some(w => w.code === 'W014'),
|
|
`Expected W014 in warnings: ${JSON.stringify(output.warnings)}`
|
|
);
|
|
});
|
|
|
|
test('W015: milestone_branch_template missing {milestone} triggers warning', () => {
|
|
writeMinimalProjectMd(tmpDir);
|
|
writeMinimalRoadmap(tmpDir, ['1']);
|
|
writeMinimalStateMd(tmpDir, '# Session State\n\nPhase 1 in progress.\n');
|
|
writeValidConfigJson(tmpDir, { milestone_branch_template: 'release/no-placeholder' });
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
|
|
|
|
const result = runGsdTools('validate health', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.ok(
|
|
output.warnings.some(w => w.code === 'W015'),
|
|
`Expected W015 in warnings: ${JSON.stringify(output.warnings)}`
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 3. Boundary conditions
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('boundary conditions', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempProject();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('context_window config accepts 500000 (boundary value)', () => {
|
|
writeMinimalProjectMd(tmpDir);
|
|
writeMinimalRoadmap(tmpDir, ['1']);
|
|
writeMinimalStateMd(tmpDir, '# Session State\n\nPhase 1 in progress.\n');
|
|
writeValidConfigJson(tmpDir, { context_window: 500000 });
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
|
|
|
|
const result = runGsdTools('validate health', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.ok(
|
|
!output.warnings.some(w => w.code === 'W013'),
|
|
`Should not have W013 for context_window=500000: ${JSON.stringify(output.warnings)}`
|
|
);
|
|
});
|
|
|
|
test('context_window config accepts 200000 (default value)', () => {
|
|
writeMinimalProjectMd(tmpDir);
|
|
writeMinimalRoadmap(tmpDir, ['1']);
|
|
writeMinimalStateMd(tmpDir, '# Session State\n\nPhase 1 in progress.\n');
|
|
writeValidConfigJson(tmpDir, { context_window: 200000 });
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
|
|
|
|
const result = runGsdTools('validate health', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.ok(
|
|
!output.warnings.some(w => w.code === 'W013'),
|
|
`Should not have W013 for context_window=200000: ${JSON.stringify(output.warnings)}`
|
|
);
|
|
});
|
|
|
|
test('W013 does NOT fire when context_window is absent from config', () => {
|
|
writeMinimalProjectMd(tmpDir);
|
|
writeMinimalRoadmap(tmpDir, ['1']);
|
|
writeMinimalStateMd(tmpDir, '# Session State\n\nPhase 1 in progress.\n');
|
|
writeValidConfigJson(tmpDir);
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
|
|
|
|
const result = runGsdTools('validate health', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.ok(
|
|
!output.warnings.some(w => w.code === 'W013'),
|
|
`Should not have W013 when context_window is absent: ${JSON.stringify(output.warnings)}`
|
|
);
|
|
});
|
|
|
|
test('health check handles STATE.md with no Current Phase field (no W011 crash)', () => {
|
|
writeMinimalProjectMd(tmpDir);
|
|
writeMinimalRoadmap(tmpDir, ['1']);
|
|
writeMinimalStateMd(tmpDir, '# Session State\n\nSome content but no phase reference.\n');
|
|
writeValidConfigJson(tmpDir);
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
|
|
|
|
const result = runGsdTools('validate health', tmpDir);
|
|
assert.ok(result.success, `Command should not crash: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.ok(typeof output.status === 'string', 'should return a status string');
|
|
assert.ok(Array.isArray(output.errors), 'should return errors array');
|
|
assert.ok(Array.isArray(output.warnings), 'should return warnings array');
|
|
});
|
|
|
|
test('health check handles empty ROADMAP.md (no crash)', () => {
|
|
writeMinimalProjectMd(tmpDir);
|
|
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '');
|
|
writeMinimalStateMd(tmpDir, '# Session State\n\nPhase 1.\n');
|
|
writeValidConfigJson(tmpDir);
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
|
|
|
|
const result = runGsdTools('validate health', tmpDir);
|
|
assert.ok(result.success, `Command should not crash on empty ROADMAP.md: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.ok(typeof output.status === 'string', 'should return a status string');
|
|
assert.ok(Array.isArray(output.errors), 'should return errors array');
|
|
assert.ok(Array.isArray(output.warnings), 'should return warnings array');
|
|
});
|
|
|
|
test('config.json with trailing comma -- validate health reports parse error', () => {
|
|
writeMinimalProjectMd(tmpDir);
|
|
writeMinimalStateMd(tmpDir, '# Session State\n\nPhase 1.\n');
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'config.json'),
|
|
'{"model_profile": "balanced",}'
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
'# Roadmap\n\n### Phase 1: Test Phase\n'
|
|
);
|
|
|
|
const result = runGsdTools('validate health', tmpDir);
|
|
assert.ok(result.success, `validate health should not crash on invalid JSON: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
const hasE005 = output.errors.some(e => e.code === 'E005');
|
|
assert.ok(hasE005, `Should report E005 for invalid config.json: ${JSON.stringify(output.errors)}`);
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 4. stateReplaceFieldWithFallback warning
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('stateReplaceFieldWithFallback field-miss warning', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempProject();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('advance-plan completes even when fields are missing (non-fatal)', () => {
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'STATE.md'),
|
|
`# Project State\n\n**Current Phase:** 01\n**Current Plan:** 1\n**Total Plans in Phase:** 3\n`
|
|
);
|
|
|
|
const result = runGsdTools('state advance-plan', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.ok(output.advanced === true || output.reason === 'last_plan', 'advance should complete');
|
|
});
|
|
|
|
test('validate health on 50-phase project completes in under 3000ms', () => {
|
|
// Stress test for the new health checks at scale
|
|
let roadmapContent = '# Roadmap v1.0\n\n';
|
|
for (let i = 1; i <= 50; i++) {
|
|
roadmapContent += `- [${i <= 25 ? 'x' : ' '}] Phase ${i}: Feature ${i}\n`;
|
|
}
|
|
roadmapContent += '\n';
|
|
for (let i = 1; i <= 50; i++) {
|
|
const pad = String(i).padStart(2, '0');
|
|
roadmapContent += `### Phase ${i}: Feature ${i}\n\n**Goal:** Build feature ${i}\n**Plans:** 1 plans\n\n`;
|
|
}
|
|
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), roadmapContent);
|
|
|
|
writeMinimalProjectMd(tmpDir);
|
|
writeMinimalStateMd(tmpDir, '# Session State\n\n**Current Phase:** 26\n**Status:** Planning\n');
|
|
writeValidConfigJson(tmpDir);
|
|
|
|
const phasesDir = path.join(tmpDir, '.planning', 'phases');
|
|
for (let i = 1; i <= 50; i++) {
|
|
const pad = String(i).padStart(2, '0');
|
|
const phaseDir = path.join(phasesDir, `${pad}-feature-${i}`);
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
fs.writeFileSync(path.join(phaseDir, `${pad}-01-PLAN.md`), `# Plan ${i}\n`);
|
|
if (i <= 25) {
|
|
fs.writeFileSync(path.join(phaseDir, `${pad}-01-SUMMARY.md`), `# Summary ${i}\n`);
|
|
}
|
|
}
|
|
|
|
const { performance } = require('perf_hooks');
|
|
const start = performance.now();
|
|
const result = runGsdTools('validate health', tmpDir);
|
|
const elapsed = performance.now() - start;
|
|
|
|
assert.ok(result.success, `validate health should succeed: ${result.error}`);
|
|
assert.ok(elapsed < 3000, `Should complete in under 3000ms, took ${elapsed.toFixed(0)}ms`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.ok(typeof output.status === 'string', 'Should return a status string');
|
|
});
|
|
});
|