import { describe, it, expect } from 'vitest'; import { writeSurface } from './surfaceFixture.js'; import { execFileSync } from 'node:child_process'; import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join, normalize } from 'node:path'; import { McpCommand, READ_ONLY_TOOLS, orientation } from '../mcpCommand.js'; /** * Story 7.1 — a local, read-only server any client can start. * * The interesting test here is the third one. AC-3 forbids a port, a network request, a spawned process and a credential * read, and the SDK's declared dependencies include an HTTP server stack, a process spawner and OAuth — so the claim is * MEASURED against the shipped files rather than taken on trust. */ const SDK = 'node_modules/@modelcontextprotocol/sdk/dist/esm'; describe('the server completes a handshake and declares its capabilities (AC-1)', () => { it('is a command a client can launch as `hexasync mcp`', () => { const cmd = McpCommand(); expect(cmd.name()).toBe('mcp'); // The help text is the integration instruction, so a client author does not have to read the source. expect(cmd.helpInformation() + String(cmd.description())).toMatch( /standard input and output/i, ); }); it('speaks the protocol over stdio: initialize gets a result with capabilities', () => { /** * Driven as a real client would: spawn the built CLI, write one JSON-RPC frame, read the reply. Nothing about the * handshake is asserted from our own code — if the SDK's framing changed, this fails. */ const cli = join(process.cwd(), 'apps/cli/dist/main.js'); expect(existsSync(cli), `${cli} is not built — run \`yarn build\``).toBe( true, ); const request = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'test', version: '1' }, }, }) + '\n'; const out = execFileSync('node', [cli, 'mcp'], { input: request, encoding: 'utf8', timeout: 20_000, }); const reply = JSON.parse(out.split('\n').filter(Boolean)[0]!) as { result?: { capabilities?: Record; serverInfo?: { name?: string }; instructions?: string; }; error?: unknown; }; expect(reply.error).toBeUndefined(); expect(reply.result?.serverInfo?.name).toBe('hexasync'); expect(reply.result?.capabilities).toHaveProperty('tools'); // AC-4's orientation reaches a client that has not called anything yet. expect(reply.result?.instructions ?? '').toMatch( /before you say you are done/i, ); }); }); describe('no tool writes anything (AC-2)', () => { it('every registered tool is a reader, by construction', () => { // The registry, not a naming convention: a tool that wrote something would have to arrive by a different path. expect(READ_ONLY_TOOLS.length).toBeGreaterThan(0); for (const tool of READ_ONLY_TOOLS) { expect(typeof tool.read, tool.name).toBe('function'); expect(tool, tool.name).not.toHaveProperty('write'); } }); it('⛔ the command file makes no filesystem-mutating or process call', () => { /** * The complement to the registry check: the registry could be honest while the action handler wrote something. * * ⚠️ Comments are STRIPPED first. The first version scanned the whole file and failed on this command's own * docblock — the word *"spawner"*, in the sentence explaining that nothing is spawned. That is the third time in * this feature that a prose explanation has been caught by the rule it documents, so the check reads code. */ const source = readFileSync('apps/cli/src/commands/mcpCommand.ts', 'utf8') .replace(/\/\*[\s\S]*?\*\//g, '') .replace(/^\s*\/\/.*$/gm, ''); for (const forbidden of [ 'writeFileSync', 'appendFileSync', 'mkdirSync', 'rmSync', 'unlinkSync', 'renameSync', ]) { expect(source, forbidden).not.toContain(forbidden); } expect(source).not.toMatch(/\bexecFileSync\b|\bspawn\w*\(|child_process/); // …and the stripping did not eat the file: the imports it DOES make are still there. expect(source).toContain('readFileSync'); }); }); describe('⛔ it opens no port, makes no request, spawns nothing, reads no credential (AC-3)', () => { /** * The SDK's declared runtime dependencies include `express`, `hono`, `@hono/node-server`, `cors`, * `express-rate-limit`, `eventsource`, `cross-spawn`, `jose` and `pkce-challenge` — an HTTP server stack, a process * spawner and OAuth, against an AC that forbids all four things they do. * * They live behind the SDK's OTHER entry points. This walks the shipped ESM from the two this command imports and * fails if any becomes reachable — so a future import of an HTTP transport breaks the build instead of quietly * opening a port. */ const FORBIDDEN = [ 'express', 'hono', '@hono/node-server', 'cors', 'express-rate-limit', 'eventsource', 'eventsource-parser', 'cross-spawn', 'jose', 'pkce-challenge', 'raw-body', ]; /** Every external package reachable from a set of ESM entry points. */ function reachable(entries: string[]): Set { const seen = new Set(); const external = new Set(); const stack = [...entries]; const IMPORT = /\bfrom\s+["']([^"']+)["']|import\s*\(\s*["']([^"']+)["']/g; while (stack.length) { const file = stack.pop()!; if (seen.has(file) || !existsSync(file)) continue; seen.add(file); const src = readFileSync(file, 'utf8'); for (const match of src.matchAll(IMPORT)) { const spec = match[1] ?? match[2]; if (!spec || spec.startsWith('node:')) continue; if (spec.startsWith('.')) { const base = normalize(join(dirname(file), spec)); for (const candidate of [ base, `${base}.js`, join(base, 'index.js'), ]) { if (existsSync(candidate)) { stack.push(candidate); break; } } } else { external.add( spec.startsWith('@') ? spec.split('/').slice(0, 2).join('/') : spec.split('/')[0]!, ); } } } return external; } it('the walk finds the SDK at all, so an empty answer cannot pass', () => { // Without this, a moved dist path would make every assertion below vacuous. expect( existsSync(join(SDK, 'server/stdio.js')), `${SDK} — did the SDK layout change?`, ).toBe(true); }); it('no server, spawn or credential package is reachable from the stdio entry points', () => { const packages = reachable([ join(SDK, 'server/mcp.js'), join(SDK, 'server/stdio.js'), ]); expect(packages.size).toBeGreaterThan(0); for (const forbidden of FORBIDDEN) { expect( packages, `${forbidden} is reachable — AC-3 no longer holds`, ).not.toContain(forbidden); } }); it('…and they ARE reachable from the HTTP entry point, so the walk is not blind', () => { /** * The control. If the walk simply found nothing, the assertion above would pass for the wrong reason — so this * proves the same code detects the forbidden packages when they really are there. */ /** * ⛔ This asserted `packages.size > 0`, which `{ zod }` alone satisfies — so a walk blind to precisely the * FORBIDDEN set would have passed the control that exists to detect exactly that. It also opened with * `if (!existsSync(http)) return`, erasing the control without a trace if the SDK moved its entry point. * Measured: the HTTP walk yields `@hono/node-server`, `zod`, `content-type` — one forbidden member, so the * control has to name what it found rather than count it. */ const http = join(SDK, 'server/streamableHttp.js'); expect( existsSync(http), `the SDK no longer has ${http} — this control cannot run and must be repointed`, ).toBe(true); const packages = reachable([http]); const found = FORBIDDEN.filter((name) => packages.has(name)); expect( found, 'the walk found no forbidden package where they are known to be — it is blind', ).not.toHaveLength(0); }); it('the command imports only the two stdio entry points', () => { const source = readFileSync('apps/cli/src/commands/mcpCommand.ts', 'utf8'); const sdkImports = [ ...source.matchAll(/@modelcontextprotocol\/sdk\/([^'"]+)/g), ].map((m) => m[1]); expect(sdkImports.sort()).toEqual(['server/mcp.js', 'server/stdio.js']); }); }); describe('orientation answers an agent that knows nothing (AC-4)', () => { const workspace = (withIndex: boolean): string => { const dir = mkdtempSync(join(tmpdir(), 'mcp-orient-')); if (withIndex) { writeSurface(dir); } return dir; }; it('states what a template is, the two runtimes, and validate-before-done', () => { const dir = workspace(true); try { const text = orientation(dir); // From the generated index — one computation per question, not a second copy written here. expect(text).toMatch(/two runtimes/i); expect(text).toMatch(/never invent an identity/i); // …and the one clause the index does NOT carry, because it is agent workflow rather than template vocabulary. expect(text).toMatch(/hexasync compose/); expect(text).toMatch(/re-validate|Fix, re-validate/i); } finally { rmSync(dir, { recursive: true, force: true }); } }); it('stops before the vocabulary lists — those are what the retrieval tools are for', () => { const dir = workspace(true); try { const text = orientation(dir); expect(text).not.toContain('## Collections'); expect(text).not.toContain('## Worker step types'); // Bounded (PB-2): an orientation that pasted the whole index would defeat its own purpose. expect(text.length).toBeLessThan(4_000); } finally { rmSync(dir, { recursive: true, force: true }); } }); it('⛔ says plainly when nothing is installed, rather than answering from nothing', () => { // NFR-7. An agent told nothing proceeds on its own assumptions, which is what this surface exists to prevent. const dir = workspace(false); try { const text = orientation(dir); expect(text).toMatch(/No HexaSync knowledge is installed/i); expect(text).toMatch(/intellisense install/); expect(text).toMatch(/Do not guess at identifiers/i); // The workflow requirement survives even with no index, because it does not depend on one. expect(text).toMatch(/hexasync compose/); } finally { rmSync(dir, { recursive: true, force: true }); } }); }); describe('the server stamps itself with a version that moves (PS-1)', () => { it('reports the CLI package version, not a frozen literal', () => { /** * ⛔ `version: '1.0.0'` was hardcoded while `apps/cli/package.json` read `2608.7.4`. A client logs and caches this * stamp, and one that never changes tells it nothing ever changed — the opposite of what a stamp is for. */ const pkg = JSON.parse(readFileSync('apps/cli/package.json', 'utf8')) as { version: string; }; const dir = mkdtempSync(join(tmpdir(), 'mcp-stamp-')); try { const out = execFileSync( 'node', [join(process.cwd(), 'apps/cli/dist/main.js'), 'mcp'], { input: `${JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 't', version: '1' }, }, })}\n`, cwd: dir, encoding: 'utf8', }, ); const reply = JSON.parse(out.split('\n')[0]!) as { result?: { serverInfo?: { version?: string } }; }; expect(reply.result?.serverInfo?.version).toBe(pkg.version); expect(reply.result?.serverInfo?.version).not.toBe('1.0.0'); } finally { rmSync(dir, { recursive: true, force: true }); } }); });