Files
msd-core/tests/vscode-subagent-dispatch.test.cjs
Tom Boucher a0fafedfa0 feat(#2103): drive VS Code through the Embeddable Orchestration System (ADR-1239)
VS Code is a net-new EoS runtime that — unlike every prior migration — is NOT
CLI-installed (Marketplace/VSIX extension). It has zero runtime==='vscode'
branches in bin/install.js and stays that way (regression-guarded); it is driven
entirely through the negotiated imperative Host-Integration adapter.

Registry + validator (the hard part):
- capabilities/vscode/capability.json (role:runtime): full hostIntegration block
  (imperative / palette / active vscode.lm model / engine hook bus /
  sandboxed-storage / mcp transport / sandboxed-web runtime; dispatch nested,
  maxDepth 5 per VS Code's documented subagent depth).
- capability-validator.cjs extended so a role:runtime capability can legitimately
  declare "extension-distributed, no config directory": new configHome.kind:'none'
  + installSurface:'none' (+ GATE-A pairing + the parity maps), with localConfigDir
  and configHome.name made conditional on kind!=='none'. All 18 runtimes still
  validate; getDirName returns a distinct sentinel (not '.claude') for a no-config
  runtime.
- The add-a-registry-runtime tax: NON_INSTALLABLE_RUNTIMES exemption in the
  runtime-flags drift guard, vscode added to global-config-home SPECIAL_CASED,
  EXPECTED_PROFILES.vscode='ide', and the config-adapter/derivation/pin-count
  guards updated. No golden-install fixture, model-catalog, or CONFIGURATION rows
  (vscode never enters allRuntimes).

Dispatch + extension surface:
- Fixed vscode/extension.js's createHub()-no-args bug (every dispatch was
  UnknownCommand, masked by a vacuous reachability test) — now reuses the shared
  dispatchGsdCommand subprocess-shim (Node/desktop); the reachability test is
  tightened to assert real dispatch.
- Promoted the #1933 host binding to a shipped vscode/host-binding.js; activate()
  now composes the model/hookBus/stateIO seams through it. Corrected the model
  seam to VS Code's real API (vscode.lm.selectChatModels() -> model.sendRequest();
  vscode.lm.sendRequest does not exist) so the binding actually composes on real
  desktop VS Code instead of throwing.
- New vscode/browser.js Web Extension entry with ZERO Node APIs (the engine's
  config/capability loading is Node-bound, so the web entry registers the surface
  and directs full dispatch to the native MCP server — honestly documented).
- UPGRADE 1: GSD skills as native Language Model Tools (contributes.languageModelTools
  + vscode.lm.registerTool), invoke() dispatching through the hub.
- UPGRADE 2: native subagent dispatch wired onto #runSubagent /
  chat.subagents.allowInvocationsFromSubagents (fail-soft on API availability,
  maxDepth 5 enforced).
- vscode/package.json: browser entry, engines.vscode ^1.105, chatParticipants +
  languageModelTools contributions; fixed a stale activationPoints->activationEvents
  manifest key. Added "vscode" to the package files array.

Docs (## vscode matrix section) + changeset (Added).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 23:24:42 -04:00

149 lines
6.1 KiB
JavaScript

'use strict';
/**
* VS Code #runSubagent wiring test — #2103 UPGRADE 2.
*
* VS Code 1.105+ lets the primary chat agent invoke registered chat
* participants / Language Model Tools as a nested agent turn via
* `#runSubagent`, gated by the `chat.subagents.allowInvocationsFromSubagents`
* setting (default off; nested depth max 5 when enabled — docs/agents/subagents.md,
* release-notes/v1_105.md). There is no separate extension-side "subagent
* contribution" registration API: VS Code's chat engine surfaces the already
* -registered chat participant + languageModelTools directly. This extension's
* own contribution is `registerSubagentDispatch` — availability detection
* (fail-soft on older/Insiders-gated hosts) plus a belt-and-suspenders
* maxDepth:5 ceiling (dispatchAsSubagent) that mirrors
* capabilities/vscode/capability.json's hostIntegration.dispatch.maxDepth,
* independent of whatever the host itself enforces.
*
* Mock vscode only — no real VS Code host in CI.
*/
const { test } = require('node:test');
const assert = require('node:assert/strict');
const extension = require('../vscode/extension.js');
const { createTempDir, cleanup } = require('./helpers.cjs');
function mockVscodeWithSubagentSupport(allow) {
return {
workspace: {
getConfiguration(section) {
return {
get(key) {
if (section === 'chat.subagents' && key === 'allowInvocationsFromSubagents') return allow;
return undefined;
},
};
},
},
};
}
test('registerSubagentDispatch reports maxDepth matching capabilities/vscode/capability.json (dispatch.maxDepth:5)', () => {
const cap = require('../capabilities/vscode/capability.json');
const wiring = extension.registerSubagentDispatch(mockVscodeWithSubagentSupport(true));
assert.equal(wiring.maxDepth, cap.runtime.hostIntegration.dispatch.maxDepth,
'the extension-side depth cap must mirror the descriptor-declared maxDepth');
assert.equal(extension.GSD_MAX_SUBAGENT_DEPTH, cap.runtime.hostIntegration.dispatch.maxDepth);
});
test('registerSubagentDispatch: available:true when chat.subagents.allowInvocationsFromSubagents is configured', () => {
const wiring = extension.registerSubagentDispatch(mockVscodeWithSubagentSupport(true));
assert.equal(wiring.available, true);
assert.equal(typeof wiring.dispatchAsSubagent, 'function');
});
test('registerSubagentDispatch: available:false (fail-soft, no throw) when the setting is absent (older/Insiders-gated VS Code)', () => {
assert.doesNotThrow(() => {
const wiring = extension.registerSubagentDispatch({});
assert.equal(wiring.available, false);
});
});
test('registerSubagentDispatch: available:false (fail-soft) when vscode.workspace.getConfiguration itself throws', () => {
const throwingVscode = {
workspace: { getConfiguration() { throw new Error('simulated host failure'); } },
};
assert.doesNotThrow(() => {
const wiring = extension.registerSubagentDispatch(throwingVscode);
assert.equal(wiring.available, false);
});
});
test('REACHABILITY: a background-eligible dispatchAsSubagent call at depth 0 dispatches through the shared hub and returns REAL output', async () => {
const dir = createTempDir();
try {
const result = JSON.parse(await extension.dispatchAsSubagent({
family: 'progress', subcommand: 'json', cwd: dir, depth: 0,
}));
assert.equal(result.ok, true);
const parsed = JSON.parse(result.stdout);
assert.equal(typeof parsed.percent, 'number', 'the real progress command ran (engine reached)');
} finally {
cleanup(dir);
}
});
// #2103 FIX (adversarial review, MINOR — boundary gap): the repo's own
// TESTING-STANDARDS mandate exercising limit-1/limit/limit+1 around a boundary.
// The maxDepth:5 ceiling previously covered only 5 (limit) and 6 (limit+1) —
// this completes the triple with depth 4 (limit-1).
test('dispatchAsSubagent enforces the maxDepth:5 ceiling — depth 4 (limit-1) still dispatches', async () => {
const dir = createTempDir();
try {
const result = JSON.parse(await extension.dispatchAsSubagent({
family: 'progress', subcommand: 'json', cwd: dir, depth: 4,
}));
assert.equal(result.ok, true, 'depth one below the ceiling must be allowed');
} finally {
cleanup(dir);
}
});
test('dispatchAsSubagent enforces the maxDepth:5 ceiling — depth 5 (at limit) still dispatches', async () => {
const dir = createTempDir();
try {
const result = JSON.parse(await extension.dispatchAsSubagent({
family: 'progress', subcommand: 'json', cwd: dir, depth: 5,
}));
assert.equal(result.ok, true, 'depth exactly at the ceiling must still be allowed');
} finally {
cleanup(dir);
}
});
test('dispatchAsSubagent enforces the maxDepth:5 ceiling — depth 6 (over limit) is refused, never throws', async () => {
const result = JSON.parse(await extension.dispatchAsSubagent({
family: 'progress', subcommand: 'json', depth: 6,
}));
assert.equal(result.ok, false);
assert.match(result.stderr, /exceeds maxDepth/);
assert.equal(result.code, null);
});
test('dispatchAsSubagent defaults depth to 0 when omitted (a direct, non-nested call is always allowed)', async () => {
const dir = createTempDir();
try {
const result = JSON.parse(await extension.dispatchAsSubagent({ family: 'progress', subcommand: 'json', cwd: dir }));
assert.equal(result.ok, true);
} finally {
cleanup(dir);
}
});
// ── Web mode: #runSubagent availability detection is registered identically,
// but there is no dispatchAsSubagent on web (no engine dispatch at all — see
// browser.js's header comment). ─────────────────────────────────────────────
test('WEB MODE: browser.js detectSubagentSupport uses the same fail-soft availability contract', () => {
const browser = require('../vscode/browser.js');
assert.doesNotThrow(() => {
const wiring = browser.detectSubagentSupport(mockVscodeWithSubagentSupport(true));
assert.equal(wiring.available, true);
});
assert.doesNotThrow(() => {
const wiring = browser.detectSubagentSupport({});
assert.equal(wiring.available, false);
});
});