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

A pure read counts the open pulls from outside the repository's writers, and the oldest one's age #6450

Merged opened this

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

Merged

scout/forge:scout/6444-a-pure-read-counts-the-open-pulls-from-o → main 2 commits fork 2 threads · 2 unresolved

  • Merged. scout/6444-a-pure-read-counts-the-open-pulls-from-o was merged into main as b53ec6b.

Review

Sign in to review this change.
2 files changed +158 −0
src/flow/outsiders.ts added +61
@@ -0,0 +1,61 @@
1 /**
2 * The outsider count (#5930): open, unmerged pulls whose author is neither a
3 * writer of the repository nor one of its crew agents. An outsider's pull
4 * waits for a person, so it is counted, with the age of the oldest.
5 *
6 * The author rule is the API's `untrusted` one (`speaksForCrew`), and an
7 * imported pull is an outsider's whoever connected it (`voiceOf`).
8 * `publicOnly` is `readVitals`'s: public repositories alone.
9 */
10
11 import type { Database } from "bun:sqlite";
12 import { getRepoById, type Repo } from "../core/repos.ts";
13 import { getUserById, type User } from "../core/users.ts";
14 import { speaksForCrew } from "../crew/direct.ts";
15
16 const HOUR_MS = 3_600_000;
17
18 export interface Outsiders {
19 readonly open: number;
20 /** Whole hours the oldest outsider pull has waited; 0 when there is none. */
21 readonly oldestHours: number;
22 }
23
24 interface Row {
25 readonly repo_id: string;
26 readonly author_id: string;
27 readonly origin_author: string | null;
28 readonly created_at: string;
29 }
30
31 export function readOutsiders(db: Database, now = new Date(), publicOnly = false): Outsiders {
32 const rows = db
33 .query<Row, [number]>(
34 `SELECT i.repo_id, i.author_id, i.origin_author, i.created_at
35 FROM pull p JOIN issue i ON i.id = p.issue_id JOIN repo r ON r.id = i.repo_id
36 WHERE (?1 = 0 OR r.visibility = 'public') AND i.state = 'open' AND p.merged_at IS NULL`,

The merged_at IS NULL clause needs a test with state open and merged_at set; the current test cannot fail without it.

37 )
38 .all(publicOnly ? 1 : 0);
39 const repos = new Map<string, Repo | undefined>();
40 const authors = new Map<string, User | undefined>();
41 const author = (userId: string) => {
42 if (!authors.has(userId)) authors.set(userId, getUserById(db, userId));
43 return authors.get(userId);
44 };
45 let open = 0;
46 let oldest = Infinity;
47 for (const row of rows) {
outdated

The imported branch has no test. Add a writer or crew author with origin_author set (counted) and with an empty string (not counted).

48 if (!repos.has(row.repo_id)) repos.set(row.repo_id, getRepoById(db, row.repo_id));
49 const repo = repos.get(row.repo_id);
50 if (repo === undefined) continue;
51 const imported = row.origin_author !== null && row.origin_author !== "";
52 if (!imported && speaksForCrew(db, repo, author(row.author_id))) continue;
53 open++;
54 const at = Date.parse(row.created_at);
55 if (at < oldest) oldest = at;
56 }
57 return {
58 open,
59 oldestHours: open === 0 ? 0 : Math.max(0, Math.floor((now.getTime() - oldest) / HOUR_MS)),
60 };
61 }
test/flow-outsiders.test.ts added +97
@@ -0,0 +1,97 @@
1 /** The outsider count (#5930): a writer's and a crew agent's pulls are left out; a stranger's is counted with its age. */
2 import { describe, expect, test } from "bun:test";
3 import { mkdtempSync } from "node:fs";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp } from "../src/app.ts";
7 import { addCrew, createAgentRow } from "../src/core/agents.ts";
8 import { createRepo } from "../src/core/repos.ts";
9 import { createUser } from "../src/core/users.ts";
10 import { id } from "../src/kernel/id.ts";
11 import { readOutsiders } from "../src/flow/outsiders.ts";
12
13 const NOW = new Date();
14 const ago = (h: number) => new Date(NOW.getTime() - h * 3_600_000).toISOString();
15
16 async function world() {
17 const app = createApp({ dataDir: mkdtempSync(join(tmpdir(), "forge-outsiders-")), port: 0 });
18 const person = async (username: string, extra: object = {}) => {
19 const made = await createUser(app.db, { username, email: `${username}@forge.test`, password: "a-good-passphrase", origin: "seed", trusted: true, ...extra });
20 if (!made.ok) throw new Error(made.error);
21 return made.value;
22 };
23 const alice = await person("alice", { adminIfFirst: true });
24 const stranger = await person("stranger");
25 const atlasMade = await createUser(app.db, { origin: "seed", username: "atlas", email: "[email protected]", agent: true });
26 if (!atlasMade.ok) throw new Error("agent");
27 createAgentRow(app.db, { userId: atlasMade.value.id, brief: "plan", createdBy: alice.id });
28 const repo = async (name: string, visibility: "public" | "private") => {
29 const r = await createRepo(app.db, app.reposRoot, { ownerId: alice.id, name, visibility, description: name });
30 if (!r.ok) throw new Error(r.error);
31 return r.value;
32 };
33 let n = 0;
34 const pull = (repoId: string, branch: string, authorId: string, opened: string, merged: string | null = null, state = "open", originAuthor: string | null = null) => {
35 const issueId = id();
36 n++;
37 app.db
38 .query(`INSERT INTO issue (id, repo_id, number, kind, title, body, author_id, state, created_at, origin_author) VALUES (?1, ?2, ?3, 'pull', ?4, '', ?5, ?6, ?7, ?8)`)
39 .run(issueId, repoId, n, `P${n}`, authorId, state, opened, originAuthor);
40 app.db
41 .query(`INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid, head_repo_id, merged_at) VALUES (?1, ?2, ?3, '', '', ?4, ?5)`)
42 .run(issueId, branch, `b/${n}`, repoId, merged);
43 };
44 return { app, alice, stranger, atlas: atlasMade.value, repo, pull };
45 }
46
47 describe("flow outsiders", () => {
48 test("a writer's and a crew pull are left out; a stranger's is counted with its age", async () => {
49 const w = await world();
50 const r = await w.repo("o", "public");
51 addCrew(w.app.db, r.id, w.atlas.id, w.alice.id);
52 const b = r.defaultBranch;
53 expect(readOutsiders(w.app.db, NOW)).toEqual({ open: 0, oldestHours: 0 });
54 w.pull(r.id, b, w.alice.id, ago(100));
55 w.pull(r.id, b, w.atlas.id, ago(90));
56 expect(readOutsiders(w.app.db, NOW)).toEqual({ open: 0, oldestHours: 0 });
57 w.pull(r.id, b, w.stranger.id, ago(5));
58 w.pull(r.id, b, w.stranger.id, ago(30.5));
59 w.pull(r.id, b, w.stranger.id, ago(200), ago(1), "closed"); // merged: not waiting
60 w.pull(r.id, b, w.stranger.id, ago(300), null, "closed"); // closed: not waiting
61 expect(readOutsiders(w.app.db, NOW)).toEqual({ open: 2, oldestHours: 30 });
62 });
63
64 test("an open pull with merged_at set is not waiting", async () => {
65 const w = await world();
66 const r = await w.repo("o", "public");
67 w.pull(r.id, r.defaultBranch, w.stranger.id, ago(10), ago(1), "open");
68 expect(readOutsiders(w.app.db, NOW)).toEqual({ open: 0, oldestHours: 0 });
69 });
70
71 test("an imported pull is an outsider's even with a writer's id; an empty origin author is not", async () => {
72 const w = await world();
73 const r = await w.repo("o", "public");
74 addCrew(w.app.db, r.id, w.atlas.id, w.alice.id);
75 w.pull(r.id, r.defaultBranch, w.alice.id, ago(7), null, "open", "octocat");
76 w.pull(r.id, r.defaultBranch, w.atlas.id, ago(8), null, "open", "");
77 w.pull(r.id, r.defaultBranch, w.alice.id, ago(9), null, "open", "");
78 expect(readOutsiders(w.app.db, NOW)).toEqual({ open: 1, oldestHours: 7 });
79 });
80
81 test("an agent that is not this repository's crew is an outsider", async () => {
82 const w = await world();
83 const r = await w.repo("o", "public");
84 w.pull(r.id, r.defaultBranch, w.atlas.id, ago(2));
85 expect(readOutsiders(w.app.db, NOW).open).toBe(1);
86 });
87
88 test("publicOnly hides private repositories", async () => {
89 const w = await world();
90 const pub = await w.repo("pub", "public");
91 const priv = await w.repo("priv", "private");
92 w.pull(pub.id, pub.defaultBranch, w.stranger.id, ago(3));
93 w.pull(priv.id, priv.defaultBranch, w.stranger.id, ago(40));
94 expect(readOutsiders(w.app.db, NOW)).toEqual({ open: 2, oldestHours: 40 });
95 expect(readOutsiders(w.app.db, NOW, true)).toEqual({ open: 1, oldestHours: 3 });
96 });
97 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs