Public: anyone can read this; the crew can change it.

A test fails when an MCP card names a tool tools/list does not answer, or a card kind has no ui:// resource #5387

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/5372-a-test-fails-when-an-mcp-card-names-a-to → main 1 commit fork

  • Merged. scout/5372-a-test-fails-when-an-mcp-card-names-a-to was merged into main as 7538c7b.

Review

Sign in to review this change.
1 file changed +78 −0
test/mcp-cards-coverage.test.ts added +78
@@ -0,0 +1,78 @@
1 /**
2 * The MCP Apps cards stay true: every tool a card names is a tool `tools/list`
3 * answers, and every card kind is a `ui://forge/<kind>` resource that lists and
4 * reads. The lists come from the module and the live answers, never a copy.
5 */
6 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
7 import { mkdtemp, rm } from "node:fs/promises";
8 import { tmpdir } from "node:os";
9 import { join } from "node:path";
10 import { createApp, type App } from "../src/app.ts";
11 import { createToken } from "../src/core/tokens.ts";
12 import { createUser } from "../src/core/users.ts";
13 import { serve, type Servers } from "../src/http/server.ts";
14 import { CARD_KINDS, CARD_OF_TOOL } from "../src/web/studio/cards.ts";
15 import { quietLogs } from "./helpers/globals.ts";
16
17 quietLogs();
18
19 describe("MCP cards coverage", () => {
20 let dataDir: string;
21 let app: App;
22 let servers: Servers;
23 let token = "";
24 let id = 1;
25
26 const rpc = async (method: string, params: unknown = {}) => {
27 const res = await fetch(`${servers.url}/mcp`, {
28 method: "POST",
29 headers: {
30 "content-type": "application/json",
31 accept: "application/json, text/event-stream",
32 authorization: `Bearer ${token}`,
33 },
34 body: JSON.stringify({ jsonrpc: "2.0", id: id++, method, params }),
35 });
36 return (await res.json()) as any;
37 };
38
39 beforeAll(async () => {
40 dataDir = await mkdtemp(join(tmpdir(), "forge-mcp-cards-"));
41 app = createApp({ dataDir, port: 0 });
42 servers = await serve(app);
43 const a = await createUser(app.db, { origin: "seed", trusted: true,
44 username: "alice", email: "[email protected]", password: "a-good-passphrase", adminIfFirst: true });
45 if (!a.ok) throw new Error("users");
46 token = createToken(app.db, a.value.id, "admin", "admin").token;
47 });
48
49 afterAll(async () => {
50 servers.stop();
51 app.close();
52 await rm(dataDir, { recursive: true, force: true });
53 });
54
55 test("every tool a card names is in tools/list", async () => {
56 const names = new Set<string>((await rpc("tools/list")).result.tools.map((t: any) => t.name));
57 const missing = Object.keys(CARD_OF_TOOL).filter((t) => !names.has(t));
58 expect(missing).toEqual([]);
59 });
60
61 test("every card a tool names is a card kind", () => {
62 const kinds: readonly string[] = CARD_KINDS;
63 const unknown = Object.values(CARD_OF_TOOL).filter((k) => !kinds.includes(k));
64 expect(unknown).toEqual([]);
65 });
66
67 test("every card kind is listed as ui://forge/<kind> and reads", async () => {
68 const listed = new Set<string>((await rpc("resources/list")).result.resources.map((r: any) => r.uri));
69 for (const kind of CARD_KINDS) {
70 const uri = `ui://forge/${kind}`;
71 expect(listed.has(uri)).toBe(true);
72 const read = await rpc("resources/read", { uri });
73 expect(read.error).toBeUndefined();
74 expect(read.result.contents[0].uri).toBe(uri);
75 expect(read.result.contents[0].text.length).toBeGreaterThan(0);
76 }
77 });
78 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs