* test(#2939): prove shouldFlattenDispatch ignores the depth budget Failing-first regression for #2939. shouldFlattenDispatch checks only background+backgroundDispatch, never nested/subagentToolkit/maxDepth, so a maxDepth:1 descriptor (no room for a bg orchestrator plus a leaf) is told it may background. Row 1 (codex-like, maxDepth:1) asserts true (flatten) and fails today; rows 2/3 guard the unchanged depth-sufficient cases. * fix(#2939): honor the declared depth budget in shouldFlattenDispatch shouldFlattenDispatch checked only background+backgroundDispatch, never nested/subagentToolkit/maxDepth, so a maxDepth:1 descriptor (no room for a backgrounded orchestrator plus a delegated leaf) was told it may background — producing a depth-2 tree (Codex MultiAgent V2) the declared contract cannot support. canBackground now ALSO requires nested:true + subagentToolkit:"full" + a depth budget > 1 (or unbounded -1), reusing the exact predicate shape from bin/install.js _normalizeDispatchCallSpan and matching degradationFor's treatment of maxDepth===1 as flat. Non-finite/missing maxDepth fails closed to flatten. Correct the two existing pins that asserted the buggy output (bare {bg,bgDispatch} now fail-closes on missing depth; the codex-like maxDepth:1 pin flips to flatten) and add a maxDepth:2 negative-space row. * fix(#2939): propagate depth-aware flatten to all pinned descriptors + tests The isolated adversarial review found the depth-aware predicate reclassifies codex/kimi/kimi-code (previously background-eligible under the two-field rule) to flatten — the correct behavior, since each lacks what a backgrounded nesting orchestrator needs: - codex: maxDepth:1 (no room for a depth-2 leaf) - kimi: nested:false (cannot host a nesting orchestrator) - kimi-code: subagentToolkit:'built-in-only' (cannot delegate to full subagents) Only cursor (maxDepth:2) remains background-eligible. Update the three test files that pinned the old contract (host-integration-descriptors EXPECTED_FLATTEN, kimi-upgrades UPGRADE 2, trae-imperative-reference), and align the unbounded convention to maxDepth < 0 (matching degradationFor/negotiateHostCapabilities) with an accurate docstring noting the deliberate nested-check addition over _normalizeDispatchCallSpan. * fix(#2939): update dispatch-should-flatten CLI query pins for codex The depth-aware rule (a0ad0f680) reclassifies codex (maxDepth:1) to flatten, but command-routing-hub.test.cjs exercises the contract through the CLI query route (runGsdTools query dispatch-should-flatten), not a direct shouldFlattenDispatch call — so neither the reviewer's caller-search nor a grep for the symbol found it; only the full gsd-test matrix did. Update the codex query assertions to shouldFlatten=true (maxDepth:1 insufficient), preserving cursor (maxDepth:2 → false) and the backgroundDispatch:true descriptor field. * chore(#2939): add changeset fragment pr:0 placeholder backfilled with the real PR number once the PR exists. * fix(#2939): rephrase changeset for product-name-purity + opencode flatten pin Two failures from the full gsd-test matrix on the prior sha: 1. product-name-purity: changeset fragments must not include parenthetical product descriptions (they render verbatim into CHANGELOG.md). 'Codex (and kimi/kimi-code)' tripped it — rephrase to lead with the behavior, naming runtimes inline without the parenthetical. lint:ci changeset-lint does not catch this; only the test does. 2. opencode-imperative-reference: the #2087-retraction pin flipped only the two background booleans and asserted shouldFlatten:false. Under #2939 that is no longer sufficient (opencode lacks nested + full toolkit + depth budget), so the retracted axes now correctly flatten — update the pin to true with rationale. * chore(#2939): backfill changeset PR number 3063 --------- Co-authored-by: sim <sim@local>
136 lines
4.9 KiB
JavaScript
136 lines
4.9 KiB
JavaScript
'use strict';
|
|
process.env.GSD_TEST_MODE = '1';
|
|
|
|
/**
|
|
* Regression test for #2939 — `shouldFlattenDispatch` ignores the declared
|
|
* depth budget, so a runtime advertising `maxDepth:1` (no room for a background
|
|
* orchestrator plus a delegated leaf) is still told it may background.
|
|
*
|
|
* Root cause: `shouldFlattenDispatch` (src/host-integration.cts) checked ONLY
|
|
* `dispatch.background` and `dispatch.backgroundDispatch`, never `nested`,
|
|
* `subagentToolkit`, or `maxDepth`. With the live Codex descriptor
|
|
* (background:true, backgroundDispatch:true, nested:true, subagentToolkit:"full",
|
|
* maxDepth:1) it returned `shouldFlatten:false`, which then permitted a depth-2
|
|
* orchestration tree the declared contract cannot support.
|
|
*
|
|
* The fix reconciles `shouldFlattenDispatch` with the depth-budget convention
|
|
* already used in the same file (`degradationFor`: nested && depth>=2 is
|
|
* full-depth; maxDepth===1 is flat) and in `bin/install.js`
|
|
* (`_normalizeDispatchCallSpan`: subagentToolkit==='full' && (maxDepth===-1 ||
|
|
* maxDepth>1)). A host may background only if it can background AND has a depth
|
|
* budget sufficient for a backgrounded orchestrator plus a delegated leaf.
|
|
*
|
|
* Matrix: .gsd/bug/fix/2939-dispatch-flatten-maxdepth/50-test-matrix.md
|
|
*/
|
|
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
const { shouldFlattenDispatch } = require('../gsd-core/bin/lib/host-integration.cjs');
|
|
|
|
/** The live Codex-shaped descriptor (the bug input), with per-test depth overrides. */
|
|
function codexLike(overrides = {}) {
|
|
return {
|
|
namedDispatch: true,
|
|
nested: true,
|
|
maxDepth: 1,
|
|
background: true,
|
|
subagentToolkit: 'full',
|
|
backgroundDispatch: true,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('shouldFlattenDispatch depth budget (#2939)', () => {
|
|
test('codexLikeMaxDepth1Flattens', () => {
|
|
// Row 1 (failing-first regression): maxDepth:1 cannot host a bg orchestrator
|
|
// (depth 1) AND a delegated leaf (depth 2) → must flatten (inline).
|
|
assert.strictEqual(
|
|
shouldFlattenDispatch(codexLike({ maxDepth: 1 })),
|
|
true,
|
|
'maxDepth:1 is insufficient for a backgrounded orchestrator plus a leaf → flatten',
|
|
);
|
|
});
|
|
|
|
test('maxDepth2BackgroundsUnchanged', () => {
|
|
// Row 2: maxDepth:2 leaves room → background permitted, unchanged from today.
|
|
assert.strictEqual(
|
|
shouldFlattenDispatch(codexLike({ maxDepth: 2 })),
|
|
false,
|
|
'maxDepth:2 is sufficient → background permitted (unchanged)',
|
|
);
|
|
});
|
|
|
|
test('maxDepthUnboundedBackgroundsUnchanged', () => {
|
|
// Row 3: maxDepth:-1 (unbounded) → background permitted, unchanged.
|
|
assert.strictEqual(
|
|
shouldFlattenDispatch(codexLike({ maxDepth: -1 })),
|
|
false,
|
|
'maxDepth:-1 (unbounded) → background permitted (unchanged)',
|
|
);
|
|
});
|
|
|
|
test('nestedFalseFlattensRegardlessOfDepth', () => {
|
|
// Row 4 / acceptance #4: nested:false cannot host a nesting orchestrator →
|
|
// flatten regardless of maxDepth.
|
|
assert.strictEqual(
|
|
shouldFlattenDispatch(codexLike({ nested: false, maxDepth: 5 })),
|
|
true,
|
|
'nested:false → flatten regardless of maxDepth',
|
|
);
|
|
});
|
|
|
|
test('nonFullToolkitFlattens', () => {
|
|
// Row 5 / acceptance #4: a non-full toolkit cannot delegate → flatten
|
|
// regardless of maxDepth.
|
|
assert.strictEqual(
|
|
shouldFlattenDispatch(codexLike({ subagentToolkit: 'read-only', maxDepth: 5 })),
|
|
true,
|
|
'subagentToolkit!=="full" → flatten regardless of maxDepth',
|
|
);
|
|
});
|
|
|
|
test('backgroundFalseStillFlattens', () => {
|
|
// Row 6 / negative-space: background:false → flatten (the existing
|
|
// background-boolean fail-closed path is unchanged).
|
|
assert.strictEqual(
|
|
shouldFlattenDispatch(codexLike({ background: false, maxDepth: 5 })),
|
|
true,
|
|
'background:false → flatten (unchanged)',
|
|
);
|
|
});
|
|
|
|
test('backgroundDispatchFalseStillFlattens', () => {
|
|
// Row 7 / negative-space: backgroundDispatch:false → flatten (unchanged).
|
|
assert.strictEqual(
|
|
shouldFlattenDispatch(codexLike({ backgroundDispatch: false, maxDepth: 5 })),
|
|
true,
|
|
'backgroundDispatch:false → flatten (unchanged)',
|
|
);
|
|
});
|
|
|
|
test('maxDepth0Flattens', () => {
|
|
// Row 9: maxDepth:0 (zero depth budget) → flatten.
|
|
assert.strictEqual(
|
|
shouldFlattenDispatch(codexLike({ maxDepth: 0 })),
|
|
true,
|
|
'maxDepth:0 → flatten (zero depth budget)',
|
|
);
|
|
});
|
|
|
|
test('maxDepthMissingFlattens', () => {
|
|
// Row 10: maxDepth missing/non-number → flatten (fail-closed on absent
|
|
// budget, mirrors degradationFor treating non-finite as 0).
|
|
assert.strictEqual(
|
|
shouldFlattenDispatch(codexLike({ maxDepth: undefined })),
|
|
true,
|
|
'maxDepth missing → flatten (fail-closed)',
|
|
);
|
|
assert.strictEqual(
|
|
shouldFlattenDispatch(codexLike({ maxDepth: 'deep' })),
|
|
true,
|
|
'maxDepth non-number → flatten (fail-closed)',
|
|
);
|
|
});
|
|
});
|