Files
msd-core/tests/fix-2598-opencode-background-dispatch.test.cjs
Tom Boucher a3853472de fix(#2598): declare OpenCode subagent dispatch synchronous, not background (#2682)
* fix(#2598): declare OpenCode subagent dispatch synchronous, not background

capabilities/opencode/capability.json advertised dispatch.background: true and
dispatch.backgroundDispatch: true. negotiateHostCapabilities and every
degradationFor / shouldFlattenDispatch consumer trusts these per-field values, so
declaring a capability the host lacks OVERSTATES it — the opposite of the
fail-closed posture the negotiation is built for.

The issue's own citations needed checking before acting: the host-integration
matrix (ADR-1239's designated deployment source-of-truth) documented `true` with
NEWER evidence than the issue cited, and explicitly marked the issue's
sst/opencode#5887 reference as a stale snapshot superseded by #2087. git log
confirms #2087 deliberately flipped these from false to true, citing OpenCode
v1.15.0/v1.17 as "background subagents enabled by default in all modes". Applying
the issue as filed would, on that evidence, have REGRESSED a deliberate update.

So the claim was verified against current upstream rather than either document.
`packages/opencode/src/effect/runtime-flags.ts` on `dev` today reads:

    experimentalBackgroundSubagents: enabledByExperimental("OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS")

`enabledByExperimental` falls back to the `experimental` flag and `bool()`
defaults to false — the parameter is hidden from the model unless an operator
opts in by env var. Upstream #29638 is still OPEN and confirms the session loop
`tasks.pop()`s one subtask at a time. #2087's "default-on in all modes" reading
does not hold against current dev.

The issue's CONCLUSION is therefore right even though part of its evidence was
superseded: concurrent dispatch cannot be relied on, so both fields are false.

The matrix rows are corrected with the verified citation rather than reverted to
the old #5887 quote, so the record shows why the value is false TODAY rather than
re-asserting evidence that was legitimately superseded. Neighbouring sub-fields
are untouched and pinned by test: namedDispatch, subagentToolkit, and
isolation:'orchestrator-worktree' (which works via `opencode run --dir` at the OS
process level and is unaffected — #2584 does not depend on this value either way).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TCwhbMuY37DzRMCfzTABJ

* fix(#2598): re-pin the dispatch contract tests to synchronous OpenCode dispatch

gsd-test on the descriptor change came back FAILED (5 unique, both node
versions). The failures were not incidental — they were deliberate contract-pin
tests encoding #2087's decision, one named literally "background UPGRADE":

  tests/host-integration-descriptors.test.cjs
    - EXPECTED_FLATTEN[opencode] === false (background-eligible)
    - the derived background-eligible set pin
  tests/opencode-imperative-reference.test.cjs
    - "descriptor declares background dispatch true/true (v1.15/v1.17 upgrade)"
    - "background UPGRADE changes shouldFlattenDispatch: false now"

So this is a recorded decision being reversed, not drift being corrected, and it
is reversed on evidence: current upstream `dev` gates the capability behind
OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS (default false) and upstream #29638
(OPEN) confirms the session loop still handles one subtask at a time. The issue
is filed by the maintainer and explicitly directs "update golden-parity /
validator fixtures as needed", which sanctions re-pinning.

Behavioral consequence, verified: shouldFlattenDispatch(opencode) now returns
TRUE, so GSD serializes opencode dispatch instead of trusting concurrency it
cannot get. That is the correct fail-closed direction and is safe today — no
shipped GSD flow drives OpenCode background waves (per the issue), and
isolation:'orchestrator-worktree' is unaffected because it works at the OS
process level via `opencode run --dir`, not via the native subagent.

Each re-pinned test now asserts the retracted contract in the opposite
direction — feeding the #2087 axes back in must still yield "would not flatten" —
so a silent re-flip of either field is caught rather than merely un-asserted.

lint:ci exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TCwhbMuY37DzRMCfzTABJ

* chore(#2598): backfill changeset pr number (#2682)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 21:47:54 -04:00

92 lines
4.1 KiB
JavaScript

/**
* #2598 — the OpenCode descriptor declared background/concurrent subagent
* dispatch that OpenCode does not actually provide by default.
*
* `capabilities/opencode/capability.json` carried
* `runtime.hostIntegration.dispatch.background: true` and
* `dispatch.backgroundDispatch: true`. OpenCode's native subagent dispatch
* (Task tool / `@`-mention / `subtask`) is synchronous: the `background`
* parameter is hidden from the model behind the opt-in
* `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` flag, which defaults to false
* (`enabledByExperimental(...)` over a `bool()` that defaults false), and the
* session loop still `tasks.pop()`s one subtask at a time (upstream #14195,
* #29638 — the latter still open).
*
* `negotiateHostCapabilities` and every `degradationFor`/`shouldFlattenDispatch`
* consumer TRUSTS these per-field values, so declaring a capability the host
* lacks overstates it — the opposite of the fail-closed posture the negotiation
* exists to enforce.
*
* History note: these fields were flipped to `true` by #2087 citing a reading of
* OpenCode v1.17 as "background subagents enabled by default in all modes".
* That reading does not hold against current upstream `dev`, where the flag is
* opt-in. This test pins the corrected values so a future descriptor edit cannot
* silently re-assert an unsupported capability.
*/
// allow-test-rule: runtime-contract-is-the-product #2598 — the descriptor JSON and the
// host-integration matrix ARE the negotiated contract; asserting their values is behavioral.
'use strict';
process.env.GSD_TEST_MODE = '1';
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 DESCRIPTOR = path.join(ROOT, 'capabilities', 'opencode', 'capability.json');
const MATRIX = path.join(ROOT, 'docs', 'reference', 'host-integration-capability-matrix.md');
function opencodeDispatch() {
const parsed = JSON.parse(fs.readFileSync(DESCRIPTOR, 'utf8'));
return parsed.runtime.hostIntegration.dispatch;
}
describe('#2598: OpenCode does not declare background/concurrent subagent dispatch', () => {
test('descriptor declares background: false', () => {
assert.equal(
opencodeDispatch().background,
false,
'OpenCode subagent dispatch is synchronous unless an experimental opt-in flag is set',
);
});
test('descriptor declares backgroundDispatch: false', () => {
assert.equal(
opencodeDispatch().backgroundDispatch,
false,
'concurrent dispatch requires an opt-in flag, so it must not be declared as available',
);
});
test('the capabilities that ARE real are left intact', () => {
// Narrow the blast radius: this fix must not quietly downgrade neighbouring
// sub-fields that were never in question.
const d = opencodeDispatch();
assert.equal(d.namedDispatch, true, 'named subagent dispatch is genuinely supported');
assert.equal(d.subagentToolkit, 'full', 'the general subagent has full tool access');
assert.equal(d.isolation, 'orchestrator-worktree',
'isolation is orchestrator-managed via `opencode run --dir`, unaffected by #2598');
});
test('the host-integration matrix agrees with the descriptor', () => {
// ADR-1239 designates the matrix the deployment source-of-truth; a
// descriptor/matrix disagreement is how this defect survived in the first
// place (the matrix said true, the ADR binding table said false).
const matrix = fs.readFileSync(MATRIX, 'utf8');
const section = matrix.slice(matrix.indexOf('## opencode'));
const end = section.indexOf('\n## ');
const opencodeSection = end === -1 ? section : section.slice(0, end);
for (const field of ['dispatch.background', 'dispatch.backgroundDispatch']) {
const row = opencodeSection.split('\n').find((l) => l.startsWith(`| ${field} |`));
assert.ok(row, `matrix must document ${field} for opencode`);
const value = row.split('|')[2].trim();
assert.equal(value, 'false', `matrix ${field} must match the descriptor`);
}
});
});