* 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>
This commit is contained in:
5
.changeset/curious-quails-glide.md
Normal file
5
.changeset/curious-quails-glide.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3063
|
||||
---
|
||||
**Dispatch flattening now honors the declared nesting depth budget, so runtimes that cannot host a backgrounded orchestrator plus a delegated leaf run inline instead of producing an unsupported depth-2 tree** — `shouldFlattenDispatch` checked only the two background booleans, so a host advertising `maxDepth:1` was told it may background, which under Codex MultiAgent V2 produced a depth-2 orchestration tree the declared contract forbids. The decision now also requires `nested` + a full subagent toolkit + a depth budget greater than 1 or unbounded, reusing the convention already in `degradationFor` and `_normalizeDispatchCallSpan`. Runtimes lacking any of those — codex at `maxDepth:1`, kimi with `nested:false`, kimi-code with a built-in-only toolkit — now correctly run inline, the safer path that keeps worktree isolation and verification in force; only cursor remains background-eligible. (#2939)
|
||||
@@ -569,13 +569,34 @@ function negotiateHostCapabilities(
|
||||
* may be backgrounded.
|
||||
*
|
||||
* A host may background only if it can reliably background a nesting-capable
|
||||
* orchestrator — i.e. both `background` AND `backgroundDispatch` are
|
||||
* explicitly `true`. Any other value (false, missing, 'undocumented') fails
|
||||
* closed to inline (the always-safe path).
|
||||
* orchestrator that still has room to delegate to a leaf — i.e. ALL of:
|
||||
* - `background === true` AND `backgroundDispatch === true` (it can background at all), AND
|
||||
* - `nested === true` AND `subagentToolkit === 'full'` (it can host a nesting orchestrator), AND
|
||||
* - a depth budget greater than 1, or unbounded (`maxDepth < 0`): a budget of exactly 1
|
||||
* is consumed by the backgrounded orchestrator itself (depth 1) and leaves no room for the
|
||||
* delegated leaf (depth 2) its own contract would require.
|
||||
*
|
||||
* Any other value (false, missing, 'undocumented', or an insufficient depth budget) fails closed
|
||||
* to inline (the always-safe path). This closes #2939, where a `maxDepth:1` descriptor
|
||||
* (the live Codex capability) was told it may background a nesting orchestrator that then
|
||||
* produced a depth-2 tree its declared contract cannot support.
|
||||
*
|
||||
* The depth-budget test mirrors the convention already used elsewhere in the codebase:
|
||||
* `degradationFor` (same file) treats `nested && depth >= 2` as full-depth, `maxDepth === 1`
|
||||
* as flat, and any `maxDepth < 0` as unbounded; `negotiateHostCapabilities` (same file) also
|
||||
* treats `maxDepth < 0` as unbounded; `bin/install.js`'s `_normalizeDispatchCallSpan` uses
|
||||
* `subagentToolkit === 'full' && (maxDepth === -1 || maxDepth > 1)`. This function adopts the
|
||||
* broader `maxDepth < 0` = unbounded convention from `degradationFor`/`negotiateHostCapabilities`
|
||||
* (every shipped descriptor uses `-1` for unbounded, so the two conventions agree on live input).
|
||||
* It is STRICTER than `_normalizeDispatchCallSpan` in one respect: it also requires `nested === true`,
|
||||
* because a host that cannot nest cannot host a backgrounded orchestrator that delegates —
|
||||
* `_normalizeDispatchCallSpan` runs per-call after the dispatch decision and does not need that gate.
|
||||
*
|
||||
* This graduates the #853 prose rule (originally `RUNTIME === 'codex'`, then
|
||||
* extended to cursor) to a typed, documentation-sourced decision; codex AND
|
||||
* cursor are both background-eligible in the registry. See
|
||||
* extended to cursor) to a typed, documentation-sourced decision; of the shipped
|
||||
* background-capable hosts only cursor (`maxDepth:2`) remains background-eligible under the
|
||||
* depth-aware rule — codex (`maxDepth:1`), kimi (`nested:false`), and kimi-code
|
||||
* (`built-in-only` toolkit) now correctly flatten. See
|
||||
* docs/reference/host-integration-capability-matrix.md.
|
||||
*
|
||||
* Null-safety: if dispatch is null, undefined, or not an object, returns true
|
||||
@@ -585,8 +606,18 @@ type UnvalidatedDispatch = (Partial<DispatchCapability> & { background?: unknown
|
||||
|
||||
function shouldFlattenDispatch(dispatch: UnvalidatedDispatch): boolean {
|
||||
if (!dispatch || typeof dispatch !== 'object') return true;
|
||||
// Can background at all: both background flags must be explicitly true.
|
||||
const canBackground = dispatch.background === true && dispatch.backgroundDispatch === true;
|
||||
return !canBackground;
|
||||
if (!canBackground) return true;
|
||||
// #2939: can background a NESTING orchestrator with room to delegate. A depth budget of 1
|
||||
// is consumed by the backgrounded orchestrator itself; it needs > 1 (or unbounded, maxDepth < 0)
|
||||
// to host a delegated leaf at depth 2. Non-finite/missing maxDepth fails closed (no budget →
|
||||
// flatten), mirroring degradationFor's treatment of non-finite depth as 0.
|
||||
const canNest = dispatch.nested === true && dispatch.subagentToolkit === 'full';
|
||||
if (!canNest) return true;
|
||||
const depth = typeof dispatch.maxDepth === 'number' && Number.isFinite(dispatch.maxDepth) ? dispatch.maxDepth : 0;
|
||||
const depthSufficient = depth < 0 || depth > 1;
|
||||
return !depthSufficient;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1693,23 +1693,25 @@ describe('bug-853 — manager/autonomous gate background dispatch by runtime', (
|
||||
|
||||
describe('dispatch-should-flatten query — behavioral', () => {
|
||||
// #853 / #1708: The typed query replaces prose-level RUNTIME===codex checks.
|
||||
// shouldFlattenDispatch returns false only when both dispatch.background AND
|
||||
// dispatch.backgroundDispatch are true in the capability registry.
|
||||
// #2939: shouldFlattenDispatch is now DEPTH-AWARE — a host may background only if it can
|
||||
// background AND host a nesting orchestrator with a depth budget > 1 (nested:true +
|
||||
// subagentToolkit:"full" + maxDepth > 1 or unbounded). The two-field background rule is the
|
||||
// first gate, not the whole decision.
|
||||
//
|
||||
// Registry values (from host-integration-capability-matrix.md):
|
||||
// codex: background=true, backgroundDispatch=true → shouldFlatten=false (may background)
|
||||
// claude: background=true, backgroundDispatch=false → shouldFlatten=true (must inline)
|
||||
// cursor: background=true, backgroundDispatch=true → shouldFlatten=false (may background)
|
||||
// unknown: no entry → fail-closed → shouldFlatten=true (must inline)
|
||||
// codex: background=true, backgroundDispatch=true, BUT maxDepth:1 → shouldFlatten=true (must inline — #2939: depth budget insufficient for a bg orchestrator + leaf)
|
||||
// claude: background=true, backgroundDispatch=false → shouldFlatten=true (must inline)
|
||||
// cursor: background=true, backgroundDispatch=true, maxDepth:2 → shouldFlatten=false (may background — the one shipped host with a sufficient depth budget)
|
||||
// unknown: no entry → fail-closed → shouldFlatten=true (must inline)
|
||||
|
||||
test('runtime=codex → shouldFlatten=false (background dispatch safe)', () => {
|
||||
test('runtime=codex → shouldFlatten=true (#2939: maxDepth:1 cannot host a bg orchestrator + leaf)', () => {
|
||||
const tmpDir = createTempProject();
|
||||
try {
|
||||
const result = runGsdTools(['query', 'dispatch-should-flatten', '--raw'], tmpDir, {
|
||||
GSD_RUNTIME: 'codex',
|
||||
});
|
||||
assert.ok(result.success, `Expected success, got error: ${result.error}`);
|
||||
assert.strictEqual(result.output, 'false', `codex should return false (may background), got: ${result.output}`);
|
||||
assert.strictEqual(result.output, 'true', `#2939: codex maxDepth:1 should return true (must inline), got: ${result.output}`);
|
||||
} finally {
|
||||
cleanupDir(tmpDir);
|
||||
}
|
||||
@@ -1770,7 +1772,10 @@ describe('dispatch-should-flatten query — behavioral', () => {
|
||||
assert.fail(`Expected valid JSON output, got: ${result.output}`);
|
||||
}
|
||||
assert.strictEqual(parsed.runtime, 'codex');
|
||||
assert.strictEqual(parsed.shouldFlatten, false);
|
||||
// #2939: codex maxDepth:1 → shouldFlatten:true (depth budget insufficient). backgroundDispatch
|
||||
// stays true on the descriptor (codex CAN background a single agent); only the flatten
|
||||
// consequence changes.
|
||||
assert.strictEqual(parsed.shouldFlatten, true);
|
||||
assert.ok(parsed.dispatch !== null && typeof parsed.dispatch === 'object', 'dispatch should be an object');
|
||||
assert.strictEqual(parsed.dispatch.backgroundDispatch, true);
|
||||
} finally {
|
||||
@@ -1797,9 +1802,9 @@ describe('dispatch-should-flatten query — behavioral', () => {
|
||||
const result = runGsdTools(['query', 'dispatch-should-flatten', '--raw'], tmpDir, {
|
||||
GSD_RUNTIME: '',
|
||||
});
|
||||
// config.runtime=codex with GSD_RUNTIME cleared → codex backgrounds → shouldFlatten=false
|
||||
// #2939: config.runtime=codex with GSD_RUNTIME cleared → codex maxDepth:1 → shouldFlatten=true (must inline)
|
||||
assert.ok(result.success, `Expected success, got error: ${result.error}`);
|
||||
assert.strictEqual(result.output, 'false', `config.runtime=codex (GSD_RUNTIME cleared) should return false (may background), got: ${result.output}`);
|
||||
assert.strictEqual(result.output, 'true', `#2939: config.runtime=codex (GSD_RUNTIME cleared) should return true (must inline, maxDepth:1), got: ${result.output}`);
|
||||
} finally {
|
||||
cleanupDir(tmpDir);
|
||||
}
|
||||
|
||||
@@ -272,28 +272,36 @@ describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
|
||||
|
||||
// ─── shouldFlattenDispatch per-host (#853 discriminator) ─────────────────────
|
||||
|
||||
// Expected: false (may background) for codex, cursor, kimi, and kimi-code;
|
||||
// true (must inline) for the other 14.
|
||||
// Expected: false (may background) ONLY for cursor — the one shipped host whose
|
||||
// dispatch declares nested:true + subagentToolkit:"full" + a depth budget > 1.
|
||||
// true (must inline) for the other 17.
|
||||
//
|
||||
// #2939: the depth-aware rule reclassifies codex/kimi/kimi-code, which the old
|
||||
// two-field (background+backgroundDispatch) rule admitted as background-eligible
|
||||
// despite each lacking what a backgrounded nesting orchestrator actually needs:
|
||||
// - codex: nested:true + full toolkit, BUT maxDepth:1 (no room for a depth-2 leaf)
|
||||
// - kimi: nested:false (cannot host a nesting orchestrator at all)
|
||||
// - kimi-code: subagentToolkit:'built-in-only' (cannot delegate to full subagents)
|
||||
// cursor (maxDepth:2) remains the only background-capable host with a sufficient budget.
|
||||
const EXPECTED_FLATTEN = {
|
||||
antigravity: true,
|
||||
augment: true,
|
||||
claude: true,
|
||||
cline: true,
|
||||
codebuddy: true,
|
||||
codex: false,
|
||||
codex: true,
|
||||
copilot: true,
|
||||
cursor: false,
|
||||
hermes: true,
|
||||
kilo: true,
|
||||
// #2095: Kimi Upgrade 2 — Kimi's Agent tool takes a run_in_background
|
||||
// call-time param (Context7 agents.html) → backgroundDispatch flipped to
|
||||
// true → dispatch.background/backgroundDispatch both true → NOT
|
||||
// force-flattened (mirrors the #2087 OpenCode precedent below).
|
||||
kimi: false,
|
||||
// #2454: Kimi Code (Node CLI) — same background-dispatch model as Python
|
||||
// kimi-cli per Kimi Code docs (dispatch.background/backgroundDispatch both
|
||||
// true) → NOT force-flattened.
|
||||
'kimi-code': false,
|
||||
// #2095/#2939: Kimi CAN background a single agent (backgroundDispatch:true), BUT
|
||||
// nested:false means a backgrounded kimi agent cannot nest the plan-checker/executor/
|
||||
// verifier pipeline the workflows require → flatten. backgroundDispatch stays true on
|
||||
// the descriptor (UPGRADE 2 holds); only the flatten consequence changes.
|
||||
kimi: true,
|
||||
// #2454/#2939: Kimi Code declares background/backgroundDispatch both true, BUT
|
||||
// subagentToolkit:'built-in-only' cannot delegate to full subagents → flatten.
|
||||
'kimi-code': true,
|
||||
// #2598: OpenCode's background subagents sit behind the opt-in
|
||||
// OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS flag (default false), and the
|
||||
// session loop still handles one subtask at a time (upstream #29638, OPEN).
|
||||
|
||||
@@ -955,9 +955,12 @@ describe('Phase B: shouldFlattenDispatch — contract pin', () => {
|
||||
'shouldFlattenDispatch must be exported from host-integration module');
|
||||
});
|
||||
|
||||
test('{background:true, backgroundDispatch:true} → false (background OK)', () => {
|
||||
assert.strictEqual(shouldFlattenDispatch({ background: true, backgroundDispatch: true }), false,
|
||||
'canBackground=true when both background===true AND backgroundDispatch===true → flatten=false');
|
||||
test('{background:true, backgroundDispatch:true} → true (no depth budget declared → fail-closed/flatten)', () => {
|
||||
// #2939: can background AT ALL, but declares no nested/toolkit/maxDepth, so the depth
|
||||
// budget is unknown → fail-closed to inline (flatten). A real background-eligible host
|
||||
// also carries nested:true + subagentToolkit:"full" + a sufficient maxDepth.
|
||||
assert.strictEqual(shouldFlattenDispatch({ background: true, backgroundDispatch: true }), true,
|
||||
'canBackground=true but no depth budget declared → fail-closed/flatten=true');
|
||||
});
|
||||
|
||||
test('{background:true, backgroundDispatch:false} → true (must flatten)', () => {
|
||||
@@ -1003,12 +1006,24 @@ describe('Phase B: shouldFlattenDispatch — contract pin', () => {
|
||||
'non-object dispatch (string) must fail-closed to true');
|
||||
});
|
||||
|
||||
// #853 codex-like profile: full dispatch including backgroundDispatch:true → background OK
|
||||
test('#853 codex-like: {namedDispatch:true,nested:true,maxDepth:1,background:true,subagentToolkit:"full",backgroundDispatch:true} → false (background OK)', () => {
|
||||
// #2939: the codex-like profile with maxDepth:1 now FLATTENS. A depth budget of 1 is consumed
|
||||
// by the backgrounded orchestrator itself (depth 1) and leaves no room for the delegated leaf
|
||||
// (depth 2) its own contract requires. This corrects the prior pin, which asserted the buggy
|
||||
// shouldFlatten:false output that permitted a depth-2 tree the descriptor cannot support.
|
||||
test('#2939 codex-like: {namedDispatch:true,nested:true,maxDepth:1,background:true,subagentToolkit:"full",backgroundDispatch:true} → true (flatten — depth budget insufficient)', () => {
|
||||
assert.strictEqual(
|
||||
shouldFlattenDispatch({ namedDispatch: true, nested: true, maxDepth: 1, background: true, subagentToolkit: 'full', backgroundDispatch: true }),
|
||||
true,
|
||||
'maxDepth:1 is insufficient for a backgrounded orchestrator plus a delegated leaf → flatten=true',
|
||||
);
|
||||
});
|
||||
|
||||
// #2939 negative-space: the same codex-like profile with a SUFFICIENT depth budget backgrounds.
|
||||
test('#2939 codex-like maxDepth:2 → false (background OK — depth budget sufficient)', () => {
|
||||
assert.strictEqual(
|
||||
shouldFlattenDispatch({ namedDispatch: true, nested: true, maxDepth: 2, background: true, subagentToolkit: 'full', backgroundDispatch: true }),
|
||||
false,
|
||||
'codex-like dispatch with backgroundDispatch:true must be background-OK (flatten=false)',
|
||||
'maxDepth:2 leaves room for a backgrounded orchestrator plus a leaf → background OK',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
135
tests/issue-2939-dispatch-flatten-maxdepth.test.cjs
Normal file
135
tests/issue-2939-dispatch-flatten-maxdepth.test.cjs
Normal file
@@ -0,0 +1,135 @@
|
||||
'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)',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -253,12 +253,17 @@ test('UPGRADE 2: capabilities/kimi/capability.json declares dispatch.background
|
||||
assert.equal(d.backgroundDispatch, true);
|
||||
});
|
||||
|
||||
test('UPGRADE 2: negotiateHostCapabilities against kimi axes yields effective.dispatch.backgroundDispatch === true, and shouldFlattenDispatch is false (background now allowed)', () => {
|
||||
test('UPGRADE 2: negotiateHostCapabilities against kimi axes yields effective.dispatch.backgroundDispatch === true, but shouldFlattenDispatch is true (#2939: nested:false cannot host a nesting orchestrator)', () => {
|
||||
const KIMI_AXES = KIMI_CAP.runtime.hostIntegration;
|
||||
const { effective } = negotiateHostCapabilities(KIMI_AXES);
|
||||
// UPGRADE 2 still holds: the descriptor declares backgroundDispatch:true (kimi CAN background
|
||||
// a single agent). #2939 changes only the FLATTEN consequence: kimi's nested:false means a
|
||||
// backgrounded kimi agent cannot itself nest the plan-checker/executor/verifier pipeline the
|
||||
// workflows require, so the orchestrator must run inline (flatten) even though backgrounding
|
||||
// a single agent is possible.
|
||||
assert.equal(effective.dispatch.backgroundDispatch, true);
|
||||
assert.equal(shouldFlattenDispatch(effective.dispatch), false,
|
||||
'kimi may now background — dispatch must not be flattened to inline');
|
||||
assert.equal(shouldFlattenDispatch(effective.dispatch), true,
|
||||
'#2939: kimi nested:false → a backgrounded orchestrator cannot nest the pipeline → flatten');
|
||||
});
|
||||
|
||||
test('UPGRADE 2: a corrupted/undeclared dispatch still fails closed to inline (shouldFlattenDispatch === true)', () => {
|
||||
|
||||
@@ -72,11 +72,14 @@ test('synchronous dispatch force-flattens; the retracted axes would not have', (
|
||||
// capability must serialize rather than be trusted.
|
||||
assert.equal(shouldFlattenDispatch(OC_AXES.dispatch), true,
|
||||
'with background:false, GSD must force-flatten opencode dispatch (fail closed)');
|
||||
// Pin the retracted contract so a silent re-flip is caught: had the #2087
|
||||
// values been accurate, dispatch would NOT have been flattened.
|
||||
// #2939: pin the retracted contract so a silent re-flip is caught. Under the depth-aware
|
||||
// rule, flipping ONLY the two background booleans is no longer sufficient to background —
|
||||
// opencode's axes lack nested:true + subagentToolkit:"full" + a depth budget > 1, so even
|
||||
// the #2087 background values still flatten. A future accurate declaration would need to
|
||||
// establish the full nesting capability, not just the background booleans.
|
||||
const retracted = { ...OC_AXES.dispatch, background: true, backgroundDispatch: true };
|
||||
assert.equal(shouldFlattenDispatch(retracted), false,
|
||||
'the #2087 axes did not flatten — that is exactly the overstatement #2598 retracts');
|
||||
assert.equal(shouldFlattenDispatch(retracted), true,
|
||||
'#2939: the #2087 background-only values still flatten — opencode lacks nested + full toolkit + depth budget');
|
||||
});
|
||||
|
||||
test('opencode extension-event surface includes the #2087 additions (permission + session.error)', () => {
|
||||
|
||||
@@ -101,9 +101,13 @@ test('AC-SPECIFIC: trae real dispatch axes fail CLOSED to inline (shouldFlattenD
|
||||
// this fail-closed default without a deliberate test update.
|
||||
assert.equal(shouldFlattenDispatch(TRAE_AXES.dispatch), true);
|
||||
|
||||
// Sanity: if backgroundDispatch WERE true (all else equal), the same shape
|
||||
// would NOT flatten — proving backgroundDispatch is what flips the result.
|
||||
assert.equal(shouldFlattenDispatch({ ...TRAE_AXES.dispatch, backgroundDispatch: true }), false);
|
||||
// #2939: flipping ONLY backgroundDispatch to true is NO LONGER sufficient to
|
||||
// background. trae's axes declare nested:'undocumented' (not true) and
|
||||
// subagentToolkit:'undocumented' (not 'full'), so even with backgroundDispatch
|
||||
// true the depth/nesting gate (canNest) fails closed to flatten. A future
|
||||
// doc-sourcing pass must also establish nested:true + subagentToolkit:'full' +
|
||||
// a sufficient maxDepth before trae may background.
|
||||
assert.equal(shouldFlattenDispatch({ ...TRAE_AXES.dispatch, backgroundDispatch: true }), true);
|
||||
});
|
||||
|
||||
test('a partial/empty trae descriptor degrades to the safe floor, not the programmatic-cli baseline', () => {
|
||||
|
||||
Reference in New Issue
Block a user