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

An outsider's report gets the same "Received" sentence from one module, through the API and MCP #5580

Merged opened this

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

Merged

mason/forge:mason/5560-an-outsider-s-report-gets-the-same-recei → main 3 commits fork

  • Merged. mason/5560-an-outsider-s-report-gets-the-same-recei was merged into main as ae162b2.

Review

Sign in to review this change.
4 files changed +113 −4
src/actions/report_words.ts added +9
@@ -0,0 +1,9 @@
1 /**
2 * The words an outsider reads back after filing a report. Every door — the
3 * API's POST …/reports and the MCP `report` tool — answers from here, so
4 * the sentence reads the same wherever the report came in.
5 */
6
7 /** The answer to a report that was filed and now waits for a person. */
8 export const REPORT_RECEIVED =
9 "Received. A person who maintains this repository will read it; nothing acts on it before they do.";
src/http/api.ts +2 −2
@@ -465,6 +465,7 @@ import {
465465 waitingReports,
466466 } from "../actions/reports.ts";
467467 import { BODY_MAX, REQUEST_MAX, TITLE_MAX } from "../core/reports.ts";
468 import { REPORT_RECEIVED } from "../actions/report_words.ts";
468469 import { ADMIN_WORDS } from "../actions/admin_words.ts";
469470 import { SPEND_WORDS } from "../actions/spend_words.ts";
470471
@@ -3324,8 +3325,7 @@ export function apiRoutes(router: Router<Handler>): void {
33243325 {
33253326 id: filed.value.id,
33263327 state: filed.value.state,
3327 message:
3328 "Received. A person who maintains this repository will read it; nothing acts on it before they do.",
3328 message: REPORT_RECEIVED,
33293329 },
33303330 202,
33313331 );
src/http/mcp.ts +2 −2
@@ -29,6 +29,7 @@ import { body, boundedBody, json } from "./respond.ts";
2929 import type { Router } from "./router.ts";
3030 import { clientBucket } from "./peer.ts";
3131 import { fileReport } from "../actions/reports.ts";
32 import { REPORT_RECEIVED } from "../actions/report_words.ts";
3233 import { BODY_MAX, REQUEST_MAX, TITLE_MAX } from "../core/reports.ts";
3334 import { everydayOf } from "../core/everyday.ts";
3435 import { type Args, visible, plansOnly, str, REPO_ARG, lean } from "./mcp/shared.ts";
@@ -257,8 +258,7 @@ function reportCall(ctx: Ctx, message: unknown): unknown | undefined {
257258 JSON.stringify({
258259 id: filed.value.id,
259260 state: filed.value.state,
260 message:
261 "Received. A person who maintains this repository will read it; nothing acts on it before they do.",
261 message: REPORT_RECEIVED,
262262 }),
263263 );
264264 }
test/report-words.test.ts added +100
@@ -0,0 +1,100 @@
1 /**
2 * An outsider's report reads back the same sentence from every door, because
3 * every door reads it from src/actions/report_words.ts. A door that retypes
4 * the words fails here even while they still match.
5 */
6 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
7 import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
8 import { tmpdir } from "node:os";
9 import { join } from "node:path";
10 import { REPORT_RECEIVED } from "../src/actions/report_words.ts";
11 import { createRepo } from "../src/actions/repos.ts";
12 import { createApp, type App } from "../src/app.ts";
13 import { createUser } from "../src/core/users.ts";
14 import { serve, type Servers } from "../src/http/server.ts";
15
16 describe("the words a report reads back", () => {
17 let dataDir: string;
18 let app: App;
19 let servers: Servers;
20 let base: string;
21 let source = 0;
22 const from = () => ({ "x-forwarded-for": `198.51.100.${++source}` });
23
24 beforeAll(async () => {
25 dataDir = await mkdtemp(join(tmpdir(), "forge-report-words-"));
26 app = createApp({ dataDir, port: 0 });
27 servers = await serve(app);
28 base = servers.url;
29 const alice = await createUser(app.db, {
30 origin: "seed",
31 trusted: true,
32 username: "alice",
33 email: "[email protected]",
34 password: "a-good-passphrase",
35 adminIfFirst: true,
36 });
37 if (!alice.ok) throw new Error(alice.error);
38 const made = await createRepo(app, alice.value, {
39 name: "game",
40 visibility: "public",
41 });
42 if (!made.ok) throw new Error(String(made.error));
43 });
44 afterAll(async () => {
45 servers.stop();
46 app.close();
47 await rm(dataDir, { recursive: true, force: true });
48 });
49
50 test("POST …/reports answers the module's sentence", async () => {
51 const res = await fetch(`${base}/api/v1/repos/alice/game/reports`, {
52 method: "POST",
53 headers: { "content-type": "application/json", ...from() },
54 body: JSON.stringify({ title: "Stuck on loading", body: "b" }),
55 });
56 expect(res.status).toBe(202);
57 expect(((await res.json()) as { message: string }).message).toBe(
58 REPORT_RECEIVED,
59 );
60 });
61
62 test("the MCP `report` tool answers the module's sentence", async () => {
63 const res = await fetch(`${base}/mcp`, {
64 method: "POST",
65 headers: { "content-type": "application/json", ...from() },
66 body: JSON.stringify({
67 jsonrpc: "2.0",
68 id: 1,
69 method: "tools/call",
70 params: {
71 name: "report",
72 arguments: { repo: "alice/game", title: "Stuck on loading" },
73 },
74 }),
75 });
76 expect(res.status).toBe(200);
77 const answer = (await res.json()) as {
78 result: { isError?: boolean; content: { text: string }[] };
79 };
80 expect(answer.result.isError).toBeUndefined();
81 expect(JSON.parse(answer.result.content[0]!.text).message).toBe(
82 REPORT_RECEIVED,
83 );
84 });
85
86 test("no file under src/ but the module types the sentence out", async () => {
87 const root = join(import.meta.dir, "..", "src");
88 const typed: string[] = [];
89 for (const entry of await readdir(root, { recursive: true })) {
90 if (!entry.endsWith(".ts")) continue;
91 const text = await readFile(join(root, entry), "utf8");
92 if (text.includes(REPORT_RECEIVED)) typed.push(entry);
93 }
94 expect(typed).toEqual([join("actions", "report_words.ts")]);
95 for (const door of ["http/api.ts", "http/mcp.ts"]) {
96 const text = await readFile(join(root, door), "utf8");
97 expect(text).toContain('from "../actions/report_words.ts"');
98 }
99 });
100 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs