Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
157 lines
6.6 KiB
JavaScript
157 lines
6.6 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 fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
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.MSD_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 {
|
|
// #3217 (ADR-3180 §7.6 rule 4): a free-form ROADMAP.md (no version
|
|
// token) is COMPLETE scope for windowing (§7.1) — without this, a
|
|
// bare temp dir has no ROADMAP.md at all (UNREADABLE) and `percent`
|
|
// is withheld (null), breaking this reachability proxy.
|
|
fs.mkdirSync(path.join(dir, '.planning'), { recursive: true });
|
|
fs.writeFileSync(path.join(dir, '.planning', 'ROADMAP.md'), '# Roadmap\n');
|
|
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);
|
|
});
|
|
});
|