Files
msd-core/tests/chunked-planning-parallel.test.cjs
Tom Boucher f9f72cb54c enhance(#3777): opt-in concurrent per-plan planners in chunked mode (#4346)
* test(#3777): add failing-first coverage for concurrent per-plan planner dispatch

Extracts and executes the real bash blocks this PR is about to add to
plan-phase.md and chunked-planning-mode.md (CHUNKED_PARALLEL resolution and
the BATCH_PLAN_IDS dedup guard), plus config-set/config-get coverage for the
new planning.chunked_parallel key. Expected RED against the current shipped
workflow text — the extraction anchors do not exist yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(#3777): dispatch chunked mode's per-plan planners concurrently within a Wave

Adds opt-in planning.chunked_parallel (default false, byte-identical to the
existing serial loop). When true and the runtime's negotiated dispatch
capacity (dispatch-capacity, #3673) is greater than 1, chunked planning's
per-plan Tasks that share one outline Wave are issued together instead of
one at a time; a later Wave still waits for the current one to be verified
on disk and committed. A host with no declared maxConcurrency (most
non-Claude runtimes today) stays serial regardless of the setting.

Resolution and the Plan-ID dedup guard live in chunked-planning-mode.md
itself (gated on the section's own CHUNKED_MODE skip-check) rather than in
plan-phase.md, so a non-chunked run pays no extra gsd_run calls.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(#3777): repoint extraction at chunked-planning-mode.md after the move

CHUNKED_PARALLEL resolution moved out of plan-phase.md into
chunked-planning-mode.md itself (see the preceding commit); update the
test's extraction path and header comment to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(#3777): relocate the canonical runtime-launcher preamble before its first use

The CHUNKED_PARALLEL resolution block's two gsd_run calls landed earlier in
the file than the sole existing preamble (in the commit step), which
tests/runtime-launcher-parity.test.cjs's (B) check requires to precede every
gsd_run call in the file. Move the preamble (not duplicate it) to the top of
the resolution block; the commit step's fenced block now just calls
gsd_run directly.

Caught by the GREEN checkpoint gsd-test run before push.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(#3777): strip the canonical preamble from the extracted resolution block

The CHUNKED_PARALLEL resolution fence now carries the relocated
runtime-launcher preamble as its first line (previous commit). Extracting
the whole fence and running it after the test's own gsd_run stub let the
embedded preamble's own resolver logic `unset -f gsd_run` and exit 1 before
reaching the resolution logic, since no real gsd-tools.cjs exists in the
temp script dir — every test calling runChunkedParallelResolution() failed.

Strip the preamble (sourced from gsd-core/workflows/_runtime-launcher.snippet.sh,
the same file scripts/sync-runtime-launcher.cjs treats as canonical) before
splicing in the stub, so this suite tests only the resolution logic it is
actually about.

Caught by the post-rebase gsd-test run before push.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(#3777): add the How-To page the phase gate requires

Enablement is 2 commands (config-set, then --chunked), which this repo's
own doc-quadrant gate flags as how-to-owed: a reference table cannot carry
a sequence. Covers enablement, the dispatch-capacity gate's honest
"most runtimes today: no effect" case, and the two accepted trade-offs.

An earlier reasoning pass (recorded in .gsd/phase/.../70-docs.json before
this commit) had incorrectly claimed #3034 shipped with no equivalent
how-to page, as precedent for skipping one here. That claim was false —
docs/how-to/enable-parallel-reviewer-lanes.md exists and is indexed. The
phase gate caught the omission before merge; corrected here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(#3777): backfill changeset PR number

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 18:57:17 -04:00

343 lines
14 KiB
JavaScript

'use strict';
/**
* Failing-first tests for #3777 (opt-in concurrent per-plan planners in
* chunked mode).
*
* Design: .gsd/phase/feat-3777-chunked-parallel-planners/40-design.md
* Test matrix: .gsd/phase/feat-3777-chunked-parallel-planners/50-test-matrix.md
*
* Two real, shipped bash blocks are extracted and EXECUTED (never re-typed), both from
* chunked-planning-mode.md:
* 1. the `CHUNKED_PARALLEL` resolution (config x dispatch-capacity), read once per
* chunked run, ahead of §8.5.1, so a non-chunked run never pays for it.
* 2. the `BATCH_PLAN_IDS` dedup guard (§8.5.2 step 1).
* Wave grouping itself is orchestrator (LLM) comprehension, not a bash block —
* see the design doc's "Known limits" — so it has no extraction test here.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const {
createTempDir,
createTempProject,
cleanup,
readFileNormalized,
runGsdTools,
} = require('./helpers.cjs');
const { runHook } = require('./helpers/process-seam.cjs');
const { HOOK_FANOUT_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
const { scanFencedBlocks } = require('../gsd-core/bin/lib/markdown-sectionizer.cjs');
const REPO_ROOT = path.join(__dirname, '..');
const CHUNKED_MODE_MD_PATH = path.join(
REPO_ROOT, 'gsd-core', 'workflows', 'plan-phase', 'steps', 'chunked-planning-mode.md',
);
const RUNTIME_LAUNCHER_SNIPPET_PATH = path.join(REPO_ROOT, 'gsd-core', 'workflows', '_runtime-launcher.snippet.sh');
// ─── extraction (source-text-is-the-product) ──────────────────────────────
/** Finds the first ```bash/```sh fence in `content` whose text contains every string in `mustInclude`. */
function extractBashFenceContaining(content, mustInclude, label, filePath) {
const lines = content.split(/\r?\n/);
for (const fenced of scanFencedBlocks(lines)) {
if (fenced.closeLineIdx === -1) continue;
if (!['bash', 'sh'].includes((fenced.infoString || '').trim())) continue;
const block = lines.slice(fenced.openLineIdx + 1, fenced.closeLineIdx).join('\n');
if (mustInclude.every((s) => block.includes(s))) return block;
}
throw new Error(`extractBashFenceContaining: no fence matching ${label} found in ${filePath} (looked for ${JSON.stringify(mustInclude)})`);
}
/**
* The canonical runtime-launcher preamble (source of truth for
* scripts/sync-runtime-launcher.cjs) sits as the first line of the
* CHUNKED_PARALLEL resolution fence in production (tests/runtime-launcher-parity.test.cjs
* owns verifying its placement/uniqueness). Strip it here so this suite tests
* only the resolution logic it's actually about, not the preamble's own
* gsd_run-shim-resolution behavior (which would stomp this file's `gsd_run`
* stub — see #3777 investigation).
*/
function stripRuntimeLauncherPreamble(block) {
const preamble = readFileNormalized(RUNTIME_LAUNCHER_SNIPPET_PATH).replace(/\n+$/, '');
if (!block.includes(preamble)) {
throw new Error('stripRuntimeLauncherPreamble: canonical preamble not found in extracted block — extraction anchor or preamble content may have drifted');
}
return block.split(preamble).join('').replace(/^\s+/, '');
}
function extractChunkedParallelResolution() {
const content = readFileNormalized(CHUNKED_MODE_MD_PATH);
const block = extractBashFenceContaining(
content,
['CHUNKED_PARALLEL_CFG', 'DISPATCH_CAPACITY', 'CHUNKED_PARALLEL='],
'#3777 CHUNKED_PARALLEL resolution',
CHUNKED_MODE_MD_PATH,
);
return stripRuntimeLauncherPreamble(block);
}
function extractBatchPlanIdsDedup() {
const content = readFileNormalized(CHUNKED_MODE_MD_PATH);
return extractBashFenceContaining(
content,
['BATCH_PLAN_IDS=', 'WAVE_PLAN_IDS'],
'#3777 BATCH_PLAN_IDS dedup guard',
CHUNKED_MODE_MD_PATH,
);
}
// ─── runners ────────────────────────────────────────────────────────────────
/**
* Runs the real extracted CHUNKED_PARALLEL resolution block with a `gsd_run`
* stub spliced in front of it. Returns the resolved `CHUNKED_PARALLEL` value
* as a string ("true"/"false"), exactly as production code reads it.
*/
function runChunkedParallelResolution(t, opts) {
const scriptDir = createTempDir('gsd-3777-script-');
t.after(() => cleanup(scriptDir));
const block = extractChunkedParallelResolution();
const stub = [
'gsd_run() {',
' if [ "$1" = "query" ] && [ "$2" = "config-get" ] && [ "$3" = "planning.chunked_parallel" ]; then',
' if [ "$STUB_CONFIG_GET_FAILS" = "1" ]; then',
' return 1',
' fi',
' printf %s "$STUB_CONFIG_VALUE"',
' return 0',
' fi',
' if [ "$1" = "query" ] && [ "$2" = "dispatch-capacity" ]; then',
' if [ "$STUB_CAPACITY_FAILS" = "1" ]; then',
' return 1',
' fi',
' printf %s "$STUB_CAPACITY_VALUE"',
' return 0',
' fi',
' return 0',
'}',
].join('\n');
const script = [
'#!/usr/bin/env bash',
'set -u',
stub,
block,
'printf "RESULT:%s" "$CHUNKED_PARALLEL"',
].join('\n');
const scriptPath = path.join(scriptDir, 'resolve.sh');
fs.writeFileSync(scriptPath, script, { mode: 0o755 });
const env = {
...process.env,
STUB_CONFIG_VALUE: opts.configValue === null || opts.configValue === undefined ? '' : opts.configValue,
STUB_CONFIG_GET_FAILS: opts.configGetFails ? '1' : '0',
STUB_CAPACITY_VALUE: opts.capacityValue === null || opts.capacityValue === undefined ? '' : String(opts.capacityValue),
STUB_CAPACITY_FAILS: opts.capacityFails ? '1' : '0',
};
const result = runHook(scriptPath, [], {
interpreter: 'bash',
cwd: scriptDir,
env,
timeoutMs: HOOK_FANOUT_TIMEOUT_MS,
});
const stdout = result.stdout || '';
const match = /RESULT:(\S*)/.exec(stdout);
return {
outcome: result.outcome,
exitCode: result.exitCode,
stderr: result.stderr,
chunkedParallel: match ? match[1] : null,
};
}
/**
* Runs the real extracted BATCH_PLAN_IDS dedup block with `WAVE_PLAN_IDS`
* seeded from `waveIds` (already space-separated, matching how the
* orchestrator populates it from outline rows).
*/
function runBatchDedup(t, waveIds) {
const scriptDir = createTempDir('gsd-3777-dedup-script-');
t.after(() => cleanup(scriptDir));
const block = extractBatchPlanIdsDedup();
const script = [
'#!/usr/bin/env bash',
'set -u',
`WAVE_PLAN_IDS='${waveIds}'`,
block,
'printf "RESULT:%s" "$BATCH_PLAN_IDS"',
].join('\n');
const scriptPath = path.join(scriptDir, 'dedup.sh');
fs.writeFileSync(scriptPath, script, { mode: 0o755 });
const result = runHook(scriptPath, [], {
interpreter: 'bash',
cwd: scriptDir,
timeoutMs: HOOK_FANOUT_TIMEOUT_MS,
});
const stdout = result.stdout || '';
const match = /RESULT:(.*)$/.exec(stdout);
const batch = match ? match[1].trim() : '';
return {
outcome: result.outcome,
exitCode: result.exitCode,
stderr: result.stderr,
batchIds: batch.length > 0 ? batch.split(/\s+/) : [],
};
}
// ─── #1/#2 — default and explicit-disabled stay serial ────────────────────
describe('#3777 default and explicit-disabled CHUNKED_PARALLEL resolution stays serial', () => {
test('defaultsToSerialWhenKeyUnset', (t) => {
const result = runChunkedParallelResolution(t, { configValue: null, capacityValue: 20 });
assert.equal(result.outcome, 'exited');
assert.equal(result.chunkedParallel, 'false');
});
test('staysSerialWhenExplicitlyDisabled', (t) => {
const result = runChunkedParallelResolution(t, { configValue: 'false', capacityValue: 20 });
assert.equal(result.chunkedParallel, 'false');
});
});
// ─── #3 — opt-in with sufficient capacity ──────────────────────────────────
describe('#3777 opt-in with sufficient dispatch capacity', () => {
test('enablesParallelWhenConfigTrueAndCapacityAboveOne', (t) => {
const result = runChunkedParallelResolution(t, { configValue: 'true', capacityValue: 20 });
assert.equal(result.chunkedParallel, 'true');
});
});
// ─── #4/#5/#6 — capacity boundary (limit-1, limit, limit+1) ────────────────
describe('#3777 dispatch-capacity boundary gates the opt-in', () => {
test('staysSerialWhenCapacityIsOne', (t) => {
const result = runChunkedParallelResolution(t, { configValue: 'true', capacityValue: 1 });
assert.equal(result.chunkedParallel, 'false', 'capacity=1 (the fail-closed floor) must degrade to serial even when the config opts in');
});
test('enablesParallelAtCapacityTwo', (t) => {
const result = runChunkedParallelResolution(t, { configValue: 'true', capacityValue: 2 });
assert.equal(result.chunkedParallel, 'true', 'capacity=2 is already above the floor and must enable concurrent dispatch');
});
test('staysSerialWhenCapacityIsZero', (t) => {
// Defensive: routeDispatchCapacity never legitimately emits 0, but the
// resolution's own arithmetic comparison must not misbehave on it either.
const result = runChunkedParallelResolution(t, { configValue: 'true', capacityValue: 0 });
assert.equal(result.chunkedParallel, 'false');
});
});
// ─── #7 — non-canonical truthy values stay serial ──────────────────────────
describe('#3777 non-canonical truthy config values stay serial', () => {
test('nonCanonicalTruthyValuesStaySerial', async (t) => {
const nearMisses = ['TRUE', 'True', '1', 'yes', 'on', ' true', 'true '];
for (const value of nearMisses) {
await t.test(`chunked_parallel="${value}"`, (t2) => {
const result = runChunkedParallelResolution(t2, { configValue: value, capacityValue: 20 });
assert.equal(result.chunkedParallel, 'false', `value "${value}" must not opt into parallel dispatch`);
});
}
});
});
// ─── #8/#9 — broken tooling fails safe to serial ───────────────────────────
describe('#3777 broken config/capacity tooling fails safe to serial', () => {
test('configGetFailureFallsBackToSerial', (t) => {
const result = runChunkedParallelResolution(t, { configValue: 'true', capacityValue: 20, configGetFails: true });
assert.equal(result.chunkedParallel, 'false');
});
test('capacityQueryFailureFallsBackToSerial', (t) => {
const result = runChunkedParallelResolution(t, { configValue: 'true', capacityValue: 20, capacityFails: true });
assert.equal(result.chunkedParallel, 'false');
});
});
// ─── #10/#11/#12 — BATCH_PLAN_IDS dedup guard ──────────────────────────────
describe('#3777 duplicate Plan ID in a Wave dispatches once', () => {
test('duplicatePlanIdInBatchDispatchesOnce', (t) => {
const result = runBatchDedup(t, '03-01 03-02 03-01');
assert.equal(result.outcome, 'exited');
assert.deepEqual(result.batchIds, ['03-01', '03-02'], 'no two plans in a parallel batch may declare the same output path');
});
});
describe('#3777 batch dedup preserves outline row order', () => {
test('preservesOutlineOrderInBatch', (t) => {
const result = runBatchDedup(t, '03-03 03-01 03-02');
assert.deepEqual(result.batchIds, ['03-03', '03-01', '03-02']);
});
});
describe('#3777 an empty Wave produces an empty batch', () => {
test('emptyWaveProducesEmptyBatch', (t) => {
const result = runBatchDedup(t, '');
assert.equal(result.outcome, 'exited');
assert.deepEqual(result.batchIds, []);
});
});
// ─── #13/#14/#15 — config-set registers planning.chunked_parallel ─────────
describe('#3777 planning.chunked_parallel config key', () => {
test('configSetAcceptsAndPersistsChunkedParallel', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const setResult = runGsdTools('config-set planning.chunked_parallel true', tmpDir);
assert.ok(setResult.success, `config-set failed: ${setResult.error}`);
const configPath = path.join(tmpDir, '.planning', 'config.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
assert.equal(config.planning?.chunked_parallel, true);
assert.equal(typeof config.planning?.chunked_parallel, 'boolean');
const getResult = runGsdTools('config-get planning.chunked_parallel --raw', tmpDir);
assert.ok(getResult.success, `config-get failed: ${getResult.error}`);
assert.equal((getResult.output || '').trim(), 'true');
});
test('configSetPersistsBooleanFalse', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const setResult = runGsdTools('config-set planning.chunked_parallel false', tmpDir);
assert.ok(setResult.success, `config-set failed: ${setResult.error}`);
const configPath = path.join(tmpDir, '.planning', 'config.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
assert.equal(config.planning?.chunked_parallel, false);
assert.equal(typeof config.planning?.chunked_parallel, 'boolean');
});
test('rejectsUnregisteredNeighbouringKey', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
// Extra "l" — proves the whitelist is load-bearing and the two tests
// above are not vacuous (they'd pass even for an unregistered key if
// config-set accepted anything).
const result = runGsdTools('config-set planning.chunked_parallell true', tmpDir);
assert.equal(result.success, false, 'an unregistered near-miss key must be rejected');
});
});