* chore(#2371): representative gate-fixture corpus + document-shaped property test Adds tests/fixtures/representative/ — a permanent corpus of verbatim, incident-sourced fixtures (never author-invented) from #2286, #2347, #2365, #2366, each labeled with its expected gate verdict in a MANIFEST.json and driven through the real CLI gate entrypoint via tests/representative-corpus.test.cjs. Adds a document-shaped fast-check property test alongside the existing writer-seeded bijection test in tests/api-coverage.test.cjs: the existing generator produces rows and renders them through the writer, so the document shape is a constant and it cannot fail against a decoy table; the new one generates the document space instead. Two gates (#2365, #2347) are still open, so their corpus/property assertions are marked with node:test's official `todo` option — the test executes and reports its failure without affecting the process exit code (https://nodejs.org/api/test.html#test-options). The audit-uat corpus (#2286, fixed by #2317) is a normal passing assertion, proving the methodology works end to end and not just cataloguing gaps. Records the fixture-provenance rule in CONTRIBUTING.md: a gate's fixtures may not be derived from the gate's own writer, grammar, or docstring examples; a negative fixture must come from a source that doesn't know the gate exists. No production src/*.cts changes — validation only, per #2371's scope. Closes #2371 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#2371): address orthogonal-review findings — dedup generators, fix field naming, wire dead fields Standards-axis review findings, all fixed: - Deduplicated the row-shape generators (capabilityGen/rowGen/validRowGen) that were copy-pasted between the parse/render bijection test and the new document-shaped property test in tests/api-coverage.test.cjs — a future edit to one could have silently desynced the two properties. Hoisted to a single module-scope declaration both tests reference. - Renamed decision-coverage-guard/MANIFEST.json's expectedOutcome -> expectedReason. It asserted against the gate's `reason` field, but this codebase already has a real, different `outcome` field at parser altitude (extractDecisions' DecisionOutcome) — naming the manifest field after the wrong altitude's term was exactly the ambiguity the "Fixture provenance" rule this PR adds exists to eliminate. - Removed the unused `role` field from three MANIFEST.json files (never read by any test) and wired the previously-dead per-fixture `expectedMinItems` in audit-uat/MANIFEST.json into a real per-file assertion in tests/representative-corpus.test.cjs, using cmdAuditUat's `results` array — catches a regression that moves items between the two fixture files while preserving the aggregate total, which the existing total_items check alone would miss. No changes to test intent or coverage — same assertions, correctly named and fully wired. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: remove dead capabilityState/capabilityWriter requires from gsd-tools.cjs Surfaced by the mandatory pre-PR lint gate (no-unused-vars) while preparing this PR — unrelated to #2371's own changes, but a defect found while working is fixed in place rather than deferred. Leftover from #2368/#2370 (merged just before this branch rebased onto it): the case 'capability' arm that needed these two requires was relocated to bin/lib/capability-command-router.cjs, which already requires both modules directly (lines 24-25) and is their only real consumer (cmdCapabilityState, resolveCapabilityRuntimeState, cmdCapabilitySet). The two requires left behind in gsd-tools.cjs had zero other references in the file and were never re-exported — confirmed via grep across the file and its module.exports. Behavior-preserving: Node's require cache means the underlying modules still load exactly once via capability-command-router.cjs's own requires; gsd-tools.cjs never used its now-removed local bindings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#2371): replace todo-marked assertions with characterization tests gsd-test's own JSONL result parser (gsd-test-runner's internal/pipeline/parse.go, verified directly against that repo's source) has no concept of node:test's `todo` option — it only recognizes kind:"pass"|"fail" and hard-errors on anything else. A { todo: true } test whose body throws is counted as a real failure in gsd-test's own verdict, exactly as if it weren't marked todo — proven by an actual gsd-test run against this branch, which reported outcome:"failed" with all six todo-marked assertions (the property test plus five representative-corpus fixtures) in the failure list, each carrying the correct raw node:test `todo` field the tool's parser simply doesn't read. Replaces todo with characterization: MANIFEST.json now carries both the correct target verdict (expected*) and the exact current observed verdict (currentBuggyOutput, directly verified against live CLI output for all five fixtures). Tests assert currentBuggyOutput — an honest, non-vacuous pin of today's known-broken reality that passes today and will fail loudly the moment the referenced fix changes the observed output, at which point the assertion should be flipped to expected* and currentBuggyOutput deleted. The document-shaped property test switches from throwing fc.assert to non-throwing fc.check (returns RunDetails per fast-check's own docs) and asserts report.failed === true directly, for the same reason. Updates all prose (CONTRIBUTING.md, the fixture READMEs) that previously claimed todo would be respected — that claim was factually wrong for this repo's actual tooling and must not ship. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: regenerate golden-install-parity fixtures after rebase onto next Rebasing onto the current next (which now includes #2381's todo-severity changes to gsd-core/bin/gsd-tools.cjs) produced real conflicts in all 18 golden-install-parity fixtures — expected, since both branches changed the same gsd-tools.cjs hash entry. Resolved by taking one side to unblock the rebase, then regenerating fresh from source via npm run gen:golden and verifying the result; every file's diff is exactly the one hash line for gsd-core/bin/gsd-tools.cjs, correcting a stale intermediate hash from the arbitrary conflict pick. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
262 lines
12 KiB
JavaScript
262 lines
12 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Representative-corpus gate tests (#2371).
|
|
*
|
|
* Every fixture under tests/fixtures/representative/ is verbatim (or a
|
|
* minimal faithful subset) of a real reported artifact — never invented to
|
|
* match a gate's own grammar. See tests/fixtures/representative/README.md
|
|
* for the full rationale and CONTRIBUTING.md's "Fixture provenance" rule.
|
|
*
|
|
* Each gate is driven through its real CLI entrypoint (gate-verdict
|
|
* altitude), matching the established pattern in
|
|
* tests/api-coverage-gate-e2e.test.cjs and tests/decisions.test.cjs — never
|
|
* the parser function called in isolation.
|
|
*
|
|
* Three fixtures (across api-coverage-detector, api-coverage-matrix,
|
|
* decision-coverage-guard) encode gates that are still open bugs (#2365,
|
|
* #2366, #2347). For those, MANIFEST.json carries BOTH the correct target
|
|
* verdict (`expected*` — what the fix must produce) and the exact CURRENT
|
|
* observed verdict (`currentBuggyOutput` — what today's code actually
|
|
* returns). The test asserts against `currentBuggyOutput`: an honest,
|
|
* non-vacuous characterization of today's known-broken reality, not a fake
|
|
* pass. This assertion WILL fail, loudly, the moment the underlying bug is
|
|
* fixed and the gate starts returning something other than the pinned
|
|
* buggy value — at which point whoever's fix landed must update the
|
|
* assertion to check `expected*` instead (and can delete `currentBuggyOutput`).
|
|
*
|
|
* Why not node:test's `todo` option: this repo's own test-runner
|
|
* (gsd-test / gsd-test-runner v1.6.2) has no concept of it. Its JSONL
|
|
* result parser (internal/pipeline/parse.go's parseJSONL, gsd-test-runner
|
|
* repo) only recognizes `kind: "pass" | "fail"` — verified directly against
|
|
* that source — so a `{ todo: true }` test whose body throws is still
|
|
* counted as a real failure in the tool's own verdict. Characterization
|
|
* (assert the known-current value) sidesteps this because the test
|
|
* genuinely passes today; it needs no runner-level "expected failure"
|
|
* feature at all.
|
|
*
|
|
* The audit-uat corpus (#2286, fixed by #2317) has no currentBuggyOutput:
|
|
* it already asserts the correct behavior directly, because the bug is
|
|
* already fixed — proof the methodology works end to end, not just a
|
|
* record of gaps.
|
|
*/
|
|
|
|
const { describe, test, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { execFileSync } = require('node:child_process');
|
|
|
|
const { cleanup } = require('./helpers.cjs');
|
|
|
|
const TOOLS_PATH = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs');
|
|
const FIXTURES_ROOT = path.join(__dirname, 'fixtures', 'representative');
|
|
|
|
const TEST_ENV_BASE = {
|
|
GSD_SESSION_KEY: '',
|
|
CODEX_THREAD_ID: '',
|
|
CLAUDE_SESSION_ID: '',
|
|
CLAUDE_CODE_SSE_PORT: '',
|
|
OPENCODE_SESSION_ID: '',
|
|
GEMINI_SESSION_ID: '',
|
|
CURSOR_SESSION_ID: '',
|
|
WINDSURF_SESSION_ID: '',
|
|
TERM_SESSION: '',
|
|
WT_SESSION: '',
|
|
TMUX_PANE: '',
|
|
ZELLIJ_SESSION_NAME: '',
|
|
TTY: '',
|
|
SSH_TTY: '',
|
|
};
|
|
|
|
function runTools(args, cwd) {
|
|
try {
|
|
const stdout = execFileSync(process.execPath, [TOOLS_PATH, ...args], {
|
|
cwd,
|
|
encoding: 'utf-8',
|
|
env: { ...process.env, ...TEST_ENV_BASE },
|
|
timeout: 60000,
|
|
});
|
|
return { success: true, output: stdout.trim(), error: '' };
|
|
} catch (err) {
|
|
return {
|
|
success: false,
|
|
output: err.stdout?.toString().trim() || '',
|
|
error: err.stderr?.toString().trim() || err.message,
|
|
};
|
|
}
|
|
}
|
|
|
|
function readManifest(gateDir) {
|
|
const raw = fs.readFileSync(path.join(FIXTURES_ROOT, gateDir, 'MANIFEST.json'), 'utf8');
|
|
return JSON.parse(raw);
|
|
}
|
|
|
|
function readFixture(gateDir, file) {
|
|
return fs.readFileSync(path.join(FIXTURES_ROOT, gateDir, file), 'utf8');
|
|
}
|
|
|
|
function makeProject() {
|
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-repcorpus-'));
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true });
|
|
fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), '{}', 'utf8');
|
|
return tmpDir;
|
|
}
|
|
|
|
function makePhaseDir(projectDir, slug) {
|
|
const dir = path.join(projectDir, '.planning', 'phases', slug);
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
return dir;
|
|
}
|
|
|
|
// ─── api-coverage-detector (#2365) ────────────────────────────────────────────
|
|
|
|
describe('representative corpus — api-coverage detector (#2365)', () => {
|
|
let tmpDir;
|
|
afterEach(() => { if (tmpDir) { cleanup(tmpDir); tmpDir = null; } });
|
|
|
|
const manifest = readManifest('api-coverage-detector');
|
|
|
|
for (const fx of manifest.fixtures) {
|
|
const label = fx.currentBuggyOutput ? `${fx.file} → currently detected:true (#2365)` : `${fx.file} → detected:false`;
|
|
test(label, () => {
|
|
tmpDir = makeProject();
|
|
const phaseDir = makePhaseDir(tmpDir, '01-repcorpus');
|
|
const body = readFixture('api-coverage-detector', fx.file);
|
|
fs.writeFileSync(path.join(phaseDir, '01-PLAN.md'), `# Plan\n${body}\n`, 'utf8');
|
|
|
|
const r = runTools(['check', 'api-coverage.verify-pre', phaseDir, '--raw'], tmpDir);
|
|
assert.ok(r.success, `gate should succeed (JSON). stderr: ${r.error}`);
|
|
const j = JSON.parse(r.output);
|
|
|
|
if (fx.currentBuggyOutput) {
|
|
assert.strictEqual(j.detected, fx.currentBuggyOutput.detected,
|
|
`${fx.file}: expected today's known-buggy detected:${fx.currentBuggyOutput.detected}, got ${JSON.stringify(j)}. ` +
|
|
`If this now differs, #2365 may be fixed — check against expectedDetected:${fx.expectedDetected} instead.`);
|
|
assert.strictEqual(j.signals?.[0]?.verb, fx.currentBuggyOutput.signal.verb, `${fx.file}: signal.verb`);
|
|
assert.strictEqual(j.signals?.[0]?.noun, fx.currentBuggyOutput.signal.noun, `${fx.file}: signal.noun`);
|
|
} else {
|
|
assert.strictEqual(j.detected, fx.expectedDetected,
|
|
`${fx.file}: expected detected:${fx.expectedDetected}, got ${JSON.stringify(j)}`);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// ─── api-coverage-matrix (#2366) ──────────────────────────────────────────────
|
|
|
|
describe('representative corpus — api-coverage matrix (#2366)', () => {
|
|
let tmpDir;
|
|
afterEach(() => { if (tmpDir) { cleanup(tmpDir); tmpDir = null; } });
|
|
|
|
const manifest = readManifest('api-coverage-matrix');
|
|
|
|
for (const fx of manifest.fixtures) {
|
|
const label = fx.currentBuggyOutput
|
|
? `${fx.file} → currently silently-corrupted + spurious errors (#2366)`
|
|
: `${fx.file} → exactly the canonical rows, 0 errors`;
|
|
test(label, () => {
|
|
tmpDir = makeProject();
|
|
const phaseDir = makePhaseDir(tmpDir, '01-repcorpus');
|
|
fs.writeFileSync(path.join(phaseDir, '01-PLAN.md'), `# Plan\n${fx.pairedPlan}\n`, 'utf8');
|
|
fs.writeFileSync(path.join(phaseDir, 'COVERAGE.md'), readFixture('api-coverage-matrix', fx.file), 'utf8');
|
|
|
|
const r = runTools(['check', 'api-coverage.verify-pre', phaseDir, '--raw'], tmpDir);
|
|
assert.ok(r.success, `gate should succeed (JSON). stderr: ${r.error}`);
|
|
const j = JSON.parse(r.output);
|
|
|
|
if (fx.currentBuggyOutput) {
|
|
assert.strictEqual(j.block, fx.currentBuggyOutput.block,
|
|
`${fx.file}: expected today's known-buggy block:${fx.currentBuggyOutput.block}, got ${JSON.stringify(j)}. ` +
|
|
`If this now differs, #2366 may be fixed — check against expectedBlock:${fx.expectedBlock} instead.`);
|
|
assert.strictEqual(j.error_count, fx.currentBuggyOutput.error_count, `${fx.file}: error_count`);
|
|
assert.deepStrictEqual(j.errors, fx.currentBuggyOutput.errors, `${fx.file}: errors`);
|
|
} else {
|
|
assert.strictEqual(j.block, fx.expectedBlock, `${fx.file}: block. Got ${JSON.stringify(j)}`);
|
|
assert.deepStrictEqual(j.counts, fx.expectedCounts, `${fx.file}: counts. Got ${JSON.stringify(j)}`);
|
|
assert.strictEqual((j.errors || []).length, fx.expectedErrorCount,
|
|
`${fx.file}: errors. Got ${JSON.stringify(j.errors)}`);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// ─── audit-uat (#2286, fixed by #2317 — asserts correct behavior directly) ────
|
|
|
|
describe('representative corpus — audit-uat (#2286, fixed by #2317)', () => {
|
|
let tmpDir;
|
|
afterEach(() => { if (tmpDir) { cleanup(tmpDir); tmpDir = null; } });
|
|
|
|
const manifest = readManifest('audit-uat');
|
|
|
|
test('Gaps-section + human-verification-frontmatter fixtures both surface as real items', () => {
|
|
tmpDir = makeProject();
|
|
const phaseDir = makePhaseDir(tmpDir, '01-repcorpus');
|
|
for (const fx of manifest.fixtures) {
|
|
fs.writeFileSync(
|
|
path.join(phaseDir, `01${fx.filenameSuffix}`),
|
|
readFixture('audit-uat', fx.file),
|
|
'utf8',
|
|
);
|
|
}
|
|
|
|
const r = runTools(['audit-uat', '--raw'], tmpDir);
|
|
assert.ok(r.success, `audit-uat should succeed. stderr: ${r.error}`);
|
|
const j = JSON.parse(r.output);
|
|
assert.ok(
|
|
j.summary.total_items >= manifest.expectedTotalItems,
|
|
`expected total_items >= ${manifest.expectedTotalItems}, got ${JSON.stringify(j.summary)}`,
|
|
);
|
|
// Per-fixture check (not just the aggregate): a regression that moves
|
|
// items between files while preserving the total would slip past the
|
|
// total_items check above but not this one.
|
|
for (const fx of manifest.fixtures) {
|
|
const fileName = `01${fx.filenameSuffix}`;
|
|
const fileResult = j.results.find((r2) => r2.file === fileName);
|
|
assert.ok(fileResult, `expected a result entry for ${fileName}, got ${JSON.stringify(j.results)}`);
|
|
assert.ok(
|
|
fileResult.items.length >= fx.expectedMinItems,
|
|
`${fileName}: expected items.length >= ${fx.expectedMinItems}, got ${fileResult.items.length}`,
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── decision-coverage-guard (#2347) ──────────────────────────────────────────
|
|
|
|
describe('representative corpus — decision-coverage guard (#2347)', () => {
|
|
let tmpDir;
|
|
afterEach(() => { if (tmpDir) { cleanup(tmpDir); tmpDir = null; } });
|
|
|
|
const manifest = readManifest('decision-coverage-guard');
|
|
|
|
for (const fx of manifest.fixtures) {
|
|
const label = fx.currentBuggyOutput
|
|
? `${fx.file} → currently passed:true, skipped:true (#2347)`
|
|
: `${fx.file} → outcome could-not-parse, passed:false`;
|
|
test(label, () => {
|
|
tmpDir = makeProject();
|
|
const phaseDir = makePhaseDir(tmpDir, '01-repcorpus');
|
|
const contextPath = path.join(phaseDir, 'CONTEXT.md');
|
|
fs.writeFileSync(contextPath, readFixture('decision-coverage-guard', fx.file), 'utf8');
|
|
|
|
const r = runTools(['query', 'check.decision-coverage-plan', phaseDir, contextPath], tmpDir);
|
|
assert.ok(r.success, `gate should succeed (JSON). stderr: ${r.error}`);
|
|
const j = JSON.parse(r.output);
|
|
|
|
if (fx.currentBuggyOutput) {
|
|
assert.strictEqual(j.passed, fx.currentBuggyOutput.passed,
|
|
`${fx.file}: expected today's known-buggy passed:${fx.currentBuggyOutput.passed}, got ${JSON.stringify(j)}. ` +
|
|
`If this now differs, #2347 may be fixed — check against expectedPassed:${fx.expectedPassed} instead.`);
|
|
assert.strictEqual(j.skipped, fx.currentBuggyOutput.skipped, `${fx.file}: skipped`);
|
|
assert.strictEqual(j.reason, fx.currentBuggyOutput.reason, `${fx.file}: reason`);
|
|
assert.strictEqual(j.total, fx.currentBuggyOutput.total, `${fx.file}: total`);
|
|
} else {
|
|
assert.strictEqual(j.passed, fx.expectedPassed, `${fx.file}: passed. Got ${JSON.stringify(j)}`);
|
|
assert.strictEqual(j.reason, fx.expectedReason, `${fx.file}: reason. Got ${JSON.stringify(j)}`);
|
|
}
|
|
});
|
|
}
|
|
});
|