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

A red that main passes is the pulls' own: no sentinel issue a builder can only decline #8793

Merged opened this

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

Merged

mason/forge:mason/8786-a-red-that-main-passes-is-the-pulls-own → main 1 commit fork

  • Merged. mason/8786-a-red-that-main-passes-is-the-pulls-own was merged into main as 967558c.

Review

Sign in to review this change.
5 files changed +353 −104
src/checks/shared_red.ts +109 −45
@@ -9,6 +9,11 @@
99 * for main's next green check, as a red main's pulls do, and the vitals
1010 * carry one card per shared test in place of a park per pull.
1111 *
12 * Many pulls failing one test is not enough (#8786): it is the lane's only
13 * when main's newest check fails it too, or a re-run passed it (a flake).
14 * A test main passes and no re-run passed is each pull's own
15 * ({@link pullsOwn}): its builder gets the follow-up, and no card is made.
16 *
1217 * Plain data: failed pull checks in the window, read by the `run_repo`
1318 * index (an id is time-sortable, so the window is a range), their recorded
1419 * test names unpacked by SQLite's own `json_each`.
@@ -39,38 +44,17 @@ const QUEUED_SLACK_MS = 6 * 3_600_000;
3944 export const NO_TEST_NAME = "(unnamed)";
4045
4146 /**
42 * Suites that are gates on the tree itself (#2868): each reads the source and
43 * fails the pull that added the thing it forbids, the same way on any
44 * machine. Many pulls tripping one in an hour is many pulls each adding
45 * what it forbids (a class DESIGN.md does not name), not a lane's red, so they never make one.
46 * A name here is a suite's `describe` prefix, or one test's full name when
47 * the rest of its suite is not a gate, and a test keeps it existing.
48 * Reserved names (#7809): each pull adding a top-level page without
49 * reserving it ("keeper", "feed", "favicon.ico") failed it, main green.
50 * OpenAPI coverage (#8140): each pull adding an /api/v1 route without its
51 * row, or a route file without regenerating the index, failed it, main green.
52 * MCP tools (#7906): each pull adding a tool without `bun scripts/mcp-tools.ts`
53 * failed the index check, main green.
54 * The rest are top-level tests named whole (#8184): each reads only the tree.
47 * Tests whose red is each pull's own however many pulls trip it, for what
48 * {@link pullsOwn} cannot see. A name here is a suite's `describe` prefix,
49 * or one test's full name, and a test keeps it existing.
50 *
51 * Empty since #8786: every gate once listed here (#2868's design check and
52 * polling scan, #7809's reserved names, #8140's OpenAPI rows and route index,
53 * #7906's tool index, #8184's import, README and link checks) reads only the
54 * tree, so main's check passes it and a pull's re-run fails it again: the
55 * rule calls it the pull's own. Add a name only for a red the rule misjudges.
5556 */
56 export const TREE_GATES: readonly string[] = [
57 "design check: nothing added outside the system (#2678) > ",
58 "no polling in src/web > ",
59 "reserved names > every top-level page of the forge is a reserved name",
60 // A route a pull added without its row in docs/openapi.
61 "every registered /api/v1 route has an OpenAPI row or is listed as not yet described",
62 "NOT_YET_DESCRIBED has no entry that is now described",
63 "NOT_YET_DESCRIBED is sorted and names registered routes",
64 // An import a pull added from scripts/ or test/ into src/.
65 "nothing under src/ imports from outside src/ (scripts/, test/, …)",
66 // A README a pull grew past its limit, or a docs/ link it broke.
67 "README.md stays under 80 KB",
68 "every docs/ link in README.md resolves",
69 // A route file a pull added without regenerating the index.
70 "the gathered routes > the index is current: run `bun scripts/openapi-routes.ts`",
71 // A tool file a pull added without regenerating the index (#7906).
72 "the gathered tools > the index is current: run `bun scripts/mcp-tools.ts`",
73 ];
57 export const TREE_GATES: readonly string[] = [];
7458
7559 /** Whether a failing test's name is one of {@link TREE_GATES}': under a suite prefix (`… > `), or a test's full name exactly. */
7660 export const isTreeGate = (test: string): boolean =>
@@ -129,8 +113,9 @@ function pullsPerTest(db: Database, now: number, repoId: string | null): Row[] {
129113 /**
130114 * Whether a failed check fails only what the lane fails: it named at least
131115 * one test, and every one was failed by {@link SHARED_PULLS} or more pulls
132 * (this one counted, once it is recorded) in the window. A check that named
133 * nothing, or one test few others fail, is the pull's own.
116 * (this one counted, once it is recorded) in the window and is not each
117 * pull's own ({@link pullsOwn}). A check that named nothing, one test few
118 * others fail, or one main passes and no re-run passed, is the pull's own.
134119 */
135120 export function sharedWithPulls(
136121 db: Database,
@@ -139,14 +124,14 @@ export function sharedWithPulls(
139124 ): boolean {
140125 const names = run?.checkDetail?.names ?? [];
141126 if (run === undefined || names.length === 0) return false;
142 const pulls = new Map(pullsPerTest(db, now, run.repoId).map((r) => [r.test, r.pulls]));
143 return names.every((n) => (pulls.get(n) ?? 0) >= SHARED_PULLS);
127 const named = new Set(names);
128 const shared = new Set(lanes(db, pullsPerTest(db, now, run.repoId).filter((r) => named.has(r.test)), now).map((r) => r.test));
129 return names.every((n) => shared.has(n));
144130 }
145131
146132 /** Every shared test across the forge's repositories, oldest first: one query. */
147133 export function sharedTests(db: Database, now: number = Date.now()): SharedTest[] {
148 return pullsPerTest(db, now, null)
149 .filter((r) => r.pulls >= SHARED_PULLS)
134 return lanes(db, pullsPerTest(db, now, null), now)
150135 .sort((a, b) => a.since.localeCompare(b.since))
151136 .map((r) => ({ repoId: r.repo_id, repo: r.repo, test: r.test, pulls: r.pulls, since: r.since }));
152137 }
@@ -254,12 +239,15 @@ export function sharedRuns(
254239 };
255240 }
256241
257 /** Main's newest finished check, and whether it failed the named test. */
258 export function mainVerdict(
259 db: Database,
260 repoId: string,
261 test: string,
262 ): { runId: string; commitOid: string; passes: boolean } | undefined {
242 /** Main's newest finished check: its run, its commit, its state and the tests it named failing. */
243 interface MainCheck {
244 readonly runId: string;
245 readonly commitOid: string;
246 readonly state: string;
247 readonly names: readonly string[];
248 }
249
250 function mainCheck(db: Database, repoId: string): MainCheck | undefined {
263251 const row = db
264252 .query<{ id: string; commit_oid: string; state: string; check_detail: string | null }, [string]>(
265253 `SELECT r.id, r.commit_oid, r.state, r.check_detail FROM run r JOIN repo ON repo.id = r.repo_id
@@ -275,8 +263,84 @@ export function mainVerdict(
275263 } catch {
276264 names = [];
277265 }
278 const failsIt = row.state === "failed" && Array.isArray(names) && names.includes(test);
279 return { runId: row.id, commitOid: row.commit_oid, passes: row.state === "ok" || !failsIt };
266 const named = Array.isArray(names) ? names.filter((n): n is string => typeof n === "string") : [];
267 return { runId: row.id, commitOid: row.commit_oid, state: row.state, names: named };
268 }
269
270 /** Main's newest finished check, and whether it failed the named test. */
271 export function mainVerdict(
272 db: Database,
273 repoId: string,
274 test: string,
275 ): { runId: string; commitOid: string; passes: boolean } | undefined {
276 const main = mainCheck(db, repoId);
277 if (main === undefined) return undefined;
278 const failsIt = main.state === "failed" && main.names.includes(test);
279 return { runId: main.runId, commitOid: main.commitOid, passes: !failsIt };
280 }
281
282 /**
283 * Whether main's newest check ran the test and passed it: green, or red on
284 * other tests it named. A red that named nothing (a build, a timeout) read
285 * no test, so it says nothing either way.
286 */
287 const mainRanGreen = (main: MainCheck | undefined, test: string): boolean =>
288 main !== undefined && (main.state === "ok" || (main.names.length > 0 && !main.names.includes(test)));
289
290 /**
291 * A re-run since `since` that passed the test: the check runner's re-run
292 * alone recorded it a flake (`checks/flake.ts`), or a pull's later check of
293 * the same commit finished without failing it — green, or red on other
294 * tests it named.
295 */
296 const PASSED_AGAIN = `
297 SELECT 1 FROM check_flake WHERE repo_id = ?1 AND test = ?2 AND at >= ?3
298 UNION ALL
299 SELECT 1
300 FROM run f,
301 json_each(CASE WHEN json_valid(f.check_detail) THEN f.check_detail ELSE '{}' END, '$.names') n
302 JOIN run g ON g.repo_id = f.repo_id AND g.issue_id = f.issue_id AND g.commit_oid = f.commit_oid
303 AND g.kind = 'check' AND g.id > f.id
304 WHERE f.repo_id = ?1 AND f.id >= ?4 AND f.finished_at >= ?3
305 AND f.kind = 'check' AND f.issue_id IS NOT NULL AND f.state = 'failed'
306 AND n.type = 'text' AND n.value = ?2
307 AND (g.state = 'ok' OR (g.state = 'failed'
308 AND json_array_length(CASE WHEN json_valid(g.check_detail) THEN g.check_detail ELSE '{}' END, '$.names') > 0
309 AND NOT EXISTS (SELECT 1 FROM json_each(CASE WHEN json_valid(g.check_detail) THEN g.check_detail ELSE '{}' END, '$.names') m
310 WHERE m.value = ?2)))
311 LIMIT 1`;
312
313 function passedAgain(db: Database, repoId: string, test: string, since: number): boolean {
314 return (
315 db
316 .query<{ 1: number }, [string, string, string, string]>(PASSED_AGAIN)
317 .get(repoId, test, new Date(since).toISOString(), idFloor(since - QUEUED_SLACK_MS)) !== null
318 );
319 }
320
321 /**
322 * Whether a test many pulls fail is each pull's own, not the lane's (#8786):
323 * main's newest check ran it and passed it, and no re-run since `since`
324 * passed it. A page, a tool or a row each pull added, or a main test written
325 * after a pull's base, fails that pull every time and passes on main. A test
326 * main fails too, a flake that passes on a re-run (#2266's disk), or one
327 * main's verdict cannot be read for stays the lane's.
328 */
329 export function pullsOwn(db: Database, repoId: string, test: string, since: number): boolean {
330 return pullsOwnBy(mainCheck(db, repoId), db, repoId, test, since);
331 }
332
333 const pullsOwnBy = (main: MainCheck | undefined, db: Database, repoId: string, test: string, since: number): boolean =>
334 mainRanGreen(main, test) && !passedAgain(db, repoId, test, since);
335
336 /** Of rows of {@link SHARED_PULLS} or more pulls, those that are the lane's: main read once per repository. */
337 function lanes(db: Database, rows: readonly Row[], now: number): Row[] {
338 const mains = new Map<string, MainCheck | undefined>();
339 return rows.filter((r) => {
340 if (r.pulls < SHARED_PULLS) return false;
341 if (!mains.has(r.repo_id)) mains.set(r.repo_id, mainCheck(db, r.repo_id));
342 return !pullsOwnBy(mains.get(r.repo_id), db, r.repo_id, r.test, now - SHARED_WINDOW_MS);
343 });
280344 }
281345
282346 /** The most lines of a failing test's output a finding quotes. */
src/crew/sentinel.ts +53 −13
@@ -41,6 +41,7 @@ import {
4141 assertionLines,
4242 mainVerdict,
4343 pullsFailedSince,
44 pullsOwn,
4445 SHARED_PULLS,
4546 SHARED_WINDOW_MS,
4647 sharedRuns,
@@ -804,9 +805,35 @@ function settled(db: Database, cause: string, test: string | undefined, now: num
804805 };
805806 }
806807
808 /** A finding's cause, read back from its evidence, once its span has left the day. */
809 const causeOf = (evidence: readonly string[]): string | undefined =>
810 evidence.find((e) => e.startsWith("cause: "))?.slice(7);
811
812 /**
813 * Why a shared finding's test is each pull's own (#8786): main's newest
814 * check passes it and no re-run since `since` passed it. Undefined while
815 * the test is the lane's, or unknown.
816 */
817 function ownedByPulls(db: Database, cause: string, test: string | undefined, since: number): string[] | undefined {
818 if (!cause.startsWith("shared:") || test === undefined) return undefined;
819 const repoId = cause.split(":")[1] ?? "";
820 if (!pullsOwn(db, repoId, test, since)) return undefined;
821 const main = mainVerdict(db, repoId, test);
822 if (main === undefined) return undefined;
823 return [
824 `cause: ${cause}`,
825 `test: ${test}`,
826 `main: passes on main's newest check, run ${main.runId} at ${main.commitOid}`,
827 `re-runs that passed it since ${new Date(since).toISOString()}: none`,
828 ];
829 }
830
807831 /**
808832 * A shared red that passed again on its own closes its own open issue as
809 * dropped, before a builder is handed it and finds nothing to fix (#8287).
833 * dropped, before a builder is handed it and finds nothing to fix (#8287);
834 * so does one the rule now calls each pull's own (#8786: main passes it and
835 * no re-run did), saying so — its cause read from the issue's evidence when
836 * its span has left the day.
810837 * Not one a person has spoken on: they decide. If the cause opens again,
811838 * the next hunt files it fresh and quotes this one. A sentinel that may not
812839 * close issues here closes nothing and says so: no access check is skipped.
@@ -817,8 +844,8 @@ function settled(db: Database, cause: string, test: string | undefined, now: num
817844 */
818845 function settleShared(app: App, repo: Repo, actor: User, now: number): void {
819846 const open = app.db
820 .query<{ key: string; issue_id: string; evidence: string }, []>(
821 `SELECT t.key, t.issue_id, t.evidence FROM sentinel_finding t JOIN issue i ON i.id = t.issue_id
847 .query<{ key: string; issue_id: string; evidence: string; first_seen: string }, []>(
848 `SELECT t.key, t.issue_id, t.evidence, t.first_seen FROM sentinel_finding t JOIN issue i ON i.id = t.issue_id
822849 WHERE i.state = 'open' AND t.key LIKE 'degraded:shared-%'`,
823850 )
824851 .all();
@@ -830,10 +857,14 @@ function settleShared(app: App, repo: Repo, actor: User, now: number): void {
830857 const causes = new Map(spans(app.db, now - DAY_MS, now).map((s) => [`degraded:${keyPart(s.cause)}`, s.cause]));
831858 for (const t of open) {
832859 try {
833 const cause = causes.get(t.key);
834 if (cause === undefined) continue;
835 const done = settled(app.db, cause, testOf(t.evidence.split("\n")), now);
836 if (done === undefined || personSpoke(app.db, t.issue_id)) continue;
860 const evidence = t.evidence.split("\n");
861 const cause = causes.get(t.key) ?? causeOf(evidence);
862 if (cause === undefined || `degraded:${keyPart(cause)}` !== t.key) continue;
863 const test = testOf(evidence);
864 const firstSeen = Date.parse(t.first_seen);
865 const owned = ownedByPulls(app.db, cause, test, (Number.isNaN(firstSeen) ? now : firstSeen) - SHARED_WINDOW_MS);
866 const done = owned === undefined ? settled(app.db, cause, test, now) : undefined;
867 if ((owned === undefined && done === undefined) || personSpoke(app.db, t.issue_id)) continue;
837868 const issue = getIssueById(app.db, t.issue_id);
838869 if (issue === undefined || issue.repoId !== repo.id) continue;
839870 const said = addComment(
@@ -841,12 +872,19 @@ function settleShared(app: App, repo: Repo, actor: User, now: number): void {
841872 actor,
842873 repo,
843874 issue,
844 [
845 `Passed again on its own at ${new Date(done.at).toISOString()}; evidence:`,
846 ...done.evidence.map((e) => `- ${e}`),
847 "",
848 "Closing as dropped: there is nothing to fix. If it opens again it is filed fresh, quoting this.",
849 ].join("\n"),
875 owned !== undefined
876 ? [
877 "Each pull's own red, not the lane's (#8786): main's newest check passes this test and no re-run of a failing pull passed it, so each pull failed it for what it added. Evidence:",
878 ...owned.map((e) => `- ${e}`),
879 "",
880 "Closing as dropped: there is nothing for the lane to fix. Each pull's builder gets its red as its own follow-up, with the test named.",
881 ].join("\n")
882 : [
883 `Passed again on its own at ${new Date(done!.at).toISOString()}; evidence:`,
884 ...done!.evidence.map((e) => `- ${e}`),
885 "",
886 "Closing as dropped: there is nothing to fix. If it opens again it is filed fresh, quoting this.",
887 ].join("\n"),
850888 );
851889 if (!said.ok) {
852890 log.warn("sentinel could not comment", { key: t.key, error: String(said.error) });
@@ -994,6 +1032,8 @@ export function hunt(
9941032 }
9951033 // A shared red that passed again on its own is no work: the day's total does not file it again.
9961034 if (settled(app.db, f.cause ?? "", testFor(app.db, f), now) !== undefined) continue;
1035 // Nor is each pull's own red (#8786): its builders hold it, not the lane.
1036 if (ownedByPulls(app.db, f.cause ?? "", testFor(app.db, f), now - SHARED_WINDOW_MS) !== undefined) continue;
9971037 const told =
9981038 sinceFix(app, repo, actor, f, evidence, at, now) ??
9991039 (f.incident === undefined ? undefined : continuesDone(app, repo, actor, f, evidence, at, now));
test/crew-shared-red.test.ts +19 −2
@@ -1,8 +1,10 @@
11 /**
22 * A red many pulls share never parks them (#2266): through the crew's event
33 * path, with main green, three pulls whose checks fail one test in the hour
4 * are neither parked nor reworked and are checked again on main's next green
5 * check; a pull red on a test only it fails is parked as ever. Each pull has
4 * — a test a re-run passed — are neither parked nor reworked and are checked
5 * again on main's next green check; a pull red on a test only it fails is
6 * parked as ever, and so is each of many pulls red on a test main passes and
7 * no re-run passed (#8786). Each pull has
68 * already spent its automatic tries, so any red that is its own parks it.
79 */
810
@@ -83,6 +85,12 @@ describe("the crew, with a red many pulls share", () => {
8385 claimRun(app.db, run.id);
8486 announceRun(app, finishRun(app.db, run.id, { state: "ok", summary: "check passed" })!);
8587 };
88 /** The check runner re-ran the test alone on some check and it passed: random, the lane's (#8786). */
89 const flaked = (test: string) =>
90 app.db.run(
91 "INSERT INTO check_flake (id, repo_id, run_id, commit_oid, test, file, at) VALUES (?1, ?2, 'r', 'a', ?3, 'x.test.ts', ?4)",
92 [`flake-${test}`, repo.id, test, new Date().toISOString()],
93 );
8694 const open = (pull: Pull) => getIssueById(app.db, pull.id)?.state === "open";
8795 const sharedCards = () =>
8896 vitals({
@@ -93,6 +101,7 @@ describe("the crew, with a red many pulls share", () => {
93101
94102 test("three pulls red on X in the hour: none parked, one card; a fourth red on Y only is parked", async () => {
95103 mainGreen();
104 flaked("X");
96105 const pulls = [await spentPull(), await spentPull(), await spentPull()];
97106 const runs = pulls.map((p) => failed(p, ["X"]));
98107 for (const run of runs) announceRun(app, run);
@@ -120,6 +129,14 @@ describe("the crew, with a red many pulls share", () => {
120129 expect(requested).toHaveLength(3);
121130 });
122131
132 test("three pulls red on a test main passes and no re-run passed: each is its own red, parked, and no card (#8786)", async () => {
133 mainGreen();
134 const pulls = [await spentPull(), await spentPull(), await spentPull()];
135 for (const p of pulls) announceRun(app, failed(p, ["Z"]));
136 for (const p of pulls) expect(open(p)).toBe(false);
137 expect(sharedCards().map((c) => c.sentence).filter((s) => s.includes('"Z"'))).toEqual([]);
138 });
139
123140 test("the card says another repository's red is not fixable here; the forge's own says nothing of it", async () => {
124141 const prior = process.env.FORGE_SENTINEL_REPO;
125142 try {
test/sentinel-shared-settle.test.ts +76 −2
@@ -4,8 +4,9 @@
44 * passes it. The sentinel closes its own issue as dropped before a builder is
55 * handed it, on the hourly hunt and on every tick, never one a person has
66 * spoken on, never without write access, and files a repeat within the day
7 * fresh, quoting the earlier one. The tests named "exception" guard what
8 * must stay open; they pass without the change too.
7 * fresh, quoting the earlier one. One the rule now calls each pull's own
8 * (#8786) — main passes it and no re-run did — closes as dropped too, saying
9 * so. The tests named "exception" guard what must stay open.
910 */
1011 import { afterEach, beforeEach, describe, expect, test } from "bun:test";
1112 import { mkdtemp, rm } from "node:fs/promises";
@@ -77,12 +78,22 @@ beforeEach(async () => {
7778 app.db.query("UPDATE degraded_span SET closed_at = ?1 WHERE cause = ?2").run(T - 20 * MIN, cause());
7879 check("ok", T - 15 * MIN);
7980 });
81
82 /** The check runner re-ran the test alone and it passed: a flake, the lane's red (#8786). */
83 function flaked(at: number): void {
84 app.db
85 .query("INSERT INTO check_flake (id, repo_id, run_id, commit_oid, test, file, at) VALUES (?1, ?2, 'r', 'e', ?3, 'b.test.ts', ?4)")
86 .run(`flake-${at}`, repo.id, TEST, new Date(at).toISOString());
87 }
8088 afterEach(async () => {
8189 app.db.close();
8290 await rm(dir, { recursive: true, force: true });
8391 });
8492
8593 describe("a shared red that passed again on its own (#8287)", () => {
94 // A re-run passed it: the lane's red, not each pull's own (#8786).
95 beforeEach(() => flaked(T - 44 * MIN));
96
8697 test("closes its own issue as dropped, saying when and why, and files nothing new", () => {
8798 hunt(app, "alice/forge", T);
8899 const [issue, ...more] = filed();
@@ -154,6 +165,7 @@ describe("a shared red that passed again on its own (#8287)", () => {
154165 app.db
155166 .query("INSERT INTO degraded_span (cause, sentence, href, opened_at) VALUES (?1, 'It is red on 3 pulls.', '/x', ?2)")
156167 .run(cause(), T + 3 * HOUR);
168 flaked(T + 3 * HOUR);
157169 expect(hunt(app, "alice/forge", T + 3 * HOUR + 5 * MIN).filed).toBe(1);
158170 const fresh = filed().find((i) => i.id !== earlier.id)!;
159171 expect(fresh.state).toBe("open");
@@ -167,8 +179,70 @@ describe("a shared red that passed again on its own (#8287)", () => {
167179 app.db
168180 .query("INSERT INTO degraded_span (cause, sentence, href, opened_at) VALUES (?1, 'It is red on 3 pulls.', '/x', ?2)")
169181 .run(cause(), T + 25 * HOUR);
182 flaked(T + 25 * HOUR);
170183 expect(hunt(app, "alice/forge", T + 25 * HOUR + 5 * MIN).filed).toBe(1);
171184 const fresh = filed().find((i) => i.state === "open")!;
172185 expect(getIssueById(app.db, fresh.id)!.body).not.toContain("Opened again within a day");
173186 });
174187 });
188
189 describe("a shared red the rule now calls each pull's own (#8786)", () => {
190 const owned = () =>
191 listComments(app.db, filed()[0]!.id).filter((c) => c.body.startsWith("Each pull's own red, not the lane's (#8786)"));
192
193 test("closes its open issue as dropped, saying main passes it and no re-run did, and files nothing new", () => {
194 hunt(app, "alice/forge", T);
195 const [issue, ...more] = filed();
196 expect(more).toEqual([]);
197 expect(issue!.state).toBe("closed");
198 expect(issue!.closeReason).toBe("dropped");
199 expect(owned().length).toBe(1);
200 expect(owned()[0]!.body).toContain(`- test: ${TEST}`);
201 expect(owned()[0]!.body).toContain("- main: passes on main's newest check");
202 expect(owned()[0]!.body).toContain(": none");
203 // The day's degraded total does not file it again.
204 hunt(app, "alice/forge", T + 2 * HOUR);
205 expect(filed().length).toBe(1);
206 });
207
208 test("closes it while pulls still fail it, and with its span gone from the day, by the cause its evidence names", () => {
209 redPull(9, T - 10 * MIN);
210 app.db.query("DELETE FROM degraded_span WHERE cause = ?1").run(cause());
211 hunt(app, "alice/forge", T);
212 expect(filed()[0]!.state).toBe("closed");
213 expect(owned().length).toBe(1);
214 });
215
216 test("a tick between hunts closes it, before a builder's next pick", () => {
217 const prior = process.env.FORGE_SENTINEL_REPO;
218 process.env.FORGE_SENTINEL_REPO = "alice/forge";
219 try {
220 expect(sentinelTick(app, T)).toBeNull();
221 } finally {
222 if (prior === undefined) delete process.env.FORGE_SENTINEL_REPO;
223 else process.env.FORGE_SENTINEL_REPO = prior;
224 }
225 expect(filed()[0]!.state).toBe("closed");
226 expect(owned().length).toBe(1);
227 });
228
229 test("exception: a flake recorded while it was red keeps it the lane's: it stays open while pulls fail it", () => {
230 flaked(T - 44 * MIN);
231 redPull(9, T - 10 * MIN);
232 hunt(app, "alice/forge", T);
233 expect(filed()[0]!.state).toBe("open");
234 });
235
236 test("exception: main's newest check failing the test keeps it the lane's", () => {
237 check("failed", T - 10 * MIN);
238 hunt(app, "alice/forge", T);
239 expect(filed()[0]!.state).toBe("open");
240 });
241
242 test("exception: an issue a person has commented on is never closed this way", () => {
243 addComment(app.db, { issueId: filed()[0]!.id, authorId: alice.id, body: "I am looking at this" });
244 hunt(app, "alice/forge", T);
245 expect(filed()[0]!.state).toBe("open");
246 expect(owned()).toEqual([]);
247 });
248 });
test/shared-red.test.ts +96 −42
@@ -1,7 +1,8 @@
11 /**
22 * A red many pulls share (#2266), on in-memory SQLite: a test failed by
33 * three pulls in the hour is the lane's, one failed by fewer, or failed
4 * longer ago, is the pull's own; the vitals carry one card per shared test,
4 * longer ago, is the pull's own, and so is one main passes that no re-run
5 * passed (#8786); the vitals carry one card per shared test,
56 * linked to its flake issue once the sentinel has filed one.
67 */
78 import { nameUnnamed } from "../src/checks/bun-test.ts";
@@ -21,6 +22,7 @@ import {
2122 TREE_GATES,
2223 flakeHref,
2324 flakeKey,
25 pullsOwn,
2426 sharedCause,
2527 sharedTests,
2628 sharedWithPulls,
@@ -50,6 +52,18 @@ const red = (issueId: string, names: string[], repoId = "r1"): Run => {
5052 return finishRun(db, run.id, { state: "failed", summary: "check failed", checkDetail: { passed: 1, failed: names.length, names } })!;
5153 };
5254 const shared = (run: Run | undefined, now?: number) => sharedWithPulls(db, run, now);
55 /** Main's newest check: green, or red naming `names`. */
56 const mainCheck = (state: "ok" | "failed", names: string[] = []): void => {
57 const run = createRun(db, { repoId: "r1", kind: "check", env: "main", commitOid: "b".repeat(40), reason: "push" });
58 claimRun(db, run.id);
59 finishRun(db, run.id, { state, summary: state, ...(state === "failed" ? { checkDetail: { passed: 1, failed: names.length, names } } : {}) });
60 };
61 /** A pull's re-run of the same commit, green. */
62 const green = (issueId: string): void => {
63 const run = createRun(db, { repoId: "r1", kind: "check", env: `pull-${issueId}`, issueId, commitOid: "a".repeat(40), reason: "retry" });
64 claimRun(db, run.id);
65 finishRun(db, run.id, { state: "ok", summary: "ok" });
66 };
5367
5468 describe("a red many pulls share", () => {
5569 test("three pulls on one test share it; two do not", () => {
@@ -75,53 +89,93 @@ describe("a red many pulls share", () => {
7589 expect(sharedTests(db)).toEqual([]);
7690 });
7791
78 test("a gate on the tree is each pull's own red, however many trip it (#2868)", () => {
79 const gate = "design check: nothing added outside the system (#2678) > the tree is no worse than its baseline";
80 for (const i of ["i1", "i2", "i3"]) red(i, [gate]);
81 expect(shared(red("i4", [gate]))).toBe(false);
82 expect(shared(red("i5", [gate, "X"]))).toBe(false);
83 expect(sharedTests(db)).toEqual([]);
84 });
85
86 test("every reserved page a pull forgot is that pull's own red, not the lane's (#7809)", () => {
87 const gate = "reserved names > every top-level page of the forge is a reserved name";
88 for (const i of ["i1", "i2", "i3"]) red(i, [gate]);
89 expect(shared(red("i4", [gate]))).toBe(false);
90 const index = "the gathered routes > the index is current: run `bun scripts/openapi-routes.ts`";
91 for (const i of ["i1", "i2", "i3"]) red(i, [index]);
92 expect(shared(red("i4", [index]))).toBe(false);
93 expect(sharedTests(db)).toEqual([]);
94 // The rest of the suite is no gate: three pulls on it still share it.
95 const other = "reserved names > the forge's own nouns are reserved too";
96 for (const i of ["i1", "i2"]) red(i, [other]);
97 expect(shared(red("i3", [other]))).toBe(true);
98 });
99
100 test("three pulls each adding a route with no OpenAPI row are three pulls' own reds (#8184)", () => {
101 const gate = "every registered /api/v1 route has an OpenAPI row or is listed as not yet described";
102 for (const i of ["i1", "i2", "i3"]) red(i, [gate]);
103 expect(shared(red("i4", [gate]))).toBe(false);
104 const index = "the gathered routes > the index is current: run `bun scripts/openapi-routes.ts`";
105 for (const i of ["i1", "i2", "i3"]) red(i, [index]);
106 expect(shared(red("i4", [index]))).toBe(false);
92 test("three pulls fail a test main passes, and none is a shared red (#8786)", () => {
93 mainCheck("ok");
94 for (const i of ["i1", "i2"]) red(i, ["X"]);
95 const third = red("i3", ["X"]);
96 expect(shared(third)).toBe(false);
10797 expect(sharedTests(db)).toEqual([]);
98 expect(pullsOwn(db, "r1", "X", Date.now() - SHARED_WINDOW_MS)).toBe(true);
10899 const v = vitals({ db, dataDir: tmpdir(), signals: { errors: new ErrorLedger(), refused: new RefusedLedger() } });
109100 expect(v.degraded.filter((d) => d.cause.startsWith("shared:"))).toEqual([]);
110 // Named whole, not by prefix: a top-level test it begins does not hide.
111 const longer = `${gate} twice`;
112 for (const i of ["i1", "i2"]) red(i, [longer]);
113 expect(shared(red("i3", [longer]))).toBe(true);
101 // Main red on other tests it named still ran this one, and passed it.
102 mainCheck("failed", ["Y"]);
103 expect(shared(red("i4", ["X"]))).toBe(false);
114104 });
115105
116 test("three pulls each adding a tool without regenerating the index are three pulls' own reds (#7906)", () => {
117 const index = "the gathered tools > the index is current: run `bun scripts/mcp-tools.ts`";
118 for (const i of ["i1", "i2", "i3"]) red(i, [index]);
119 expect(shared(red("i4", [index]))).toBe(false);
106 test("three pulls fail a test main also fails, and it is shared (#8786)", () => {
107 mainCheck("failed", ["X"]);
108 for (const i of ["i1", "i2"]) red(i, ["X"]);
109 expect(shared(red("i3", ["X"]))).toBe(true);
110 expect(sharedTests(db).map((t) => t.test)).toEqual(["X"]);
111 });
112
113 test("a flake that passes on a pull's re-run is still the lane's, main green (#8786)", () => {
114 mainCheck("ok");
115 for (const i of ["i1", "i2"]) red(i, ["X"]);
116 const third = red("i3", ["X"]);
117 expect(shared(third)).toBe(false);
118 // i2's re-run of the same commit is green: the test is random, not i2's.
119 green("i2");
120 expect(shared(third)).toBe(true);
121 expect(sharedTests(db).map((t) => t.test)).toEqual(["X"]);
122 });
123
124 test("a re-run red on other tests it named passed this one; one naming nothing did not", () => {
125 mainCheck("ok");
126 for (const i of ["i1", "i2"]) red(i, ["X"]);
127 const third = red("i3", ["X"]);
128 red("i2", []);
129 expect(shared(third)).toBe(false);
130 const broken = red("i2", ["Y"]);
131 db.run("UPDATE run SET check_detail = '{not json' WHERE id = ?1", [broken.id]);
132 expect(shared(third)).toBe(false);
133 red("i2", ["Y"]);
134 expect(shared(third)).toBe(true);
135 });
136
137 test("a test the check runner re-ran alone and recorded a flake is the lane's (#8786)", () => {
138 mainCheck("ok");
139 for (const i of ["i1", "i2"]) red(i, ["X"]);
140 const third = red("i3", ["X"]);
141 expect(shared(third)).toBe(false);
142 db.run(
143 "INSERT INTO check_flake (id, repo_id, run_id, commit_oid, test, file, at) VALUES ('f1', 'r1', ?1, 'a', 'X', 'x.test.ts', ?2)",
144 [third.id, new Date().toISOString()],
145 );
146 expect(shared(third)).toBe(true);
147 });
148
149 test("a test that fails again on the same pull's re-run, main green, is that pull's own", () => {
150 mainCheck("ok");
151 for (const i of ["i1", "i2", "i3"]) red(i, ["X"]);
152 expect(shared(red("i3", ["X"]))).toBe(false);
120153 expect(sharedTests(db)).toEqual([]);
121 // The rest of the suite is no gate: three pulls on it still share it.
122 const other = "the gathered tools > are ordered by code point, not the machine's locale";
123 for (const i of ["i1", "i2"]) red(i, [other]);
124 expect(shared(red("i3", [other]))).toBe(true);
154 });
155
156 test("main's verdict unread (no check, or a red naming nothing) leaves a many-pull red the lane's", () => {
157 for (const i of ["i1", "i2"]) red(i, ["X"]);
158 const third = red("i3", ["X"]);
159 expect(shared(third)).toBe(true);
160 mainCheck("failed", []);
161 expect(shared(third)).toBe(true);
162 });
163
164 test("every gate once listed by hand is each pull's own by the rule, main green (#2868, #7809, #8140, #7906, #8184)", () => {
165 mainCheck("ok");
166 const gates = [
167 "design check: nothing added outside the system (#2678) > the tree is no worse than its baseline",
168 "reserved names > every top-level page of the forge is a reserved name",
169 "every registered /api/v1 route has an OpenAPI row or is listed as not yet described",
170 "the gathered routes > the index is current: run `bun scripts/openapi-routes.ts`",
171 "the gathered tools > the index is current: run `bun scripts/mcp-tools.ts`",
172 ];
173 for (const gate of gates) {
174 for (const i of ["i1", "i2", "i3"]) red(i, [gate]);
175 expect([gate, shared(red("i4", [gate]))]).toEqual([gate, false]);
176 // Checked per gate: each gate's reds share one commit, so a later gate's red passes an earlier one, as a re-run would.
177 expect(sharedTests(db).map((t) => t.test)).not.toContain(gate);
178 }
125179 });
126180
127181 test("every tree gate names a suite, or a test in it, that exists", () => {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs