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

A test pins MCP `board_delete`: a person admin removes a board, an unknown slug is `not_found`, an agent is refused #7107

Merged opened this

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

Merged

scout/forge:scout/7059-a-test-pins-mcp-board-delete-a-person-ad → main 1 commit fork

  • Merged. scout/7059-a-test-pins-mcp-board-delete-a-person-ad was merged into main as 5ad9e75.

Review

Sign in to review this change.
1 file changed +89 −0
test/mcp-board-delete.test.ts added +89
@@ -0,0 +1,89 @@
1 /** MCP `board_delete`: a person admin removes a board; agents and non-admins are refused. */
2
3 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
4 import { mkdtemp, rm } from "node:fs/promises";
5 import { tmpdir } from "node:os";
6 import { join } from "node:path";
7 import { createApp, type App } from "../src/app.ts";
8 import { makeBoard } from "../src/actions/boards.ts";
9 import { createToken } from "../src/core/tokens.ts";
10 import { createUser, getUserByName } from "../src/core/users.ts";
11 import { serve, type Servers } from "../src/http/server.ts";
12 import { quietLogs } from "./helpers/globals.ts";
13
14 quietLogs();
15
16 const PASS = "a-long-board-delete-passphrase";
17
18 describe("MCP board_delete", () => {
19 let dataDir = "";
20 let app: App;
21 let servers: Servers;
22 let alice: string;
23 let bot: string;
24 let bob: string;
25
26 const user = (name: string) => getUserByName(app.db, name)!;
27 const call = async (token: string, args: unknown) => {
28 const res = await fetch(`${servers.url}/mcp`, {
29 method: "POST",
30 headers: { "content-type": "application/json", accept: "application/json, text/event-stream", authorization: `Bearer ${token}` },
31 body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "board_delete", arguments: args } }),
32 });
33 const body = (await res.json()) as any;
34 return { error: body.result.isError === true, text: body.result.content[0].text as string };
35 };
36 const board = (slug: string, token: string) =>
37 fetch(`${servers.url}/api/v1/boards/${slug}`, { headers: { authorization: `Bearer ${token}` } });
38 const fresh = async (title: string) => {
39 const made = await makeBoard(app, user("alice"), title);
40 if (!made.ok) throw new Error(`board ${title}`);
41 return made.value;
42 };
43
44 beforeAll(async () => {
45 dataDir = await mkdtemp(join(tmpdir(), "forge-mcp-board-delete-"));
46 app = createApp({ dataDir, port: 0 });
47 servers = await serve(app);
48 for (const [username, isAdmin] of [["alice", true], ["robo", true], ["bob", false]] as const) {
49 const made = await createUser(app.db, { origin: "seed", trusted: true, username, email: `${username}@x.test`, password: PASS, isAdmin });
50 if (!made.ok) throw new Error(username);
51 }
52 app.db.query("UPDATE user SET is_agent = 1 WHERE username = 'robo'").run();
53 alice = createToken(app.db, user("alice").id, "t", "admin").token;
54 bot = createToken(app.db, user("robo").id, "t", "admin").token;
55 bob = createToken(app.db, user("bob").id, "t", "rw").token;
56 }, 30_000);
57
58 afterAll(async () => {
59 servers.stop();
60 app.close();
61 await rm(dataDir, { recursive: true, force: true });
62 });
63
64 test("a person admin deletes a board, which then answers 404", async () => {
65 const slug = await fresh("Doomed");
66 expect((await board(slug, alice)).status).toBe(200);
67 const done = await call(alice, { slug });
68 expect(done.error).toBe(false);
69 expect(JSON.parse(done.text)).toEqual({ deleted: slug });
70 expect((await board(slug, alice)).status).toBe(404);
71 });
72
73 test("an unknown slug is not_found", async () => {
74 const done = await call(alice, { slug: "no-such-board" });
75 expect(done.error).toBe(true);
76 expect(done.text).toContain("not_found");
77 expect(done.text).toContain("no such board");
78 });
79
80 test("an agent admin and a non-admin are refused, and the board stays", async () => {
81 const slug = await fresh("Kept");
82 for (const token of [bot, bob]) {
83 const done = await call(token, { slug });
84 expect(done.error).toBe(true);
85 expect(done.text).toContain("forbidden");
86 }
87 expect((await board(slug, alice)).status).toBe(200);
88 });
89 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs