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

A test pins MCP `feed_chips`: switching a chip off hides it from `feed` and `asks`, and bad input changes nothing #7072

Merged opened this

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

Merged

scout/forge:scout/7063-a-test-pins-mcp-feed-chips-switching-a-c → main 1 commit fork

  • Merged. scout/7063-a-test-pins-mcp-feed-chips-switching-a-c was merged into main as c2b9bde.

Review

Sign in to review this change.
1 file changed +168 −0
test/mcp-feed-chips.test.ts added +168
@@ -0,0 +1,168 @@
1 /** `feed_chips` through `/mcp`: a switched-off chip leaves `feed` and `asks`, bad input stores nothing, and chips are one's own. */
2 import { afterAll, beforeAll, describe, expect, setDefaultTimeout, 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 { addToCrew, createAgent } from "../src/actions/crew.ts";
8 import { assignIssue } from "../src/actions/issues.ts";
9 import { createOrg } from "../src/actions/orgs.ts";
10 import { openPull } from "../src/actions/pulls.ts";
11 import { commitToBranch, createRepo } from "../src/actions/repos.ts";
12 import { chipsOf } from "../src/core/feed_chips.ts";
13 import { getIssueById } from "../src/core/issues.ts";
14 import { claimRun, createRun, finishRun } from "../src/core/runs.ts";
15 import { createToken } from "../src/core/tokens.ts";
16 import { createUser, getUserByName, type User } from "../src/core/users.ts";
17 import { serve, type Servers } from "../src/http/server.ts";
18 import { quietLogs } from "./helpers/globals.ts";
19 import { mcpTool } from "./helpers/mcp.ts";
20
21 quietLogs();
22 setDefaultTimeout(30_000);
23
24 describe("MCP feed_chips", () => {
25 let dataDir = "";
26 let app: App;
27 let servers: Servers;
28 let alice: User;
29 let bob: User;
30 let aliceTok = "";
31 let bobTok = "";
32 let pullId = "";
33
34 const chips = (tok: string, args: unknown = {}) => mcpTool(servers.url, tok, "feed_chips", args);
35 const feedIds = async (tok: string): Promise<string[]> =>
36 ((await mcpTool(servers.url, tok, "feed", {})).value.items as { id: string }[]).map((i) => i.id);
37 const askCount = async (tok: string): Promise<number> =>
38 (await mcpTool(servers.url, tok, "asks", {})).value.rows.length;
39 const pullItem = async (tok: string) =>
40 ((await mcpTool(servers.url, tok, "feed", {})).value.items as { id: string; kind: string }[]).filter(
41 (i) => i.kind === "pull",
42 );
43
44 beforeAll(async () => {
45 dataDir = await mkdtemp(join(tmpdir(), "forge-mcp-feed-chips-"));
46 app = createApp({ dataDir, port: 0, disk: () => ({ freeBytes: 500e9, totalBytes: 1000e9 }) });
47 servers = await serve(app);
48 for (const name of ["alice", "bob"]) {
49 const made = await createUser(app.db, {
50 origin: "seed",
51 trusted: true,
52 username: name,
53 email: `${name}@forge.local`,
54 password: "a-good-passphrase",
55 adminIfFirst: name === "alice",
56 });
57 if (!made.ok) throw new Error(made.error);
58 if (name === "alice") alice = made.value;
59 else bob = made.value;
60 }
61 aliceTok = createToken(app.db, alice.id, "rw", "rw").token;
62 bobTok = createToken(app.db, bob.id, "rw", "rw").token;
63 await createAgent(app, alice, { username: "bot" });
64 const made = await createRepo(app, alice, { name: "open", visibility: "public" });
65 if (!made.ok) throw new Error(String(made.error));
66 const repo = made.value;
67 await commitToBranch(app, alice, repo, {
68 message: "seed",
69 files: [{ path: "README.md", content: "# open\n" }],
70 });
71 addToCrew(app, alice, repo, "bot");
72 await commitToBranch(app, alice, repo, {
73 branch: "bot/1",
74 base: "main",
75 message: "Chip work",
76 files: [{ path: "F.md", content: "1\n" }],
77 });
78 const opened = await openPull(app, getUserByName(app.db, "bot")!, repo, {
79 title: "Chip work",
80 body: "",
81 baseBranch: "main",
82 headBranch: "bot/1",
83 });
84 if (!opened.ok) throw new Error(String(opened.error));
85 pullId = opened.value.id;
86 const run = createRun(app.db, {
87 repoId: repo.id,
88 kind: "check",
89 issueId: pullId,
90 env: `pull-${opened.value.number}`,
91 commitOid: opened.value.headOid,
92 reason: "pull",
93 });
94 claimRun(app.db, run.id);
95 finishRun(app.db, run.id, { state: "ok", summary: "ok" });
96 expect(assignIssue(app, alice, repo, getIssueById(app.db, pullId)!, alice).ok).toBe(true);
97 if (!createOrg(app, alice, "guild").ok) throw new Error("org");
98 });
99
100 afterAll(async () => {
101 servers.stop();
102 app.close();
103 await rm(dataDir, { recursive: true, force: true });
104 });
105
106 test("with no kind it lists the switched-off chips, none at first", async () => {
107 expect((await chips(aliceTok)).value).toEqual({ kinds: [], people: [] });
108 });
109
110 test("a kind chip switched off leaves the pull out of feed and asks; on brings it back", async () => {
111 expect(await pullItem(aliceTok)).toHaveLength(1);
112 expect(await askCount(aliceTok)).toBe(1);
113 const off = await chips(aliceTok, { kind: "kind", value: "reviews", hidden: true });
114 expect(off.value).toEqual({ kinds: ["reviews"], people: [] });
115 expect((await chips(aliceTok)).value.kinds).toEqual(["reviews"]);
116 expect(await pullItem(aliceTok)).toHaveLength(0);
117 expect(await askCount(aliceTok)).toBe(0);
118 const on = await chips(aliceTok, { kind: "kind", value: "reviews", hidden: false });
119 expect(on.value).toEqual({ kinds: [], people: [] });
120 expect(await pullItem(aliceTok)).toHaveLength(1);
121 expect(await askCount(aliceTok)).toBe(1);
122 });
123
124 test("a person chip hides that person's items, by username", async () => {
125 const off = await chips(aliceTok, { kind: "person", value: "bot", hidden: true });
126 expect(off.value).toEqual({ kinds: [], people: ["bot"] });
127 expect(await pullItem(aliceTok)).toHaveLength(0);
128 expect(await askCount(aliceTok)).toBe(0);
129 expect((await chips(aliceTok, { kind: "person", value: "bot", hidden: false })).value.people).toEqual([]);
130 expect(await pullItem(aliceTok)).toHaveLength(1);
131 expect(await askCount(aliceTok)).toBe(1);
132 });
133
134 test("an unknown name or an org answers not_found", async () => {
135 for (const value of ["nobody", "guild"]) {
136 const r = await chips(aliceTok, { kind: "person", value, hidden: true });
137 expect(r.error).toContain("not_found");
138 }
139 expect(chipsOf(app.db, alice.id).people.size).toBe(0);
140 });
141
142 test("a missing or non-boolean hidden, or a kind with no chip, answers invalid and stores nothing", async () => {
143 const bads: unknown[] = [
144 { kind: "kind", value: "reviews" },
145 { kind: "kind", value: "reviews", hidden: "true" },
146 { kind: "kind", value: "reviews", hidden: 1 },
147 { kind: "person", value: "bot" },
148 { kind: "person", value: "bot", hidden: "yes" },
149 { kind: "kind", value: "runs", hidden: true },
150 { kind: "kind", hidden: true },
151 ];
152 for (const args of bads) expect((await chips(aliceTok, args)).error).toContain("invalid");
153 const stored = chipsOf(app.db, alice.id);
154 expect(stored.kinds.size + stored.people.size).toBe(0);
155 expect((await chips(aliceTok)).value).toEqual({ kinds: [], people: [] });
156 });
157
158 test("one person's chips never change another's feed", async () => {
159 const before = await feedIds(bobTok);
160 await chips(aliceTok, { kind: "kind", value: "reviews", hidden: true });
161 await chips(aliceTok, { kind: "person", value: "bot", hidden: true });
162 expect((await chips(bobTok)).value).toEqual({ kinds: [], people: [] });
163 expect(await feedIds(bobTok)).toEqual(before);
164 expect(await pullItem(bobTok)).toHaveLength(1);
165 await chips(aliceTok, { kind: "kind", value: "reviews", hidden: false });
166 await chips(aliceTok, { kind: "person", value: "bot", hidden: false });
167 });
168 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs