Files
msd-core/tests/vscode-lm-tools.test.cjs
Jakub Zych a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
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.
2026-10-06 01:47:40 +02:00

184 lines
9.0 KiB
JavaScript

'use strict';
/**
* VS Code Language Model Tools test — #2103 UPGRADE 1.
*
* Proves the MSD extension's Language Model Tools are keystone-WIRED:
* 1. contributes.languageModelTools is present in package.json and its
* `name` entries match the runtime registration names (extension.js's
* LM_TOOLS / browser.js's LM_TOOL_NAMES) exactly — a mismatch here would
* mean VS Code rejects the tool registration at activation.
* 2. registerLanguageModelTools() calls vscode.lm.registerTool for every
* manifest entry (mock vscode.lm — no real VS Code host available in CI).
* 3. A registered tool's invoke() dispatches through the SAME shared
* dispatchMsdCommand as msd.invoke (desktop) and returns REAL output —
* the "user can invoke X" proof, matching the reachability-test pattern.
* 4. The desktop and web entries register the identical tool NAME set (only
* the invoke() behavior differs — real dispatch vs. honest web-mode message).
*/
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const pkg = require('../vscode/package.json');
const extension = require('../vscode/extension.js');
const browser = require('../vscode/browser.js');
const { createTempDir, cleanup } = require('./helpers.cjs');
class FakeTextPart {
constructor(text) { this.text = text; }
}
class FakeToolResult {
constructor(parts) { this.parts = parts; }
}
// `workspaceCwd`: createLanguageModelTool's invoke() resolves cwd via
// resolveWorkspaceCwd(vscode) — vscode.workspace.workspaceFolders[0].uri.fsPath
// — NOT via a `cwd` field on the invoke() options object (LanguageModelToolInvocationOptions
// has no such field on the real API; see extension.js's #2103 FIX comment on
// createLanguageModelTool). A test that wants a real dispatch to run against a
// fixture directory must mock workspace.workspaceFolders here, not pass
// `{ cwd }` in the options object handed to invoke() (that field is simply
// never read).
function mockVscodeLm(workspaceCwd) {
const registered = [];
return {
lm: {
registerTool(name, impl) {
registered.push({ name, impl });
return { dispose() {} };
},
},
workspace: workspaceCwd
? { workspaceFolders: [{ uri: { fsPath: workspaceCwd } }] }
: undefined,
LanguageModelTextPart: FakeTextPart,
LanguageModelToolResult: FakeToolResult,
registered,
};
}
test('package.json contributes.languageModelTools is present with well-formed entries', () => {
assert.ok(pkg.contributes && Array.isArray(pkg.contributes.languageModelTools),
'contributes.languageModelTools must be an array');
assert.ok(pkg.contributes.languageModelTools.length > 0, 'must declare at least one tool');
for (const tool of pkg.contributes.languageModelTools) {
assert.equal(typeof tool.name, 'string');
assert.ok(tool.name.length > 0);
assert.equal(typeof tool.toolReferenceName, 'string');
assert.equal(typeof tool.displayName, 'string');
assert.equal(typeof tool.modelDescription, 'string');
assert.equal(typeof tool.userDescription, 'string');
assert.equal(tool.canBeReferencedInPrompt, true);
assert.ok(Array.isArray(tool.tags));
assert.equal(typeof tool.inputSchema, 'object');
}
});
test('manifest tool names exactly match extension.js LM_TOOLS registration names', () => {
const manifestNames = pkg.contributes.languageModelTools.map((t) => t.name).sort();
const runtimeNames = extension.LM_TOOLS.map((t) => t.name).sort();
assert.deepEqual(runtimeNames, manifestNames,
'a mismatch here means VS Code would reject the runtime registerTool call against the manifest');
});
test('manifest tool names exactly match browser.js LM_TOOL_NAMES (web entry registers the same surface)', () => {
const manifestNames = pkg.contributes.languageModelTools.map((t) => t.name).sort();
assert.deepEqual([...browser.LM_TOOL_NAMES].sort(), manifestNames);
});
test('REACHABILITY (desktop): registerLanguageModelTools registers every manifest tool via vscode.lm.registerTool', () => {
const mock = mockVscodeLm();
const context = { subscriptions: [] };
const count = extension.registerLanguageModelTools(mock, context);
assert.equal(count, pkg.contributes.languageModelTools.length);
assert.equal(mock.registered.length, pkg.contributes.languageModelTools.length);
assert.equal(context.subscriptions.length, pkg.contributes.languageModelTools.length);
assert.deepEqual(mock.registered.map((r) => r.name).sort(), extension.LM_TOOLS.map((t) => t.name).sort());
});
test('REACHABILITY (desktop): msd_progress tool.invoke() dispatches through the 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');
// The LM tool's invoke() resolves cwd via vscode.workspace.workspaceFolders
// (resolveWorkspaceCwd), not via an options.cwd field — pass the fixture
// dir through the mock's workspace so dispatch actually runs against it.
const mock = mockVscodeLm(dir);
extension.registerLanguageModelTools(mock, { subscriptions: [] });
const progressTool = mock.registered.find((r) => r.name === 'msd_progress');
assert.ok(progressTool, 'msd_progress must be registered');
const result = await progressTool.impl.invoke({ input: {} }, {});
assert.ok(result instanceof FakeToolResult, 'invoke must return a LanguageModelToolResult');
assert.ok(Array.isArray(result.parts) && result.parts.length === 1);
assert.ok(result.parts[0] instanceof FakeTextPart, 'result part must be a LanguageModelTextPart');
const parsed = JSON.parse(result.parts[0].text);
assert.equal(typeof parsed.percent, 'number', 'the real progress command ran (engine reached, not a stub)');
} finally {
cleanup(dir);
}
});
test('REACHABILITY (desktop): msd_plan_phase tool.invoke() forwards the "phase" input through dispatch (real, not UnknownCommand)', async () => {
const dir = createTempDir();
try {
// The LM tool's invoke() resolves cwd via vscode.workspace.workspaceFolders
// (resolveWorkspaceCwd), not via an options.cwd field — pass the fixture
// dir through the mock's workspace so dispatch actually runs against it.
const mock = mockVscodeLm(dir);
extension.registerLanguageModelTools(mock, { subscriptions: [] });
const planPhaseTool = mock.registered.find((r) => r.name === 'msd_plan_phase');
assert.ok(planPhaseTool);
const result = await planPhaseTool.impl.invoke({ input: { phase: 'nonexistent-phase-8675309' } }, {});
const parsed = JSON.parse(result.parts[0].text);
// Real dispatch reaches msd-tools.cjs and returns a structured "phase not
// found" response (proves the engine was reached) — not the manifest's
// own family/subcommand rejected as unknown.
assert.equal(parsed.phase, 'nonexistent-phase-8675309');
assert.ok('error' in parsed || 'plans' in parsed, 'expected a real phase-plan-index response shape');
} finally {
cleanup(dir);
}
});
test('msd_workstreams tool.invoke() dispatches through the hub and returns REAL output', async () => {
const dir = createTempDir();
try {
// The LM tool's invoke() resolves cwd via vscode.workspace.workspaceFolders
// (resolveWorkspaceCwd), not via an options.cwd field — pass the fixture
// dir through the mock's workspace so dispatch actually runs against it.
const mock = mockVscodeLm(dir);
extension.registerLanguageModelTools(mock, { subscriptions: [] });
const wsTool = mock.registered.find((r) => r.name === 'msd_workstreams');
const result = await wsTool.impl.invoke({ input: {} }, {});
const parsed = JSON.parse(result.parts[0].text);
assert.ok('workstreams' in parsed || 'mode' in parsed, 'expected a real workstream list response shape');
} finally {
cleanup(dir);
}
});
test('registerLanguageModelTools fails soft (returns 0, does not throw) when vscode.lm is absent', () => {
assert.doesNotThrow(() => {
const count = extension.registerLanguageModelTools({}, { subscriptions: [] });
assert.equal(count, 0);
});
});
test('WEB MODE: browser.js registerLanguageModelTools registers the same names but invoke() returns an honest web-mode message (no engine dispatch)', async () => {
const mock = mockVscodeLm();
const count = browser.registerLanguageModelTools(mock, { subscriptions: [] });
assert.equal(count, browser.LM_TOOL_NAMES.length);
const progressTool = mock.registered.find((r) => r.name === 'msd_progress');
const result = await progressTool.impl.invoke({ input: {} }, {});
assert.match(result.parts[0].text, /web mode/i);
assert.match(result.parts[0].text, /MCP server/);
});