fix(#4264): wire both TDD dispatch backends to phase.tdd-applicable (#4284)

This commit is contained in:
Tom Boucher
2026-09-04 16:08:42 -04:00
committed by GitHub
parent 580059251a
commit 2f4f7538e9
27 changed files with 297 additions and 2 deletions

View File

@@ -91,6 +91,17 @@ test('#3637: embed PERFORMANCE is gated — an un-embedded template halts before
);
});
test('#4266: embed PERFORMANCE is gated — an un-substituted TDD_APPLICABLE marker halts before spawn', () => {
// Matches the ${AGENT_SKILLS} gate immediately above: a third performance gate for
// the #4266/#4272 TDD-applicability predicate, halting before worktree creation
// when the marker survives composition.
assert.match(
content,
/grep -q '\\\$\{TDD_APPLICABLE'[\s\S]{0,300}exit 1/,
'an un-substituted TDD_APPLICABLE marker means the TDD-applicability decision was not resolved into the prompt — halt',
);
});
test('#3637: the gsd-executor ROLE DEFINITION is a mandatory embed with provenance', () => {
// The agent-skills query alone is conditional (its block is a skills list
// whenever the project configures agent_skills; only unconfigured

View File

@@ -284,6 +284,7 @@
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",

View File

@@ -356,6 +356,7 @@
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",

View File

@@ -356,6 +356,7 @@
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",

View File

@@ -284,6 +284,7 @@
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",

View File

@@ -286,6 +286,7 @@
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",

View File

@@ -356,6 +356,7 @@
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",

View File

@@ -320,6 +320,7 @@
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",

View File

@@ -285,6 +285,7 @@
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",

View File

@@ -284,6 +284,7 @@
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",

View File

@@ -284,6 +284,7 @@
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",

View File

@@ -356,6 +356,7 @@
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",

View File

@@ -285,6 +285,7 @@
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",

View File

@@ -321,6 +321,7 @@
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",

View File

@@ -356,6 +356,7 @@
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",

View File

@@ -251,6 +251,7 @@
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",

View File

@@ -284,6 +284,7 @@
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",

View File

@@ -284,6 +284,7 @@
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",

View File

@@ -284,6 +284,7 @@
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",

View File

@@ -356,6 +356,7 @@
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",

View File

@@ -0,0 +1,211 @@
'use strict';
/**
* #4264/#4265/#4266 (epic #4272 Phase 2) — TDD_APPLICABLE is USED (as a
* `${TDD_APPLICABLE ? ... : ...}` ternary, per #3990) in both executor
* dispatch backends but was never ASSIGNED anywhere: `phase.tdd-applicable`
* (Phase 1, #4273) computed the predicate, but no workflow ever called it.
* That is a silent "absence resolves to false" defect (ADR-3473 §8.4) — an
* un-substituted `${TDD_APPLICABLE ...}` marker is JS-truthy as literal text,
* so a real TDD plan would have silently dropped its RED/GREEN/REFACTOR
* procedure while never producing an error.
*
* These are shape assertions on the raw workflow-markdown TEXT: the deployed
* text IS the runtime-loaded product (an LLM orchestrator reads it top to
* bottom and composes/executes it) — same rationale as
* tdd-single-statement.test.cjs's header comment. `readFileSync` here targets
* `.md` files only, never a `.cjs`/`.js`/`.ts` source path, so
* `local/no-source-grep` does not apply and no `allow-test-rule` marker is
* needed (confirmed against tdd-single-statement.test.cjs, which reads the
* same two files with no such marker).
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.join(__dirname, '..');
const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8');
const HARNESS_PATH = 'gsd-core/workflows/execute-phase.md';
const WORKTREE_PATH = 'gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md';
const TDD_STEP_FRAGMENT_PATH = 'gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md';
// #4272-phase-6 byte-ceiling extraction (two commits ago): the harness
// backend's TDD_APPLICABLE assignment + phase.tdd-applicable call moved OUT
// of execute-phase.md and into the tdd-applicability-resolution.md step
// fragment; execute-phase.md itself now carries only a one-line REFERENCE to
// that fragment. The worktree backend was not affected — it still carries
// everything inline in executor-isolation-dispatch.md. `referenceFile` is
// where the reader/orchestrator encounters the backend (and, for the
// harness, where the one-line pointer + the ${TDD_APPLICABLE...} USE live);
// `assignmentFile` is where TDD_APPLICABLE is actually ASSIGNED.
const BACKENDS = {
'execute-phase.md': { referenceFile: HARNESS_PATH, assignmentFile: TDD_STEP_FRAGMENT_PATH },
'executor-isolation-dispatch.md': { referenceFile: WORKTREE_PATH, assignmentFile: WORKTREE_PATH },
};
describe('#4266 — TDD_APPLICABLE is actually computed in both backends', () => {
// A backend may assign TDD_APPLICABLE directly from the `gsd_run query
// phase.tdd-applicable` call (harness: `TDD_APPLICABLE=$(gsd_run query
// phase.tdd-applicable ...)`) or via an intermediate raw/rc pair mirroring
// the existing ISOLATION resolution pattern (worktree: `_TDD_APPLICABLE_RAW
// =$(gsd_run query phase.tdd-applicable ...)` ... `TDD_APPLICABLE=
// "$_TDD_APPLICABLE_RAW"`) — either shape is fine as long as the assignment
// exists AND is fed by a real phase.tdd-applicable call within a small
// preceding window, not a bare/silent default.
function assertTddApplicableIsComputed(name, filePath) {
const text = read(filePath);
const lines = text.split('\n');
const assignIdx = lines.findIndex((l) => /^\s*TDD_APPLICABLE=/.test(l));
assert.ok(assignIdx !== -1, `${name} must assign TDD_APPLICABLE, not just reference it`);
const windowStart = Math.max(0, assignIdx - 8);
const window = lines.slice(windowStart, assignIdx + 1).join('\n');
assert.ok(
window.includes('phase.tdd-applicable'),
`${name}'s TDD_APPLICABLE assignment must be fed by a phase.tdd-applicable call within a few lines above it, got window: ${window}`,
);
}
test('harness backend (execute-phase.md) assigns TDD_APPLICABLE via phase.tdd-applicable', () => {
// Assignment now lives in the extracted tdd-applicability-resolution.md
// step fragment, not in execute-phase.md itself (#4272-phase-6).
assertTddApplicableIsComputed('execute-phase.md', BACKENDS['execute-phase.md'].assignmentFile);
});
test('worktree backend (executor-isolation-dispatch.md) assigns TDD_APPLICABLE via phase.tdd-applicable', () => {
assertTddApplicableIsComputed('executor-isolation-dispatch.md', BACKENDS['executor-isolation-dispatch.md'].assignmentFile);
});
for (const [name, cfg] of Object.entries(BACKENDS)) {
test(`${name}'s phase.tdd-applicable call is plan-scoped, not phase-scoped`, () => {
const text = read(cfg.assignmentFile);
const callLine = text.split('\n').find((l) => l.includes('phase.tdd-applicable') && l.includes('gsd_run query'));
assert.ok(callLine, `${name} must carry a gsd_run query phase.tdd-applicable call`);
assert.ok(
callLine.includes('{phase_dir}') && callLine.includes('{plan_file}'),
`${name}'s phase.tdd-applicable call must pass the per-plan {phase_dir}/{plan_file} path, got: ${callLine.trim()}`,
);
});
}
test('both backends assign TDD_APPLICABLE before every ${TDD_APPLICABLE...} use (document order)', () => {
// Worktree backend: assignment and every ${TDD_APPLICABLE...} use live
// in the same file — same-file ordering check.
{
const text = read(BACKENDS['executor-isolation-dispatch.md'].assignmentFile);
const lines = text.split('\n');
const assignIdx = lines.findIndex((l) => /^\s*TDD_APPLICABLE=/.test(l));
assert.ok(assignIdx !== -1, 'executor-isolation-dispatch.md has no TDD_APPLICABLE assignment');
const useIndices = [];
lines.forEach((l, i) => {
if (/\$\{TDD_APPLICABLE\b/.test(l)) useIndices.push(i);
});
assert.ok(useIndices.length > 0, 'executor-isolation-dispatch.md has no ${TDD_APPLICABLE...} use to check ordering against');
for (const useIdx of useIndices) {
assert.ok(
assignIdx < useIdx,
`executor-isolation-dispatch.md: TDD_APPLICABLE is used at line ${useIdx + 1} before it is assigned at line ${assignIdx + 1}`,
);
}
}
// Harness backend: the assignment was extracted into
// tdd-applicability-resolution.md (#4272-phase-6); execute-phase.md no
// longer contains the assignment at all, only a one-line REFERENCE to
// where it's resolved followed (later) by the ${TDD_APPLICABLE...} USE.
// Confirm the fragment does assign it, and that in execute-phase.md the
// reference precedes every use.
{
const fragmentText = read(BACKENDS['execute-phase.md'].assignmentFile);
assert.ok(
/^\s*TDD_APPLICABLE=/m.test(fragmentText),
'tdd-applicability-resolution.md has no TDD_APPLICABLE assignment',
);
const text = read(BACKENDS['execute-phase.md'].referenceFile);
const lines = text.split('\n');
const refIdx = lines.findIndex(
(l) => l.includes('tdd-applicability-resolution.md') && l.includes('TDD-applicability resolution'),
);
assert.ok(refIdx !== -1, 'execute-phase.md must carry a one-line reference to tdd-applicability-resolution.md');
const useIndices = [];
lines.forEach((l, i) => {
if (/\$\{TDD_APPLICABLE\b/.test(l)) useIndices.push(i);
});
assert.ok(useIndices.length > 0, 'execute-phase.md has no ${TDD_APPLICABLE...} use to check ordering against');
for (const useIdx of useIndices) {
assert.ok(
refIdx < useIdx,
`execute-phase.md: TDD_APPLICABLE is used at line ${useIdx + 1} before the reference to its resolution at line ${refIdx + 1}`,
);
}
}
});
test('both tdd.md embed-line comments describe the same three real sources (#4265)', () => {
for (const [name, cfg] of Object.entries(BACKENDS)) {
const text = read(cfg.referenceFile);
const line = text.split('\n').find((l) => /tdd\.md/.test(l) && /TDD_APPLICABLE \?/.test(l));
assert.ok(line, `${name} still lists tdd.md as a conditional embed entry`);
assert.ok(line.includes('type: tdd'), `${name}'s tdd.md comment must mention plan type: tdd, got: ${line.trim()}`);
assert.ok(line.includes('tdd="true"'), `${name}'s tdd.md comment must mention the tdd="true" task attribute (#4265), got: ${line.trim()}`);
assert.ok(
line.includes('workflow.tdd_mode') || line.includes('config'),
`${name}'s tdd.md comment must mention the workflow.tdd_mode config default, got: ${line.trim()}`,
);
}
});
test('worktree backend fail-closes on an un-substituted ${TDD_APPLICABLE marker, matching the ${AGENT_SKILLS} check shape', () => {
const text = read(WORKTREE_PATH);
const lines = text.split('\n');
const skillsIdx = lines.findIndex((l) => l.includes("grep -q '\\${AGENT_SKILLS}'"));
assert.ok(skillsIdx !== -1, 'worktree backend must still carry the existing ${AGENT_SKILLS} fail-closed check');
const tddCheckIdx = lines.findIndex((l) => l.includes("grep -q '\\${TDD_APPLICABLE"));
assert.ok(tddCheckIdx !== -1, 'worktree backend must carry a fail-closed grep check for an unresolved ${TDD_APPLICABLE marker');
// Same shape: FATAL message + exit 1 within the following couple of lines.
const block = lines.slice(tddCheckIdx, tddCheckIdx + 4).join('\n');
assert.ok(/FATAL:/.test(block), 'the ${TDD_APPLICABLE check must emit a FATAL message like the ${AGENT_SKILLS} check');
assert.ok(/exit 1/.test(block), 'the ${TDD_APPLICABLE check must exit 1 like the ${AGENT_SKILLS} check');
});
test('harness backend references the tdd-applicability-resolution step fragment before Agent() is called, and that fragment states a MANDATORY pre-dispatch halt covering TDD_APPLICABLE, CONTEXT_WINDOW, and AGENT_SKILLS', () => {
// #4272-phase-6 byte-ceiling extraction: the "MANDATORY pre-dispatch check"
// paragraph moved out of execute-phase.md into
// tdd-applicability-resolution.md (mirroring per-plan-executor-routing.md);
// the host file now carries only a one-line reference to it.
const text = read(HARNESS_PATH);
const lines = text.split('\n');
// The literal executor dispatch call sits alone on its own line
// (` Agent(`), inside the per-plan spawn section (step 3) — distinct
// from the many inline `` `Agent(...)` `` prose mentions elsewhere in
// this file (e.g. the runtime-compatibility table at line ~22).
const agentCallIdx = lines.findIndex((l) => /^\s*Agent\($/.test(l));
assert.ok(agentCallIdx !== -1, 'execute-phase.md must contain the literal Agent( dispatch-call line');
const before = lines.slice(0, agentCallIdx).join('\n');
assert.ok(
before.includes('execute-phase/steps/tdd-applicability-resolution.md'),
'execute-phase.md must reference the tdd-applicability-resolution.md step fragment before the Agent() call',
);
const fragment = read(TDD_STEP_FRAGMENT_PATH);
const checkIdx = fragment.indexOf('MANDATORY');
assert.ok(checkIdx !== -1, 'tdd-applicability-resolution.md must state a MANDATORY pre-dispatch check');
const checkText = fragment.slice(checkIdx);
for (const marker of ['TDD_APPLICABLE', 'CONTEXT_WINDOW', 'AGENT_SKILLS']) {
assert.ok(checkText.includes(marker), `pre-dispatch check must name ${marker}`);
}
assert.ok(/HALT/i.test(checkText), 'pre-dispatch check must instruct an explicit halt, not a soft warning');
});
test('regression: tdd-single-statement.test.cjs assertions still hold by inspection', () => {
for (const [name, cfg] of Object.entries(BACKENDS)) {
const text = read(cfg.referenceFile);
const line = text.split('\n').find((l) => /tdd\.md/.test(l) && /TDD_APPLICABLE/.test(l));
assert.ok(line, `${name} still lists tdd.md as a conditional embed entry`);
assert.ok(/TDD_APPLICABLE \?/.test(line), `${name}'s tdd.md entry must stay conditional on TDD_APPLICABLE (#3990)`);
}
});
});