import { describe, it, expect } from 'vitest'; import { writeSurface } from './surfaceFixture.js'; import { execFileSync } from 'node:child_process'; import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { authoringWorkflow } from '../mcpCommand.js'; /** * Story 7.4 — the authoring workflow is a discoverable COMMAND, not advice. * * A tool is something an agent may call; a prompt is something a client offers a user by name. The distinction is the * story: the two steps that get skipped when this is advice are asserted hardest. */ const CLI = join(process.cwd(), 'apps/cli/dist/main.js'); function withCatalog(): string { const dir = mkdtempSync(join(tmpdir(), 'mcp-prompt-')); writeSurface(dir); return dir; } describe('the workflow is listed as a prompt (AC-1)', () => { it('appears by name, taking arguments', () => { const input = [ { jsonrpc: '2.0', id: 0, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 't', version: '1' }, }, }, { jsonrpc: '2.0', method: 'notifications/initialized' }, { jsonrpc: '2.0', id: 1, method: 'prompts/list' }, ] .map((c) => JSON.stringify(c)) .join('\n') + '\n'; const out = execFileSync('node', [CLI, 'mcp'], { input, encoding: 'utf8', timeout: 25_000, }); const listed = out .split('\n') .filter(Boolean) .map( (l) => JSON.parse(l) as { id?: number; result?: { prompts?: { name: string; arguments?: unknown[] }[] }; }, ) .find((m) => m.id === 1); const prompt = listed?.result?.prompts?.find( (p) => p.name === 'author_an_integration', ); expect( prompt, 'the authoring workflow is not listed as a prompt', ).toBeDefined(); // AC-1 says "argument-taking" — a prompt with no arguments would be a static blob wearing a prompt's name. expect((prompt?.arguments ?? []).length).toBeGreaterThanOrEqual(2); }); }); describe('it names the steps in order, including the two that get skipped (AC-2)', () => { it('⛔ says STOP when the connector is not catalogued', () => { /** * The failure this exists to prevent: an agent that finds no connector invents a `systemCode`, which resolves to * nothing at run time and fails silently. So the instruction is a stop, it names the real supported codes, and it * says why guessing cannot work. */ const dir = withCatalog(); try { const text = authoringWorkflow(dir, 'Acumatica', 'orders'); expect(text).toMatch(/not in the catalog/i); expect(text).toMatch(/Stop here and tell the user/i); expect(text).toMatch(/fails silently/); expect(text).toMatch(/request to the HexaSync team/); // It names real codes, so the agent is told what exists rather than sent to look. expect(text).toMatch(/shopify-public-app/); } finally { rmSync(dir, { recursive: true, force: true }); } }); it('confirms a catalogued system by its CODE, never its display name', () => { const dir = withCatalog(); try { const text = authoringWorkflow(dir, 'shopify-public-app', 'orders'); expect(text).toMatch(/IS catalogued/); expect(text).toMatch(/never the display name/); } finally { rmSync(dir, { recursive: true, force: true }); } }); it('⛔ makes validation a gate, run TWICE, before any claim of success', () => { const dir = withCatalog(); try { const text = authoringWorkflow(dir, 'shopify-public-app', 'orders'); expect(text).toMatch(/Validate, fix, re-validate/i); // Twice, and the reason: a fix can introduce a finding. expect(text).toMatch(/again/i); expect(text).toMatch(/a fix can introduce a finding/); expect(text).toMatch( /unvalidated claim is the one failure you cannot see/, ); // And what to do when it cannot be made clean — the honest alternative to reporting success. expect(text).toMatch(/say which findings remain/); } finally { rmSync(dir, { recursive: true, force: true }); } }); it('numbers the steps, so the order is not a suggestion', () => { const dir = withCatalog(); try { const text = authoringWorkflow(dir, 'shopify-public-app', 'orders'); const steps = [...text.matchAll(/^## (\d)\./gm)].map((m) => Number(m[1])); expect(steps).toEqual([1, 2, 3, 4, 5]); // The connector check is FIRST, because everything after it is wasted if the answer is "stop". expect(text.indexOf('## 1.')).toBeLessThan(text.indexOf('## 5.')); expect(text.slice(text.indexOf('## 1.'), text.indexOf('## 2.'))).toMatch( /connector/i, ); } finally { rmSync(dir, { recursive: true, force: true }); } }); it('⛔ does not pretend the catalog is empty when none is installed', () => { // The difference between "no connector supports this" and "I cannot see the catalog" is the difference between a // correct refusal and a wrong one. const bare = mkdtempSync(join(tmpdir(), 'mcp-bare-prompt-')); try { const text = authoringWorkflow(bare, 'Shopify', 'orders'); expect(text).toMatch(/catalog is not installed/i); expect(text).toMatch(/do not assume it is supported/i); expect(text).not.toMatch(/is not in the catalog/); } finally { rmSync(bare, { recursive: true, force: true }); } }); });