Files
msd-core/tests/mcp-server-catalog.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

112 lines
6.6 KiB
JavaScript

'use strict';
/**
* Failing-first protocol-surface tests for the MCP served catalog — issue
* #3072 (epic #1671 Phase B), `.msd/phase/feat-3072-mcp-served-catalog/
* 40-design.md`.
*
* Covers 50-test-matrix.md rows 1-3 and 31-36: `resources`/`prompts`
* capability advertisement, tool-surface independence, and the
* `prompts/list` / `prompts/get` JSON-RPC surface — all exercised through
* `handleMessage` (`src/mcp-server.cts`, compiled to
* `msd-core/bin/lib/mcp-server.cjs`), mirroring `tests/msd-mcp-server.test.cjs`'s
* own structure and require path.
*
* `handleMessage` does not yet route `resources/*`/`prompts/*` at all — every
* such request currently falls through to the generic `METHOD_NOT_FOUND`
* (-32601) default case. Rows that expect a SPECIFIC refusal (not just "any
* error") therefore assert the JSON-RPC error code is NOT -32601, so the
* assertion cannot pass by accident against today's blanket fallthrough
* (rows 33, 35). No source-grep (CONTRIBUTING.md): every assertion is on
* typed JSON-RPC response fields, never on rendered prose.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const { handleMessage } = require('../msd-core/bin/lib/mcp-server.cjs');
const METHOD_NOT_FOUND = -32601;
const INVALID_PARAMS = -32602;
// ─── initialize / capabilities (rows 1-3) ───────────────────────────────────
describe('initialize / capabilities', () => {
test('initialize advertises resources and prompts (row 1)', () => {
const res = handleMessage({ jsonrpc: '2.0', id: 1, method: 'initialize' });
assert.ok(res.result, 'initialize must succeed');
const caps = res.result.capabilities;
assert.ok(caps && typeof caps.tools === 'object', 'capabilities.tools must remain declared');
assert.ok(caps && typeof caps.resources === 'object', 'capabilities.resources must be declared alongside tools');
assert.ok(caps && typeof caps.prompts === 'object', 'capabilities.prompts must be declared alongside tools');
});
test('initialize does not advertise unimplemented notifications (row 2)', () => {
const res = handleMessage({ jsonrpc: '2.0', id: 1, method: 'initialize' });
const resourcesCap = res.result && res.result.capabilities && res.result.capabilities.resources;
assert.ok(resourcesCap && typeof resourcesCap === 'object', 'capabilities.resources must exist before its shape can be checked');
assert.equal(Object.prototype.hasOwnProperty.call(resourcesCap, 'subscribe'), false, 'resources capability must not declare subscribe — nothing ever sends the notification');
assert.equal(Object.prototype.hasOwnProperty.call(resourcesCap, 'listChanged'), false, 'resources capability must not declare listChanged — nothing ever sends the notification');
});
test('catalog addition does not disturb the tool surface (row 3)', () => {
const res = handleMessage({ jsonrpc: '2.0', id: 1, method: 'tools/list' });
assert.ok(res.result, 'tools/list must succeed');
const names = res.result.tools.map((tool) => tool.name).sort();
assert.deepEqual(names, ['msd_invoke_command', 'msd_read_state', 'msd_write_state']);
});
});
// ─── prompts (rows 31-36) ────────────────────────────────────────────────────
describe('prompts — protocol surface', () => {
test('lists commands as prompts by bare name (row 31)', () => {
const res = handleMessage({ jsonrpc: '2.0', id: 1, method: 'prompts/list' });
assert.equal(res.error, undefined, 'prompts/list must not error');
assert.ok(res.result && Array.isArray(res.result.prompts), 'result.prompts must be an array');
assert.equal(res.result.prompts.length, 72, 'must list all 72 commands/msd/*.md as prompts');
for (const p of res.result.prompts) {
assert.equal(typeof p.name, 'string');
assert.equal(p.name.includes('/'), false, 'name must be the bare command, not a path');
assert.equal(p.name.endsWith('.md'), false, 'name must not carry the .md extension');
}
});
test('gets a prompt in message form (row 32)', () => {
const res = handleMessage({ jsonrpc: '2.0', id: 1, method: 'prompts/get', params: { name: 'plan-phase' } });
assert.equal(res.error, undefined, 'prompts/get on a known name must not error');
assert.equal(typeof res.result.description, 'string');
assert.ok(Array.isArray(res.result.messages) && res.result.messages.length >= 1);
const msg = res.result.messages[0];
assert.equal(msg.role, 'user');
assert.equal(msg.content.type, 'text');
assert.equal(typeof msg.content.text, 'string');
});
test('unknown prompt name errors (row 33)', () => {
const res = handleMessage({ jsonrpc: '2.0', id: 1, method: 'prompts/get', params: { name: 'not-a-real-command-xyz' } });
assert.ok(res.error, 'an unknown prompt name must produce a JSON-RPC error, not a success');
assert.equal(res.result, undefined);
assert.notEqual(res.error.code, METHOD_NOT_FOUND, 'must be a semantic unknown-prompt refusal, not merely the current unimplemented-method fallthrough');
});
test('arguments are accepted and ignored (row 34)', () => {
const withoutArgs = handleMessage({ jsonrpc: '2.0', id: 1, method: 'prompts/get', params: { name: 'plan-phase' } });
const withArgs = handleMessage({ jsonrpc: '2.0', id: 2, method: 'prompts/get', params: { name: 'plan-phase', arguments: { phase: '3' } } });
assert.equal(withoutArgs.error, undefined, 'prompts/get without arguments must succeed');
assert.equal(withArgs.error, undefined, 'prompts/get with arguments must be accepted, not rejected');
assert.deepEqual(withArgs.result, withoutArgs.result, 'no command template takes injected arguments today — content must be unchanged');
});
test('prompt name is not a path (row 35)', () => {
const res = handleMessage({ jsonrpc: '2.0', id: 1, method: 'prompts/get', params: { name: '../../../etc/passwd' } });
assert.ok(res.error, 'a path-shaped prompt name must be refused, not treated as a valid lookup key');
assert.notEqual(res.error.code, METHOD_NOT_FOUND, 'must be a semantic name-is-not-a-path refusal, not merely the current unimplemented-method fallthrough');
});
test('missing prompt name is invalid params (row 36)', () => {
const res = handleMessage({ jsonrpc: '2.0', id: 1, method: 'prompts/get', params: {} });
assert.ok(res.error, 'a missing required "name" must error');
assert.equal(res.error.code, INVALID_PARAMS, 'must be JSON-RPC INVALID_PARAMS (-32602)');
});
});