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

The probe verdicts and an app's wake from sleep are the same every run: probe, probes and apps-sleep leave the quarantine #11095

Merged opened this

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

Merged

scout/forge:scout/11084-the-probe-verdicts-and-an-app-s-wake-fro → main 6 commits fork 1 thread · 1 unresolved

  • Merged. scout/11084-the-probe-verdicts-and-an-app-s-wake-fro was merged into main as 59a9ec4.

Review

Sign in to review this change.
7 files changed +180 −150
scripts/gate.ts −15
@@ -33,21 +33,6 @@ export type Exclusion = {
3333 };
3434
3535 export const EXCLUDED: readonly Exclusion[] = [
36 {
37 file: "test/apps-sleep.test.ts",
38 reason: "red alone under --randomize (2026-11-10): an environment nobody asks for sleeps, and the next request wakes it — times out at 30s",
39 issue: 11084,
40 },
41 {
42 file: "test/probe.test.ts",
43 reason: "red alone under --randomize (2026-11-10): a scaffolded app obeys every cell: verdict mismatch, not ok",
44 issue: 11084,
45 },
46 {
47 file: "test/probes.test.ts",
48 reason: "red alone under --randomize (2026-11-10): the first clean probe after closes the issue: verdict down, not ok",
49 issue: 11084,
50 },
5136 {
5237 file: "test/rehearsal-real.test.ts",
5338 reason: "green alone (3 of 3, 2026-11-10) but unproven under the gate's load, where on main@accaa65 the second real roll's old forge did not come up",
src/apps/runtime.ts +14 −6
@@ -1389,7 +1389,7 @@ export class AppsRuntime {
13891389 state.live = live;
13901390 this.awake(state, servingAt);
13911391 state.asleep = undefined;
1392 state.lastRequestAt = Date.now();
1392 state.lastRequestAt = this.now();
13931393 state.lastError = undefined;
13941394 if (rested !== undefined && rested.dir !== live.dir)
13951395 rmSync(rested.dir, { recursive: true, force: true });
@@ -1785,7 +1785,7 @@ export class AppsRuntime {
17851785 touch(record: AppRecord, env: EnvName): void {
17861786 const state = this.envs.get(key(record.id, env));
17871787 if (state === undefined) return;
1788 state.lastRequestAt = Date.now();
1788 state.lastRequestAt = this.now();
17891789 state.keptAwake = undefined;
17901790 }
17911791
@@ -1802,7 +1802,7 @@ export class AppsRuntime {
18021802 if (!held) return;
18031803 held = false;
18041804 state.openStreams--;
1805 state.lastRequestAt = Date.now();
1805 state.lastRequestAt = this.now();
18061806 };
18071807 }
18081808
@@ -1820,7 +1820,7 @@ export class AppsRuntime {
18201820 if (!held) return;
18211821 held = false;
18221822 state.openRequests--;
1823 state.lastRequestAt = Date.now();
1823 state.lastRequestAt = this.now();
18241824 };
18251825 }
18261826
@@ -1996,7 +1996,7 @@ export class AppsRuntime {
19961996 state.asleep = undefined;
19971997 state.live = live;
19981998 this.awake(state, now);
1999 state.lastRequestAt = Date.now();
1999 state.lastRequestAt = this.now();
20002000 this.watch(state, repo, live);
20012001 write("awake");
20022002 })().finally(() => {
@@ -2174,13 +2174,21 @@ export class AppsRuntime {
21742174 : { kind: "slider", position: getEconomics(this.app.db).position };
21752175 }
21762176
2177 /**
2178 * The sweep's clock. Requests stamp `lastRequestAt` with it too, so a
2179 * window measured by the sweep starts again at a request on the same clock.
2180 */
2181 private now(): number {
2182 return this.options.clock?.() ?? Date.now();
2183 }
2184
21772185 /** Stop what nobody has asked for lately, and keep it ready to wake. */
21782186 private async sweep(): Promise<void> {
21792187 if (this.stopped) return;
21802188 await this.hibernate();
21812189 const source = this.windowSource();
21822190 const grace = this.options.busyGraceMs ?? BUSY_GRACE_MS;
2183 const now = this.options.clock?.() ?? Date.now();
2191 const now = this.now();
21842192 this.metered(() => beatAwake(this.app.db, new Date(now).toISOString()));
21852193 for (const state of this.envs.values()) {
21862194 const live = state.live;
src/contract/probes.ts +12 −4
@@ -21,7 +21,7 @@ import { openIssue, setIssueState } from "../actions/issues.ts";
2121 import { id as newId } from "../kernel/id.ts";
2222 import { createLog } from "../kernel/log.ts";
2323 import { audit } from "./audit.ts";
24 import { probe, type Knock, type Probed } from "./probe.ts";
24 import { probe, type Knock, type ProbeInput, type Probed } from "./probe.ts";
2525 import { during } from "../perf/jobs.ts";
2626
2727 const log = createLog("probes");
@@ -113,6 +113,9 @@ export function probeIssue(app: App, repo: Repo) {
113113 );
114114 }
115115
116 /** What asks the doors: the real probe, unless a test hands in its own verdicts. */
117 export type Prober = (input: ProbeInput) => Promise<Probed | undefined>;
118
116119 /**
117120 * Knock on one app now, keep the row, and open or close its issue. Returns
118121 * undefined for an app with no sound contract: there is nothing to compare
@@ -122,11 +125,12 @@ export async function probeApp(
122125 app: App,
123126 runtime: AppsRuntime,
124127 record: AppRecord,
128 knock: Prober = probe,
125129 ): Promise<ProbeRow | undefined> {
126130 const repo = getRepoById(app.db, record.repoId);
127131 if (repo === undefined) return undefined;
128132 const audited = await audit(app.reposRoot, repo);
129 const probed = await probe({
133 const probed = await knock({
130134 runtime,
131135 dataDir: app.config.dataDir,
132136 record,
@@ -193,7 +197,11 @@ export function untilNightly(now = Date.now()): number {
193197 export function startProbes(
194198 app: App,
195199 runtime: AppsRuntime,
196 options: { readonly nightly?: boolean } = {},
200 options: {
201 readonly nightly?: boolean;
202 /** The seam: a test's prober answers in place of the running app's doors. */
203 readonly knock?: Prober;
204 } = {},
197205 ): ProbesHandle {
198206 let stopped = false;
199207 const inFlight = new Set<Promise<unknown>>();
@@ -205,7 +213,7 @@ export function startProbes(
205213 };
206214 const one = (record: AppRecord): Promise<ProbeRow | undefined> =>
207215 track(
208 probeApp(app, runtime, record).catch((error) => {
216 probeApp(app, runtime, record, options.knock).catch((error) => {
209217 log.error("probe failed", { app: record.slug, error: String(error) });
210218 return undefined;
211219 }),
test/apps-busy.test.ts +12 −3
@@ -157,8 +157,12 @@ describe("apps that are busy", () => {
157157 /** What the run log gained after `before`. */
158158 const since = (before: string): string => log().slice(before.length);
159159
160 /** Put production to sleep and wake it, so each test starts rested. */
160 /**
161 * Put production to sleep and wake it, so each test starts rested. The
162 * clock comes back to the wall first: requests are stamped with it too.
163 */
161164 async function freshlyAwake(): Promise<void> {
165 ahead = 0;
162166 if (production().state !== "asleep") {
163167 await viaHost("/done");
164168 await viaHost("/answer");
@@ -268,8 +272,9 @@ describe("apps that are busy", () => {
268272 expect(now).toContain("its child processes are still working");
269273
270274 // The child finishes; the next sweep finds nothing to wait for.
275 // Asking for /done was a request at this hour: a window past it, it sleeps.
271276 expect(await (await viaHost("/done")).text()).toBe("done");
272 await sweepAt(WINDOW + 60_000);
277 await sweepAt(2 * WINDOW + 2 * 60_000);
273278 await asleep();
274279 expect(production().busy).toBeUndefined();
275280 },
@@ -286,7 +291,8 @@ describe("apps that are busy", () => {
286291 expect(production().busy).toContain("a request is still waiting");
287292 await viaHost("/answer");
288293 expect(await (await response).text()).toBe("late");
289 await sweepAt(WINDOW + 60_000);
294 // Answered at this hour; a window past the answer, nothing holds it.
295 await sweepAt(2 * WINDOW + 2 * 60_000);
290296 await asleep();
291297 });
292298
@@ -305,6 +311,9 @@ describe("apps that are busy", () => {
305311
306312 abort.abort();
307313 await reader.cancel().catch(() => {});
314 // Closed at this hour; a window past the close, nothing holds it.
315 await until(() => (production().busy === undefined ? true : undefined), 10_000);
316 await sweepAt(2 * WINDOW + 2 * 60_000);
308317 await asleep();
309318 });
310319
test/apps-sleep.test.ts +59 −9
@@ -28,8 +28,16 @@ describe("apps that sleep", () => {
2828 let record: AppRecord;
2929 let preview = "";
3030 const DOMAIN = "apps.test";
31 const SOON = { production: 700, preview: 700 };
32 const LATER = { production: 3_600_000, preview: 3_600_000 };
31 const HOUR = 3_600_000;
32 const WINDOW = { production: HOUR, preview: HOUR };
33 /**
34 * The sweep's clock runs this far ahead of the wall: the test puts an
35 * environment to sleep by moving it, never by waiting out a window. Past
36 * the window and the busy grace both, so a process that looks busy
37 * sleeps on the next sweep too.
38 */
39 let ahead = 0;
40 const clock = () => Date.now() + ahead;
3341
3442 const viaHost = (host: string, path = "/") =>
3543 fetch(`${servers.url}${path}`, { headers: { host }, redirect: "manual" });
@@ -64,7 +72,8 @@ describe("apps that sleep", () => {
6472 scheme: "http",
6573 publicPort: servers.port,
6674 forgeUrl: servers.url,
67 sleepAfterMs: SOON,
75 sleepAfterMs: WINDOW,
76 clock,
6877 sweepEveryMs: 150,
6978 });
7079 await runtime.start();
@@ -97,7 +106,15 @@ describe("apps that sleep", () => {
97106 });
98107
99108 test("an environment nobody asks for sleeps, and the next request wakes it", async () => {
100 await until(() => (stateOf("production") === "asleep" ? true : undefined));
109 ahead = 3 * HOUR;
110 try {
111 await until(
112 () => (stateOf("production") === "asleep" ? true : undefined),
113 5_000,
114 );
115 } finally {
116 ahead = 0;
117 }
101118 const asleep = runtime.status(record).find((e) => e.env === "production")!;
102119 expect(asleep.port).toBeUndefined();
103120 expect(asleep.since).toBeDefined();
@@ -109,14 +126,46 @@ describe("apps that sleep", () => {
109126 const runId = runtime
110127 .status(record)
111128 .find((e) => e.env === "production")!.runId!;
129 // Nothing was busy: the idle path put it to sleep, not the busy grace.
112130 expect(log(runId)).toContain("asleep: nothing asked for it");
113131 expect(log(runId)).toContain("awake");
outdated

This no longer proves that a request inside the window resets the idle timer. Sweeps simply do not put a woken env back to sleep, which is a weaker claim. Restore the reset check by advancing ahead in steps around a request, as described in the body. Also the log regex now accepts either wording, so it cannot tell which sleep path ran.

114 // Wanted again, it stays up past the limit.
115 await Bun.sleep(400);
132 }, 10_000);
133
134 test("a request inside the window starts it again; past it with none, it sleeps", async () => {
135 const MINUTE = 60_000;
136 // Waiting for a few sweeps to run at the clock as it stands now.
137 const sweeps = () => Bun.sleep(600);
138 const slept = () =>
139 log(
140 runtime.status(record).find((e) => e.env === "production")!.runId!,
141 ).match(/asleep: nothing asked for it/g)?.length ?? 0;
142 const before = slept();
143 try {
144 expect(stateOf("production")).toBe("running");
145 // Woken just now; the window is nearly out.
146 ahead = HOUR - 5 * MINUTE;
147 await sweeps();
148 expect(stateOf("production")).toBe("running");
149 // A request inside the window resets the idle timer...
150 expect((await viaHost(`hello.${DOMAIN}`)).status).toBe(200);
151 // ...so past the window from the wake, but not from the request, it stays up.
152 ahead = HOUR + 20 * MINUTE;
153 await sweeps();
154 expect(stateOf("production")).toBe("running");
155 // Past the window from the request, with nothing asked since: it sleeps.
156 ahead = 2 * HOUR;
157 await until(
158 () => (stateOf("production") === "asleep" ? true : undefined),
159 5_000,
160 );
161 } finally {
162 ahead = 0;
163 }
164 expect(slept()).toBe(before + 1);
165 // The next request wakes it again, for the test after this one.
116166 expect((await viaHost(`hello.${DOMAIN}`)).status).toBe(200);
117 await Bun.sleep(400);
118167 expect(stateOf("production")).toBe("running");
119 }, 30_000);
168 }, 15_000);
120169
121170 test("the forge coming back starts production warm and leaves previews asleep", async () => {
122171 const branch = await commitToBranch(app, alice, repo, {
@@ -145,7 +194,8 @@ describe("apps that sleep", () => {
145194 scheme: "http",
146195 publicPort: servers.port,
147196 forgeUrl: servers.url,
148 sleepAfterMs: LATER,
197 sleepAfterMs: WINDOW,
198 clock,
149199 sweepEveryMs: 150,
150200 });
151201 await runtime.start();
test/probe.test.ts +15 −10
@@ -2,7 +2,8 @@
22 * Is it so: the probe knocks on a scaffolded app's doors as nobody, a user
33 * and an admin, and every answer agrees with the table. Then a door is
44 * broken by hand in the running app's source — a rule that lets anyone
5 * delete — and the probe says exactly which cell the app disobeys.
5 * delete — and the probe says exactly which cell the app disobeys. Each
6 * test probes an app of its own, so neither verdict hangs on which ran first.
67 */
78 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
89 import { mkdtemp, rm } from "node:fs/promises";
@@ -36,7 +37,7 @@ describe("the probe asks the running app whether it obeys its table", () => {
3637 }
3738 };
3839 /** The newest production deploy, once it has settled — and that it is ok. */
39 const deployed = async (after: string): Promise<string> => {
40 const deployed = async (repo: Repo, after: string): Promise<string> => {
4041 let run: ReturnType<typeof listRuns>[number] | undefined;
4142 await until(() => {
4243 run = listRuns(app.db, repo.id, { kind: "deploy", limit: 50 }).find(
@@ -65,7 +66,7 @@ describe("the probe asks the running app whether it obeys its table", () => {
6566 }, 30_000);
6667 return run.commitOid;
6768 };
68 const probeNow = async (): Promise<Probed> => {
69 const probeNow = async (repo: Repo): Promise<Probed> => {
6970 const record = getAppByRepo(app.db, repo.id)!;
7071 const fresh = getRepoById(app.db, repo.id)!;
7172 const audited = await audit(app.reposRoot, fresh);
@@ -107,7 +108,7 @@ describe("the probe asks the running app whether it obeys its table", () => {
107108 });
108109 if (!made.ok) throw new Error(String(made.error));
109110 repo = made.value.repo;
110 await deployed("");
111 await deployed(repo, "");
111112 }, 150_000);
112113
113114 afterAll(async () => {
@@ -118,7 +119,7 @@ describe("the probe asks the running app whether it obeys its table", () => {
118119 });
119120
120121 test("a scaffolded app obeys every cell the probe can ask", async () => {
121 const out = await probeNow();
122 const out = await probeNow(repo);
122123 expect(out.verdict).toBe("ok");
123124 expect(out.said).toEqual([]);
124125 const asked = out.knocks.filter((k) => k.verdict !== "unprobed");
@@ -142,8 +143,12 @@ describe("the probe asks the running app whether it obeys its table", () => {
142143 // still says a stranger may not. The app's own tests would catch this
143144 // before it ever ran — the deploy runs them first — so the change also
144145 // drops them, which is what a change that gets a door wrong looks like.
146 const made = await newApp(app, alice, { name: "doors", visibility: "public" });
147 if (!made.ok) throw new Error(String(made.error));
148 const doors = made.value.repo;
149 await deployed(doors, "");
145150 const files = new Map(
146 scaffoldFiles({ name: "notes", description: "" }).map((f) => [
151 scaffoldFiles({ name: "doors", description: "" }).map((f) => [
147152 f.path,
148153 f.content,
149154 ]),
@@ -164,7 +169,7 @@ describe("the probe asks the running app whether it obeys its table", () => {
164169 expect(opened).not.toBe(door);
165170 const since = new Date().toISOString();
166171 await Bun.sleep(5);
167 const wrote = await commitToBranch(app, alice, repo, {
172 const wrote = await commitToBranch(app, alice, doors, {
168173 message: "a hand-written rule, and the tests that knew better",
169174 files: [
170175 { path: "src/actions.ts", content: broken },
@@ -176,8 +181,8 @@ describe("the probe asks the running app whether it obeys its table", () => {
176181 ],
177182 });
178183 expect(wrote.ok).toBe(true);
179 await deployed(since);
180 const out = await probeNow();
184 await deployed(doors, since);
185 const out = await probeNow(doors);
181186 expect(out.verdict).toBe("mismatch");
182187 // Past the door, the stranger was told there is no such row: 404 is
183188 // what "admitted" looks like on an id no row has.
@@ -188,5 +193,5 @@ describe("the probe asks the running app whether it obeys its table", () => {
188193 expect(
189194 out.knocks.filter((k) => k.verdict === "mismatch").map((k) => k.verb),
190195 ).toEqual(["delete"]);
191 }, 120_000);
196 }, 180_000);
192197 });
test/probes.test.ts +68 −103
@@ -2,6 +2,11 @@
22 * The probe as a service: a production deploy is followed by a knock, a
33 * mismatch opens one issue and keeps it single, and the first clean probe
44 * after closes it. The nightly round is the same knock for every app.
5 *
6 * The doors are asked by a scripted prober (`startProbes`' `knock` seam):
7 * what the running app answers is `probe.test.ts`'s business; here the
8 * verdict is whatever the test says, so it is the same every run and in
9 * every order. Each test owns its own app.
510 */
611 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
712 import { mkdtemp, rm } from "node:fs/promises";
@@ -9,9 +14,8 @@ import { tmpdir } from "node:os";
914 import { join } from "node:path";
1015 import { createApp, type App } from "../src/app.ts";
1116 import { newApp } from "../src/actions/apps.ts";
12 import { commitToBranch } from "../src/actions/repos.ts";
13 import { AppsRuntime } from "../src/apps/runtime.ts";
14 import { scaffoldFiles } from "../src/apps/scaffold.ts";
17 import type { AppsRuntime } from "../src/apps/runtime.ts";
18 import type { Probed } from "../src/contract/probe.ts";
1519 import {
1620 PROBE_ISSUE_TITLE,
1721 latestProbes,
@@ -23,50 +27,60 @@ import {
2327 } from "../src/contract/probes.ts";
2428 import type { AppRecord } from "../src/core/apps.ts";
2529 import { listIssues } from "../src/core/issues.ts";
26 import { getRepoById, type Repo } from "../src/core/repos.ts";
27 import { listRuns } from "../src/core/runs.ts";
30 import type { Repo } from "../src/core/repos.ts";
2831 import { createUser, type User } from "../src/core/users.ts";
29 import { serve, type Servers } from "../src/http/server.ts";
32
33 const SAID = "note.delete as anon: the table says no, the app said 404";
3034
3135 describe("the probe follows deploys and keeps one issue", () => {
3236 let dataDir: string;
3337 let app: App;
34 let servers: Servers;
35 let runtime: AppsRuntime;
3638 let probes: ProbesHandle;
3739 let alice: User;
38 let repo: Repo;
39 let record: AppRecord;
40 /** What the doors of each app say, by app id: the test sets it. */
41 const answers = new Map<string, "ok" | "mismatch">();
4042
41 const until = async (what: () => boolean, ms = 90_000): Promise<void> => {
43 const until = async (what: () => boolean, ms = 10_000): Promise<void> => {
4244 const start = Date.now();
4345 while (!what()) {
4446 if (Date.now() - start > ms) throw new Error("not in time");
45 await Bun.sleep(100);
47 await Bun.sleep(50);
4648 }
4749 };
48 const deployedAfter = (since: string) =>
49 until(() => {
50 const run = listRuns(app.db, repo.id, { kind: "deploy", limit: 20 }).find(
51 (r) => r.env === "production" && r.createdAt > since,
52 );
53 return run !== undefined && run.state === "ok";
50 const made = async (name: string): Promise<{ repo: Repo; record: AppRecord }> => {
51 const out = await newApp(app, alice, { name, visibility: "public" });
52 if (!out.ok) throw new Error(String(out.error));
53 return { repo: out.value.repo, record: out.value.app };
54 };
55 /** A production deploy finished green, as the runtime says it. */
56 const deployed = (repo: Repo): void => {
57 app.bus.append({
58 kind: "run.finished",
59 repoId: repo.id,
60 payload: { kind: "deploy", state: "ok", env: "production" },
5461 });
55 const probesSoFar = () => listProbes(app.db, record.id).length;
62 };
63 const probesOf = (record: AppRecord) => listProbes(app.db, record.id);
5664
5765 beforeAll(async () => {
5866 dataDir = await mkdtemp(join(tmpdir(), "forge-probes-"));
5967 app = createApp({ dataDir, port: 0 });
60 servers = await serve(app);
61 runtime = new AppsRuntime(app, {
62 domain: "apps.test",
63 scheme: "http",
64 publicPort: servers.port,
65 forgeUrl: servers.url,
66 concurrency: 1,
68 // No app ever runs: the prober below answers for its doors.
69 const runtime = {} as AppsRuntime;
70 probes = startProbes(app, runtime, {
71 nightly: false,
72 knock: async (input): Promise<Probed> => {
73 const verdict = answers.get(input.record.id) ?? "ok";
74 return {
75 repo: input.repo,
76 commitOid: input.repo.id,
77 at: new Date().toISOString(),
78 verdict,
79 knocks: [],
80 said: verdict === "mismatch" ? [SAID] : [],
81 };
82 },
6783 });
68 await runtime.start();
69 probes = startProbes(app, runtime, { nightly: false });
7084 const a = await createUser(app.db, { origin: "seed", trusted: true,
7185 username: "alice",
7286 email: "[email protected]",
@@ -75,103 +89,60 @@ describe("the probe follows deploys and keeps one issue", () => {
7589 });
7690 if (!a.ok) throw new Error(a.error);
7791 alice = a.value;
78 const made = await newApp(app, alice, { name: "notes", visibility: "public" });
79 if (!made.ok) throw new Error(String(made.error));
80 repo = made.value.repo;
81 record = made.value.app;
82 await deployedAfter("");
83 }, 150_000);
92 }, 30_000);
8493
8594 afterAll(async () => {
8695 await probes.stop();
87 await runtime.stop();
88 servers.stop();
8996 app.close();
9097 await rm(dataDir, { recursive: true, force: true });
9198 });
9299
93100 test("a production deploy is followed by a knock, and a clean app has no issue", async () => {
94 await until(() => probesSoFar() >= 1, 30_000);
101 const { repo, record } = await made("clean");
102 answers.set(record.id, "ok");
103 deployed(repo);
104 await until(() => probesOf(record).length >= 1);
95105 const latest = latestProbes(app.db, [record.id]).get(record.id)!;
96106 expect(latest.verdict).toBe("ok");
97107 expect(latest.said).toEqual([]);
98108 expect(probeIssue(app, repo)).toBeUndefined();
99 }, 60_000);
109 }, 15_000);
100110
101111 test("a disobeying deploy opens one issue; a second probe does not double it", async () => {
102 const files = new Map(
103 scaffoldFiles({ name: "notes", description: "" }).map((f) => [f.path, f.content]),
104 );
105 const actions = files.get("src/actions.ts") as string;
106 const broken = actions.replace(
107 'if (permit(who, "note", verb, row) !== "yes")',
108 'if (verb !== "delete" && permit(who, "note", verb, row) !== "yes")',
109 );
110 expect(broken).not.toBe(actions);
111 // The door asks the same table before the action does, so the rule is
112 // written there too: a hand-written rule has to open both to be heard.
113 const door = files.get("src/security.ts") as string;
114 const opened = door.replace(
115 ": !mayKnock(who, found.resource, found.verb);",
116 ': found.verb !== "delete" && !mayKnock(who, found.resource, found.verb);',
117 );
118 expect(opened).not.toBe(door);
119 const before = probesSoFar();
120 const since = new Date().toISOString();
121 await Bun.sleep(5);
122 const wrote = await commitToBranch(app, alice, repo, {
123 message: "a hand-written rule, and the tests that knew better",
124 files: [
125 { path: "src/actions.ts", content: broken },
126 { path: "src/security.ts", content: opened },
127 { path: "src/contract.test.ts", content: null },
128 { path: "src/actions.test.ts", content: null },
129 { path: "src/server.test.ts", content: null },
130 { path: "src/audit.test.ts", content: null },
131 ],
132 });
133 expect(wrote.ok).toBe(true);
134 await deployedAfter(since);
135 await until(() => probesSoFar() > before, 30_000);
112 const { repo, record } = await made("broken");
113 answers.set(record.id, "mismatch");
114 deployed(repo);
115 await until(() => probesOf(record).length >= 1);
136116 const latest = latestProbes(app.db, [record.id]).get(record.id)!;
137117 expect(latest.verdict).toBe("mismatch");
138118 const issue = probeIssue(app, repo);
139119 expect(issue).toBeDefined();
140120 expect(issue!.title).toBe(PROBE_ISSUE_TITLE);
141 expect(issue!.body).toContain("note.delete as anon: the table says no, the app said 404");
121 expect(issue!.body).toContain(SAID);
142122 expect(issue!.authorId).toBe(alice.id);
143123
144124 // A round knocks again: still one issue.
145125 const rows = await probes.round();
146 expect(rows.map((r) => r.verdict)).toEqual(["mismatch"]);
126 expect(rows.filter((r) => r.appId === record.id).map((r) => r.verdict)).toEqual([
127 "mismatch",
128 ]);
147129 expect(
148130 listIssues(app.db, repo.id, { state: "open", limit: 50 }).filter(
149131 (i) => i.title === PROBE_ISSUE_TITLE,
150132 ),
151133 ).toHaveLength(1);
152 }, 150_000);
134 }, 15_000);
153135
154136 test("the first clean probe after closes the issue", async () => {
155 const files = new Map(
156 scaffoldFiles({ name: "notes", description: "" }).map((f) => [f.path, f.content]),
157 );
158 const before = probesSoFar();
159 const since = new Date().toISOString();
160 await Bun.sleep(5);
161 const wrote = await commitToBranch(app, alice, repo, {
162 message: "the door obeys again",
163 files: [
164 { path: "src/actions.ts", content: files.get("src/actions.ts") as string },
165 { path: "src/security.ts", content: files.get("src/security.ts") as string },
166 { path: "src/contract.test.ts", content: files.get("src/contract.test.ts") as string },
167 { path: "src/actions.test.ts", content: files.get("src/actions.test.ts") as string },
168 { path: "src/server.test.ts", content: files.get("src/server.test.ts") as string },
169 { path: "src/audit.test.ts", content: files.get("src/audit.test.ts") as string },
170 ],
171 });
172 expect(wrote.ok).toBe(true);
173 await deployedAfter(since);
174 await until(() => probesSoFar() > before, 30_000);
137 const { repo, record } = await made("mended");
138 answers.set(record.id, "mismatch");
139 await probes.round();
140 expect(probeIssue(app, repo)).toBeDefined();
141
142 // The door obeys again, and the deploy that says so is knocked on.
143 answers.set(record.id, "ok");
144 deployed(repo);
145 await until(() => probesOf(record).length >= 2);
175146 expect(latestProbes(app.db, [record.id]).get(record.id)!.verdict).toBe("ok");
176147 expect(probeIssue(app, repo)).toBeUndefined();
177148 const closed = listIssues(app.db, repo.id, { state: "closed", limit: 50 }).find(
@@ -179,14 +150,8 @@ describe("the probe follows deploys and keeps one issue", () => {
179150 );
180151 expect(closed).toBeDefined();
181152 // The history is rows, newest first.
182 expect(listProbes(app.db, record.id).map((r) => r.verdict)).toEqual([
183 "ok",
184 "mismatch",
185 "mismatch",
186 "ok",
187 ]);
188 expect(getRepoById(app.db, repo.id)).toBeDefined();
189 }, 150_000);
153 expect(probesOf(record).map((r) => r.verdict)).toEqual(["ok", "mismatch"]);
154 }, 15_000);
190155
191156 test("the nightly round is scheduled for the next 09:00 UTC", () => {
192157 const at = Date.UTC(2026, 8, 21, 8, 59, 0);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs