Merge pull request #3094 from open-gsd/test/3057-wave2-liveness

chore(#3057): remove tests that report coverage they do not have — Wave 2
This commit is contained in:
Tom Boucher
2026-08-06 00:09:41 -04:00
committed by GitHub
77 changed files with 874 additions and 361 deletions

View File

@@ -116,16 +116,25 @@ jobs:
needs: changes
if: needs.changes.outputs.product_changed == 'true'
runs-on: ${{ matrix.os }}
# #2952: this lane is sharded three ways (see the matrix below), so the
# budget covers ONE shard, not the whole unit suite. Measured on run
# 30677442953: shard 1/3 7m12s, 2/3 4m32s, 3/3 3m59s. Shard 1 is the long
# pole because the unsharded aux suites (integration/security/install/slow)
# ride along on it — that is deliberate; they total ~1m35s and sharding them
# would cost more than it saves.
# #2952: the `scope: full` lane is sharded three ways (see the matrix
# below), so the budget covers ONE shard, not the whole unit suite.
# Measured on run 30677442953: shard 1/3 7m12s, 2/3 4m32s, 3/3 3m59s. Shard
# 1 is the long pole because the unsharded aux suites
# (integration/security/install/slow) ride along on it — that is
# deliberate; they total ~1m35s and sharding them would cost more than it
# saves.
#
# 15 is ~1.9x the slowest measured shard. Before sharding this same lane ran
# 15m20s against a 15-minute cap and was killed mid-run, which is the whole
# of #2952 — the number is unchanged, the work behind it is a third the size.
#
# The `scope: windows` lane is sharded three ways for the same reason
# (see #3057): on PR #3094 it reached 15m05s against this same cap and was
# CANCELLED, four shas in a row — a change to tests/helpers.cjs scoped in
# the install-heavy suites and pushed the single Windows lane over the top.
# Per #869, a timeout bump only moves that cliff; sharding removes it. The
# cap stays at 15 unchanged: each shard now does roughly a third of the
# work, so headroom improves rather than needing a raise.
# tests/ci-test-job-timeout-budget.test.cjs holds every lane here to a
# headroom factor over its own measured cost.
timeout-minutes: 15
@@ -158,6 +167,13 @@ jobs:
# gave the `test-full` lane, which measures 0.0% spread across 3 bins on
# the current table. The aux suites (integration/security/install/slow)
# total ~1m35s and are not worth sharding; they run on shard 1 only.
#
# #3057: the `scope: windows` lane is now sharded three ways too, the
# same fix applied to the same cliff (#869's stated durable follow-up).
# It hit exactly 15m05s and was CANCELLED on PR #3094, four shas
# straight, after a change to tests/helpers.cjs scoped in the
# install-heavy suites. Its shards use the same `--shard i/n`
# flag on scripts/run-tests.cjs, applied AFTER scope selection.
- os: ubuntu-latest
node-version: 22
scope: targeted
@@ -176,6 +192,15 @@ jobs:
- os: windows-latest
node-version: 24
scope: windows
shard: 1/3
- os: windows-latest
node-version: 24
scope: windows
shard: 2/3
- os: windows-latest
node-version: 24
scope: windows
shard: 3/3
steps:
# Windows lane on checkout v5.0.1 (drops includeIf; no auth flake, uses Node 24 natively).
@@ -255,7 +280,7 @@ jobs:
- name: Run scoped tests
if: matrix.scope != 'full'
run: node scripts/run-tests.cjs --files-from .ci-selected-tests.txt
run: node scripts/run-tests.cjs --files-from .ci-selected-tests.txt${{ matrix.shard && format(' --shard {0}', matrix.shard) || '' }}
# #2952: each shard runs its slice of the unit suite under c8 but renders
# NO report and enforces NO gate — it only leaves raw V8 dumps in

View File

@@ -1,5 +1,5 @@
[
"tests/agent-classification-parity.test.cjs :: runtime-contract-is-the-product — docs/AGENTS.md section layout + docs/INVENTORY.md table ARE the classification surface being validated",
"tests/agent-classification-parity.test.cjs :: source-text-is-the-product",
"tests/agent-frontmatter.test.cjs :: source-text-is-the-product",
"tests/agent-required-reading-consistency.test.cjs :: source-text-is-the-product",
"tests/agent-size-budget.test.cjs :: source-text-is-the-product",
@@ -15,23 +15,20 @@
"tests/autonomous-interactive.test.cjs :: source-text-is-the-product",
"tests/autonomous-to-flag.test.cjs :: source-text-is-the-product",
"tests/chain-flag-plan-phase.test.cjs :: source-text-is-the-product",
"tests/changeset-cli.test.cjs :: reads a product workflow .md file (not CJS source) to verify",
"tests/changeset-cli.test.cjs :: source-text-is-the-product",
"tests/check-update-config-dir.test.cjs :: structural-regression-guard",
"tests/ci-test-scope.test.cjs :: CLI usage banner presence is a user-facing contract.",
"tests/ci-test-scope.test.cjs :: CLI usage failure text is user-facing contract for this parser guard.",
"tests/claude-md.test.cjs :: source-text-is-the-product",
"tests/claude-skills-migration.test.cjs :: source-text-is-the-product",
"tests/cleanup-branch-pruning.test.cjs :: source-text-is-the-product",
"tests/cline-install.test.cjs :: source-text-is-the-product",
"tests/cline-support.test.cjs :: source-text-is-the-product",
"tests/clock-seam.test.cjs :: line 159 reads the STATE.md temp file written by readModifyWriteStateMd — this is a runtime output file assertion, not a source-grep; the API returns void so a file read-back is the only way to verify the transform was applied",
"tests/code-review-agent-skills.test.cjs :: source-text-is-the-product",
"tests/code-review-command.test.cjs :: source-text-is-the-product",
"tests/code-review-pipeline-regression.test.cjs :: source-text-is-the-product",
"tests/code-review.test.cjs :: source-text-is-the-product",
"tests/codebuddy-install.test.cjs :: source-text-is-the-product",
"tests/codex-config.test.cjs :: source-text-is-the-product",
"tests/command-contract.test.cjs :: source-text-is-the-product — commands/gsd/*.md files ARE the",
"tests/command-contract.test.cjs :: source-text-is-the-product",
"tests/commands.test.cjs :: source-text-is-the-product",
"tests/concurrency-safety.test.cjs :: source-text-is-the-product",
"tests/config-field-docs.test.cjs :: docs-parity",
@@ -47,18 +44,15 @@
"tests/discuss-phase-power.test.cjs :: source-text-is-the-product",
"tests/docs-parity-live-registry.test.cjs :: source-text-is-the-product",
"tests/drift-detection.test.cjs :: source-text-is-the-product",
"tests/edge-probe-docs-fixtures.test.cjs :: runtime-contract-is-the-product — the rendered reference/SPEC/ADR vocab surfaces are the runtime contract; this pins their bijection to the code (docs-parity)",
"tests/edge-probe-planner-contract.test.cjs :: runtime-contract-is-the-product — plan-phase.md's planner prompt is the deployed runtime contract under assertion",
"tests/edge-probe-spec-phase-contract.test.cjs :: runtime-contract-is-the-product — spec-phase.md Step 5.5 is the deployed workflow runtime contract under assertion",
"tests/edge-probe-docs-fixtures.test.cjs :: source-text-is-the-product",
"tests/edge-probe-planner-contract.test.cjs :: source-text-is-the-product",
"tests/edge-probe-spec-phase-contract.test.cjs :: source-text-is-the-product",
"tests/edit-phase.test.cjs :: source-text-is-the-product",
"tests/eslint-rules.test.cjs :: <source-grep reason> must still error",
"tests/eslint-rules.test.cjs :: pending migration",
"tests/eslint-rules.test.cjs :: source-text-is-the-product",
"tests/execute-phase-active-flags.test.cjs :: source-text-is-the-product",
"tests/execute-phase-step-5-5-deviation-doc.test.cjs :: source-text-is-the-product",
"tests/execute-phase-wave.test.cjs :: behavioral — calls gsd-tools and asserts structured output",
"tests/execute-phase-wave.test.cjs :: behavioral — exercises config-set validation, not source text",
"tests/execute-phase-wave.test.cjs :: behavioral — exercises gsd-tools wave-defaulting logic",
"tests/execute-phase-wave.test.cjs :: source-text-is-the-product",
"tests/execute-phase-worktree-artifacts.test.cjs :: source-text-is-the-product",
"tests/explore-command.test.cjs :: source-text-is-the-product",
@@ -66,9 +60,8 @@
"tests/forensics.test.cjs :: source-text-is-the-product",
"tests/frontmatter-cli.test.cjs :: source-text-is-the-product",
"tests/gates-taxonomy.test.cjs :: source-text-is-the-product",
"tests/git-base-branch.test.cjs :: runtime-contract-is-the-product",
"tests/git-base-branch.test.cjs :: runtime-contract-is-the-product — the workflow .md content IS",
"tests/gsd-check-update-worker-platform-gate.test.cjs :: structural assertion on spawn-options shape; the behavior",
"tests/git-base-branch.test.cjs :: source-text-is-the-product",
"tests/gsd-check-update-worker-platform-gate.test.cjs :: structural-regression-guard",
"tests/gsd-researcher-app-aware.test.cjs :: source-text-is-the-product",
"tests/gsd-researcher-flow-diagram.test.cjs :: source-text-is-the-product",
"tests/gsd-settings-advanced.test.cjs :: source-text-is-the-product",
@@ -80,21 +73,18 @@
"tests/install-minimal-hooks.test.cjs :: source-text-is-the-product",
"tests/install-nested-layout.test.cjs :: source-text-is-the-product",
"tests/install-runtime-artifacts.test.cjs :: source-text-is-the-product",
"tests/install.test.cjs :: runtime-contract-is-the-product",
"tests/install.test.cjs :: source-text-is-the-product",
"tests/intel.test.cjs :: source-text-is-the-product — agents/gsd-intel-updater.md IS the",
"tests/intel.test.cjs :: source-text-is-the-product — readFileSync assertions target API-SURFACE.md, which is the generated product of intelApiSurface; asserting on its text content is the only way to verify correct generation.",
"tests/inventory-headings-countfree.test.cjs :: runtime-contract-is-the-product — INVENTORY.md heading format is the shipped doc surface being locked",
"tests/intel.test.cjs :: source-text-is-the-product",
"tests/inventory-headings-countfree.test.cjs :: source-text-is-the-product",
"tests/ios-scaffold-safety.test.cjs :: source-text-is-the-product",
"tests/issue-2639-codex-toml-neutralization.test.cjs :: source-text-is-the-product",
"tests/issue-429-comment-text-gate.test.cjs :: source-text-is-the-product",
"tests/issue-498-update-backup-runtime-dir.test.cjs :: structural assertion on the deployed update.md backup bash;",
"tests/issue-57-runtime-install-no-drift.test.cjs :: delegation-presence guard. Catches wholesale removal of the registry",
"tests/issue-57-runtime-install-no-drift.test.cjs :: structural guard over bin/install.js source. Behavioral assertions",
"tests/issue-498-update-backup-runtime-dir.test.cjs :: source-text-is-the-product",
"tests/issue-57-runtime-install-no-drift.test.cjs :: structural-regression-guard",
"tests/issue-607-installer-dry-run.install.test.cjs :: integration-test-input",
"tests/issue-607-legacy-cleanup.test.cjs :: integration-test-input",
"tests/issue-787-cline-hooks-agents.test.cjs :: source-text-is-the-product",
"tests/issue-815-update-next-channel.test.cjs :: reads product workflow/command markdown to verify the --next RC channel contract — not a source-grep test",
"tests/issue-815-update-next-channel.test.cjs :: source-text-is-the-product",
"tests/locking-bugs-1909-1916-1925-1927.test.cjs :: architectural-invariant",
"tests/mcp-tool-inheritance.test.cjs :: source-text-is-the-product",
"tests/milestone-summary.test.cjs :: source-text-is-the-product",
@@ -111,7 +101,6 @@
"tests/path-replacement.test.cjs :: source-text-is-the-product",
"tests/phase-dependency-levels.test.cjs :: source-text-is-the-product",
"tests/phase.test.cjs :: source-text-is-the-product",
"tests/phase.test.cjs :: state-md-is-the-runtime-contract — regression tests for",
"tests/phase6-capability-docs.test.cjs :: source-text-is-the-product",
"tests/phase6-capstone-conformance.test.cjs :: source-text-is-the-product",
"tests/phase6-planning-capabilities.test.cjs :: source-text-is-the-product",
@@ -127,7 +116,6 @@
"tests/product-name-purity.test.cjs :: source-text-is-the-product",
"tests/profile-output.test.cjs :: source-text-is-the-product",
"tests/progress-forensic.test.cjs :: source-text-is-the-product",
"tests/prompt-budget-cli.test.cjs :: prompt-content-is-the-product",
"tests/prompt-thinning.test.cjs :: source-text-is-the-product",
"tests/quick-session-management.test.cjs :: source-text-is-the-product",
"tests/qwen-skills-migration.test.cjs :: source-text-is-the-product",
@@ -137,12 +125,11 @@
"tests/release-coverage-scope.test.cjs :: source-text-is-the-product",
"tests/release-tarball-smoke-workflow.test.cjs :: source-text-is-the-product",
"tests/release-tarball-smoke.install.test.cjs :: integration-test-input",
"tests/research-agent-profiles.test.cjs :: <runtime-contract-is-the-product> research agent .md content is the governed surface",
"tests/research-agent-profiles.test.cjs :: source-text-is-the-product",
"tests/review-default-reviewers-workflow.test.cjs :: source-text-is-the-product",
"tests/roadmap.test.cjs :: source-text-is-the-product",
"tests/run-tests-harness.test.cjs :: run-tests.cjs is a CLI test harness whose only IR is its",
"tests/runtime-launcher-parity.test.cjs :: structural parity/drift guard — asserts literal presence/absence of the canonical gsd_run launcher and the retired $GSD_SDK / `/gsd-tools` tokens across workflow markdown; there is no typed IR for \"this source file does not contain substring X\".",
"tests/runtime-name-policy.test.cjs :: runtime-contract-is-the-product — FALLBACK_ALIASES source text IS the",
"tests/runtime-launcher-parity.test.cjs :: structural-regression-guard",
"tests/runtime-name-policy.test.cjs :: source-text-is-the-product",
"tests/scan-command.test.cjs :: source-text-is-the-product",
"tests/secret-scan-lint.security.test.cjs :: source-text-is-the-product",
"tests/secure-phase.test.cjs :: source-text-is-the-product",
@@ -166,7 +153,7 @@
"tests/workflow-compat.test.cjs :: source-text-is-the-product",
"tests/workflow-guard-registration.test.cjs :: structural-regression-guard",
"tests/workflow-maintainer-skip.test.cjs :: source-text-is-the-product",
"tests/workflow-shell-pinning.test.cjs :: file-scope prefilter, not a test assertion — we need to",
"tests/workflow-shell-pinning.test.cjs :: source-text-is-the-product",
"tests/workflow-size-budget.test.cjs :: source-text-is-the-product",
"tests/workspace.test.cjs :: source-text-is-the-product",
"tests/worktree-cleanup.test.cjs :: source-text-is-the-product",

View File

@@ -1,6 +1,8 @@
'use strict';
// allow-test-rule: reads workflow YAML source as the security artifact under test #1190
// allow-test-rule: source-text-is-the-product (#1190)
// Reads the shipped .github/workflows YAML — the deployed text GitHub
// Actions executes — as the security artifact under test.
/**
* ADR-230 regression guard: PR target-branch policy.

View File

@@ -1,4 +1,5 @@
// allow-test-rule: runtime-contract-is-the-product — docs/AGENTS.md section layout + docs/INVENTORY.md table ARE the classification surface being validated
// allow-test-rule: source-text-is-the-product
// docs/AGENTS.md section layout + docs/INVENTORY.md table ARE the classification surface being validated
'use strict';
/**

View File

@@ -1,3 +1,9 @@
// allow-test-rule: structural-regression-guard (#1972)
// Reads milestone.cjs/phase.cjs/frontmatter.cjs source and parses for bare
// fs.writeFileSync call sites — a specific code pattern that must not exist
// to prevent partial-write corruption. Behavioral tests cannot distinguish
// platformWriteSync from a bare fs.writeFileSync; only source inspection can.
/**
* Structural regression guard for atomic write usage (#1972).
*

View File

@@ -327,7 +327,8 @@ describe('changeset cli extract: version-range changelog extraction (#3496)', ()
});
// F1: workflows/update.md must reference the extract subcommand invocation.
// allow-test-rule: reads a product workflow .md file (not CJS source) to verify
// allow-test-rule: source-text-is-the-product
// Reads a product workflow .md file (not CJS source) to verify
// the user-facing instruction was wired; there is no behavioural runtime to invoke.
test('F1: workflows/update.md contains concrete extract subcommand invocation', (_t) => {
const workflowPath = path.join(ROOT, 'gsd-core', 'workflows', 'update.md');
@@ -359,7 +360,8 @@ describe('changeset cli extract: version-range changelog extraction (#3496)', ()
// NOT the old broken path ($GSD_DIR/gsd-core/scripts/changeset/cli.cjs).
// The installer copies scripts/changeset/ into <configDir>/scripts/changeset/,
// so the runtime path is $GSD_DIR/scripts/changeset/cli.cjs (#935).
// allow-test-rule: reads a product workflow .md file (not CJS source) to verify
// allow-test-rule: source-text-is-the-product
// Reads a product workflow .md file (not CJS source) to verify
// the runtime install path contract; there is no behavioural runtime to invoke.
test('F2: update.md CLI path is $GSD_DIR/scripts/changeset/cli.cjs (not gsd-core/scripts/…) (#935)', (_t) => {
const workflowPath = path.join(ROOT, 'gsd-core', 'workflows', 'update.md');
@@ -377,7 +379,8 @@ describe('changeset cli extract: version-range changelog extraction (#3496)', ()
});
// F3: update.md must guard against the CLI being missing (not pure silent-swallow)
// allow-test-rule: reads a product workflow .md file (not CJS source) to verify
// allow-test-rule: source-text-is-the-product
// Reads a product workflow .md file (not CJS source) to verify
// the guard is present; there is no behavioural runtime to invoke.
test('F3: update.md has an explicit guard when changeset CLI is missing (#935)', (_t) => {
const workflowPath = path.join(ROOT, 'gsd-core', 'workflows', 'update.md');

View File

@@ -1,13 +1,20 @@
'use strict';
/**
* The full test lane is sharded, and the coverage gate that sharding displaced
* is still wired in — .github/workflows/test.yml (#2952).
* The full test lane and the scoped Windows lane are both sharded, and the
* coverage gate that sharding displaced is still wired in —
* .github/workflows/test.yml (#2952, #3057).
*
* The `scope: full` lane was the only unsharded lane in this file. It ran the
* entire unit suite under c8 on a single runner, grew past a 15-minute cap, and
* reddened `next` (#2952). Raising the cap treated the symptom; sharding is the
* shape fix, and it is the same answer #1212 reached for the Windows lane.
* shape fix, and it is the same answer #1212 reached for the Windows lane at
* the time.
*
* The `scope: windows` lane then hit the identical cliff itself: it reached
* exactly 15m05s and was CANCELLED on PR #3094, four shas in a row. Per #869's
* stated durable follow-up, it is now sharded three ways too (#3057), leaving
* `scope: targeted` as the only lane in this job with no shard.
*
* Sharding introduces two failure modes that stay GREEN while being wrong, so
* both are pinned here:
@@ -69,44 +76,64 @@ test('the full test lane is sharded and complete (#2952)', async (t) => {
const workflow = loadWorkflow('test.yml');
const include = workflow.jobs.test.strategy.matrix.include;
const fullLanes = include.filter((e) => e.scope === 'full');
const windowsLanes = include.filter((e) => e.scope === 'windows');
// The only lane in this job with no shard is `scope: targeted` — the fast,
// single-runner default lane. Both `full` and `windows` are sharded.
const shardedScopes = { full: fullLanes, windows: windowsLanes };
await t.test('the full lane is actually sharded, not a single runner', () => {
assert.ok(fullLanes.length > 0, 'expected at least one `scope: full` matrix entry');
assert.ok(
fullLanes.length > 1,
'the `scope: full` lane is back to a single unsharded entry. That is the '
+ '#2952 regression: the whole unit suite under c8 on one runner grew past '
+ 'its cap and reddened `next`.',
);
for (const lane of fullLanes) {
for (const [scope, lanes] of Object.entries(shardedScopes)) {
await t.test(`the \`scope: ${scope}\` lane is actually sharded, not a single runner`, () => {
assert.ok(lanes.length > 0, `expected at least one \`scope: ${scope}\` matrix entry`);
assert.ok(
lane.shard !== undefined,
`a \`scope: full\` matrix entry declares no shard: ${JSON.stringify(lane)}`,
lanes.length > 1,
`the \`scope: ${scope}\` lane is back to a single unsharded entry. That is `
+ 'the #2952/#3057 regression: the whole suite on one runner grows past '
+ 'its cap and reddens `next`.',
);
}
});
for (const lane of lanes) {
assert.ok(
lane.shard !== undefined,
`a \`scope: ${scope}\` matrix entry declares no shard: ${JSON.stringify(lane)}`,
);
}
});
await t.test('the declared shards form one complete set', () => {
const specs = fullLanes.map((e) => e.shard);
assert.ok(
isCompleteShardSet(specs),
`the full lane's shards ${JSON.stringify(specs)} are not a complete set. `
+ 'Every entry must share one denominator N and the numerators must be '
+ 'exactly 1..N — a missing numerator silently stops running that slice of '
+ 'the unit suite while every check stays green.',
);
});
await t.test(`the \`scope: ${scope}\` lane's declared shards form one complete set`, () => {
const specs = lanes.map((e) => e.shard);
assert.ok(
isCompleteShardSet(specs),
`the \`scope: ${scope}\` lane's shards ${JSON.stringify(specs)} are not a `
+ 'complete set. Every entry must share one denominator N and the '
+ 'numerators must be exactly 1..N — a missing numerator silently stops '
+ 'running that slice of the suite while every check stays green.',
);
});
}
await t.test('only the full lane is sharded', () => {
for (const lane of include.filter((e) => e.scope !== 'full')) {
await t.test('no other lane is sharded', () => {
for (const lane of include.filter((e) => e.scope !== 'full' && e.scope !== 'windows')) {
assert.equal(
lane.shard, undefined,
`non-full lane ${JSON.stringify(lane)} declares a shard; the scoped and `
+ 'Windows lanes run a selected file list, not a partition.',
`lane ${JSON.stringify(lane)} declares a shard but is neither \`scope: full\` `
+ 'nor `scope: windows` — the targeted lane runs a selected file list, not '
+ 'a partition.',
);
}
});
await t.test('the scoped Windows shards are passed through to run-tests.cjs', () => {
const scopedStep = workflow.jobs.test.steps.find(
(s) => s.name === 'Run scoped tests',
);
assert.ok(scopedStep, 'no "Run scoped tests" step in the test job');
assert.match(
scopedStep.run, /matrix\.shard/,
'the "Run scoped tests" step does not reference matrix.shard, so the '
+ 'windows lane\'s three shards would each run the entire selected file '
+ 'list — N times the cost, no speedup.',
);
});
await t.test('each shard runs its own slice, not the whole suite', () => {
const unitStep = workflow.jobs.test.steps.find(
(s) => typeof s.run === 'string' && s.run.includes('test:coverage:unit:raw'),

View File

@@ -59,6 +59,18 @@ const LANE_COSTS = [
// whole unit suite. Run 30677442953: shard 1/3 7m12s, 2/3 4m32s, 3/3 3m59s.
// Shard 1 is the long pole because the unsharded aux suites ride on it.
// Before sharding the same lane cost 15m20s and blew a 15-minute cap.
//
// This one `timeout-minutes` also covers the `scope: windows` matrix
// entries — GitHub applies a single job-level budget across every matrix
// combination, not one per entry. That lane is now sharded three ways too
// (#3057), but no post-sharding per-shard measurement exists yet: its only
// recorded cost is the PRE-sharding whole-suite run that hit 15m05s and was
// CANCELLED on PR #3094. Each of its three shards should now cost roughly a
// third of that (~5m), which is already comfortably under the 8m/12m this
// entry requires — so no separate LANE_COSTS entry is added on a number
// that has not actually been measured. Replace this estimate with a real
// measured shard cost once one exists, the same discipline every other
// entry here follows.
evidence: 'run 30677442953 — 7m12s slowest shard',
},
{

View File

@@ -176,9 +176,12 @@ describe('ci-test-scope.cjs', () => {
encoding: 'utf8',
});
assert.notStrictEqual(r.status, 0);
// allow-test-rule: CLI usage failure text is user-facing contract for this parser guard.
// allow-test-rule: pending-migration-to-typed-ir [#3090]
// Regex-matches the CLI's human-readable stderr formatter (usage banner +
// arg-parser Error#message) — CONTRIBUTING's own BAD example verbatim.
// scripts/ci-test-scope.cjs has no --json / frozen-reason-enum error mode
// yet; adding one is a production change out of scope here. Tracked under #3090.
assert.match(r.stderr, /--files requires a value/);
// allow-test-rule: CLI usage banner presence is a user-facing contract.
assert.match(r.stderr, /Usage:/);
});
@@ -211,7 +214,6 @@ describe('ci-test-scope.cjs', () => {
// A plain source file that matches no RULES entry but is under gsd-core/ (code path)
const result = scopeFor(['gsd-core/src/some-util.js']);
assert.strictEqual(result.code_changed, true);
// allow-test-rule: the unit-fallback contract is the exact subject of bug #408.
assert.deepStrictEqual(result.targeted_tests, ['unit'],
'targeted_tests must be [\'unit\'] when code changed but no rule matched');
});

View File

@@ -1,4 +1,4 @@
// allow-test-rule: AC2 requires asserting no `runtime === 'claude'` string-equality branch remains in bin/install.js — the descriptor-migration contract is a property of the source text, so a source-grep is the only faithful check (#2086)
// allow-test-rule: structural-regression-guard — AC2 requires asserting no `runtime === 'claude'` string-equality branch remains in bin/install.js — the descriptor-migration contract is a property of the source text, so a source-grep is the only faithful check (#2086)
'use strict';
/**

View File

@@ -1,4 +1,4 @@
// allow-test-rule: AC2 requires asserting no `runtime === 'cline'` string-equality branch remains in bin/install.js/src — the descriptor-migration contract is a property of the source text, so a source-grep is the only faithful check (#2090)
// allow-test-rule: structural-regression-guard — AC2 requires asserting no `runtime === 'cline'` string-equality branch remains in bin/install.js/src — the descriptor-migration contract is a property of the source text, so a source-grep is the only faithful check (#2090)
'use strict';
/**

View File

@@ -1,5 +1,12 @@
'use strict';
// allow-test-rule: line 159 reads the STATE.md temp file written by readModifyWriteStateMd — this is a runtime output file assertion, not a source-grep; the API returns void so a file read-back is the only way to verify the transform was applied
// allow-test-rule: pending-migration-to-typed-ir [#3090]
// This file's assert.throws(fn, /regex/) sites (acquireStateLock /
// readModifyWriteStateMd / withPlanningLock / acquireInstallMigrationLock
// timeout and error-propagation checks) regex-match the human-readable
// thrown Error#message — CONTRIBUTING's "Error / status / reason" BAD
// pattern; the fix is a frozen-enum REASON code on the thrown error, which
// requires a production change to those locking functions that is out of
// scope for this test-only change. Tracked for correction under #3090.
/**
* Deterministic clock-seam tests for acquireStateLock / withPlanningLock (issue #453).
@@ -40,6 +47,7 @@ const stateMod = require('../gsd-core/bin/lib/state.cjs');
const { acquireStateLock, releaseStateLock, readModifyWriteStateMd } = stateMod;
const { withPlanningLock } = require('../gsd-core/bin/lib/planning-workspace.cjs');
const { createTempProject, cleanup, runGsdTools } = require('./helpers.cjs');
const { stateExtractField } = require('../gsd-core/bin/lib/state-document.cjs');
// ─────────────────────────────────────────────────────────────────────────────
// 1. Fake-clock proof: acquireStateLock accepts and uses the clock seam
@@ -822,7 +830,7 @@ describe('readModifyWriteStateMd lock cleanup on error', () => {
const clock = makeFakeClock(0);
readModifyWriteStateMd(statePath, (c) => c + '\n**Patched:** yes\n', tmpDir, undefined, clock);
const content = fs.readFileSync(statePath, 'utf-8');
assert.ok(content.includes('**Patched:** yes'), 'transform must be applied');
assert.strictEqual(stateExtractField(content, 'Patched'), 'yes', 'transform must be applied');
assert.strictEqual(clock.sleepCalls.length, 0, 'no sleep when lock is immediately available');
});
});

View File

@@ -1,4 +1,4 @@
// allow-test-rule: AC2 requires asserting no `runtime === 'codex'` string-equality and no positive `isCodex` branch remain in bin/install.js/src — the descriptor-migration contract is a property of the source text, so a source-grep is the only faithful check (#2088)
// allow-test-rule: structural-regression-guard — AC2 requires asserting no `runtime === 'codex'` string-equality and no positive `isCodex` branch remain in bin/install.js/src — the descriptor-migration contract is a property of the source text, so a source-grep is the only faithful check (#2088)
'use strict';
/**

View File

@@ -1,5 +1,6 @@
// allow-test-rule: source-text-is-the-product — commands/gsd/*.md files ARE the
// deployed skill surface. Testing their contract tests the runtime behaviour.
// allow-test-rule: source-text-is-the-product
// commands/gsd/*.md files ARE the deployed skill surface. Testing their
// contract tests the runtime behaviour.
'use strict';

View File

@@ -707,8 +707,6 @@ describe('bug #2638 — sub_repos canonical location', () => {
__foldDescribe("folded:bug-3523-cjs-loadconfig-branching-strategy-warning (consolidation epic #1969 B6 #1975)", () => {
'use strict';
// allow-test-rule: validates runtime CLI stdout/stderr warning behavior, not source grep (see #3523)
/**
* Regression tests for #3523 — CJS loadConfig must not emit a false
* "unknown config key(s)" warning for `branching_strategy` when that key
@@ -833,7 +831,7 @@ describe('bug-3523 — no warning for legacy top-level branching_strategy', () =
);
// After migration write-back, config-get should find git.branching_strategy.
const result = runWithStderr(['config-get', 'git.branching_strategy'], tmpDir);
const result = runWithStderr(['config-get', 'git.branching_strategy', '--raw'], tmpDir);
assert.equal(
result.status,
@@ -845,8 +843,9 @@ describe('bug-3523 — no warning for legacy top-level branching_strategy', () =
'',
`No error should fire when reading migrated branching_strategy (#3523) — got: ${result.stderr}`
);
assert.ok(
result.stdout.includes('milestone'),
assert.equal(
result.stdout.trim(),
'milestone',
`Expected git.branching_strategy to be 'milestone' but got: ${result.stdout}`
);
});
@@ -880,6 +879,11 @@ describe('bug-3523 — double-emission reduced to single-emission', () => {
const result = runWithStderr(['resolve-model', 'planner'], tmpDir);
// allow-test-rule: pending-migration-to-typed-ir [#3090]
// Counts occurrences of a sentinel substring in the CLI's human-readable
// stderr warning text — no structured "warning count"/warning-list API is
// exposed yet; adding one is a production change out of scope here.
// Tracked under #3090.
// Count how many times the sentinel key appears in warnings
const warningLines = result.stderr
.split('\n')

View File

@@ -1808,7 +1808,7 @@ describe('#3197 — gsd-tools.cjs config-set workflow._auto_chain_active', () =>
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3086-git-create-tag-config-gate (consolidation epic #1969 B2 #1971)", () => {
// allow-test-rule: workflow-markdown-is-the-runtime-contract (see #3086)
// allow-test-rule: source-text-is-the-product (see #3086)
// Justification: complete-milestone.md IS the runtime — the agent reads and
// follows it directly. Asserting the <config-check> block is present in the
// markdown is the only way to verify the gate is wired. Per CONTEXT.md L611.
@@ -2285,7 +2285,7 @@ describe('feat-3210 / H5: enum validation for code_quality.fallow.scope and .pro
__foldDescribe("folded:bug-3212-execute-phase-stall-safe-resume (consolidation epic #1969 B3 #1972)", () => {
'use strict';
// allow-test-rule: source-text-is-product [#3212]
// allow-test-rule: source-text-is-the-product [#3212]
// The bug is in workflow/config contracts consumed by agents at runtime.
const { describe, test } = require('node:test');

View File

@@ -1,4 +1,4 @@
// allow-test-rule: AC2 requires asserting no `runtime === 'cursor'` string-equality branch remains in bin/install.js/src — the descriptor-migration contract is a property of the source text, so a source-grep is the only faithful check (#2089)
// allow-test-rule: structural-regression-guard — AC2 requires asserting no `runtime === 'cursor'` string-equality branch remains in bin/install.js/src — the descriptor-migration contract is a property of the source text, so a source-grep is the only faithful check (#2089)
'use strict';
/**

View File

@@ -1,4 +1,4 @@
// allow-test-rule: runtime-contract-is-the-product see #2568
// allow-test-rule: source-text-is-the-product see #2568
// agents/gsd-debug-session-manager.md is executed instruction text: the orchestrator
// follows it verbatim, so WHERE the commit step sits relative to the terminal vs
// non-terminal summary shapes IS the contract. The commit_docs gate it relies on is

View File

@@ -914,11 +914,11 @@ describe('bug #2950: stale deleted-command references removed from workflow file
* integration. (Test-enforced via concept-mapping audit.)
*/
// allow-test-rule: structural-IR parser for a docs guide. The .includes() (see #2840)
// calls below build a typed record (commandsPresent flags, conceptPairs
// flags, nonGoalFlags, safetyFlags); assertions run on those booleans, not
// on raw text. This is the documented escape hatch in
// scripts/lint-no-source-grep.cjs for doc-shape tests.
// allow-test-rule: source-text-is-the-product (see #2840)
// docs/issue-driven-orchestration.md's deployed prose IS the product being
// validated for required-concept coverage. The .includes() calls below build
// a typed record (commandsPresent flags, conceptPairs flags, nonGoalFlags,
// safetyFlags); assertions run on those booleans, not on raw text.
'use strict';

View File

@@ -1,4 +1,5 @@
// allow-test-rule: runtime-contract-is-the-product — the rendered reference/SPEC/ADR vocab surfaces are the runtime contract; this pins their bijection to the code (docs-parity)
// allow-test-rule: source-text-is-the-product
// the rendered reference/SPEC/ADR vocab surfaces are the runtime contract; this pins their bijection to the code (docs-parity)
// Asserts the portable reference doc (gsd-core/references/edge-probe.md) keeps its
// worked-example JSON blocks in sync with the source-of-truth fixture files under
// gsd-core/references/edge-probe-fixtures/. The fixtures are the canonical data; the

View File

@@ -1,4 +1,5 @@
// allow-test-rule: runtime-contract-is-the-product — plan-phase.md's planner prompt is the deployed runtime contract under assertion
// allow-test-rule: source-text-is-the-product
// plan-phase.md's planner prompt is the deployed runtime contract under assertion
// plan-phase.md is the deployed planning workflow contract; these checks lock
// the SPEC path wiring and quality-gate that the edge-probe review (RR-01/02/03)
// requires — assertions scope to extracted sub-blocks to avoid false positives.

View File

@@ -1,4 +1,5 @@
// allow-test-rule: runtime-contract-is-the-product — spec-phase.md Step 5.5 is the deployed workflow runtime contract under assertion
// allow-test-rule: source-text-is-the-product
// spec-phase.md Step 5.5 is the deployed workflow runtime contract under assertion
// spec-phase.md is the deployed spec workflow contract; these checks lock
// the Step 5.5 wiring so the edge-probe.cjs runtime invocation cannot
// silently rot the way the original plan-phase no-op did (reviewer finding RR-11).

View File

@@ -268,7 +268,6 @@ describe('execute-phase docs: user-facing wave flag', () => {
describe('phase-plan-index: wave grouping behavior', () => {
test('phase-plan-index groups plans by wave (DAG-bucketing: P002 depends on P001)', () => {
// allow-test-rule: behavioral — calls gsd-tools and asserts structured output
const fs = require('fs');
const path = require('path');
const tmpDir = createTempProject();
@@ -334,7 +333,6 @@ describe('phase-plan-index: wave grouping behavior', () => {
});
test('phase-plan-index defaults missing wave frontmatter to wave 1', () => {
// allow-test-rule: behavioral — exercises gsd-tools wave-defaulting logic
const fs = require('fs');
const path = require('path');
const tmpDir = createTempProject();
@@ -419,7 +417,6 @@ describe('use_worktrees config: cross-workflow structural coverage', () => {
});
test('config-set accepts workflow.use_worktrees', () => {
// allow-test-rule: behavioral — exercises config-set validation, not source text
const tmpDir = createTempProject();
try {
const result = runGsdTools('config-set workflow.use_worktrees true', tmpDir);
@@ -802,7 +799,9 @@ describe('execute-phase: between-wave manifest reset (#1369, #3384)', () => {
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3096-ai-integration-phase-parallel-race (consolidation epic #1969 B4 #1973)", () => {
'use strict';
// allow-test-rule: reads product workflow markdown (ai-integration-phase.md) to verify structural ordering contract — not a source-grep test (see #3096)
// allow-test-rule: source-text-is-the-product (see #3096)
// Reads product workflow markdown (ai-integration-phase.md) to verify
// structural ordering contract.
// Regression guard for bug #3096.
//

View File

@@ -1,4 +1,4 @@
// allow-test-rule: runtime-contract-is-the-product see #1856
// allow-test-rule: source-text-is-the-product see #1856
// The orchestrator cwd-drift guard (#48) is shell EMBEDDED in execute-phase.md.
// The shipped text IS the runtime contract, so these tests extract the block and
// EXECUTE it against real git fixtures rather than asserting on its characters —

View File

@@ -105,7 +105,9 @@ describe('gsd-executor — state.* calls use the named-only router form (#1863 r
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3097-3099-executor-worktree-path-safety (consolidation epic #1969 B7 #1976)", () => {
'use strict';
// allow-test-rule: reads markdown product files (gsd-executor.md, worktree-path-safety.md) to verify structural protocol — not source-grep (see #3097)
// allow-test-rule: source-text-is-the-product (see #3097)
// Reads markdown product files (gsd-executor.md, worktree-path-safety.md) to
// verify structural protocol.
// Regression guards for bug #3097 and #3099.
//

View File

@@ -144,31 +144,58 @@ describe('A. execGit normalization (#3071)', () => {
const sharedStub = makeFaultyGit();
// worktree-safety.cts:33 — via resolveWorktreeContext, the exported
// entry point that threads deps.execGit.
// entry point that threads deps.execGit. Derived: existsSync:()=>false
// skips the has_local_planning shortcut, so resolveWorktreeLinkage runs;
// the shared stub's benign passthrough (exitCode:0, empty stdout) for
// both --git-dir and --git-common-dir makes them resolve to the SAME
// path (tmpDir), which is the main_worktree branch
// (src/worktree-safety.cts:196-200) — a `typeof === 'string'` check
// would still pass if a shape regrowth returned e.g. reason:'not_a_git_repo'.
const wsResult = worktreeSafety.resolveWorktreeContext(tmpDir, {
execGit: sharedStub,
existsSync: () => false,
});
assert.strictEqual(typeof wsResult.effectiveRoot, 'string');
assert.strictEqual(typeof wsResult.mode, 'string');
assert.strictEqual(typeof wsResult.reason, 'string');
assert.deepStrictEqual(wsResult, {
effectiveRoot: tmpDir,
mode: 'current_directory',
reason: 'main_worktree',
});
// git-base-branch.cts:32 — trySymbolicRef takes execGit directly as its
// second positional argument (no deps wrapper).
assert.doesNotThrow(() => trySymbolicRef(tmpDir, sharedStub));
// second positional argument (no deps wrapper). Derived: the stub
// answers `git symbolic-ref ...` with exitCode:0 but empty stdout, and
// trySymbolicRef treats an empty stdout as "unset" regardless of exit
// code (src/git-base-branch.cts:105) — it must return null, not merely
// return without throwing.
assert.strictEqual(trySymbolicRef(tmpDir, sharedStub), null);
// worktree-base-ref.cts:88 — evaluateWorktreeBaseDegrade threads
// deps.execGit.
// deps.execGit. Derived: `git rev-parse HEAD` answers exitCode:0 with
// empty stdout, which is the explicit exit0-empty-stdout branch
// (src/worktree-base-ref.cts:401-403) — shouldDegrade:false,
// reason:'no-head', headAbsenceVerified:false (NOT the exit-128
// "definitive no-head" case, which would report headAbsenceVerified:true).
// A `typeof shouldDegrade === 'boolean'` check would still pass on a
// fail-closed flip to shouldDegrade:true.
const wbrResult = evaluateWorktreeBaseDegrade({ execGit: sharedStub, cwd: tmpDir });
assert.strictEqual(typeof wbrResult.shouldDegrade, 'boolean');
assert.strictEqual(typeof wbrResult.reason, 'string');
assert.deepStrictEqual(wbrResult, {
shouldDegrade: false,
reason: 'no-head',
message: null,
headSha: null,
forkRef: null,
forkSha: null,
headAbsenceVerified: false,
});
// verification.cts:226 — defaultPhaseCleanCommitTimesMs takes execGitFn
// directly as its third positional argument, typed `= typeof execGit`.
assert.doesNotThrow(() => {
const map = defaultPhaseCleanCommitTimesMs(tmpDir, ['a.md', 'b.md'], sharedStub);
assert.ok(map instanceof Map);
});
// Derived: `git log ...` answers exitCode:0 with empty stdout, which
// trips the early `logRes.stdout.length === 0` return (empty Map) at
// src/verification.cts:247 — `map instanceof Map` alone would also pass
// for a non-empty map.
const map = defaultPhaseCleanCommitTimesMs(tmpDir, ['a.md', 'b.md'], sharedStub);
assert.deepStrictEqual(map, new Map());
});
});

View File

@@ -46,9 +46,11 @@ const DEBUG_MD = path.join(__dirname, '..', 'gsd-core', 'workflows', 'debug.md')
const SESSION_MANAGER_MD = path.join(__dirname, '..', 'agents', 'gsd-debug-session-manager.md');
describe('#2257 debug non-terminal session-manager return contract', () => {
// allow-test-rule: workflow/agent prose IS the runtime contract under test #2257
// allow-test-rule: source-text-is-the-product (#2257)
// workflow/agent prose IS the runtime contract under test
const debugContent = fs.readFileSync(DEBUG_MD, 'utf-8');
// allow-test-rule: workflow/agent prose IS the runtime contract under test #2257
// allow-test-rule: source-text-is-the-product (#2257)
// workflow/agent prose IS the runtime contract under test
const managerContent = fs.readFileSync(SESSION_MANAGER_MD, 'utf-8');
const section4Start = debugContent.indexOf('## 4. Session Management');

View File

@@ -24,7 +24,7 @@
* silently re-assert an unsupported capability.
*/
// allow-test-rule: runtime-contract-is-the-product #2598 — the descriptor JSON and the
// allow-test-rule: source-text-is-the-product #2598 — the descriptor JSON and the
// host-integration matrix ARE the negotiated contract; asserting their values is behavioral.
'use strict';

View File

@@ -29,7 +29,7 @@
* disagreement is how the gap survived.
*/
// allow-test-rule: runtime-contract-is-the-product #2603 — the descriptor JSON and the
// allow-test-rule: source-text-is-the-product #2603 — the descriptor JSON and the
// host-integration matrix ARE the negotiated contract; asserting their values is behavioral.
'use strict';

View File

@@ -1,4 +1,4 @@
// allow-test-rule: runtime-contract-is-the-product #2615 — the host-integration matrix
// allow-test-rule: source-text-is-the-product #2615 — the host-integration matrix
// IS the cited source of truth for every descriptor axis (ADR-1239); asserting that a
// shipped axis value appears there, and matches, is a contract assertion.

View File

@@ -13,11 +13,12 @@
* G. Anti-regression guard: five affected workflows must NOT contain the
* duplicated bare `:-main` / `:-master` fallback pattern that was the root cause.
* They must call `gsd_run query git.base-branch` instead.
* (allow-test-rule: runtime-contract-is-the-product — the workflow .md content IS
* the runtime surface; the absence of the bad pattern is what ships to agents.)
* (see the source-text-is-the-product exemption declared below this docblock —
* the workflow .md content IS the runtime surface; the absence of the bad
* pattern is what ships to agents.)
*/
// allow-test-rule: runtime-contract-is-the-product
// allow-test-rule: source-text-is-the-product
// Justification: the workflow .md files ARE the product surface — agents read and
// execute them directly. Guard G asserts that the resolved command appears in all five
// workflows, which requires reading those workflow files. Per TESTING-STANDARDS.md §6.

View File

@@ -92,7 +92,8 @@ const CONSUMERS = [
for (const consumer of CONSUMERS) {
test(`${consumer.name} does not re-declare an inline buildParityManifest or any exclusion constant (#2266)`, () => {
// allow-test-rule: source text is the product for this anti-divergence check, see #2266
// allow-test-rule: source-text-is-the-product (see #2266)
// Source text is the product for this anti-divergence check.
const content = fs.readFileSync(path.join(ROOT, ...consumer.rel), 'utf8');
for (const { label, re } of FORBIDDEN_INLINE) {
assert.ok(

View File

@@ -22,7 +22,8 @@
* is the minimum-cost contract.
*/
// allow-test-rule: structural assertion on spawn-options shape; the behavior
// allow-test-rule: structural-regression-guard
// structural assertion on spawn-options shape; the behavior
// (Windows-only shell resolution) is platform-gated at runtime and cannot be
// reached on POSIX CI without a Windows lane.
@@ -295,7 +296,8 @@ describe('Issue #815: --next dist-tag support', () => {
* contract for the worker, the same rationale #378 carried.
*/
// allow-test-rule: structural assertion on hook delegation; the behavior being (see #378)
// allow-test-rule: structural-regression-guard (see #378)
// structural assertion on hook delegation; the behavior being
// tested (correct package name → no E404) only manifests at runtime against the
// live npm registry, which CI does not call.

View File

@@ -10,7 +10,9 @@ const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const { cleanup, createTempDir } = require('./helpers.cjs');
const os = require('os');
const { cleanup, createTempDir, tmpRootCandidates } = require('./helpers.cjs');
// ─── Test 1: Real-FS happy path ──────────────────────────────────────────────
@@ -98,3 +100,162 @@ test('cleanup does not throw when cwd is inside the target dir, and removes the
assert.strictEqual(fs.existsSync(dir), false, 'temp dir should not exist after cleanup');
});
// ─── Test 4: out-of-tmpdir refusal ───────────────────────────────────────────
test('cleanup throws and does not chdir or delete when target is outside os.tmpdir()', () => {
// __dirname (this repo's tests/ directory) must never be deleted. Whether
// it is actually outside os.tmpdir() is environment-dependent: on Linux
// os.tmpdir() is /tmp, and a CI container that checks the repo out under
// /tmp would put __dirname INSIDE tmpdir, in which case a correctly-working
// cleanup() would not refuse it -- it would delete this directory. The
// precondition assertion below verifies the "outside tmpdir" assumption
// before cleanup() is ever called, so that situation fails loudly and
// safely instead of destructively. No scratch directory is created, so
// there is nothing to tear down.
const outsideDir = __dirname;
const knownFile = path.join(outsideDir, 'helpers-cleanup.test.cjs');
// Use cleanup()'s own tmpRootCandidates() as the single source of truth
// for "is this path inside tmpdir" instead of a hand-mirrored predicate.
// The old copy here checked only path.resolve(os.tmpdir()), one of
// SEVERAL root spellings cleanup() actually accepts (it also accepts the
// realpath'd and native-realpath'd forms) — a repo checked out under a
// root this precondition didn't know about would compute "outside, safe"
// while cleanup() computed "inside, delete", turning this refusal test
// into the very destructive scenario it exists to prevent. Importing the
// real function removes that drift class entirely.
const resolvedOutsideDir = path.resolve(outsideDir);
const roots = tmpRootCandidates();
const isWindows = process.platform === 'win32';
const outsideDirForCompare = isWindows ? resolvedOutsideDir.toLowerCase() : resolvedOutsideDir;
const isInsideTmpdir = roots.some((root) => {
const rootForCompare = isWindows ? root.toLowerCase() : root;
return (
outsideDirForCompare === rootForCompare ||
outsideDirForCompare.startsWith(`${rootForCompare}${path.sep}`)
);
});
assert.strictEqual(
isInsideTmpdir,
false,
`this test cannot run safely when the repo lives under any tmpdir root ` +
`cleanup() accepts: outsideDir (${resolvedOutsideDir}) is inside one of ${JSON.stringify(roots)}`
);
const cwdBefore = process.cwd();
assert.throws(
() => cleanup(outsideDir),
(err) => err instanceof Error && err.message.includes(outsideDir),
'cleanup must throw an Error whose message names the offending path'
);
assert.strictEqual(
process.cwd(),
cwdBefore,
'cleanup must refuse before chdir, so cwd is unchanged'
);
assert.strictEqual(fs.existsSync(outsideDir), true, 'target directory must still exist after refusal');
assert.strictEqual(
fs.existsSync(knownFile),
true,
'a known file inside the target must still exist, proving the dir was not emptied'
);
});
// ─── Test 5: control — a real os.tmpdir()-rooted path still cleans up ───────
test('cleanup still removes a real os.tmpdir()-rooted directory (control)', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cleanup-control-'));
cleanup(dir);
assert.strictEqual(fs.existsSync(dir), false, 'os.tmpdir()-rooted directory should be removed');
});
// ─── Test 6b: accepted-roots set always accepts a freshly-minted tmpdir ─────
test('cleanup accepts a freshly-created os.tmpdir()-rooted dir on any platform (accepted-roots invariant)', (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cleanup-roots-'));
t.after(() => {
if (fs.existsSync(dir)) cleanup(dir);
});
assert.doesNotThrow(
() => cleanup(dir),
'cleanup must accept a directory created directly under os.tmpdir(), regardless of platform-specific canonical spelling (8.3 short/long names, /var symlink, drive-letter case)'
);
assert.strictEqual(fs.existsSync(dir), false, 'temp dir should be removed, not refused');
});
// ─── Test 6: realpath'd os.tmpdir() form is not refused (regression) ────────
test('cleanup accepts a realpath()d temp dir even when it differs from the raw path (macOS /var -> /private/var)', (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cleanup-realpath-'));
const realPath = fs.realpathSync(dir);
if (realPath !== dir) {
// macOS (and any other symlinked-tmpdir platform): the realpath'd form
// diverges from the raw mkdtempSync() path. This is exactly the shape a
// caller gets from fs.realpathSync() or from process.cwd() after
// chdir-ing into a realpath'd dir — assert the guard does NOT refuse it.
cleanup(realPath);
assert.strictEqual(fs.existsSync(realPath), false, 'realpath()d form should be removed, not refused');
assert.strictEqual(fs.existsSync(dir), false, 'raw path should also be gone (same directory)');
} else {
// Linux and any platform with no tmpdir symlink indirection: realpath()
// equals the raw path, so this branch exercises the ordinary path and
// keeps the test meaningful (non-vacuous) on both platforms.
t.after(() => {
if (fs.existsSync(dir)) cleanup(dir);
});
cleanup(dir);
assert.strictEqual(fs.existsSync(dir), false, 'temp dir should be removed');
}
});
// ─── Test 7: tmpRootCandidates() shape invariants (platform-independent) ────
//
// Test 6 above is coverage-identical to Test 5's control on any platform
// where fs.realpathSync(tmpdir) does not diverge from the raw path (Linux,
// notably — the platform the remote matrix actually runs on), so it cannot
// verify the macOS-specific fix there. These assertions hold the exported
// tmpRootCandidates() to a contract that is meaningful on every platform.
test('tmpRootCandidates() returns a well-formed, deduplicated list of absolute paths', () => {
const roots = tmpRootCandidates();
assert.ok(Array.isArray(roots) && roots.length > 0, 'must return a non-empty array');
for (const root of roots) {
assert.strictEqual(path.isAbsolute(root), true, `root must be absolute: ${root}`);
}
const isWindows = process.platform === 'win32';
const keys = roots.map((r) => (isWindows ? r.toLowerCase() : r));
const uniqueKeys = new Set(keys);
assert.strictEqual(uniqueKeys.size, keys.length, `roots must be deduplicated: ${JSON.stringify(roots)}`);
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cleanup-roots-contract-'));
try {
const resolvedDir = path.resolve(dir);
const dirForCompare = isWindows ? resolvedDir.toLowerCase() : resolvedDir;
const isUnderSomeRoot = roots.some((root) => {
const rootForCompare = isWindows ? root.toLowerCase() : root;
return (
dirForCompare === rootForCompare ||
dirForCompare.startsWith(`${rootForCompare}${path.sep}`)
);
});
assert.strictEqual(
isUnderSomeRoot,
true,
`a freshly mkdtempSync'd dir under os.tmpdir() must be under at least one returned root: ` +
`${resolvedDir} vs ${JSON.stringify(roots)}`
);
} finally {
cleanup(dir);
}
});

View File

@@ -206,11 +206,123 @@ function createTempGitProject(prefix = 'gsd-test-') {
return createFixture({ prefix, planning: true, git: true, projectDoc: true });
}
// The OS temp root has several canonical spellings, and a path a caller
// legitimately passes to cleanup() may arrive in any of them: macOS resolves
// os.tmpdir() under /var/folders/... but /var is a symlink to /private/var;
// Windows CI runners report os.tmpdir() in the 8.3 SHORT form
// (C:\Users\RUNNER~1\AppData\Local\Temp) while the caller's path is the
// expanded LONG form, and fs.realpathSync() does not reliably expand 8.3
// short names there — only fs.realpathSync.native() does; drive-letter and
// path casing can also differ (C:\ vs c:\). This function collects the full
// set of accepted temp roots — os.tmpdir()'s several spellings are one part
// of that set, not the whole of it (see below). Each probe is wrapped in its
// own try/catch — none of them may throw and crash cleanup(), they just
// contribute nothing if unavailable.
// Memoization cache for tmpRootCandidates(), keyed on the LIVE os.tmpdir()
// value (not hoisted to a plain module-level constant) — two test files in
// this suite mutate TMPDIR/TEMP/TMP mid-run and restore them afterward, so
// caching on the current os.tmpdir() read is what keeps a stale cache from
// leaking across that override instead of a one-time computation baked in
// at module load.
let _tmpRootCandidatesCacheKey;
let _tmpRootCandidatesCache;
function tmpRootCandidates() {
const cacheKey = os.tmpdir();
if (cacheKey === _tmpRootCandidatesCacheKey && _tmpRootCandidatesCache) {
return _tmpRootCandidatesCache;
}
const deduped = _computeTmpRootCandidates();
_tmpRootCandidatesCacheKey = cacheKey;
_tmpRootCandidatesCache = Object.freeze(deduped);
return _tmpRootCandidatesCache;
}
function _computeTmpRootCandidates() {
const roots = [];
try {
roots.push(path.resolve(os.tmpdir()));
} catch (_) { /* os.tmpdir() unavailable — skip this variant */ }
try {
roots.push(fs.realpathSync(os.tmpdir()));
} catch (_) { /* temp root unreadable — skip this variant */ }
try {
roots.push(fs.realpathSync.native(os.tmpdir()));
} catch (_) { /* native realpath unavailable/unreadable — skip this variant */ }
const isWindows = process.platform === 'win32';
// os.tmpdir() alone is too narrow: it honors $TMPDIR, but some tests
// (e.g. tests/config-schema.property.test.cjs's getWritableTmp()) create
// fixtures directly under the conventional system temp roots instead of
// through $TMPDIR — on macOS that is /private/tmp, which can differ from
// os.tmpdir()'s /var/folders/.../T. Probe the well-known non-Windows temp
// roots too, each independently and only if it actually exists on this
// host, so the accepted set stays a bounded, explicit list rather than an
// open-ended patch list. macOS additionally exposes /tmp as a symlink to
// /private/tmp, so both the unprefixed and /private-prefixed spellings —
// and each one's realpath — are collected.
if (!isWindows) {
for (const candidate of ['/tmp', '/private/tmp']) {
try {
if (fs.existsSync(candidate)) {
roots.push(path.resolve(candidate));
try {
roots.push(fs.realpathSync(candidate));
} catch (_) { /* exists but unreadable via realpath — skip this variant */ }
}
} catch (_) { /* existsSync itself should not throw, but fail closed if it does */ }
}
}
const seen = new Set();
const deduped = [];
for (const root of roots) {
const key = isWindows ? root.toLowerCase() : root;
if (seen.has(key)) continue;
seen.add(key);
deduped.push(root);
}
return deduped;
}
function cleanup(tmpDir) {
if (typeof tmpDir !== 'string' || tmpDir.length === 0) return;
const target = path.resolve(tmpDir);
const cwd = path.resolve(process.cwd());
const tmpRoot = path.resolve(os.tmpdir());
// The temp-root check below was previously done only inside the catch block,
// so it classified a transient Windows error but was never consulted by the
// destructive rmSync call itself — a wrong `target` would still chdir out of
// its own tree and get force-deleted. Hoisted above both the chdir and the
// rmSync so an out-of-temp-root path is refused before either can run.
// Comparison is case-insensitive on Windows (drive-letter and path casing
// vary there) and case-sensitive everywhere else; the error message below
// always prints the original-case target.
const isWindows = process.platform === 'win32';
const tmpRoots = tmpRootCandidates();
function isUnderRoots(p) {
const pForCompare = isWindows ? p.toLowerCase() : p;
return tmpRoots.some((root) => {
const rootForCompare = isWindows ? root.toLowerCase() : root;
if (pForCompare === rootForCompare) return true;
// A root that is itself a filesystem root (`/`, or `C:\` reachable via
// TMPDIR=/) already ends with path.sep — appending a second one would
// build `//`, which only the literal string `/` satisfies, refusing
// every real descendant. Only append the separator when it is not
// already there.
const prefix = rootForCompare.endsWith(path.sep)
? rootForCompare
: `${rootForCompare}${path.sep}`;
return pForCompare.startsWith(prefix);
});
}
if (!isUnderRoots(target)) {
throw new Error(
`cleanup() refused to remove a path outside the known temp roots ` +
`(${tmpRoots.join(', ')}): ${target}`
);
}
// No symlink-escape check here: fs.rmSync does not follow a top-level
// symlink — it unlinks the link itself and leaves the target intact — so
// there is no live hazard for the root-membership check above to guard
// against. That check is the one closing an actual defect.
if (cwd === target || cwd.startsWith(`${target}${path.sep}`)) {
// Windows cannot remove a directory that is the current working directory.
process.chdir(path.dirname(target));
@@ -225,9 +337,9 @@ function cleanup(tmpDir) {
} catch (error) {
// After retries, Windows can still briefly hold temp dirs open after a timed-out
// child exits. Ignore that teardown-only flake for temp roots, but rethrow everything else.
const isTmpPath = target === tmpRoot || target.startsWith(`${tmpRoot}${path.sep}`);
// By this point target is guaranteed under a temp root: the guard clauses above throw
// for any other path, so this swallow doesn't need to re-test that.
const isTransientWinErr = process.platform === 'win32'
&& isTmpPath
&& ['EBUSY', 'ENOTEMPTY', 'EPERM'].includes(error && error.code);
if (!isTransientWinErr) throw error;
}
@@ -637,4 +749,4 @@ function clearSessionEnv() {
for (const k of SESSION_ENV_KEYS) delete process.env[k];
}
module.exports = { runGsdTools, createTempDir, createTempProject, createTempGitProject, cleanup, readFileNormalized, readWorkflowCombined, parseFrontmatter, isUsageOutput, captureConsole, toPosixPath, absPlanningPath, runNpm, isolatedNpmEnv, withIsolatedProcessState, delay, waitFor, resetRuntimeWarningCaches, SESSION_ENV_KEYS, saveSessionEnv, restoreSessionEnv, clearSessionEnv, TOOLS_PATH };
module.exports = { runGsdTools, createTempDir, createTempProject, createTempGitProject, cleanup, tmpRootCandidates, readFileNormalized, readWorkflowCombined, parseFrontmatter, isUsageOutput, captureConsole, toPosixPath, absPlanningPath, runNpm, isolatedNpmEnv, withIsolatedProcessState, delay, waitFor, resetRuntimeWarningCaches, SESSION_ENV_KEYS, saveSessionEnv, restoreSessionEnv, clearSessionEnv, TOOLS_PATH };

View File

@@ -1,4 +1,4 @@
// allow-test-rule: AC2 requires asserting no `runtime === 'hermes'` string-equality branch remains in bin/install.js — the descriptor-migration contract is a property of the source text, so a source-grep is the only faithful check (#2091)
// allow-test-rule: structural-regression-guard — AC2 requires asserting no `runtime === 'hermes'` string-equality branch remains in bin/install.js — the descriptor-migration contract is a property of the source text, so a source-grep is the only faithful check (#2091)
'use strict';
/**

View File

@@ -2652,7 +2652,7 @@ describe('enh-770: gsd-config-reload.js hook script', () => {
});
test('gsd-config-reload.js contains the gsd-hook-version stamp', () => {
// allow-test-rule: runtime-contract-is-the-product — the stamp template token (see #770)
// allow-test-rule: source-text-is-the-product — the stamp template token (see #770)
// IS the product surface that the installer must find and replace with the
// real version at copy time; asserting its presence is required.
const content = fs.readFileSync(reloadScript, 'utf8');
@@ -2663,7 +2663,7 @@ describe('enh-770: gsd-config-reload.js hook script', () => {
});
test('gsd-config-reload.js reads from stdin and emits JSON output', () => {
// allow-test-rule: runtime-contract-is-the-product — the stdin-read and (see #770)
// allow-test-rule: source-text-is-the-product — the stdin-read and (see #770)
// JSON-emit pattern IS the hook contract; asserting its presence is required.
const content = fs.readFileSync(reloadScript, 'utf8');
assert.ok(
@@ -2673,7 +2673,7 @@ describe('enh-770: gsd-config-reload.js hook script', () => {
});
test('gsd-config-reload.js targets the FileChanged hook event', () => {
// allow-test-rule: runtime-contract-is-the-product — the hookEventName is (see #770)
// allow-test-rule: source-text-is-the-product — the hookEventName is (see #770)
// the protocol surface; asserting its presence verifies the contract.
const content = fs.readFileSync(reloadScript, 'utf8');
assert.ok(
@@ -2693,7 +2693,7 @@ describe('enh-770: hooks/hooks.json plugin manifest includes new hook events', (
});
test('hooks.json contains SubagentStop event', () => {
// allow-test-rule: runtime-contract-is-the-product — hooks.json IS the (see #770)
// allow-test-rule: source-text-is-the-product — hooks.json IS the (see #770)
// plugin manifest surface that Claude Code reads at plugin load time.
const content = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8'));
assert.ok(
@@ -2703,7 +2703,7 @@ describe('enh-770: hooks/hooks.json plugin manifest includes new hook events', (
});
test('hooks.json contains Stop event', () => {
// allow-test-rule: runtime-contract-is-the-product — hooks.json IS the (see #770)
// allow-test-rule: source-text-is-the-product — hooks.json IS the (see #770)
// plugin manifest surface that Claude Code reads at plugin load time.
const content = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8'));
assert.ok(
@@ -2713,7 +2713,7 @@ describe('enh-770: hooks/hooks.json plugin manifest includes new hook events', (
});
test('hooks.json contains PreCompact event', () => {
// allow-test-rule: runtime-contract-is-the-product — hooks.json IS the (see #770)
// allow-test-rule: source-text-is-the-product — hooks.json IS the (see #770)
// plugin manifest surface that Claude Code reads at plugin load time.
const content = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8'));
assert.ok(
@@ -2723,7 +2723,7 @@ describe('enh-770: hooks/hooks.json plugin manifest includes new hook events', (
});
test('hooks.json contains FileChanged event', () => {
// allow-test-rule: runtime-contract-is-the-product — hooks.json IS the (see #770)
// allow-test-rule: source-text-is-the-product — hooks.json IS the (see #770)
// plugin manifest surface that Claude Code reads at plugin load time.
const content = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8'));
assert.ok(

View File

@@ -1153,7 +1153,7 @@ describe('readCmdNames() — tolerates missing commands/gsd directory (#1223)',
});
// ─── Section N: Antigravity .agents canonical workspace dir (#791) ─────────────
// allow-test-rule: runtime-contract-is-the-product
// allow-test-rule: source-text-is-the-product
// Reads deployed agent .md files whose text IS the product surface the
// Antigravity runtime loads at startup (path references, command names).
@@ -1289,7 +1289,7 @@ describe('install — --devin-desktop CLI flag routes to windsurf runtime (#792)
});
});
// ─── Section N: Windsurf workflow slash-command install (#1615) ─────────────
// allow-test-rule: runtime-contract-is-the-product
// allow-test-rule: source-text-is-the-product
// Reads deployed workflow .md files whose text IS the product surface the
// Windsurf runtime loads at startup (path references, command names).

View File

@@ -4,7 +4,8 @@
* Covers: query, status, diff, validate, snapshot, patch-meta,
* extract-exports, enabled/disabled gating, and CLI routing via gsd-tools.
*/
// allow-test-rule: source-text-is-the-product — readFileSync assertions target API-SURFACE.md, which is the generated product of intelApiSurface; asserting on its text content is the only way to verify correct generation.
// allow-test-rule: source-text-is-the-product
// readFileSync assertions target API-SURFACE.md, which is the generated product of intelApiSurface; asserting on its text content is the only way to verify correct generation.
'use strict';
@@ -1107,8 +1108,8 @@ describe('intelApiSurface', () => {
});
describe('#1000 regression: gsd-intel-updater emits canonical intel filenames', () => {
// allow-test-rule: source-text-is-the-product — agents/gsd-intel-updater.md IS the
// system prompt the intel-updater agent runs under; asserting its filename references
// allow-test-rule: source-text-is-the-product
// agents/gsd-intel-updater.md IS the system prompt the intel-updater agent runs under; asserting its filename references
// verifies the deployed agent surface contract matches the INTEL_FILES the CLI reads.
const agentPromptPath = path.join(__dirname, '..', 'agents', 'gsd-intel-updater.md');
const agentPrompt = fs.readFileSync(agentPromptPath, 'utf8');
@@ -1357,9 +1358,9 @@ describe('bug #2351: intel updater kilo layout support', () => {
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3290-intel-updater-layout-block (consolidation epic #1969 B7 #1976)", () => {
// allow-test-rule: source-text-is-the-product — agents/gsd-intel-updater.md IS (see #3290)
// the deployed agent instruction set. Asserting its text content tests the
// deployed behaviour contract, not internal implementation.
// allow-test-rule: source-text-is-the-product (see #3290)
// agents/gsd-intel-updater.md IS the deployed agent instruction set. Asserting
// its text content tests the deployed behaviour contract, not internal implementation.
'use strict';

View File

@@ -1,4 +1,5 @@
// allow-test-rule: runtime-contract-is-the-product — INVENTORY.md heading format is the shipped doc surface being locked
// allow-test-rule: source-text-is-the-product
// INVENTORY.md heading format is the shipped doc surface being locked
'use strict';
/**

View File

@@ -1,4 +1,3 @@
// allow-test-rule: behavioral-fs-fixture (#2828)
'use strict';
// Regression guard for #2828: on a flat unmilestoned roadmap (no versioned milestone

View File

@@ -76,7 +76,9 @@ describe('Issue #498: the live repo passes the drift lint', () => {
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-170-workflow-fallback-install-hint (consolidation epic #1969 B4 #1973)", () => {
'use strict';
// allow-test-rule: workflow markdown is shipped product text; this test validates fallback hint literals across all workflow files (see #170)
// allow-test-rule: source-text-is-the-product (see #170)
// Workflow markdown is shipped product text; this test validates fallback
// hint literals across all workflow files.
const { test } = require('node:test');
const assert = require('node:assert/strict');

View File

@@ -18,9 +18,11 @@
* program; asserting their shape is asserting on the deployed contract.
*/
// allow-test-rule: structural assertion on the deployed update.md backup bash;
// the data-loss behavior only manifests against a real install during a clean
// reinstall, which CI does not perform.
// allow-test-rule: source-text-is-the-product
// update.md's bash blocks ARE the deployed /gsd:update program; asserting
// their shape is asserting on the deployed contract. The data-loss behavior
// only manifests against a real install during a clean reinstall, which CI
// does not perform.
'use strict';

View File

@@ -159,7 +159,8 @@ describe('issue-57 AC2 — config-mutation dispatch is closed over the explicit
}
});
// allow-test-rule: structural guard over bin/install.js source. Behavioral assertions
// allow-test-rule: structural-regression-guard
// structural guard over bin/install.js source. Behavioral assertions
// cannot observe inline `runtime === '...'` config branching, so this enforces that
// every inline per-runtime branch references a runtime the adapter registry knows
// about — a NEW branch against an unregistered runtime name fails here. It matches
@@ -184,7 +185,8 @@ describe('issue-57 AC2 — config-mutation dispatch is closed over the explicit
);
});
// allow-test-rule: structural guard over bin/install.js source (#2103). VS Code
// allow-test-rule: structural-regression-guard (#2103)
// structural guard over bin/install.js source. VS Code
// (capabilities/vscode/capability.json) is a registry runtime (role:runtime, for
// validator/host-integration coverage) but is NEVER CLI-installed — it is a
// Marketplace/VSIX extension with no --vscode flag and no allRuntimes membership
@@ -214,7 +216,8 @@ describe('issue-57 AC2 — config-mutation dispatch is closed over the explicit
);
});
// allow-test-rule: delegation-presence guard. Catches wholesale removal of the registry
// allow-test-rule: structural-regression-guard
// delegation-presence guard. Catches wholesale removal of the registry
// dispatch (a regression to scattered per-runtime config branching). Presence-style, not
// absence-grep, so it does not bite on incidental non-config `runtime === '...'` checks.
test('bin/install.js requires the config adapter registry and dispatches through it', () => {

View File

@@ -1,5 +1,7 @@
'use strict';
// allow-test-rule: reads product workflow/command markdown to verify the --next RC channel contract — not a source-grep test
// allow-test-rule: source-text-is-the-product
// Reads product workflow/command markdown to verify the --next RC channel
// contract.
// Issue #815: `/gsd-update --next` (alias `--rc`) must thread the @next dist-tag
// through the whole update flow (version check + install) while leaving the
@@ -102,7 +104,9 @@ describe('update.md — no bare ~.claude path references (#2470)', () => {
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3130-update-npx-robust-invocation (consolidation epic #1969 B4 #1973)", () => {
'use strict';
// allow-test-rule: reads product workflow markdown (update.md) to verify structural invocation contract — not a source-grep test (see #3130)
// allow-test-rule: source-text-is-the-product (see #3130)
// Reads product workflow markdown (update.md) to verify structural
// invocation contract.
// Regression guard for bug #3130.
//

View File

@@ -1,8 +1,9 @@
// allow-test-rule: behavioral-subprocess-test — see #2505 — Phase 5 kimi-variant
// disambiguation is verified via install.js subprocess output capture, since
// the disambiguateKimiVariant function is inline in bin/install.js (not
// exported). The test sets a disposable HOME, creates the probe config files,
// and asserts on the printed notices.
// allow-test-rule: pending-migration-to-typed-ir [#3090]
// Phase 5 kimi-variant disambiguation is verified via install.js subprocess
// output capture (regex on printed notices), since disambiguateKimiVariant
// is inline in bin/install.js (not exported) and emits no structured/JSON
// output. Exporting the function and/or adding a --json notice mode is a
// production change out of scope here. Tracked under #3090.
process.env.GSD_TEST_MODE = '1';
const { test, describe, before, after } = require('node:test');

View File

@@ -930,7 +930,7 @@ describe('TABLE_SCHEMAS parity: registry headers must appear verbatim in their s
*/
function assertHeaderInFile(relPath, variant) {
const fullPath = path.join(ROOT, relPath);
const content = fs.readFileSync(fullPath, 'utf8'); // allow-test-rule: runtime-contract-is-the-product — template/registry parity (#2242)
const content = fs.readFileSync(fullPath, 'utf8'); // allow-test-rule: source-text-is-the-product — template/registry parity (#2242)
const expectedHeader = buildHeader(variant);
const normalizedExpected = normalize(expectedHeader);
const found = content

View File

@@ -1,5 +1,5 @@
// allow-test-rule: structural-regression-guard see #2517
// allow-test-rule: runtime-contract-is-the-product see #2684
// allow-test-rule: source-text-is-the-product see #2684
// Guards the omit-when-inherit fix: workflow orchestrators must instruct the agent to
// OMIT the model= param from Agent() calls when the *_model var is "inherit" or empty.
// Without it, model="" is passed verbatim and 404s on non-Claude runtimes

View File

@@ -1,4 +1,4 @@
// allow-test-rule: runtime-contract-is-the-product (see #1073) — this guard asserts the
// allow-test-rule: source-text-is-the-product (see #1073) — this guard asserts the
// ABSENCE of phantom pre-migration issue references in repo text (docs, tests,
// workflows). The file *content* is the product surface here (#1073): dangling
// refs that don't exist in open-gsd/gsd-core mislead triage and manufacture

View File

@@ -1,4 +1,4 @@
// allow-test-rule: AC2 requires asserting no `runtime === 'opencode'` string-equality branch remains in bin/install.js/src — the descriptor-migration contract is a property of the source text, so a source-grep is the only faithful check (#2087)
// allow-test-rule: structural-regression-guard — AC2 requires asserting no `runtime === 'opencode'` string-equality branch remains in bin/install.js/src — the descriptor-migration contract is a property of the source text, so a source-grep is the only faithful check (#2087)
'use strict';
/**

View File

@@ -1,10 +1,6 @@
// 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.
// allow-test-rule: state-md-is-the-runtime-contract — regression tests for
// bug #3517 assert the exact STATE.md fields written by phase.complete;
// STATE.md IS the product surface being verified, not source code.
// Migration to typed-IR parser tracked in #2974.
/**
* GSD Tools Tests - Phase
@@ -5757,6 +5753,13 @@ describe('bug-3287 — init plan-phase exposes expected_phase_dir with project_c
// ─────────────────────────────────────────────────────────────────────────────
{
// Typed STATE.md surfaces (#3090) — replaces raw regex/substring matching
// on STATE.md content written by phase.complete in this bug-#3517 block.
const { stateExtractField } = require('../gsd-core/bin/lib/state-document.cjs');
const { extractFrontmatter } = require('../gsd-core/bin/lib/frontmatter.cjs');
const { parseMarkdownTable } = require('../gsd-core/bin/lib/markdown-table.cjs');
const { parsePhaseFromProse } = require('../gsd-core/bin/lib/phase-id.cjs');
function runSdkQuery(args, cwd) {
if (Array.isArray(args) && args[0] === 'phase.complete') {
writePassedVerificationForPhase(cwd, args[1]);
@@ -5986,24 +5989,24 @@ describe('bug-3287 — init plan-phase exposes expected_phase_dir with project_c
assert.ok(r1.success, `first call failed: ${r1.error}`);
const stateAfter1 = fs.readFileSync(statePath, 'utf8');
const match1 = stateAfter1.match(/completed_phases:\s*(\d+)/);
assert.ok(match1, 'completed_phases not found in frontmatter after first call');
const progress1 = extractFrontmatter(stateAfter1).progress;
assert.ok(progress1, 'progress not found in frontmatter after first call');
assert.equal(
Number(match1[1]),
Number(progress1.completed_phases),
2,
`After first call: completed_phases should be 2 (derived from ROADMAP: phases 4 and 5 complete), got ${match1[1]}`,
`After first call: completed_phases should be 2 (derived from ROADMAP: phases 4 and 5 complete), got ${progress1.completed_phases}`,
);
const r2 = runSdkQuery(['phase.complete', '5'], tmpDir);
assert.ok(r2.success, `second call failed: ${r2.error}`);
const stateAfter2 = fs.readFileSync(statePath, 'utf8');
const match2 = stateAfter2.match(/completed_phases:\s*(\d+)/);
assert.ok(match2, 'completed_phases not found in frontmatter after second call');
const progress2 = extractFrontmatter(stateAfter2).progress;
assert.ok(progress2, 'progress not found in frontmatter after second call');
assert.equal(
Number(match2[1]),
Number(progress2.completed_phases),
2,
`After second call (same phase): completed_phases must remain 2 (idempotent), got ${match2[1]}`,
`After second call (same phase): completed_phases must remain 2 (idempotent), got ${progress2.completed_phases}`,
);
});
@@ -6015,16 +6018,16 @@ describe('bug-3287 — init plan-phase exposes expected_phase_dir with project_c
assert.ok(r.success, `call failed: ${r.error}`);
const state = fs.readFileSync(statePath, 'utf8');
const stoppedMatch = state.match(/stopped_at:\s*(.+)/);
assert.ok(stoppedMatch, 'stopped_at not found in frontmatter');
const stoppedAt = extractFrontmatter(state).stopped_at;
assert.ok(stoppedAt, 'stopped_at not found in frontmatter');
assert.ok(
!stoppedMatch[1].includes('05-03-PLAN.md'),
`stopped_at should not still say "Completed 05-03-PLAN.md" — got: ${stoppedMatch[1]}`,
!stoppedAt.includes('05-03-PLAN.md'),
`stopped_at should not still say "Completed 05-03-PLAN.md" — got: ${stoppedAt}`,
);
assert.ok(
stoppedMatch[1].toLowerCase().includes('phase 5') ||
stoppedMatch[1].toLowerCase().includes('complete'),
`stopped_at should reference phase 5 completion, got: ${stoppedMatch[1]}`,
stoppedAt.toLowerCase().includes('phase 5') ||
stoppedAt.toLowerCase().includes('complete'),
`stopped_at should reference phase 5 completion, got: ${stoppedAt}`,
);
});
@@ -6036,10 +6039,8 @@ describe('bug-3287 — init plan-phase exposes expected_phase_dir with project_c
assert.ok(r.success, `call failed: ${r.error}`);
const state = fs.readFileSync(statePath, 'utf8');
const lastUpdatedMatch = state.match(/last_updated:\s*(.+)/);
assert.ok(lastUpdatedMatch, 'last_updated not found in frontmatter');
const raw = lastUpdatedMatch[1].trim().replace(/^"(.*)"$/, '$1');
const raw = extractFrontmatter(state).last_updated;
assert.ok(raw, 'last_updated not found in frontmatter');
// Must have been refreshed — not the stale seed value from setupPhase3517Project
assert.notEqual(
@@ -6073,10 +6074,10 @@ describe('bug-3287 — init plan-phase exposes expected_phase_dir with project_c
assert.ok(r.success, `call failed: ${r.error}`);
const state = fs.readFileSync(statePath, 'utf8');
const match = state.match(/total_plans:\s*(\d+)/);
assert.ok(match, 'total_plans not found in frontmatter');
const totalPlans = Number(match[1]);
assert.ok(Number.isFinite(totalPlans) && totalPlans > 0, `total_plans must be a positive number, got: ${match[1]}`);
const progress = extractFrontmatter(state).progress;
assert.ok(progress && progress.total_plans !== undefined, 'total_plans not found in frontmatter');
const totalPlans = Number(progress.total_plans);
assert.ok(Number.isFinite(totalPlans) && totalPlans > 0, `total_plans must be a positive number, got: ${progress.total_plans}`);
});
test('frontmatter completed_plans is updated from SUMMARY file count after phase.complete', () => {
@@ -6087,9 +6088,9 @@ describe('bug-3287 — init plan-phase exposes expected_phase_dir with project_c
assert.ok(r.success, `call failed: ${r.error}`);
const state = fs.readFileSync(statePath, 'utf8');
const match = state.match(/completed_plans:\s*(\d+)/);
assert.ok(match, 'completed_plans not found in frontmatter');
const completedPlans = Number(match[1]);
const progress = extractFrontmatter(state).progress;
assert.ok(progress && progress.completed_plans !== undefined, 'completed_plans not found in frontmatter');
const completedPlans = Number(progress.completed_plans);
assert.equal(
completedPlans,
10,
@@ -6105,9 +6106,9 @@ describe('bug-3287 — init plan-phase exposes expected_phase_dir with project_c
assert.ok(r.success, `call failed: ${r.error}`);
const state = fs.readFileSync(statePath, 'utf8');
const match = state.match(/percent:\s*(\d+)/);
assert.ok(match, 'percent not found in frontmatter');
assert.equal(Number(match[1]), 67, `percent should be 67 (2/3 phases), got: ${match[1]}`);
const progress = extractFrontmatter(state).progress;
assert.ok(progress && progress.percent !== undefined, 'percent not found in frontmatter');
assert.equal(Number(progress.percent), 67, `percent should be 67 (2/3 phases), got: ${progress.percent}`);
});
test('state frontmatter and numeric phase line reflect next phase after phase.complete', () => {
@@ -6118,8 +6119,19 @@ describe('bug-3287 — init plan-phase exposes expected_phase_dir with project_c
assert.ok(r.success, `call failed: ${r.error}`);
const state = fs.readFileSync(statePath, 'utf8');
assert.match(state, /completed_phases:\s*2/, 'completed_phases must be updated in frontmatter');
assert.match(state, /Phase:\s*0?6\b/, 'numeric Phase line should advance to phase 6');
const progress = extractFrontmatter(state).progress;
assert.equal(
Number(progress && progress.completed_phases),
2,
`completed_phases must be updated in frontmatter, got: ${progress && progress.completed_phases}`,
);
const phaseLine = stateExtractField(state, 'Phase');
const { phase: nextPhase } = parsePhaseFromProse(phaseLine);
assert.equal(
Number(nextPhase),
6,
`numeric Phase line should advance to phase 6, got Phase line: ${phaseLine}`,
);
});
test('prose-block STATE keeps next phase name without field-miss warnings (#1316)', () => {
@@ -6143,16 +6155,27 @@ describe('bug-3287 — init plan-phase exposes expected_phase_dir with project_c
);
const state = fs.readFileSync(path.join(planningDir, 'STATE.md'), 'utf8');
assert.match(state, /current_phase:\s*"?33"?/, 'current_phase frontmatter must advance to 33');
assert.match(
state,
/^Phase:\s*33\s+—\s+Follow Up Implementation\b/m,
`Current Position Phase line must keep the next phase name; state:\n${state}`,
const currentPhase = extractFrontmatter(state).current_phase;
assert.equal(
String(currentPhase),
'33',
`current_phase frontmatter must advance to 33, got: ${currentPhase}`,
);
const phaseLine = stateExtractField(state, 'Phase');
const { phase: nextPhase, name: nextPhaseName } = parsePhaseFromProse(phaseLine);
assert.equal(Number(nextPhase), 33, `Current Position Phase line must advance to 33; got Phase line: ${phaseLine}`);
assert.equal(
nextPhaseName,
'Follow Up Implementation',
`Current Position Phase line must keep the next phase name; got Phase line: ${phaseLine}`,
);
const lastActivity = stateExtractField(state, 'Last activity');
assert.match(
state,
/^Last activity:\s*\d{4}-\d{2}-\d{2}\s+—\s+Phase 32 complete/m,
`Last activity line must use the template em-dash delimiter with narrative; state:\n${state}`,
lastActivity || '',
/^\d{4}-\d{2}-\d{2}\s+—\s+Phase 32 complete/,
`Last activity line must use the template em-dash delimiter with narrative; got: ${lastActivity}`,
);
});
@@ -6164,10 +6187,14 @@ describe('bug-3287 — init plan-phase exposes expected_phase_dir with project_c
assert.ok(r.success, `call failed: ${r.error}`);
const state = fs.readFileSync(statePath, 'utf8');
assert.match(
state,
/\|\s*5\s*\|\s*7\s*\|/,
`By Phase table should have a row for phase 5 with 7 summaries.\nState:\n${state}`,
const table = parseMarkdownTable(state);
assert.ok(table.ok, `By Phase table must parse; reason: ${table.ok ? '' : table.reason}`);
const row = table.value.rows.find((r) => r.Phase.trim() === '5');
assert.ok(row, `By Phase table should have a row for phase 5.\nState:\n${state}`);
assert.equal(
row.Plans.trim(),
'7',
`By Phase table row for phase 5 should show 7 summaries, got row: ${JSON.stringify(row)}`,
);
});
@@ -6181,10 +6208,24 @@ describe('bug-3287 — init plan-phase exposes expected_phase_dir with project_c
const state = fs.readFileSync(statePath, 'utf8');
assert.match(state, /completed_phases:\s*2/, 'completed_phases must be 2 (4 and 5 complete)');
assert.match(state, /percent:\s*67/, 'percent must be 67%');
const hasPhase6 = /Phase:\s*0?6/.test(state) || /current_phase:\s*0?6/.test(state);
assert.ok(hasPhase6, `STATE.md must reference Phase 6 as current after completing Phase 5.\nState:\n${state}`);
const fm = extractFrontmatter(state);
assert.equal(
Number(fm.progress && fm.progress.completed_phases),
2,
`completed_phases must be 2 (4 and 5 complete), got: ${fm.progress && fm.progress.completed_phases}`,
);
assert.equal(
Number(fm.progress && fm.progress.percent),
67,
`percent must be 67%, got: ${fm.progress && fm.progress.percent}`,
);
const phaseLine = stateExtractField(state, 'Phase');
const { phase: bodyPhase } = parsePhaseFromProse(phaseLine);
const hasPhase6 = Number(bodyPhase) === 6 || Number(fm.current_phase) === 6;
assert.ok(
hasPhase6,
`STATE.md must reference Phase 6 as current after completing Phase 5. body Phase line: ${phaseLine}, frontmatter current_phase: ${fm.current_phase}`,
);
});
});
}

View File

@@ -148,7 +148,7 @@ describe('reference files contain key content from original mode sections', () =
__foldDescribe("folded:bug-3320-planner-deep-work-rules (consolidation epic #1969 B4 #1973)", () => {
'use strict';
// allow-test-rule: source-text-is-product [#3320]
// allow-test-rule: source-text-is-the-product [#3320]
// The bug is a contradiction in prompt/workflow source text. These assertions
// intentionally pin the contract words that planner agents consume.

View File

@@ -1,4 +1,4 @@
// allow-test-rule: runtime-contract-is-the-product — asserts GSD workflow/template markdown prose, the executable contract (#138, #2117)
// allow-test-rule: source-text-is-the-product — asserts GSD workflow/template markdown prose, the executable contract (#138, #2117)
'use strict';
// Policy regression test for issue #138:

View File

@@ -1,4 +1,4 @@
// allow-test-rule: runtime-contract-is-the-product (see #644) — the rendered reference doc's worked-example
// allow-test-rule: source-text-is-the-product (see #644) — the rendered reference doc's worked-example
// vocab surface is the runtime contract; this pins its bijection to the source-of-truth fixtures (docs-parity).
//
// RED-first parity contract: the portable reference doc (gsd-core/references/prohibition-probe.md) keeps

View File

@@ -1,4 +1,4 @@
// allow-test-rule: runtime-contract-is-the-product (see #644) — plan-phase.md's planner prompt is the deployed
// allow-test-rule: source-text-is-the-product (see #644) — plan-phase.md's planner prompt is the deployed
// runtime contract under assertion (the workflow PROSE is the product).
//
// RED-first PROSE-PRESENCE contract for the plan-phase lift of confirmed prohibitions. plan-phase.md

View File

@@ -1,4 +1,4 @@
// allow-test-rule: runtime-contract-is-the-product (see #644) — the must_haves.prohibitions: block is the
// allow-test-rule: source-text-is-the-product (see #644) — the must_haves.prohibitions: block is the
// runtime plan-contract surface; this pins its parse/round-trip/projection bijection to the code.
//
// RED-first schema contract for the `must_haves.prohibitions:` SIBLING block (ADR-550 Decision 3 —

View File

@@ -1,4 +1,4 @@
// allow-test-rule: runtime-contract-is-the-product (see #644) — spec-phase.md Step 5.6 is the deployed workflow
// allow-test-rule: source-text-is-the-product (see #644) — spec-phase.md Step 5.6 is the deployed workflow
// runtime contract under assertion (the workflow PROSE is the product; ADR-550 D5 forbids a JS engine here).
//
// RED-first PROSE-PRESENCE contract for the prohibition probe's Step 5.6 (ADR-550 D1 DIVERGENCE,

View File

@@ -1,4 +1,4 @@
// allow-test-rule: runtime-contract-is-the-product (see #644) — the prohibition validators and the verify-time
// allow-test-rule: source-text-is-the-product (see #644) — the prohibition validators and the verify-time
// disposition are the deployed safety contract; this pins them against the CANONICAL fixture corpus
// and the ADR-550 D4 "judgment is never silently green" invariant so the code can never drift from
// its own documented intent again.

View File

@@ -1,4 +1,4 @@
// allow-test-rule: runtime-contract-is-the-product (see #644) — the verify-time disposition of a test-tier
// allow-test-rule: source-text-is-the-product (see #644) — the verify-time disposition of a test-tier
// prohibition is the deployed safety contract; this pins its fail-closed default to the code.
//
// RED-first SAFETY HALF of ADR-550 Decision 5(d) [maintainer decision 2026-06-12 "B-with-guard"].

View File

@@ -89,7 +89,8 @@ describe('bug #1529: getProjectInstructionFile ↔ gsd-tools query parity', () =
});
describe('bug #1529: new-project.md workflow uses the shared policy query', () => {
// allow-test-rule: structural drift guard for #1529 — the workflow's bash block MUST invoke the
// allow-test-rule: structural-regression-guard (#1529)
// the workflow's bash block MUST invoke the
// shared `gsd_run query project-instruction-file` query rather than a hardcoded
// codex-only `if/else` branch; there is no typed IR for "this bash block calls a
// specific gsd-tools query instead of a hardcoded mapping".

View File

@@ -1,12 +1,14 @@
'use strict';
// allow-test-rule: prompt-content-is-the-product
// The prompt-budget CLI writes an assembled, trimmed prompt string to disk.
// Testing that the prompt omits a dropped section (research) requires a
// content assertion on the output file — the file content IS the product.
// Structured metadata (omitted[], hardFailed, etc.) is always the primary
// assertion; text content checks are secondary and only used to verify the
// trim policy was applied correctly to the assembled output.
// allow-test-rule: pending-migration-to-typed-ir [#3090]
// The prompt-budget CLI writes an assembled, trimmed prompt string to disk —
// a computed/assembled output, not shipped source text, so this is not
// source-text-is-the-product (same mistake as the STATE.md-as-deployed-
// artifact miscategorization). Structured metadata (omitted[], hardFailed,
// etc.) is the primary assertion; the secondary raw substring check that the
// dropped research content is actually absent from the assembled prompt has
// no typed IR to convert to — prompt text is unstructured by nature.
// Tracked under #3090.
/**
* prompt-budget-cli.test.cjs

View File

@@ -1,5 +1,8 @@
'use strict';
// allow-test-rule: structural guard-placement verification in bin/install.js requires source-text analysis; install.js is a non-exportable CLI script and the guard must be in a specific lexical scope which require()+behavior cannot verify #1188
// allow-test-rule: structural-regression-guard (#1188)
// Guard-placement verification in bin/install.js requires source-text
// analysis; install.js is a non-exportable CLI script and the guard must be
// in a specific lexical scope which require()+behavior cannot verify.
/**
* Governance tests for the gsd-core repository root layout.

View File

@@ -1,4 +1,5 @@
// allow-test-rule: <runtime-contract-is-the-product> research agent .md content is the governed surface
// allow-test-rule: source-text-is-the-product
// research agent .md content is the governed surface
// The 7 researcher agent .md files are the deployed AI agent definitions — their
// frontmatter and @-includes ARE what the runtime loads. Asserting on their content
// is asserting on the deployed contract, not the test author's source code.

View File

@@ -1,8 +1,3 @@
// allow-test-rule: behavioral-query-coverage — see #2505 — this test exercises the
// resolve-dispatch-type query end-to-end (subprocess) AND the pure
// resolveDispatchType function (require), covering the runtime-aware dispatch
// contract from epic #2505 Phase 4 (#2508). The query is the workflow-facing
// surface; the function is the pure projection.
process.env.GSD_TEST_MODE = '1';
const { test, describe } = require('node:test');

View File

@@ -1529,7 +1529,9 @@ describe('bug #730 — milestone (Phase Details) section scope resolution', () =
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3128-roadmap-plan-count-slug-layout (consolidation epic #1969 B3 #1972)", () => {
'use strict';
// allow-test-rule: reads roadmap.cjs source to verify isPlanFile pattern was adopted — structural contract prevents silent regression to old filter (see #3128)
// allow-test-rule: structural-regression-guard (see #3128)
// Reads roadmap.cjs source to verify isPlanFile pattern was adopted —
// structural contract prevents silent regression to the old filter.
// Regression guard for bug #3128.
//

View File

@@ -1,4 +1,4 @@
// allow-test-rule: runtime-contract-is-the-product agent .md instruction surface see #1205
// allow-test-rule: source-text-is-the-product agent .md instruction surface see #1205
// agents/gsd-roadmapper.md is the deployed agent — the Granularity Calibration table
// AND the phase_id_convention instructions ARE the deployed behavior. Asserting on
// their prose asserts what runs in production (#163, #1205).

View File

@@ -1,7 +1,10 @@
// allow-test-rule: run-tests.cjs is a CLI test harness whose only IR is its
// stable stderr line `run-tests: suite="X" files=N: name1 name2 ...` plus its
// exit code. No typed IR is exposable from a shell script; the printed line
// IS the contract this test pins. See docs/TESTING-SUITES.md and issue #3597.
// allow-test-rule: pending-migration-to-typed-ir [#3090]
// run-tests.cjs is a CLI test harness with no --json/structured output mode;
// these tests regex/substring-match its human-readable stderr (usage errors,
// the `run-tests: suite="X" files=N: name1 name2 ...` selection line) instead
// of a frozen typed IR. Adding a structured output mode is a production
// change out of scope here. See docs/TESTING-SUITES.md and issue #3597.
// Tracked under #3090.
//
// Tests for scripts/run-tests.cjs --suite filtering (issue #3597).
//

View File

@@ -973,7 +973,11 @@ test('claude runtime does NOT rewrite the runtime default — stamping is non-cl
// ---------------------------------------------------------------------------
test('regression: every edited workflow gets codex-stamped (source↔engine parity, all surfaces) (#1515)', () => {
// allow-test-rule: emitted workflow runtime-resolution shell block is the runtime contract surface (#1515) — asserts on engine-transformed output of the real source
// allow-test-rule: pending-migration-to-typed-ir [#3090]
// `out` is _applyRuntimeRewrites's engine-transformed shell text, not shipped
// source — substring-matching it is the "Rendered file" row CONTRIBUTING
// requires a typed-IR builder for. No such IR exists yet for the shell
// rewrite output; production change out of scope here. Tracked under #3090.
const WORKFLOWS = ['execute-phase.md', 'autonomous.md', 'manager.md', 'diagnose-issues.md', 'quick.md'];
const CLAUDE_RUNTIME = 'config-get runtime --default claude --raw 2>/dev/null || echo "claude"';
const CODEX_RUNTIME = 'config-get runtime --default codex --raw 2>/dev/null || echo "codex"';
@@ -1065,7 +1069,11 @@ const FALSE_WT_LINE = 'config-get workflow.use_worktrees --default false --raw 2
// ---------------------------------------------------------------------------
test('parity: every non-Claude runtime stamps its own runtime default and use_worktrees=false on all workflows (#1521)', () => {
// allow-test-rule: emitted workflow runtime-resolution shell block is the runtime contract surface (#1521)
// allow-test-rule: pending-migration-to-typed-ir [#3090]
// `out` is _applyRuntimeRewrites's engine-transformed shell text, not shipped
// source — substring-matching it is the "Rendered file" row CONTRIBUTING
// requires a typed-IR builder for. No such IR exists yet for the shell
// rewrite output; production change out of scope here. Tracked under #3090.
for (const rt of NON_CLAUDE) {
for (const wf of WORKFLOWS) {
const src = fs.readFileSync(
@@ -1181,7 +1189,10 @@ test('property: _stampNonClaudeRuntimeDefaults is idempotent (#1521)', () => {
// ---------------------------------------------------------------------------
test('execute-phase.md, quick.md, and diagnose-issues.md guards are generalized to != "claude" (not Codex-specific) (#1521)', () => {
// allow-test-rule: emitted workflow runtime-resolution shell block is the runtime contract surface (#1521)
// allow-test-rule: source-text-is-the-product (#1521)
// Reads the raw shipped workflow .md source directly (not engine-transformed
// output) and asserts on its literal guard-clause text — the deployed prose
// IS the runtime contract here.
// #2584 Phase 3 (#2627): execute-phase.md graduated PAST the `!= "claude"`
// guard — worktree isolation there is now keyed on the negotiated
// `dispatch.isolation` capability, so no runtime name appears in its guard at
@@ -1226,7 +1237,9 @@ test('execute-phase.md, quick.md, and diagnose-issues.md guards are generalized
// ---------------------------------------------------------------------------
test('manager.md and autonomous.md gate run_in_background on FLATTEN=false, not a runtime name (#1521, graduated by #1708)', () => {
// allow-test-rule: orchestration dispatch gating in manager/autonomous .md is the runtime contract surface (#1521/#1708)
// allow-test-rule: source-text-is-the-product (#1521/#1708)
// Reads the raw shipped manager.md/autonomous.md workflow prose directly —
// the deployed orchestration dispatch gating text IS the runtime contract.
const manager = fs.readFileSync(
path.join(__dirname, '..', 'gsd-core', 'workflows', 'manager.md'),
'utf8',
@@ -1263,7 +1276,9 @@ test('manager.md and autonomous.md gate run_in_background on FLATTEN=false, not
});
test('manager.md and autonomous.md no longer contain old "not claude" background-dispatch gating (#1521)', () => {
// allow-test-rule: orchestration dispatch gating in manager/autonomous .md is the runtime contract surface (#1521)
// allow-test-rule: source-text-is-the-product (#1521)
// Reads the raw shipped manager.md/autonomous.md workflow prose directly —
// the deployed orchestration dispatch gating text IS the runtime contract.
const manager = fs.readFileSync(
path.join(__dirname, '..', 'gsd-core', 'workflows', 'manager.md'),
'utf8',

View File

@@ -881,7 +881,9 @@ describe('descriptor-driven parity: 13 non-probe registry runtimes × no-env-var
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3126-global-skills-base-runtime-path (consolidation epic #1969 B3 #1972)", () => {
'use strict';
// allow-test-rule: last three tests read init.cjs source to verify delegation contract to runtime-homes.cjs — structural guard, no behavioral IR exposed (see #3126)
// allow-test-rule: structural-implementation-guard (see #3126)
// Last three tests read init.cjs source to verify delegation contract to
// runtime-homes.cjs — no behavioral IR exposed yet for this wiring point.
// Regression guard for bug #3126.
//

View File

@@ -21,7 +21,8 @@
* can satisfy gsd_run for Codex shim-only installs.
*/
// allow-test-rule: structural parity/drift guard — asserts literal presence/absence of the canonical gsd_run launcher and the retired $GSD_SDK / `/gsd-tools` tokens across workflow markdown; there is no typed IR for "this source file does not contain substring X".
// allow-test-rule: structural-regression-guard
// structural parity/drift guard — asserts literal presence/absence of the canonical gsd_run launcher and the retired $GSD_SDK / `/gsd-tools` tokens across workflow markdown; there is no typed IR for "this source file does not contain substring X".
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
@@ -790,7 +791,8 @@ describe('runtime-launcher-parity — agents (#1041)', () => {
* When all three miss, exit non-zero.
*/
// allow-test-rule: structural/behavioral regression for the ~/.claude fallback arm in (see #211)
// allow-test-rule: structural-regression-guard (see #211)
// structural/behavioral regression for the ~/.claude fallback arm in
// the gsd_run launcher snippet -- asserts literal substring presence and exercises the
// bash resolution path via execFileSync; there is no typed IR for "snippet contains arm X".
@@ -998,7 +1000,8 @@ describe('bug-211: launcher ~/.claude home fallback', () => {
* (sync-runtime-launcher.cjs was re-run after editing the snippet).
*/
// allow-test-rule: structural/behavioral regression for non-Claude runtime-home (see #891)
// allow-test-rule: structural-regression-guard (see #891)
// structural/behavioral regression for non-Claude runtime-home
// fallback arms in the gsd_run launcher snippet -- asserts literal substring
// presence for each runtime-home probe and exercises the bash resolution paths
// via execFileSync; there is no typed IR for "snippet contains arm X".
@@ -1606,7 +1609,8 @@ describe('bug-3668: workflow SDK resolver supports installed user projects', { s
* (C) Precedence: repo-local .claude/ wins over $HOME/.claude/ when both exist.
*/
// allow-test-rule: structural/behavioral regression for the repo-local .claude/ install (see #444)
// allow-test-rule: structural-regression-guard (see #444)
// structural/behavioral regression for the repo-local .claude/ install
// arm in the gsd_run launcher snippet -- asserts literal substring presence and exercises
// the bash resolution path via execFileSync; there is no typed IR for "snippet contains arm X".

View File

@@ -54,8 +54,8 @@ describe('runtime-name-policy windsurf alias parity — manifest vs FALLBACK_ALI
test('manifest and FALLBACK_ALIASES windsurf alias sets are identical', () => {
// Read FALLBACK_ALIASES from source to detect manual drift before a build.
const srcPath = path.join(ROOT, 'src', 'runtime-name-policy.cts');
// allow-test-rule: runtime-contract-is-the-product — FALLBACK_ALIASES source text IS the
// product contract for runtimes that can't load the manifest at runtime; verifying
// allow-test-rule: source-text-is-the-product
// FALLBACK_ALIASES source text IS the product contract for runtimes that can't load the manifest at runtime; verifying
// both surfaces contain the same windsurf aliases catches manual-mirror drift.
const src = fs.readFileSync(srcPath, 'utf8');
const match = src.match(/windsurf:\s*\[([^\]]+)\]/);

View File

@@ -634,7 +634,7 @@ describe('SECURE: threat-model-anchored behaviour', () => {
});
// ─── 8. Regression: security config variables resolved before use (#1625) ────
// allow-test-rule: runtime-contract-is-the-product — secure-phase.md prose is the executed contract (#1625)
// allow-test-rule: source-text-is-the-product — secure-phase.md prose is the executed contract (#1625)
describe('SECURE: security config variables resolved before use (#1625)', () => {
const wfPath = path.join(WORKFLOWS_DIR, 'secure-phase.md');
@@ -718,7 +718,9 @@ describe('SECURE: security config variables resolved before use (#1625)', () =>
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3120-secure-phase-empty-register (consolidation epic #1969 B4 #1973)", () => {
'use strict';
// allow-test-rule: reads product workflow markdown (secure-phase.md) to verify structural guard contract — not a source-grep test (see #3120)
// allow-test-rule: source-text-is-the-product (see #3120)
// Reads product workflow markdown (secure-phase.md) to verify structural
// guard contract.
// Regression guard for bug #3120.
//

View File

@@ -6918,10 +6918,6 @@ describe('bug #3454: state mutation must preserve literal $N amounts', () => {
__foldDescribe("folded:bug-3489-complete-phase-idempotent (consolidation epic #1969 B2 #1971)", () => {
'use strict';
// allow-test-rule: source-text-is-the-product (see #3489)
// State.md is the deployed artifact; asserting on its literal text content
// tests the deployed contract.
/**
* Regression test for #3489
*
@@ -6941,6 +6937,7 @@ const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { createTempProject, cleanup, runGsdTools } = require('./helpers.cjs');
const { stateExtractField } = require('../gsd-core/bin/lib/state-document.cjs');
describe('bug #3489: state complete-phase must be idempotent', () => {
let tmpDir;
@@ -7030,8 +7027,9 @@ describe('bug #3489: state complete-phase must be idempotent', () => {
assert.ok(result.success, `command failed: ${result.error || result.output}`);
const after = fs.readFileSync(statePath, 'utf8');
assert.ok(
after.includes('**Status:** Phase 03 complete'),
assert.equal(
stateExtractField(after, 'Status'),
'Phase 03 complete',
`expected Status updated to "Phase 03 complete", got:\n${after}`,
);
@@ -7051,7 +7049,6 @@ describe('bug #3489: state complete-phase must be idempotent', () => {
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-397-state-preserve-executor-authored (consolidation epic #1969 B2 #1971)", () => {
'use strict';
// allow-test-rule: reads runtime STATE.md written to temp dir — behavioral output test, not source-grep (see #397)
// Regression tests for bug #397.
//
@@ -7086,6 +7083,9 @@ const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { cleanup } = require('./helpers.cjs');
const { stateExtractField } = require('../gsd-core/bin/lib/state-document.cjs');
const { collectSection } = require('../gsd-core/bin/lib/markdown-sectionizer.cjs');
const { stripFrontmatter } = require('../gsd-core/bin/lib/frontmatter.cjs');
const ROOT = path.join(__dirname, '..');
const TOOLS_PATH = path.join(ROOT, 'gsd-core', 'bin', 'gsd-tools.cjs');
@@ -7279,12 +7279,12 @@ describe('bug #397: executor-authored STATE.md fields must be preserved', () =>
const r = runGsdState(['record-session', '--stopped-at', 'Plan 1 complete'], dir);
assert.ok(r.success, `record-session failed: ${r.error}`);
const after = readState(dir);
const rfMatch = after.match(/Resume File:\s*(.+)/i);
assert.ok(rfMatch, 'Resume File field not found in STATE.md after record-session');
const resumeFile = stateExtractField(after, 'Resume File');
assert.ok(resumeFile, 'Resume File field not found in STATE.md after record-session');
assert.strictEqual(
rfMatch[1].trim(),
resumeFile,
'/home/user/my-custom-context.md',
`record-session overwrote executor-authored Resume File with '${rfMatch[1].trim()}'`,
`record-session overwrote executor-authored Resume File with '${resumeFile}'`,
);
} finally {
cleanup(dir);
@@ -7298,12 +7298,12 @@ describe('bug #397: executor-authored STATE.md fields must be preserved', () =>
const r = runGsdState(['record-session', '--stopped-at', 'Plan complete'], dir);
assert.ok(r.success, `record-session failed: ${r.error}`);
const after = readState(dir);
const rfMatch = after.match(/Resume File:\s*(.+)/i);
assert.ok(rfMatch, 'Resume File field not found in STATE.md after record-session');
const resumeFile = stateExtractField(after, 'Resume File');
assert.ok(resumeFile, 'Resume File field not found in STATE.md after record-session');
assert.strictEqual(
rfMatch[1].trim(),
resumeFile,
'None',
`Expected 'None' to remain when it was already 'None', got: ${rfMatch[1].trim()}`,
`Expected 'None' to remain when it was already 'None', got: ${resumeFile}`,
);
} finally {
cleanup(dir);
@@ -7317,12 +7317,12 @@ describe('bug #397: executor-authored STATE.md fields must be preserved', () =>
const r = runGsdState(['record-session', '--resume-file', '/tmp/new-resume.md'], dir);
assert.ok(r.success, `record-session failed: ${r.error}`);
const after = readState(dir);
const rfMatch = after.match(/Resume File:\s*(.+)/i);
assert.ok(rfMatch, 'Resume File field not found in STATE.md after record-session');
const resumeFile = stateExtractField(after, 'Resume File');
assert.ok(resumeFile, 'Resume File field not found in STATE.md after record-session');
assert.strictEqual(
rfMatch[1].trim(),
resumeFile,
'/tmp/new-resume.md',
`Expected explicit --resume-file value to be written, got: ${rfMatch[1].trim()}`,
`Expected explicit --resume-file value to be written, got: ${resumeFile}`,
);
} finally {
cleanup(dir);
@@ -7337,13 +7337,19 @@ describe('bug #397: executor-authored STATE.md fields must be preserved', () =>
const r = runGsdState(['advance-plan'], dir);
assert.ok(r.success, `advance-plan failed: ${r.error}`);
const after = readState(dir);
// The Configuration-level Status must not be clobbered
const statusMatch = after.match(/^Status:\s*(.+)/m);
assert.ok(statusMatch, 'Status field not found after advance-plan');
// The Configuration-level Status must not be clobbered. Scope extraction
// to the body (frontmatter stripped): the frontmatter `status:` key and
// the body `Status:` field name collide case-insensitively under
// stateExtractField's plain-line pattern (#1255's landmine — see
// state-transition.cts), and frontmatter's `status` is a normalized
// enum (normalizeStateStatus), not the executor-authored prose this
// assertion is proving was preserved.
const status = stateExtractField(stripFrontmatter(after), 'Status');
assert.ok(status, 'Status field not found after advance-plan');
assert.strictEqual(
statusMatch[1].trim(),
status,
'Awaiting QA sign-off before proceeding',
`advance-plan overwrote executor-authored Status: got '${statusMatch[1].trim()}'`,
`advance-plan overwrote executor-authored Status: got '${status}'`,
);
} finally {
cleanup(dir);
@@ -7358,17 +7364,24 @@ describe('bug #397: executor-authored STATE.md fields must be preserved', () =>
const r = runGsdState(['advance-plan'], dir);
assert.ok(r.success, `advance-plan failed: ${r.error}`);
const after = readState(dir);
const statusMatch = after.match(/^Status:\s*(.+)/m);
assert.ok(statusMatch, 'Status field not found after advance-plan');
// Scope extraction to the body (frontmatter stripped): stateExtractField's
// plain-line pattern matches the frontmatter `status:` key before the body
// `Status:` field (case-insensitive collision — #1255). Frontmatter status
// is normalizeStateStatus's coarser enum (e.g. both "Verifying Phase N"
// and "Phase complete — ready for verification" normalize to the SAME
// 'verifying' value), so asserting it here would be a WEAKER check than
// the body prose this test exists to prove was replaced.
const status = stateExtractField(stripFrontmatter(after), 'Status');
assert.ok(status, 'Status field not found after advance-plan');
// 'Ready to execute' is a known default and should be replaced
assert.notStrictEqual(
statusMatch[1].trim(),
status,
'Ready to execute',
`Status should have been updated from 'Ready to execute' after phase-complete, but was not`,
);
assert.ok(
statusMatch[1].includes('Phase complete') || statusMatch[1].includes('ready for verification'),
`Expected phase-complete Status text, got: '${statusMatch[1].trim()}'`,
status.includes('Phase complete') || status.includes('ready for verification'),
`Expected phase-complete Status text, got: '${status}'`,
);
} finally {
cleanup(dir);
@@ -7384,12 +7397,12 @@ describe('bug #397: executor-authored STATE.md fields must be preserved', () =>
assert.ok(r.success, `advance-plan failed: ${r.error}`);
const after = readState(dir);
// The top-level Last Activity (in Configuration section) must be preserved
const laMatch = after.match(/^Last Activity:\s*(.+)/im);
assert.ok(laMatch, 'Last Activity field not found after advance-plan');
const lastActivity = stateExtractField(after, 'Last Activity');
assert.ok(lastActivity, 'Last Activity field not found after advance-plan');
assert.strictEqual(
laMatch[1].trim(),
lastActivity,
'Unblocked after infra fix — merged PR #88 manually',
`advance-plan overwrote executor-authored Last Activity: got '${laMatch[1].trim()}'`,
`advance-plan overwrote executor-authored Last Activity: got '${lastActivity}'`,
);
} finally {
cleanup(dir);
@@ -7404,21 +7417,21 @@ describe('bug #397: executor-authored STATE.md fields must be preserved', () =>
const r = runGsdState(['advance-plan'], dir);
assert.ok(r.success, `advance-plan failed: ${r.error}`);
const after = readState(dir);
const posMatch = after.match(/##\s*Current Position\s*\n([\s\S]*?)(?=\n##|$)/i);
assert.ok(posMatch, 'Current Position section not found after advance-plan');
const posBody = posMatch[1];
const posStatusMatch = posBody.match(/^Status:\s*(.+)/m);
assert.ok(posStatusMatch, 'Status field not found in Current Position section');
const section = collectSection(after, (h) => h.text.trim() === 'Current Position');
assert.ok(section, 'Current Position section not found after advance-plan');
const posBody = section.body;
const posStatus = stateExtractField(posBody, 'Status');
assert.ok(posStatus, 'Status field not found in Current Position section');
assert.strictEqual(
posStatusMatch[1].trim(),
posStatus,
'On hold — waiting for upstream dependency merge',
`advance-plan overwrote executor-authored Current Position Status: got '${posStatusMatch[1].trim()}'`,
`advance-plan overwrote executor-authored Current Position Status: got '${posStatus}'`,
);
const posActivityMatch = posBody.match(/^Last activity:\s*(.+)/im);
assert.ok(posActivityMatch, 'Last activity field not found in Current Position section');
const posActivity = stateExtractField(posBody, 'Last activity');
assert.ok(posActivity, 'Last activity field not found in Current Position section');
assert.ok(
posActivityMatch[1].includes('blocked by infra'),
`advance-plan overwrote executor-authored Current Position Last activity: got '${posActivityMatch[1].trim()}'`,
posActivity.includes('blocked by infra'),
`advance-plan overwrote executor-authored Current Position Last activity: got '${posActivity}'`,
);
} finally {
cleanup(dir);
@@ -9978,7 +9991,6 @@ describe('buildStateFrontmatter cache invalidation (#1967)', () => {
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3127-state-begin-phase-idempotent (consolidation epic #1969 B3 #1972)", () => {
'use strict';
// allow-test-rule: reads runtime STATE.md written to temp dir — behavioral output test, not source-grep (see #3127)
// Regression tests for bug #3127.
//
@@ -10003,6 +10015,7 @@ const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { cleanup } = require('./helpers.cjs');
const { stateExtractField } = require('../gsd-core/bin/lib/state-document.cjs');
const ROOT = path.join(__dirname, '..');
@@ -10079,9 +10092,9 @@ describe('bug #3127: state.begin-phase idempotency guard', () => {
cmdStateBeginPhase(dir, '5', 'test-phase', 8, false);
const after = fs.readFileSync(path.join(dir, '.planning', 'STATE.md'), 'utf8');
// Current Plan must not have been reset to 1
const planMatch = after.match(/^Current Plan:\s*(\S+)/m);
if (planMatch) {
assert.notStrictEqual(planMatch[1], '1',
const currentPlan = stateExtractField(after, 'Current Plan');
if (currentPlan !== null) {
assert.notStrictEqual(currentPlan, '1',
'begin-phase reset Current Plan to 1 on a mid-flight phase — idempotency guard not applied');
}
} finally {
@@ -10098,9 +10111,10 @@ describe('bug #3127: state.begin-phase idempotency guard', () => {
cmdStateBeginPhase(dir, '5', 'test-phase', 8, false);
const after = fs.readFileSync(path.join(dir, '.planning', 'STATE.md'), 'utf8');
// The rich stopped_at narrative must be preserved
const stoppedAt = stateExtractField(after, 'stopped_at');
assert.ok(
after.includes('Plan 02 SHIPPED') || after.includes('Wave 2 GREEN'),
'begin-phase overwrote stopped_at narrative on a mid-flight phase',
stoppedAt && (stoppedAt.includes('Plan 02 SHIPPED') || stoppedAt.includes('Wave 2 GREEN')),
`begin-phase overwrote stopped_at narrative on a mid-flight phase; got: ${stoppedAt}`,
);
} finally {
cleanup(dir);
@@ -10116,9 +10130,9 @@ describe('bug #3127: state.begin-phase idempotency guard', () => {
cmdStateBeginPhase(dir, '5', 'test-phase', 8, false);
const after = fs.readFileSync(path.join(dir, '.planning', 'STATE.md'), 'utf8');
// Normal path: Current Plan should become 1 (or stay 1)
const planMatch = after.match(/^Current Plan:\s*(\S+)/m);
if (planMatch) {
assert.strictEqual(planMatch[1], '1',
const currentPlan = stateExtractField(after, 'Current Plan');
if (currentPlan !== null) {
assert.strictEqual(currentPlan, '1',
'begin-phase should set Current Plan to 1 on a fresh phase');
}
} finally {
@@ -10142,9 +10156,10 @@ describe('bug #3127: state.begin-phase idempotency guard', () => {
try {
cmdStateBeginPhase(dir, '5', 'test-phase', 8, false);
const after = fs.readFileSync(path.join(dir, '.planning', 'STATE.md'), 'utf8');
const lastActivity = stateExtractField(after, 'Last activity');
assert.ok(
after.includes(PINNED_DATE),
`begin-phase must update Last Activity date to the pinned date ${PINNED_DATE} even on resume (safe field)`,
lastActivity && lastActivity.includes(PINNED_DATE),
`begin-phase must update Last Activity date to the pinned date ${PINNED_DATE} even on resume (safe field); got: ${lastActivity}`,
);
} finally {
// Restore env vars before cleanup to avoid leaking state to other tests.

View File

@@ -1,4 +1,4 @@
// allow-test-rule: runtime-contract-is-the-product (see #1867) — the rendered reference doc's taxonomy table IS the runtime contract; this pins its bijection to the code (docs-parity, ADR-456 exception matrix)
// allow-test-rule: source-text-is-the-product (see #1867) — the rendered reference doc's taxonomy table IS the runtime contract; this pins its bijection to the code (docs-parity, ADR-456 exception matrix)
// Asserts gsd-core/references/ui-consideration-probe.md keeps its taxonomy id column in
// sync with the source-of-truth UI_TAXONOMY (built .cjs), and that the closed compiled
// taxonomy stays DISJOINT from the open-prose domain-probes.md bank (the mixed-axis boundary,

View File

@@ -1742,14 +1742,14 @@ describe('bug-967 verify key-links strict file-path contract', () => {
// This test reads the canonical docs file and asserts the example is consistent
// with the strict-path contract.
//
// allow-test-rule: <runtime-contract-is-the-product> the plan-md.md reference (see #967)
// allow-test-rule: source-text-is-the-product the plan-md.md reference (see #967)
// example IS the documented authoring surface for key_links; asserting it uses
// a file path (not an endpoint) directly tests the documented contract.
test('docs/reference/plan-md.md key_links example uses a relative file path for to:, not an HTTP endpoint', () => {
// Locate plan-md.md relative to this test file's repo root
const docPath = path.join(__dirname, '..', 'docs', 'reference', 'plan-md.md');
assert.ok(fs.existsSync(docPath), `plan-md.md not found at ${docPath}`);
const content = fs.readFileSync(docPath, 'utf-8'); // allow-test-rule: <runtime-contract-is-the-product> the plan-md.md reference example IS the documented authoring surface for key_links; asserting it uses a file path (not an endpoint) directly tests the documented contract. (see #967)
const content = fs.readFileSync(docPath, 'utf-8'); // allow-test-rule: source-text-is-the-product the plan-md.md reference example IS the documented authoring surface for key_links; asserting it uses a file path (not an endpoint) directly tests the documented contract. (see #967)
// Find the key_links block in the annotated example (the first YAML frontmatter fence)
// The bad old value was: to: "/api/feed"

View File

@@ -56,8 +56,10 @@ function listWorkflowFiles() {
.map((e) => path.join(WORKFLOWS_DIR, e));
// Filter to files that have at least one Windows hosted-runner reference.
// allow-test-rule: file-scope prefilter, not a test assertion — we need to
// detect whether a workflow file targets Windows runners at all. The pwsh
// allow-test-rule: source-text-is-the-product
// Reads the shipped .github/workflows/*.yml — the deployed text GitHub
// Actions executes — to detect whether a workflow targets Windows runners
// at all. The pwsh
// stderr-swallow class is windows-only, so files that never mention
// windows-hosted labels are out of scope. Exposing a typed IR from production
// code is not appropriate here because the source-of-truth is the YAML

View File

@@ -20,7 +20,8 @@ const assert = require('node:assert/strict');
const path = require('node:path');
const childProcess = require('node:child_process');
const fc = require('fast-check');
const { createTempGitProject, createTempDir, cleanup } = require('./helpers.cjs');
const { createTempDir, cleanup } = require('./helpers.cjs');
const { createFixture } = require('./fixtures/index.cjs');
const { makeFaultyGit } = require('./helpers/faulty-deps.cjs');
const WORKTREE_SAFETY_PATH = path.join(
@@ -127,8 +128,8 @@ describe('resolveWorktreeContext', () => {
assert.strictEqual(context.mode, 'current_directory');
});
// Counter-test: timeout returns object with effectiveRoot (Contract 6)
test('returns valid result on timeout, not throw', () => {
// Counter-test: timeout returns the canonical degraded shape, not just an object (Contract 6)
test('returns effectiveRoot=cwd, mode=current_directory, reason=git_timed_out on timeout, not throw', () => {
let threw = false;
let result;
try {
@@ -137,11 +138,11 @@ describe('resolveWorktreeContext', () => {
threw = true;
}
assert.strictEqual(threw, false, 'must not throw on timeout');
assert.strictEqual(typeof result, 'object');
assert.ok(
typeof result.effectiveRoot === 'string',
'must return effectiveRoot string even on timeout'
);
assert.deepStrictEqual(result, {
effectiveRoot: '/tmp',
mode: 'current_directory',
reason: 'git_timed_out',
});
});
// ─── #3050 DEFECT 2: timeout must be distinguishable from not_git_repo ─────
@@ -439,7 +440,7 @@ describe('planWorktreePrune', () => {
});
// Counter-test: timeout path (Contract 6)
test('returns action=skip when execGit times out', () => {
test('returns action=skip, reason=git_timed_out when execGit times out', () => {
let threw = false;
let result;
try {
@@ -450,9 +451,10 @@ describe('planWorktreePrune', () => {
assert.strictEqual(threw, false, 'must not throw on timeout');
assert.strictEqual(typeof result, 'object');
assert.strictEqual(result.action, 'skip');
assert.ok(
typeof result.reason === 'string' && result.reason.length > 0,
'must return a non-empty reason when git times out'
assert.strictEqual(
result.reason,
'git_timed_out',
'must surface the specific git_timed_out reason, not a generic non-empty string'
);
});
@@ -524,11 +526,15 @@ describe('executeWorktreePrunePlan', () => {
});
// Counter-test: timeout path (Contract 6)
test('returns ok:false when plan is skip (timeout path)', () => {
test('returns {ok:false, action:skip, reason:git_timed_out, pruned:[]} when plan is skip (timeout path)', () => {
const plan = planWorktreePrune('/tmp', {}, { execGit: makeTimeoutStub() });
const result = executeWorktreePrunePlan(plan, { execGit: makeTimeoutStub() });
assert.strictEqual(typeof result, 'object');
assert.strictEqual(result.ok, false, 'must return ok:false on timeout');
assert.deepStrictEqual(result, {
ok: false,
action: 'skip',
reason: 'git_timed_out',
pruned: [],
});
});
// AC4 strict: timedOut must be surfaced as a first-class field
@@ -578,7 +584,7 @@ describe('listLinkedWorktreePaths', () => {
});
// Counter-test: failure path (Contract 6)
test('returns ok:false on timeout, not throw', () => {
test('returns ok:false, reason:git_timed_out on timeout, not throw', () => {
let threw = false;
let result;
try {
@@ -588,9 +594,10 @@ describe('listLinkedWorktreePaths', () => {
}
assert.strictEqual(threw, false, 'must not throw on timeout');
assert.strictEqual(result.ok, false);
assert.ok(
typeof result.reason === 'string' && result.reason.length > 0,
'must return non-empty reason on timeout'
assert.strictEqual(
result.reason,
'git_timed_out',
'must surface the specific git_timed_out reason, not a generic non-empty string'
);
});
@@ -641,8 +648,11 @@ describe('inspectWorktreeHealth', () => {
]);
});
// Counter-test: timeout path (Contract 6)
test('returns ok:false when git times out', () => {
// Counter-test: timeout path (Contract 6). This function's own reason on
// timeout is not pinned by any sibling test in this file (unlike
// planWorktreePrune/listLinkedWorktreePaths/snapshotWorktreeInventory,
// which each have a dedicated "reason is git_timed_out" test) — pin it here.
test('returns {ok:false, reason:git_timed_out, findings:[]} when git times out', () => {
let threw = false;
let result;
try {
@@ -651,13 +661,16 @@ describe('inspectWorktreeHealth', () => {
threw = true;
}
assert.strictEqual(threw, false, 'must not throw on timeout');
assert.strictEqual(typeof result, 'object');
assert.strictEqual(result.ok, false);
assert.deepStrictEqual(result, {
ok: false,
reason: 'git_timed_out',
findings: [],
});
});
test('findings is empty array (not undefined) on timeout', () => {
test('findings is an empty array, not undefined, on timeout', () => {
const result = inspectWorktreeHealth('/tmp', {}, { execGit: makeTimeoutStub() });
assert.strictEqual(Array.isArray(result.findings), true, 'findings must be an array even when ok:false');
assert.deepStrictEqual(result.findings, [], 'findings must be [] (not undefined) even when ok:false');
});
// Counter-test (B5, #3057): a statSync throw must surface as its own
@@ -734,7 +747,7 @@ describe('snapshotWorktreeInventory', () => {
});
// Counter-test: timeout path (Contract 6)
test('returns ok:false with reason on timeout, not throw', () => {
test('returns ok:false, reason:git_timed_out on timeout, not throw', () => {
let threw = false;
let result;
try {
@@ -745,9 +758,10 @@ describe('snapshotWorktreeInventory', () => {
assert.strictEqual(threw, false, 'must not throw on timeout');
assert.strictEqual(typeof result, 'object');
assert.strictEqual(result.ok, false);
assert.ok(
typeof result.reason === 'string' && result.reason.length > 0,
'must return non-empty reason on timeout'
assert.strictEqual(
result.reason,
'git_timed_out',
'must surface the specific git_timed_out reason, not a generic non-empty string'
);
});
@@ -3536,15 +3550,20 @@ describe('worktree-safety: resolveWorktreeRoot and pruneOrphanedWorktrees reloca
describe('worktree-safety: resolveWorktreeRoot behaviour', () => {
const worktreeSafety = require(WORKTREE_SAFETY_PATH);
test('resolveWorktreeRoot(createTempGitProject()) returns {root, reason} with a non-empty root', (t) => {
const dir = createTempGitProject('gsd-wt-root-');
// NOTE: createTempGitProject() always seeds .planning/ (createFixture's
// planning:true default), which makes resolveWorktreeContext short-circuit
// on the has_local_planning branch (src/worktree-safety.cts:203-216) BEFORE
// ever calling git — so a fixture built with it can never reach the
// git-based main_worktree path this test's name is about. Use a git fixture
// WITHOUT .planning/ (and without the projectDoc PROJECT.md, which — since
// projectDoc defaults to `git` in createFixture — would otherwise silently
// recreate the .planning/ directory it's writing into) so resolveWorktreeRoot
// actually reaches resolveWorktreeLinkage's real git rev-parse comparison.
test('resolveWorktreeRoot(git repo with no local .planning) reaches the git-based main_worktree path, returns {root: dir, reason: main_worktree}', (t) => {
const dir = createFixture({ prefix: 'gsd-wt-root-', planning: false, git: true, projectDoc: false });
t.after(() => cleanup(dir));
const result = worktreeSafety.resolveWorktreeRoot(dir);
assert.ok(result && typeof result === 'object', 'must return an object, not a bare string');
assert.ok(typeof result.root === 'string' && result.root.length > 0,
`Expected non-empty root string, got: ${JSON.stringify(result)}`);
assert.ok(typeof result.reason === 'string' && result.reason.length > 0,
`Expected non-empty reason string, got: ${JSON.stringify(result)}`);
assert.deepStrictEqual(result, { root: dir, reason: 'main_worktree' });
});
test('resolveWorktreeRoot propagates git_timed_out via the injected execGit seam (#3050)', () => {
@@ -4280,7 +4299,12 @@ describe('bug-3707: reapOrphanWorktrees — adversarial edge cases', () => {
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3129-validate-commit-git-bypass (consolidation epic #1969 B5 #1974)", () => {
'use strict';
// allow-test-rule: reads hook shell script to verify delegation pattern — structural contract test, not source-grep (see #3129)
// allow-test-rule: structural-regression-guard (see #3129)
// Reads the gsd-validate-commit.sh hook source to verify it delegates to
// git-cmd.js isGitSubcommand() rather than the old regex — a specific code
// pattern that must (and must not) exist; behavioral tests of tokenize()/
// isGitSubcommand() cannot observe which detection strategy the hook itself
// calls.
// Regression tests for bug #3129.
//