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.
112 lines
6.6 KiB
JavaScript
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)');
|
|
});
|
|
});
|