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

A test pins the run-cap park: it is noted and asked once per window, and a quoted reason is cut at a sentence #6927

Merged opened this

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

Merged

scout/forge:scout/6904-a-test-pins-the-run-cap-park-it-is-noted → main 1 commit fork

  • Merged. scout/6904-a-test-pins-the-run-cap-park-it-is-noted was merged into main as 47b1a02.

Review

Sign in to review this change.
2 files changed +250 −1
src/crew/guard.ts +6 −1
@@ -491,7 +491,12 @@ export function quoteCut(text: string): string {
491491 let end = -1;
492492 for (const m of head.matchAll(/[.!?](?=\s|$)/g))
493493 if (m.index! >= QUOTE_MAX - QUOTE_WINDOW) end = m.index! + 1;
494 return end > 0 ? head.slice(0, end) : `${head.trimEnd()}…`;
494 if (end > 0) return head.slice(0, end);
495 // The mark takes a unit of the max, and a pair is never left half.
496 let room = QUOTE_MAX - 1;
497 const last = t.charCodeAt(room - 1);
498 if (last >= 0xd800 && last <= 0xdbff) room--;
499 return `${t.slice(0, room).trimEnd()}…`;
495500 }
496501
497502 const quoted = (text: string): string[] =>
test/crew-guard-cap.test.ts added +244
@@ -0,0 +1,244 @@
1 /**
2 * The run cap's edges: when an issue has used its runs (`capReached`), when
3 * its park was already written (`capNoted`), and how a park quotes a review
4 * (`quoteCut`). The wider flow through `queueAgentRun` is in
5 * crew-guard.test.ts; here the clock is passed in.
6 */
7 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
8 import { mkdtemp, rm } from "node:fs/promises";
9 import { tmpdir } from "node:os";
10 import { join } from "node:path";
11 import { createApp, type App } from "../src/app.ts";
12 import { addToCrew, createAgent } from "../src/actions/crew.ts";
13 import { addComment, openIssue } from "../src/actions/issues.ts";
14 import { createRepo } from "../src/actions/repos.ts";
15 import { getAgent, type Agent } from "../src/core/agents.ts";
16 import { getIssueById, type Issue } from "../src/core/issues.ts";
17 import { getRepoById, type Repo } from "../src/core/repos.ts";
18 import { claimRun, createRun, finishRun } from "../src/core/runs.ts";
19 import { createUser, getUserByName, type User } from "../src/core/users.ts";
20 import {
21 CAP_NOTE,
22 CAP_WINDOW_MS,
23 capNoted,
24 capReached,
25 quoteCut,
26 } from "../src/crew/guard.ts";
27 import { quietLogs } from "./helpers/globals.ts";
28
29 quietLogs();
30
31 const T0 = Date.parse("2030-01-10T00:00:00.000Z");
32 const at = (ms: number) => new Date(T0 + ms);
33 const HOUR = 3_600_000;
34 const CAP = 3;
35
36 let dataDir: string;
37 let app: App;
38 let alice: User;
39 let mallory: User;
40 let bot: Agent;
41 let repo: Repo;
42 let made = 0;
43
44 const ok = <T>(
45 r: { ok: true; value: T } | { ok: false; error: unknown },
46 ): T => {
47 if (!r.ok) throw new Error(JSON.stringify(r.error));
48 return r.value;
49 };
50
51 function issueOf(): Issue {
52 const issue = ok(
53 openIssue(app, alice, getRepoById(app.db, repo.id)!, {
54 title: `Cap ${++made}`,
55 }),
56 );
57 return getIssueById(app.db, issue.id)!;
58 }
59
60 /** A run on `issue` started `ms` after T0. */
61 function run(issue: Issue, ms: number, reason = "assigned", summary = "done") {
62 const r = createRun(app.db, {
63 repoId: repo.id,
64 kind: "agent",
65 issueId: issue.id,
66 actorId: bot.user.id,
67 reason,
68 });
69 claimRun(app.db, r.id);
70 finishRun(app.db, r.id, { state: "ok", summary, costUsd: 0 });
71 app.db
72 .query("UPDATE run SET created_at = ?1 WHERE id = ?2")
73 .run(at(ms).toISOString(), r.id);
74 }
75
76 /** A comment by `user` on `issue`, written `ms` after T0. */
77 function say(issue: Issue, user: User, body: string, ms: number) {
78 const c = ok(addComment(app, user, repo, issue, body));
79 app.db
80 .query("UPDATE comment SET created_at = ?1 WHERE id = ?2")
81 .run(at(ms).toISOString(), c.id);
82 }
83
84 beforeAll(async () => {
85 dataDir = await mkdtemp(join(tmpdir(), "forge-guard-cap-"));
86 app = createApp({ dataDir, port: 0 });
87 alice = ok(
88 await createUser(app.db, {
89 origin: "seed",
90 trusted: true,
91 username: "alice",
92 email: "[email protected]",
93 password: "a-good-passphrase",
94 adminIfFirst: true,
95 }),
96 );
97 mallory = ok(
98 await createUser(app.db, {
99 origin: "seed",
100 trusted: true,
101 username: "mallory",
102 email: "[email protected]",
103 password: "a-good-passphrase",
104 }),
105 );
106 ok(await createAgent(app, alice, { username: "bot" }));
107 repo = ok(
108 await createRepo(app, alice, { name: "square", visibility: "public" }),
109 );
110 expect(addToCrew(app, alice, repo, "bot").ok).toBe(true);
111 bot = getAgent(app.db, getUserByName(app.db, "bot")!.id)!;
112 }, 30_000);
113
114 afterAll(async () => {
115 app?.close();
116 await rm(dataDir, { recursive: true, force: true });
117 });
118
119 const reached = (issue: Issue, now = at(HOUR)) =>
120 capReached(app.db, repo, issue, CAP, now);
121 const noted = (issue: Issue, now = at(10 * HOUR)) =>
122 capNoted(app.db, repo, issue, now);
123
124 describe("capReached", () => {
125 test("under the cap is not capped, at the cap is", () => {
126 const issue = issueOf();
127 run(issue, 0);
128 run(issue, 1);
129 expect(reached(issue)).toBe(false);
130 run(issue, 2);
131 expect(reached(issue)).toBe(true);
132 });
133
134 test("runs older than the window no longer count", () => {
135 const issue = issueOf();
136 for (let n = 0; n < CAP; n++) run(issue, n);
137 expect(reached(issue, at(CAP_WINDOW_MS - HOUR))).toBe(true);
138 expect(reached(issue, at(CAP_WINDOW_MS + HOUR))).toBe(false);
139 });
140
141 test("a check-failure run, a conflict, a review, a retry, an interrupted and a skipped run are not starts", () => {
142 const issue = issueOf();
143 run(issue, 0);
144 run(issue, 1, "check_failed");
145 run(issue, 2, "conflict");
146 run(issue, 3, "review");
147 run(issue, 4, "assigned", "interrupted: restart");
148 run(issue, 5, "assigned", "skipped: nothing new");
149 createRun(app.db, {
150 repoId: repo.id,
151 kind: "agent",
152 issueId: issue.id,
153 actorId: bot.user.id,
154 reason: "assigned",
155 attempt: 1,
156 });
157 expect(reached(issue)).toBe(false);
158 run(issue, 6);
159 run(issue, 7);
160 expect(reached(issue)).toBe(true);
161 });
162
163 test("a director's word restarts the count; a reader's does not", () => {
164 const issue = issueOf();
165 for (let n = 0; n < CAP; n++) run(issue, n);
166 say(issue, mallory, "Try again!", HOUR / 2);
167 expect(reached(issue)).toBe(true);
168 say(issue, alice, "Smaller radius.", HOUR);
169 expect(reached(issue, at(2 * HOUR))).toBe(false);
170 for (let n = 0; n < CAP; n++) run(issue, 2 * HOUR + n);
171 expect(reached(issue, at(3 * HOUR))).toBe(true);
172 });
173 });
174
175 describe("capNoted", () => {
176 test("an issue without the note is not noted", () => {
177 expect(noted(issueOf())).toBe(false);
178 });
179
180 test("after the note, a review or a reader's comment starts no second note", () => {
181 const issue = issueOf();
182 for (let n = 0; n < CAP; n++) run(issue, n);
183 say(issue, bot.user, CAP_NOTE, 2 * HOUR);
184 expect(noted(issue)).toBe(true);
185 say(issue, mallory, "Try again!", 3 * HOUR);
186 say(issue, bot.user, "A review of the push.", 4 * HOUR);
187 expect(noted(issue)).toBe(true);
188 });
189
190 test("a director's word starts a new window in which the note is not yet written", () => {
191 const issue = issueOf();
192 say(issue, bot.user, CAP_NOTE, 2 * HOUR);
193 expect(noted(issue)).toBe(true);
194 say(issue, alice, "Smaller radius.", 3 * HOUR);
195 expect(noted(issue)).toBe(false);
196 say(issue, bot.user, CAP_NOTE, 5 * HOUR);
197 expect(noted(issue)).toBe(true);
198 });
199
200 test("a note older than the window is not noted", () => {
201 const issue = issueOf();
202 say(issue, bot.user, CAP_NOTE, 0);
203 expect(noted(issue, at(CAP_WINDOW_MS - HOUR))).toBe(true);
204 expect(noted(issue, at(CAP_WINDOW_MS + HOUR))).toBe(false);
205 });
206 });
207
208 describe("quoteCut", () => {
209 test("short text is returned unchanged, trimmed", () => {
210 expect(quoteCut("Looks fine. Ship it.")).toBe("Looks fine. Ship it.");
211 expect(quoteCut(" padded \n")).toBe("padded");
212 expect(quoteCut("a".repeat(600))).toBe("a".repeat(600));
213 });
214
215 test("long text is cut at the last sentence end inside the window", () => {
216 const text = `${"a".repeat(520)}. ${"b".repeat(30)}! ${"c".repeat(300)}`;
217 expect(quoteCut(text)).toBe(`${"a".repeat(520)}. ${"b".repeat(30)}!`);
218 });
219
220 test("a sentence end before the window is not used; the cut is marked", () => {
221 const text = `${"a".repeat(100)}. ${"b".repeat(700)}`;
222 const cut = quoteCut(text);
223 expect(cut.endsWith("…")).toBe(true);
224 expect(cut.length).toBeLessThanOrEqual(600);
225 });
226
227 test("it never exceeds its max, whatever the text", () => {
228 for (const text of [
229 "x".repeat(5000),
230 "Yes. ".repeat(500),
231 "line\n".repeat(400),
232 `${"a".repeat(599)}.${"b".repeat(50)}`,
233 ])
234 expect(quoteCut(text).length).toBeLessThanOrEqual(600);
235 });
236
237 test("it never splits a surrogate pair", () => {
238 for (const pad of [597, 598, 599, 600]) {
239 const cut = quoteCut(`${"a".repeat(pad)}${"😀".repeat(40)}`);
240 expect(cut.length).toBeLessThanOrEqual(600);
241 expect(cut.isWellFormed()).toBe(true);
242 }
243 });
244 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs