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>
97 lines
4.7 KiB
JavaScript
97 lines
4.7 KiB
JavaScript
'use strict';
|
|
/**
|
|
* Property test: getDirName is a pure projection of each runtime descriptor's
|
|
* `localConfigDir`. Because 1.7.0 (ADR-1016 / ADR-1239) makes runtimes
|
|
* pluggable data descriptors, this test asserts the DERIVATION CONTRACT —
|
|
* `getDirName(id) === registry.runtimes[id].runtime.localConfigDir` — for
|
|
* EVERY runtime currently in the registry, rather than pinning a frozen
|
|
* per-runtime golden snapshot that would have to be hand-edited every time a
|
|
* runtime is added or removed. Adding a new runtime descriptor requires zero
|
|
* changes here; if the derivation breaks for any runtime, this fails loudly.
|
|
*
|
|
* Also covers:
|
|
* - the fail-closed fallback (`getDirName('unknown')` / `getDirName('')` → '.claude');
|
|
* - a structural cross-check that every descriptor's localConfigDir is a
|
|
* non-empty dot-dir string.
|
|
*
|
|
* #2103: vscode's `configHome.kind === 'none'` (no file-projected config
|
|
* directory at all) is the first descriptor with `localConfigDir: null` — it
|
|
* is carved out of the "non-empty dot-dir string" invariant below and
|
|
* asserted against getDirName's documented NO_LOCAL_CONFIG_DIR_SENTINEL
|
|
* instead (mirrors the carve-out in
|
|
* tests/non-claude-runtimes-registry-derivation.test.cjs).
|
|
*
|
|
* ADR-1239 Phase B (#1679). Behavioral tests only: assert on returned values.
|
|
*/
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const runtimeNamePolicy = require('../gsd-core/bin/lib/runtime-name-policy.cjs');
|
|
const registry = require('../gsd-core/bin/lib/capability-registry.cjs');
|
|
|
|
const { getDirName, NO_LOCAL_CONFIG_DIR_SENTINEL } = runtimeNamePolicy;
|
|
|
|
const RUNTIME_IDS = Object.keys(registry.runtimes);
|
|
|
|
// Runtimes whose configHome.kind === 'none' have NO file-projected config
|
|
// directory at all (localConfigDir is legitimately null) — carved out of the
|
|
// "non-empty dot-dir string" invariant, the same way dot-home-nested's
|
|
// .parent is a conditional carve-out in the validator.
|
|
const NO_LOCAL_CONFIG_DIR_RUNTIMES = new Set(
|
|
RUNTIME_IDS.filter((id) => {
|
|
const desc = registry.runtimes[id] && registry.runtimes[id].runtime;
|
|
return !!(desc && desc.configHome && desc.configHome.kind === 'none');
|
|
}),
|
|
);
|
|
|
|
test('getDirName(id) projects each descriptor runtime.localConfigDir (derivation contract, count-agnostic)', () => {
|
|
assert.ok(RUNTIME_IDS.length > 0, 'registry must contain at least one runtime');
|
|
for (const id of RUNTIME_IDS) {
|
|
const desc = registry.runtimes[id] && registry.runtimes[id].runtime;
|
|
if (NO_LOCAL_CONFIG_DIR_RUNTIMES.has(id)) {
|
|
// #2103: no file-projected config dir — getDirName must return the
|
|
// documented sentinel, not the '.claude' default and not null.
|
|
assert.strictEqual(desc.localConfigDir, null,
|
|
`${id}: configHome.kind === 'none' runtimes must declare localConfigDir: null`);
|
|
assert.strictEqual(
|
|
getDirName(id),
|
|
NO_LOCAL_CONFIG_DIR_SENTINEL,
|
|
`getDirName('${id}') must equal the documented no-local-config-dir sentinel`);
|
|
continue;
|
|
}
|
|
const expected = desc && desc.localConfigDir;
|
|
assert.ok(typeof expected === 'string' && expected.length > 0,
|
|
`registry.runtimes['${id}'].runtime.localConfigDir must be a non-empty string`);
|
|
assert.strictEqual(
|
|
getDirName(id),
|
|
expected,
|
|
`getDirName('${id}') must equal the descriptor localConfigDir '${expected}'`);
|
|
}
|
|
});
|
|
|
|
test('getDirName fallback: unknown / empty runtime returns ".claude" (fail-closed)', () => {
|
|
assert.strictEqual(getDirName('unknown'), '.claude');
|
|
assert.strictEqual(getDirName(''), '.claude');
|
|
assert.strictEqual(getDirName('__nonexistent_runtime__'), '.claude');
|
|
});
|
|
|
|
test('registry cross-check: every runtimes[id].runtime.localConfigDir is a non-empty dot-dir string, except configHome.kind==="none" runtimes (localConfigDir: null)', () => {
|
|
for (const [id, entry] of Object.entries(registry.runtimes)) {
|
|
if (!entry || typeof entry !== 'object') continue;
|
|
const runtimeBlock = entry.runtime;
|
|
if (!runtimeBlock || typeof runtimeBlock !== 'object') continue;
|
|
if (NO_LOCAL_CONFIG_DIR_RUNTIMES.has(id)) {
|
|
assert.strictEqual(runtimeBlock.localConfigDir, null,
|
|
`registry.runtimes['${id}'].runtime.localConfigDir must be null (configHome.kind === 'none')`);
|
|
continue;
|
|
}
|
|
const dir = runtimeBlock.localConfigDir;
|
|
assert.strictEqual(typeof dir, 'string',
|
|
`registry.runtimes['${id}'].runtime.localConfigDir must be a string (got: ${typeof dir})`);
|
|
assert.ok(dir.length > 0,
|
|
`registry.runtimes['${id}'].runtime.localConfigDir must be non-empty`);
|
|
assert.ok(dir.startsWith('.'),
|
|
`registry.runtimes['${id}'].runtime.localConfigDir must start with '.' (got: ${JSON.stringify(dir)})`);
|
|
}
|
|
});
|