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

An app's home reads its policy line under 2 ms when its tip has not moved #6166

Merged opened this

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

Merged

scout/forge:scout/6147-an-app-s-home-reads-its-policy-line-unde → main 2 commits fork 2 threads · 2 unresolved

  • Merged. scout/6147-an-app-s-home-reads-its-policy-line-unde was merged into main as c0df7e8.

Review

Sign in to review this change.
2 files changed +141 −7
src/contract/audit.ts +39 −7
@@ -12,9 +12,11 @@
1212 * audit that leaves out what it cannot read reads as a clean bill.
1313 */
1414
15 import { readFileSync } from "node:fs";
1516 import { readFileAt } from "../apps/checkout.ts";
1617 import type { Repo } from "../core/repos.ts";
1718 import { resolveOid } from "../git/merge.ts";
19 import { isSafeOid, isSafeRef } from "../git/guard.ts";
1820 import { repoDir } from "../git/repos.ts";
1921 import {
2022 VERBS,
@@ -108,14 +110,44 @@ export async function policyBrief(
108110 return { identities: standing.identities, lines };
109111 }
110112
111 /** Read one repository's policy at the tip of its default branch. */
113 /** Most (repository, tip) pairs remembered; the oldest is dropped first. */
114 const CACHE_MAX = 512;
115
116 /** A standing is a pure function of (directory, tip), so it is safe to keep. */
117 const remembered = new Map<string, Standing>();
118
119 /**
120 * The tip of a branch without spawning git: a loose ref is one small file.
121 * Anything else (packed, symbolic, odd) answers undefined and git is asked.
122 */
123 function looseTip(dir: string, branch: string): string | undefined {

Only the loose-ref path is exercised. Add a test where refs are packed (git pack-refs --all), so the resolveOid fallback and post-push invalidation are covered.

124 if (!isSafeRef(`refs/heads/${branch}`)) return undefined;
125 try {
126 const text = readFileSync(`${dir}/refs/heads/${branch}`, "utf8").trim();
127 return isSafeOid(text) ? text : undefined;
128 } catch {
129 return undefined;
130 }
131 }
132
133 /**
134 * Read one repository's policy at the tip of its default branch. The result
135 * is kept by (repository, tip): a push moves the tip and so the next read
136 * reads again; a tip that has not moved costs one small file read.
137 */
112138 export async function audit(reposRoot: string, repo: Repo): Promise<Audited> {
113139 const dir = repoDir(reposRoot, repo.ownerName, repo.name);
114 const oid = await resolveOid(dir, `refs/heads/${repo.defaultBranch}`);
140 const oid =
141 looseTip(dir, repo.defaultBranch) ??
142 (await resolveOid(dir, `refs/heads/${repo.defaultBranch}`));
115143 if (oid === undefined) return { repo, standing: { kind: "unread" } };
116 return {
117 repo,
118 oid,
119 standing: standingOf(await readFileAt(dir, oid, "contract.json")),
120 };
144 const key = `${dir}\0${oid}`;
145 let standing = remembered.get(key);
146 if (standing === undefined) {
147 standing = standingOf(await readFileAt(dir, oid, "contract.json"));
148 if (remembered.size >= CACHE_MAX)
149 remembered.delete(remembered.keys().next().value!);
150 remembered.set(key, standing);
151 }
152 return { repo, oid, standing };
121153 }
test/contract-audit-cache.test.ts added +102
@@ -0,0 +1,102 @@
1 /**
2 * `audit` keeps a repository's policy by (repository, tip): a warm read is
3 * fast, and a push that changes forge.json is seen on the next read.
4 */
5 import { afterAll, beforeAll, expect, test } from "bun:test";
6 import { existsSync, mkdtempSync, rmSync } from "node:fs";
7 import { tmpdir } from "node:os";
8 import { join } from "node:path";
9 import { createApp, type App } from "../src/app.ts";
10 import { commitToBranch, createRepo } from "../src/actions/repos.ts";
11 import { repoDir } from "../src/git/repos.ts";
12 import { audit } from "../src/contract/audit.ts";
13 import { createUser, getUserByName } from "../src/core/users.ts";
14 import { getRepo } from "../src/core/repos.ts";
15
16 let root = "";
17 let app: App;
18
19 beforeAll(() => {
20 root = mkdtempSync(join(tmpdir(), "forge-audit-cache-"));
21 app = createApp({ dataDir: root, port: 0 });
22 });
23 afterAll(() => {
24 app.close();
25 rmSync(root, { recursive: true, force: true });
26 });
27
28 const contract = (read: string) =>
29 JSON.stringify({
30 roles: { anon: { above: [], held: { by: "everyone" } } },
31 resources: {
32 thing: {
33 can: { create: "anon", read, update: "anon", delete: "anon", list: "anon" },
34 },
35 },
36 });
37
38 test("a warm audit is served from the cache and a new tip is read afresh", async () => {
39 const u = await createUser(app.db, {
40 origin: "seed",
41 trusted: true,
42 username: "alice",
43 email: "[email protected]",
44 password: "a-good-passphrase",
45 adminIfFirst: true,
46 });
47 if (!u.ok) throw new Error(u.error);
48 const alice = u.value;
49 const made = await createRepo(app, alice, { name: "cached" });
50 if (!made.ok) throw new Error(String(made.error));
51 const write = async (text: string) => {
52 const r = await commitToBranch(app, alice, made.value, {
53 message: "policy",
54 files: [{ path: "contract.json", content: text }],
55 });
56 if (!r.ok) throw new Error(JSON.stringify(r.error));
57 };
58 const repo = () => getRepo(app.db, "alice", "cached")!;
59
60 await write(contract("anon"));
61 const first = await audit(app.reposRoot, repo());
62 expect(first.standing.kind).toBe("sound");
63
64 const warm = await audit(app.reposRoot, repo());
65 expect(warm.standing).toBe(first.standing);
outdated

This wall-clock bound will flake under CI load. The identity check on the next line already proves the cache hit, so remove this line.

66
67 await write(JSON.stringify({ roles: {}, resources: {} }));
68 const changed = await audit(app.reposRoot, repo());
69 expect(changed.oid).not.toBe(first.oid);
70 expect(changed.standing).not.toBe(first.standing);
71 });
72
73 test("a packed ref takes the git path, agrees with git, and a push still invalidates", async () => {
74 const alice = getUserByName(app.db, "alice")!;
75 const made = await createRepo(app, alice, { name: "packed" });
76 if (!made.ok) throw new Error(String(made.error));
77 const write = async (text: string) => {
78 const r = await commitToBranch(app, alice, made.value, {
79 message: "policy",
80 files: [{ path: "contract.json", content: text }],
81 });
82 if (!r.ok) throw new Error(JSON.stringify(r.error));
83 };
84 const repo = () => getRepo(app.db, "alice", "packed")!;
85 const dir = repoDir(app.reposRoot, "alice", "packed");
86 const git = (...args: string[]) =>
87 Bun.spawnSync(["git", "-C", dir, ...args]).stdout.toString().trim();
88
89 await write(contract("anon"));
90 git("pack-refs", "--all");
91 expect(existsSync(join(dir, "refs/heads", repo().defaultBranch))).toBe(false);
92
93 const packed = await audit(app.reposRoot, repo());
94 expect(packed.oid).toBe(git("rev-parse", `refs/heads/${repo().defaultBranch}`));
95 expect(packed.standing.kind).toBe("sound");
96
97 await write(JSON.stringify({ roles: {}, resources: {} }));
98 const next = await audit(app.reposRoot, repo());
99 expect(next.oid).not.toBe(packed.oid);
100 expect(next.oid).toBe(git("rev-parse", `refs/heads/${repo().defaultBranch}`));
101 expect(next.standing).not.toBe(packed.standing);
102 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs