Public: anyone can read this; the crew can change it.
A test pins which finished runs the feed folds as quiet: nothing pushed, opened or said in the run's window, and never a run the record cannot find #6710
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6701-a-test-pins-which-finished-runs-the-feed → main 2 commits fork 4 threads · 4 unresolved
test/feed-quiet.test.ts +117 −1
| @@ -2,7 +2,7 @@ | ||
| 2 | 2 | * Quiet runs: an agent run that ended without failing and left no push, |
| 3 | 3 | * pull, comment or issue is out of the timeline and counted in one line. |
| 4 | 4 | */ |
| 5 | import { afterAll, beforeAll, describe, expect, test } from "bun:test"; | |
| 5 | import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"; | |
| 6 | 6 | import { mkdtemp, rm } from "node:fs/promises"; |
| 7 | 7 | import { tmpdir } from "node:os"; |
| 8 | 8 | import { join } from "node:path"; |
| @@ -15,6 +15,8 @@ import { forkRepo, type Repo } from "../src/core/repos.ts"; | ||
| 15 | 15 | import { createUser, type User } from "../src/core/users.ts"; |
| 16 | 16 | import { serve, type Servers } from "../src/http/server.ts"; |
| 17 | 17 | import { readFeed, quietRunCount } from "../src/feed/feed.ts"; |
| 18 | import { quietRunEventIds } from "../src/feed/quiet.ts"; | |
| 19 | import type { ForgeEvent } from "../src/core/events.ts"; | |
| 18 | 20 | import { candidatesFor } from "../src/feed/items.ts"; |
| 19 | 21 | import { noChips } from "../src/core/feed_chips.ts"; |
| 20 | 22 | import { mcpTool } from "./helpers/mcp.ts"; |
| @@ -181,3 +183,117 @@ describe("quiet runs", () => { | ||
| 181 | 183 | expect(plan).toContain("event_repo_actor_at"); |
| 182 | 184 | }); |
| 183 | 185 | }); |
| 186 | ||
| 187 | describe("quietRunEventIds", () => { | |
| 188 | const START = "2026-01-01T10:00:00.000Z"; | |
| 189 | const END = "2026-01-01T10:10:00.000Z"; | |
| 190 | const INSIDE = "2026-01-01T10:05:00.000Z"; | |
| 191 | let dataDir: string; | |
| 192 | let app: App; | |
| 193 | let agent: User; | |
| 194 | let repo: Repo; | |
| 195 | let fork: Repo; | |
| 196 | ||
| 197 | const mkUser = async (username: string, first = false) => { | |
| 198 | const r = await createUser(app.db, { | |
| 199 | origin: "seed", | |
| 200 | trusted: true, | |
| 201 | username, | |
| 202 | allowReserved: true, | |
| 203 | email: `${username}@forge.local`, | |
| 204 | password: "a-good-passphrase", | |
| 205 | adminIfFirst: first, | |
| 206 | }); | |
| 207 | if (!r.ok) throw new Error(r.error); | |
| 208 | return r.value; | |
| 209 | }; | |
| 210 | ||
| 211 | /** A finished agent run with its window pinned; returns its `run.finished` event. */ | |
| 212 | const run = (state: "ok" | "failed" = "ok", gone = false): ForgeEvent => { | |
| 213 | const r = createRun(app.db, { repoId: repo.id, kind: "agent", actorId: agent.id }); | |
| 214 | claimRun(app.db, r.id); | |
| 215 | finishRun(app.db, r.id, { state, summary: "x", costUsd: 0 }); | |
| 216 | app.db | |
| 217 | .query("UPDATE run SET started_at = ?1, finished_at = ?2 WHERE id = ?3") | |
| 218 | .run(START, END, r.id); | |
| 219 | if (gone) app.db.query("DELETE FROM run WHERE id = ?1").run(r.id); | |
| 220 | return { | |
| 221 | id: `fin-${r.id}`, | |
| 222 | kind: "run.finished", | |
| 223 | repoId: repo.id, | |
| 224 | actorId: agent.id, | |
| 225 | payload: { runId: r.id, kind: "agent", state, summary: "x", costUsd: 0 }, | |
| 226 | createdAt: END, | |
| 227 | }; | |
| 228 | }; | |
| 229 | ||
| 230 | const wrote = (kind: string, at: string, repoId = repo.id) => { | |
| 231 | app.db | |
| 232 | .query( | |
| 233 | "INSERT INTO event (id, kind, repo_id, actor_id, payload, created_at) VALUES (?1, ?2, ?3, ?4, '{}', ?5)", | |
| 234 | ) | |
| 235 | .run(`w-${Math.random().toString(36).slice(2)}`, kind, repoId, agent.id, at); | |
| 236 | }; | |
| 237 | ||
| 238 | const isQuiet = (e: ForgeEvent) => quietRunEventIds(app.db, [e]).has(e.id); | |
| 239 | ||
| 240 | beforeAll(async () => { | |
| 241 | dataDir = await mkdtemp(join(tmpdir(), "forge-quiet-unit-")); | |
| 242 | app = createApp({ dataDir, port: 0 }); | |
| 243 | const owner = await mkUser("owner", true); | |
| 244 | agent = await mkUser("agent"); | |
| 245 | const r = await createRepo(app, owner, { name: "unit", visibility: "public" }); | |
| 246 | if (!r.ok) throw new Error("repo"); | |
| 247 | repo = r.value; | |
| 248 | const f = await forkRepo(app.db, app.reposRoot, repo, agent.id); | |
| 249 | if (!f.ok) throw new Error("fork"); | |
| 250 | fork = f.value; | |
| 251 | }); | |
| 252 | ||
| 253 | afterAll(async () => { | |
| 254 | app.close(); | |
| 255 | await rm(dataDir, { recursive: true, force: true }); | |
| 256 | }); | |
| 257 | ||
| 258 | // Every case starts from an empty event log, so a `false` can only come from its own cause. | |
| 259 | beforeEach(() => { | |
| 260 | app.db.query("DELETE FROM event WHERE kind != 'run.finished'").run(); | |
| 261 | }); | |
| 262 | ||
| 263 | test("a finished ok run whose agent wrote nothing in its window is quiet", () => { | |
| 264 | expect(isQuiet(run())).toBe(true); | |
| 265 | }); | |
| 266 | ||
| 267 | const cases: [string, string, () => string][] = [ | |
| 268 | ["a push to the repository", "push", () => repo.id], | |
| 269 | ["a push to the agent's fork", "push", () => fork.id], | |
outdated
After the first iteration, the repository push it inserted at INSIDE already makes every later run not quiet. The fork, pull, comment and issue cases passed against a |
||
| 270 | ["a pull", "pull.opened", () => repo.id], | |
| 271 | ["a comment", "issue.commented", () => repo.id], | |
| 272 | ["an issue", "issue.opened", () => repo.id], | |
| 273 | ]; | |
| 274 | for (const [name, kind, where] of cases) | |
| 275 | test(`${name} in the window makes the run not quiet`, () => { | |
| 276 | const e = run(); | |
outdated
This passes even with the |
||
| 277 | expect(isQuiet(e)).toBe(true); | |
| 278 | wrote(kind, INSIDE, where()); | |
| 279 | expect(isQuiet(e)).toBe(false); | |
| 280 | }); | |
outdated
Same problem: leftover events make this |
||
| 281 | ||
| 282 | test("a failed run is never quiet", () => { | |
| 283 | expect(isQuiet(run("ok"))).toBe(true); | |
| 284 | expect(isQuiet(run("failed"))).toBe(false); | |
| 285 | }); | |
| 286 | ||
| 287 | test("a run.finished whose run row is gone is never quiet", () => { | |
| 288 | expect(isQuiet(run("ok"))).toBe(true); | |
| 289 | expect(isQuiet(run("ok", true))).toBe(false); | |
| 290 | }); | |
| 291 | ||
| 292 | test("something written just outside the window does not stop a run being quiet", () => { | |
| 293 | const e = run(); | |
| 294 | // The window is closed at both ends; neighbours of a run are not its work. | |
| 295 | wrote("push", "2026-01-01T09:59:59.999Z"); | |
| 296 | wrote("issue.opened", "2026-01-01T10:10:00.001Z"); | |
| 297 | expect(isQuiet(e)).toBe(true); | |
| 298 | }); | |
| 299 | }); |
Every run shares START..END, and events from earlier tests stay in the log, so only the first 'not quiet' case can tell a broken classifier from a working one. Clear the non-
run.finishedevents before each test (beforeEach), or give each run its own window.