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

A planned issue with `Relieves:` starts while its file is hot: #7299, #7424 and #7429 are seen through, or the map says why not #7496

Merged opened this

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

Merged

torque/forge:torque/7489-a-planned-issue-with-relieves-starts-whi → main 1 commit fork

  • Merged. torque/7489-a-planned-issue-with-relieves-starts-whi was merged into main as 16f9cd9.

Review

Sign in to review this change.
1 file changed +129 −0
test/held-relieves-map.test.ts added +129
@@ -0,0 +1,129 @@
1 /**
2 * GET …/map and the MCP `map` tool see a `Relieves:` issue through its own hot
3 * file (#7489). The bodies are #7299's, #7424's and #7429's as the forge
4 * stores them: `Relieves:` above a backticked, comma-separated `Files:` line.
5 */
6 import { afterAll, beforeAll, expect, test } from "bun:test";
7 import { mkdtemp, rm } from "node:fs/promises";
8 import { tmpdir } from "node:os";
9 import { join } from "node:path";
10 import { createApp, type App } from "../src/app.ts";
11 import { createRepo } from "../src/actions/repos.ts";
12 import { createIssue } from "../src/core/issues.ts";
13 import { createRun } from "../src/core/runs.ts";
14 import type { User } from "../src/core/users.ts";
15 import { HOT_PULLS, rememberPullFiles } from "../src/crew/hot_files.ts";
16 import { serve, type Servers } from "../src/http/server.ts";
17 import { seedAdmin } from "./helpers/admin.ts";
18
19 const TUI = {
20 title: "A terminal screen adds its own palette entry from its own module, so a new screen no longer edits src/tui/forge.ts",
21 body: "For: the crew, which today cannot start five ready issues because `src/tui/forge.ts` is in 7 open pulls: every new terminal screen adds its palette entry there. Steering means work is not held behind one file.\n\nDone:\n- [ ] Each palette entry (id, title, hint, who it is offered to, what it opens) is declared next to its screen, e.g. an exported `entry` in `src/tui/screens/<name>.ts`, and one list in a new `src/tui/screens/entries.ts` names them; `src/tui/forge.ts` reads that list instead of pushing each entry by hand.\n- [ ] Every entry offered today is offered to exactly the same people, with the same id, title and hint, in the same order; a test compares the palette for a person admin, a member, an agent and a signed-out session before and after.\n- [ ] AGENTS.md or the terminal's doc says a new screen adds its entry in its own module and one line in `entries.ts`.\n\nRelieves: src/tui/forge.ts\nFiles: `src/tui/forge.ts`, a new `src/tui/screens/entries.ts`, the screen modules whose entries move, one test.\nRisk: medium; it conflicts with every open pull that adds a palette entry, so it lands when `src/tui/forge.ts` is no longer hot.",
22 };
23 const CLI = {
24 title: "Each `forge` command lives in its own module under src/cli/commands/, and main.ts dispatches from one table",
25 body: "Intent: a new command stops being an edit to `src/cli/main.ts`, so command-line work no longer queues behind it. Today #7352, #7378, #7379 and #7380 all wait on that file.\n\nDone:\n- [ ] The commands already in `src/cli/main.ts` (`login`, `logout`, `whoami`, `sync`, `status`, `ls`, `pin`, `unpin`, `open`, `ui` and any that have landed since) each move to `src/cli/commands/<name>.ts`. Each exports `{ name, usage, run(out, args) }`.\n- [ ] `src/cli/main.ts` keeps the flags, `EXIT` and one table built from `src/cli/commands/index.ts`. `forge help` prints the same lines in the same order as before.\n- [ ] Behaviour is unchanged: the existing CLI tests pass untouched, and a new test checks that every module in `src/cli/commands/` is in the table and in `forge help`.\n- [ ] `AGENTS.md` (or `docs/CLI.md` if it exists) says in one line: a new command is a new file in `src/cli/commands/` plus one line in `index.ts`.\n\nRelieves: src/cli/main.ts\nFiles: `src/cli/main.ts`, `src/cli/commands/` (new), a new test.\n\nRisk: a pure move with no change in what any command does or prints.",
26 };
27 const MCP = {
28 title: "The MCP tool table is generated one file per tool under docs/mcp/, so two pulls adding tools never edit the same doc",
29 body: "Intent: MCP work stops queueing behind `docs/MCP.md`. Every tool change today regenerates its one table, so the file stays hot (#5323 waits on it).\n\nDone:\n- [ ] `bun scripts/mcp-tools.ts` writes `docs/mcp/tools/<name>.md` (one line: the tool's `doc` and whether it writes) for each tool, and removes files for tools that are gone. `docs/MCP.md` keeps its prose and, between its `tools:begin`/`tools:end` markers, one sentence linking the directory instead of the table.\n- [ ] `test/mcp-tools.test.ts` and the test that every MCP tool in the index is in the docs (#7252) check the per-tool files instead of the table, with the same failure words naming the command to run.\n- [ ] `GET /api/v1/…` or the MCP `index` tool, if either reads the table today, reads the files.\n- [ ] Nothing else in `docs/MCP.md` changes.\n\nRelieves: docs/MCP.md\nFiles: `scripts/mcp-tools.ts`, `docs/MCP.md`, `docs/mcp/tools/` (new), `test/mcp-tools.test.ts`.\n\nRisk: a pure move of generated text. It lands once docs/MCP.md is relieved.",
30 };
31 const HOT = ["src/tui/forge.ts", "src/cli/main.ts", "docs/MCP.md"];
32
33 let dataDir = "";
34 let app: App, servers: Servers, alice: User, bearer = "";
35
36 beforeAll(async () => {
37 dataDir = await mkdtemp(join(tmpdir(), "forge-relieves-map-"));
38 app = createApp({ dataDir, port: 0 });
39 servers = await serve(app);
40 const admin = await seedAdmin(app, { username: "alice", email: "[email protected]", password: "correct-horse-battery" });
41 if (!admin.ok) throw new Error(admin.error);
42 alice = admin.value;
43 const minted = await fetch(`${servers.url}/api/v1/tokens`, {
44 method: "POST",
45 headers: { "content-type": "application/json" },
46 body: JSON.stringify({ adminConfirm: true, username: "alice", password: "correct-horse-battery", name: "cli", scope: "admin" }),
47 });
48 bearer = ((await minted.json()) as { token: string }).token;
49 });
50
51 afterAll(async () => {
52 servers.stop?.();
53 app.close();
54 await rm(dataDir, { recursive: true, force: true });
55 });
56
57 /** A repository whose remembered open pulls make every path in `HOT` hot, one more pull than the limit. */
58 async function hotRepo(name: string): Promise<string> {
59 const made = await createRepo(app, alice, { name, visibility: "public", description: "" });
60 if (!made.ok) throw new Error(String(made.error));
61 rememberPullFiles(
62 made.value.id,
63 Array.from({ length: HOT_PULLS + 1 }, (_, i) => ({ number: 9000 + i, files: [...HOT, `src/own${i}.ts`] })),
64 );
65 return made.value.id;
66 }
67
68 function open(repoId: string, issue: { title: string; body: string }) {
69 const r = createIssue(app.db, { repoId, authorId: alice.id, ...issue });
70 if (!r.ok) throw new Error(r.error);
71 return r.value;
72 }
73
74 type Held = { by: string; detail: string } | null;
75 type MapBody = { ready: { number: number; held: Held }[]; startable: number[] };
76
77 async function apiMap(name: string): Promise<MapBody> {
78 return (await (await fetch(`${servers.url}/api/v1/repos/alice/${name}/map`)).json()) as MapBody;
79 }
80
81 async function mcpMap(name: string): Promise<MapBody> {
82 const res = await fetch(`${servers.url}/mcp`, {
83 method: "POST",
84 headers: {
85 "content-type": "application/json",
86 accept: "application/json, text/event-stream",
87 authorization: `Bearer ${bearer}`,
88 },
89 body: JSON.stringify({
90 jsonrpc: "2.0",
91 id: 1,
92 method: "tools/call",
93 params: { name: "map", arguments: { app: `alice/${name}` } },
94 }),
95 });
96 const body = (await res.json()) as { result: { content: { text: string }[] } };
97 return JSON.parse(body.result.content[0]!.text) as MapBody;
98 }
99
100 const heldOf = (map: MapBody, n: number): Held | undefined => map.ready.find((r) => r.number === n)?.held;
101
102 test("the three planned bodies are held by nothing on their own hot files; a plain one on the same file is", async () => {
103 const repoId = await hotRepo("relieved");
104 const tui = open(repoId, TUI);
105 const cli = open(repoId, CLI);
106 const mcp = open(repoId, MCP);
107 const plain = open(repoId, { title: "Add a palette entry", body: "Files: `src/tui/forge.ts`, one test." });
108 for (const map of [await apiMap("relieved"), await mcpMap("relieved")]) {
109 expect(heldOf(map, tui.number)).toBeNull();
110 expect(heldOf(map, cli.number)).toBeNull();
111 expect(heldOf(map, mcp.number)).toBeNull();
112 expect(heldOf(map, plain.number)).toEqual({ by: "hot_file", detail: `src/tui/forge.ts is in ${HOT_PULLS + 1} open pulls` });
113 expect(map.startable.sort((a, b) => a - b)).toEqual([tui.number, cli.number, mcp.number]);
114 }
115 });
116
117 test("a second issue relieving the same path waits on the one in flight, and the map says by whom", async () => {
118 const repoId = await hotRepo("relieving");
119 const first = open(repoId, TUI);
120 createRun(app.db, { repoId, kind: "agent", issueId: first.id });
121 const second = open(repoId, TUI);
122 for (const map of [await apiMap("relieving"), await mcpMap("relieving")]) {
123 expect(heldOf(map, first.number)?.by).toBe("running");
124 expect(heldOf(map, second.number)).toEqual({
125 by: "hot_file",
126 detail: `waits: src/tui/forge.ts is already being relieved by #${first.number}`,
127 });
128 }
129 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs