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

A test pins MCP `teams`: a person admin changes a team's charter, dial or members, planned first, and a malformed change is refused #7194

Merged opened this

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

Merged

scout/forge:scout/7167-a-test-pins-mcp-teams-a-person-admin-cha → main 1 commit fork

  • Merged. scout/7167-a-test-pins-mcp-teams-a-person-admin-cha was merged into main as 250d132.

Review

Sign in to review this change.
1 file changed +134 −0
test/mcp-teams.test.ts added +134
@@ -0,0 +1,134 @@
1 /** The `teams` tool: a person admin plans, then changes, a team; anyone else is told there is no such tool. */
2 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp, type App } from "../src/app.ts";
7 import { createAgent } from "../src/actions/crew.ts";
8 import { listTeams } from "../src/core/teams.ts";
9 import { createToken } from "../src/core/tokens.ts";
10 import { createUser } from "../src/core/users.ts";
11 import { serve, type Servers } from "../src/http/server.ts";
12
13 describe("MCP teams", () => {
14 let dataDir: string;
15 let app: App;
16 let servers: Servers;
17 let admin: string;
18 let agentAdmin: string;
19 let plain: string;
20 let slug: string;
21 let botId: string;
22 let id = 1;
23
24 const tool = async (token: string, args: unknown) => {
25 const res = await fetch(`${servers.url}/mcp`, {
26 method: "POST",
27 headers: {
28 "content-type": "application/json",
29 accept: "application/json, text/event-stream",
30 authorization: `Bearer ${token}`,
31 },
32 body: JSON.stringify({
33 jsonrpc: "2.0",
34 id: id++,
35 method: "tools/call",
36 params: { name: "teams", arguments: args },
37 }),
38 });
39 const body = (await res.json()) as any;
40 const text = body.result.content[0].text as string;
41 return { text, isError: body.result.isError === true };
42 };
43 const team = () => listTeams(app.db).find((t) => t.slug === slug)!;
44
45 beforeAll(async () => {
46 dataDir = await mkdtemp(join(tmpdir(), "forge-mcp-teams-"));
47 app = createApp({ dataDir, port: 0 });
48 servers = await serve(app);
49 const mk = async (username: string) => {
50 const u = await createUser(app.db, {
51 origin: "seed",
52 trusted: true,
53 username,
54 email: `${username}@forge.local`,
55 password: "a-good-passphrase",
56 });
57 if (!u.ok) throw new Error(u.error);
58 return u.value;
59 };
60 const alice = await mk("alice");
61 const bob = await mk("bob");
62 app.db.run("UPDATE user SET is_admin = 1 WHERE id = ?", [alice.id]);
63 app.db.run("UPDATE user SET is_admin = 0 WHERE id = ?", [bob.id]);
64 const bot = await createAgent(app, { ...alice, isAdmin: true }, { username: "bot" });
65 if (!bot.ok) throw new Error(bot.error);
66 botId = bot.value.user.id;
67 app.db.run("UPDATE user SET is_admin = 1 WHERE id = ?", [botId]);
68 admin = createToken(app.db, alice.id, "admin", "admin").token;
69 plain = createToken(app.db, bob.id, "admin", "admin").token;
70 agentAdmin = createToken(app.db, botId, "admin", "admin").token;
71 slug = listTeams(app.db)[0]!.slug;
72 }, 30_000);
73
74 afterAll(async () => {
75 servers.stop();
76 app.close();
77 await rm(dataDir, { recursive: true, force: true });
78 });
79
80 test("without a slug it lists the teams", async () => {
81 const r = await tool(admin, {});
82 expect(r.isError).toBe(false);
83 expect(JSON.parse(r.text).teams.map((t: any) => t.slug)).toContain(slug);
84 });
85
86 test("a dial is planned by default, changes nothing, and applies with dryRun false", async () => {
87 const before = team();
88 const plan = await tool(admin, { slug, dial: 7 });
89 expect(plan.isError).toBe(false);
90 expect(plan.text).toContain("Would change");
91 expect(team()).toEqual(before);
92 const said = await tool(admin, { slug, dial: 7, dryRun: true });
93 expect(said.isError).toBe(false);
94 expect(team().dial).toBe(before.dial);
95
96 const done = await tool(admin, { slug, dial: 7, charter: "Ship small.", members: [botId], dryRun: false });
97 expect(done.isError).toBe(false);
98 expect(team().dial).toBe(7);
99 expect(team().charter).toBe("Ship small.");
100 expect(team().members).toEqual([botId]);
101 });
102
103 test("a malformed change is refused, planned or applied, and the team is unchanged", async () => {
104 const before = team();
105 const bads: Record<string, unknown>[] = [
106 { dial: "50" },
107 { dial: 101 },
108 { dial: -1 },
109 { dial: 1.5 },
110 { members: "bot" },
111 { members: [1, 2] },
112 { members: ["nobody"] },
113 { charter: 5 },
114 ];
115 for (const change of bads)
116 for (const dryRun of [true, false]) {
117 const r = await tool(admin, { slug, ...change, dryRun });
118 expect(r.isError).toBe(true);
119 expect(team()).toEqual(before);
120 }
121 expect((await tool(admin, { slug, dial: "50" })).text).toContain("invalid_body");
122 expect((await tool(admin, { slug: "nope", dial: 5 })).isError).toBe(true);
123 });
124
125 test("an agent admin and a non-admin are refused, and nothing changes", async () => {
126 const before = team();
127 for (const token of [agentAdmin, plain]) {
128 expect((await tool(token, {})).isError).toBe(true);
129 expect((await tool(token, { slug, dial: 3 })).isError).toBe(true);
130 expect((await tool(token, { slug, dial: 3, dryRun: false })).isError).toBe(true);
131 }
132 expect(team()).toEqual(before);
133 });
134 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs