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

A test holds the MCP `issue` tool to its `blockedBy` and `blocks`, and to leaving out an issue the caller cannot see #9074

Merged opened this

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

Merged

torque/forge:torque/9071-a-test-holds-the-mcp-issue-tool-to-its-b → main 1 commit fork

  • Merged. torque/9071-a-test-holds-the-mcp-issue-tool-to-its-b was merged into main as 3a02d6a.

Review

Sign in to review this change.
1 file changed +108 −0
test/mcp-issue-deps.test.ts added +108
@@ -0,0 +1,108 @@
1 /**
2 * The `issue` tool answers `blockedBy` and `blocks` as the REST read does,
3 * and leaves out an issue the caller cannot see (a sealed one).
4 */
5 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
6 import { mkdtemp, rm } from "node:fs/promises";
7 import { tmpdir } from "node:os";
8 import { join } from "node:path";
9 import { createApp, type App } from "../src/app.ts";
10 import { createRepo } from "../src/actions/repos.ts";
11 import { createIssue } from "../src/core/issues.ts";
12 import { addDependency } from "../src/core/dependencies.ts";
13 import { createToken } from "../src/core/tokens.ts";
14 import { createUser } from "../src/core/users.ts";
15 import { serve, type Servers } from "../src/http/server.ts";
16
17 describe("MCP issue dependencies", () => {
18 let dataDir: string;
19 let app: App;
20 let servers: Servers;
21 let aliceTok: string;
22 let bobTok: string;
23 let id = 1;
24 const no: Record<"a" | "b" | "c" | "s" | "t", number> = { a: 0, b: 0, c: 0, s: 0, t: 0 };
25
26 const tool = async (token: string, number: number) => {
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({
35 jsonrpc: "2.0",
36 id: id++,
37 method: "tools/call",
38 params: { name: "issue", arguments: { repo: "alice/game", number } },
39 }),
40 });
41 const body: any = await res.json();
42 return JSON.parse(body.result.content[0].text);
43 };
44 const pairs = (xs: any[]) => xs.map((x) => ({ number: x.number, title: x.title }));
45
46 beforeAll(async () => {
47 dataDir = await mkdtemp(join(tmpdir(), "forge-mcp-issue-deps-"));
48 app = createApp({ dataDir, port: 0 });
49 servers = await serve(app);
50 const a = await createUser(app.db, { origin: "seed", trusted: true,
51 username: "alice", email: "[email protected]",
52 password: "a-good-passphrase", adminIfFirst: true });
53 if (!a.ok) throw new Error(a.error);
54 const b = await createUser(app.db, { origin: "seed", trusted: true,
55 username: "bob", email: "[email protected]", password: "a-good-passphrase" });
56 if (!b.ok) throw new Error(b.error);
57 aliceTok = createToken(app.db, a.value.id, "ro", "ro").token;
58 bobTok = createToken(app.db, b.value.id, "ro", "ro").token;
59 const repo = await createRepo(app, a.value, { name: "game", visibility: "public" });
60 if (!repo.ok) throw new Error(String(repo.error));
61 const mk = (title: string, sealed = false) => {
62 const r = createIssue(app.db, { repoId: repo.value.id, authorId: a.value.id, title, sealed });
63 if (!r.ok) throw new Error(r.error);
64 return r.value;
65 };
66 const A = mk("Issue A"), B = mk("Issue B"), C = mk("Issue C");
67 const S = mk("Sealed blocker", true), T = mk("Sealed dependant", true);
68 no.a = A.number; no.b = B.number; no.c = C.number; no.s = S.number; no.t = T.number;
69 for (const [i, bl] of [[A, B], [C, A], [A, S], [T, A]] as const) {
70 const r = addDependency(app.db, i, bl, a.value.id);
71 if (!r.ok) throw new Error(r.error);
72 }
73 }, 30_000);
74
75 afterAll(async () => {
76 servers.stop();
77 app.close();
78 await rm(dataDir, { recursive: true, force: true });
79 });
80
81 test("blockedBy and blocks name the pairs the REST read returns", async () => {
82 const r = await tool(bobTok, no.a);
83 expect(pairs(r.blockedBy)).toEqual([{ number: no.b, title: "Issue B" }]);
84 expect(pairs(r.blocks)).toEqual([{ number: no.c, title: "Issue C" }]);
85 const res = await fetch(`${servers.url}/api/v1/repos/alice/game/issues/${no.a}/dependencies`, {
86 headers: { authorization: `Bearer ${bobTok}` },
87 });
88 const rest: any = await res.json();
89 expect(pairs(rest.blockedBy)).toEqual(pairs(r.blockedBy));
90 expect(pairs(rest.blocks)).toEqual(pairs(r.blocks));
91 });
92
93 test("a sealed blocker the caller cannot see is left out of blockedBy", async () => {
94 const r = await tool(bobTok, no.a);
95 expect(r.blockedBy.map((x: any) => x.number)).not.toContain(no.s);
96 expect(JSON.stringify(r)).not.toContain("Sealed blocker");
97 const own = await tool(aliceTok, no.a);
98 expect(own.blockedBy.map((x: any) => x.number)).toEqual([no.b, no.s]);
99 });
100
101 test("a sealed dependant the caller cannot see is left out of blocks", async () => {
102 const r = await tool(bobTok, no.a);
103 expect(r.blocks.map((x: any) => x.number)).not.toContain(no.t);
104 expect(JSON.stringify(r)).not.toContain("Sealed dependant");
105 const own = await tool(aliceTok, no.a);
106 expect(own.blocks.map((x: any) => x.number)).toEqual([no.c, no.t]);
107 });
108 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs