Files
msd-core/tests/vscode-extension-reachability.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

198 lines
9.5 KiB
JavaScript

'use strict';
/**
* VS Code extension reachability test — ADR-1239 Phase D / #1942, upgraded #2103.
*
* Proves the VS Code extension is keystone-WIRED: the gsd.invoke handler
* dispatches through gsd-tools.cjs (subprocess-reuse — the shared
* `dispatchGsdCommand` in gsd-core/bin/lib/shell-command-projection.cjs) and
* returns REAL output, not just a registration on a stub. The handler is
* exported separately from activate() so it is testable WITHOUT a VS Code host.
*
* The original cut called `createHub()` with NO args — no hub factory in the
* tree fully populates a hub, so every dispatch silently answered
* UnknownCommand. The prior version of this test only asserted the result was
* "a JSON object" — a VACUOUS assertion that passed whether or not dispatch
* actually worked. Dispatch is now exercised with a real read-only
* family/subcommand (progress/json) against a real temp project, matching the
* sibling tests/pi-extension-reachability.test.cjs pattern — no fake
* dispatcher injected, because the whole point of "reachability" is that the
* real engine is reached.
*/
const { test } = require('node:test');
const assert = require('node:assert/strict');
const Module = require('node:module');
const path = require('node:path');
const extension = require('../vscode/extension.js');
const { activate, dispatchGsdCommand, resolveEngineRoot, resolveWorkspaceCwd } = extension;
const { createTempDir, cleanup } = require('./helpers.cjs');
const shellCommandProjection = require('../gsd-core/bin/lib/shell-command-projection.cjs');
test('the extension exports activate + dispatchGsdCommand + resolveEngineRoot', () => {
assert.equal(typeof activate, 'function');
assert.equal(typeof dispatchGsdCommand, 'function');
assert.equal(typeof resolveEngineRoot, 'function');
});
test('REACHABILITY: dispatchGsdCommand dispatches a real family/subcommand through gsd-tools.cjs and returns REAL output (keystone wired, not UnknownCommand)', async () => {
const dir = createTempDir();
try {
const result = await dispatchGsdCommand({ family: 'progress', subcommand: 'json', cwd: dir });
assert.equal(typeof result, 'string', 'returns a string result');
const parsed = JSON.parse(result);
assert.ok(parsed !== null && typeof parsed === 'object', 'dispatch produced a result object');
assert.equal(parsed.ok, true, `expected ok:true (real dispatch), got: ${result}`);
assert.equal(typeof parsed.stdout, 'string');
assert.ok(parsed.stdout.length > 0, 'stdout must be non-empty');
const inner = JSON.parse(parsed.stdout);
assert.equal(typeof inner.percent, 'number',
'the real progress command ran (proves the engine was reached — not UnknownCommand)');
assert.equal(parsed.code, 0);
} finally {
cleanup(dir);
}
});
test('REACHABILITY: an unknown family surfaces ok:false without throwing (not a silent UnknownCommand success)', async () => {
const dir = createTempDir();
try {
const result = await dispatchGsdCommand({ family: 'no-such-family-8675309', cwd: dir });
const parsed = JSON.parse(result);
assert.equal(parsed.ok, false);
assert.match(parsed.stderr, /no-such-family-8675309|Unknown command/);
} finally {
cleanup(dir);
}
});
test('dispatchGsdCommand works with default args (no args → gsd-tools.cjs --help, a real working default)', async () => {
const result = await dispatchGsdCommand();
assert.equal(typeof result, 'string');
const parsed = JSON.parse(result); // must be valid JSON
assert.equal(parsed.ok, true, `expected the --help default to be ok:true, got: ${result}`);
assert.match(parsed.stdout, /Usage: gsd-tools/, 'the --help default produced real usage output');
});
test('resolveEngineRoot finds the gsd-core/ dir from the extension location', () => {
const root = resolveEngineRoot(__dirname + '/../vscode');
const fs = require('fs');
assert.ok(fs.existsSync(require('path').join(root, 'gsd-core')),
'resolveEngineRoot finds a dir containing gsd-core/');
});
test('the extension manifest declares the gsd.invoke command', () => {
const pkg = require('../vscode/package.json');
assert.ok(pkg.contributes && pkg.contributes.commands, 'manifest has commands');
const cmd = pkg.contributes.commands.find((c) => c.command === 'gsd.invoke');
assert.ok(cmd, 'manifest declares gsd.invoke');
assert.ok(pkg.engines && pkg.engines.vscode, 'manifest declares VS Code engine');
assert.equal(pkg.main, './extension.js', 'manifest main points to extension.js');
});
// ── #2103 FIX (adversarial review, MAJOR): all three desktop dispatch
// surfaces previously called dispatchGsdCommand WITHOUT a cwd, silently
// defaulting to the extension host's own process.cwd() instead of the user's
// project — meaning GSD ran against the wrong directory. resolveWorkspaceCwd
// resolves vscode.workspace.workspaceFolders[0].uri.fsPath, computed fresh
// per-invocation (never cached at activate() time). ─────────────────────────
test('resolveWorkspaceCwd resolves workspaceFolders[0].uri.fsPath when a workspace is open', () => {
const mockVscode = { workspace: { workspaceFolders: [{ uri: { fsPath: '/tmp/some-workspace' } }] } };
assert.equal(resolveWorkspaceCwd(mockVscode), '/tmp/some-workspace');
});
test('resolveWorkspaceCwd falls back to process.cwd() when no workspace folder is open (fail-soft, never throws)', () => {
assert.doesNotThrow(() => {
assert.equal(resolveWorkspaceCwd({ workspace: { workspaceFolders: [] } }), process.cwd());
assert.equal(resolveWorkspaceCwd({ workspace: {} }), process.cwd());
assert.equal(resolveWorkspaceCwd({}), process.cwd());
});
});
test('#2103 FIX: all three desktop dispatch surfaces (gsd.invoke command, @gsd chat participant, LM tool invoke) thread the resolved workspace folder as cwd — not process.cwd()', async () => {
// Spy on the SHARED subprocess-shim dispatchGsdCommand (the one every
// surface ultimately calls via extension.js's own dispatchGsdCommand) so we
// observe the actual `cwd` each surface passes, without needing a real
// gsd-tools.cjs project rooted at the mock workspace path.
const originalShimDispatch = shellCommandProjection.dispatchGsdCommand;
const calls = [];
shellCommandProjection.dispatchGsdCommand = (args) => {
calls.push(args);
return originalShimDispatch(args);
};
const registeredCommands = {};
let chatHandler = null;
const toolImpls = [];
const mockVscode = {
commands: {
registerCommand(id, handler) { registeredCommands[id] = handler; return { dispose() {} }; },
},
chat: {
createChatParticipant(id, handler) { chatHandler = handler; return { id, dispose() {} }; },
},
lm: {
registerTool(name, impl) { toolImpls.push({ name, impl }); return { dispose() {} }; },
},
workspace: {
workspaceFolders: [{ uri: { fsPath: '/tmp/mock-gsd-workspace' } }],
getConfiguration: () => ({ get: () => undefined }),
},
LanguageModelTextPart: class { constructor(t) { this.text = t; } },
LanguageModelToolResult: class { constructor(parts) { this.parts = parts; } },
};
const originalLoad = Module._load;
Module._load = function (request, ...rest) {
if (request === 'vscode') return mockVscode;
return originalLoad.call(this, request, ...rest);
};
const extensionPath = path.join(__dirname, '..', 'vscode', 'extension.js');
const hostBindingPath = path.join(__dirname, '..', 'vscode', 'host-binding.js');
try {
delete require.cache[extensionPath];
delete require.cache[hostBindingPath];
const freshExtension = require(extensionPath);
const context = {
subscriptions: [],
globalState: { get: () => undefined, update: () => Promise.resolve() },
};
freshExtension.activate(context);
// Surface 1: gsd.invoke command handler.
calls.length = 0;
await registeredCommands['gsd.invoke']({ family: 'progress', subcommand: 'json' });
assert.equal(calls.length, 1, 'gsd.invoke handler must dispatch exactly once');
assert.equal(calls[0].cwd, '/tmp/mock-gsd-workspace', 'gsd.invoke must thread the workspace folder as cwd');
// Surface 1b: an explicit args.cwd still takes precedence over the resolved workspace.
calls.length = 0;
await registeredCommands['gsd.invoke']({ family: 'progress', subcommand: 'json', cwd: '/explicit/override' });
assert.equal(calls[0].cwd, '/explicit/override', 'an explicit cwd argument must still override the resolved workspace folder');
// Surface 2: @gsd chat participant handler.
calls.length = 0;
let markdownOut = '';
await chatHandler({ prompt: 'progress json' }, {}, { markdown: (t) => { markdownOut += t; } }, {});
assert.equal(calls.length, 1, 'the chat participant handler must dispatch exactly once');
assert.equal(calls[0].cwd, '/tmp/mock-gsd-workspace', 'the chat participant must thread the workspace folder as cwd');
assert.ok(markdownOut.length > 0);
// Surface 3: Language Model Tool invoke().
calls.length = 0;
const progressTool = toolImpls.find((t) => t.name === 'gsd_progress');
assert.ok(progressTool, 'gsd_progress tool must be registered');
await progressTool.impl.invoke({ input: {} }, {});
assert.equal(calls.length, 1, 'the LM tool invoke() must dispatch exactly once');
assert.equal(calls[0].cwd, '/tmp/mock-gsd-workspace', 'the LM tool invoke() must thread the workspace folder as cwd');
} finally {
Module._load = originalLoad;
shellCommandProjection.dispatchGsdCommand = originalShimDispatch;
delete require.cache[extensionPath];
delete require.cache[hostBindingPath];
}
});