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

Every new forge comes with its own source as a repo, wired to trevato/forge upstream, so its crew can work on it #10992

Merged opened this in answer to

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

Merged

torque/forge:torque/10989-every-new-forge-comes-with-its-own-sourc → main 3 commits fork 4 threads · 4 unresolved

  • Merged. torque/10989-every-new-forge-comes-with-its-own-sourc was merged into main as a35466b.

Review

Sign in to review this change.
6 files changed +499 −55
docs/features/own-source.md added +50
@@ -0,0 +1,50 @@
1 # A child forge's own source
2
3 Every new child forge is born with `<admin>/forge`, a fork of the parent's code
4 (`src/spawn/forge_code.ts`), so its crew can work on the forge itself.
5
6 - Its `forgeRepo` setting names it (an admin's earlier choice stands), so the
7 forge shows that repository as itself.
8 - The parent is its upstream, recorded as `forgeCode` in `parent.json`: take
9 updates, or send a change back as a pull on the parent through the origin.
10 - Its crew is seated on it, each with the role they hold on the child's other
11 repositories.
12
13 Merging there runs on the next image; it does not redeploy the child.
14
15 ## Existing children: a recorded one-shot
16
17 `src/spawn/forge_code_backfill.ts` runs at the parent's boot. The first run
18 writes `forge-code-backfill.json` under the parent's data directory: the
19 children that exist then, and what became of each. Later boots read that file
20 and open nothing once every child is settled; children born afterwards are
21 made with their source and are not in it. No child names live in source.
22
23 - A child whose `parent.json` already records its source is settled without
24 opening its database.
25 - A child that is awake (open here or in a cell) is skipped and tried at the
26 next boot: opening its database writes, and its write lock is its own.
27 - A failure is recorded as an error and retried at the next boot.
28
29 A `forge` repository a person made is left alone. "We made it" is a record:
30 `forgeCodeStarted` in `parent.json`, written before the repository is created.
31 A run that died after creating the repo (fetch failed, or `parent.json` not
32 yet written) leaves the record, and the next pass finishes the fetch; a repo
33 without the record is a person's and stays theirs.
34
35 ## Both doors, and what was exercised
36
37 The terminal needs nothing of its own: `<admin>/forge` is an ordinary public
38 repository there, listed and opened like any other. Settings (`forgeRepo`) is
39 a web page. Exercised by `test/forge-code-own-source.test.ts`: a spawned child
40 has the repo, the setting and the parent as origin; backfill is idempotent,
41 finishes an unfinished run, leaves a person's repo alone, and is a recorded
42 one-shot; adoption seats the crew with copied roles and keeps an earlier
43 `forgeRepo`.
44
45 ## Why it goes before the pipeline list
46
47 A forge that cannot be worked on from inside cannot take part in the pipeline
48 list (#6534, #6611, ...): those items are changes to the child's own code, and
49 each needs a repository on the child to land in and an upstream to send back
50 to. This is the ground they stand on.
src/actions/spawn.ts +5 −55
@@ -38,6 +38,7 @@ import { createUser, getUserByName, type User } from "../core/users.ts";
3838 import { runGit } from "../git/env.ts";
3939 import { repoDir } from "../git/repos.ts";
4040 import { commitFiles } from "../git/write.ts";
41 import { adoptForgeCode, forkForgeCode } from "../spawn/forge_code.ts";
4142 import { birthFromTemplate, type TemplateMark } from "../spawn/template_birth.ts";
4243 import { sha256 } from "../kernel/hash.ts";
4344 import { secret } from "../kernel/id.ts";
@@ -60,7 +61,7 @@ import { quotaRefusal } from "../spawn/quota.ts";
6061 import { personRefusal } from "../spawn/sleep.ts";
6162 import { planSpawn, type SpawnInput, type SpawnPlan, type SpawnSource } from "../spawn/plan.ts";
6263 import { DEFAULT_SIZE, isSize, type Size } from "../spawn/sizes.ts";
63 import { chainForChild, DEFAULT_MODE, forgeLinkOf, type FollowMode, type Hop } from "../spawn/chain.ts";
64 import { DEFAULT_MODE } from "../spawn/chain.ts";
6465 import { requireUser } from "./access.ts";
6566
6667 export interface Spawned {
@@ -467,6 +468,9 @@ async function copyInto(
467468 setCrewRole(child.db, repo.id, theirs.user.id, role);
468469 }
469470
471 // The fork is the child's own source: its `forgeRepo`, and the crew seated on it.
472 adoptForgeCode(child, org.username, founder.id);
473
470474 // The child's own keys, made in its own directory now, so they exist
471475 // before its first boot and are provably not the parent's: the key that
472476 // seals its secrets, and its SSH host key.
@@ -587,60 +591,6 @@ async function startFresh(
587591 return ok(true);
588592 }
589593
590 /**
591 * The parent's own code, forked into the child's organisation with its full
592 * history. Returns the parent's `owner/name` when forked, undefined when the
593 * forge does not name its code (`forgeRepo`) or the repo is not there —
594 * a child is still spawned, just without the fork.
595 *
596 * A cross-forge fork: git carries the history, and the upstream stays
597 * reachable through the parent's address in `parent.json`. The in-database
598 * follow (`fork_parent_id`) is for forks on one forge; across two, the
599 * child pulls the parent's default branch when its owner asks.
600 */
601 async function forkForgeCode(
602 app: App,
603 child: App,
604 orgName: string,
605 mode: FollowMode,
606 ): Promise<Result<{ upstream: string; mode: FollowMode; tip: string; chain: readonly Hop[] } | undefined, string>> {
607 // A spawned forge names no `forgeRepo` of its own: its code is the fork
608 // it was born with, so a grandchild forks that.
609 const ref = app.config.forgeRepo ?? (await forgeLinkOf(app))?.repo;
610 if (ref === undefined) return ok(undefined);
611 const parts = ref.split("/");
612 if (parts.length !== 2 || !parts[0] || !parts[1]) return ok(undefined);
613 const [o, n] = parts as [string, string];
614 const parent = getRepo(app.db, o, n);
615 if (!parent) return ok(undefined);
616 const name = "forge";
617 let repo = getRepo(child.db, orgName, name);
618 if (!repo) {
619 const made = await createRepo(child.db, child.reposRoot, {
620 ownerId: getOrg(child.db, orgName)!.id,
621 name,
622 visibility: "public",
623 description: `A fork of ${o}/${n}: the forge's own code, following its parent.`,
624 defaultBranch: parent.defaultBranch,
625 });
626 if (!made.ok) return err(`could not make the child's forge repo: ${made.error}`);
627 repo = made.value;
628 }
629 const fetched = await runGit(
630 [
631 "fetch",
632 "--no-write-fetch-head",
633 repoDir(app.reposRoot, o, n),
634 "+refs/heads/*:refs/heads/*",
635 "+refs/tags/*:refs/tags/*",
636 ],
637 { cwd: repoDir(child.reposRoot, orgName, name), timeoutMs: 600_000 },
638 );
639 if (!fetched.ok) return err(`could not copy the forge's code: ${fetched.error.stderr}`);
640 const born = await chainForChild(app, `${o}/${n}`);
641 return ok({ upstream: `${o}/${n}`, mode, tip: born?.tip ?? "", chain: born?.chain ?? [] });
642 }
643
644594 /**
645595 * The model key a child already holds, opened with the child's own key; or
646596 * undefined when it has none yet (or the child's database is not there).
src/main.ts +7
@@ -28,6 +28,7 @@ import { offerStartingBoards } from "./actions/boards.ts";
2828 import { syncAllPillars } from "./actions/pillars.ts";
2929 import { bootHostedChildren, childAwake } from "./spawn/host.ts";
3030 import { stopCellChildren } from "./spawn/cell.ts";
31 import { backfillChildren } from "./spawn/forge_code_backfill.ts";
3132 import { firstBoot } from "./spawn/first-boot.ts";
3233 import { previewIdentityKey } from "./spawn/preview-owner.ts";
3334 import { PREVIEW_ADMIN, seedPreview } from "./spawn/preview-seed.ts";
@@ -440,6 +441,12 @@ if (apps && presence) warmBehind(app.log, presence, () => apps.startingCount());
440441 void backfillCarried(app.db, app.reposRoot).catch((error) =>
441442 app.log.error("could not backfill what deploys carried", { error: String(error) }),
442443 );
444 // Children born before each came with its own source get it (#10989).
445 void backfillChildren(app)
446 .then((done) => {
447 if (Object.keys(done).length > 0) app.log.info("forge-code backfill", { done: JSON.stringify(done) });
448 })
449 .catch((error) => app.log.error("could not backfill children's forge code", { error: String(error) }));
443450 // A forge still draining on this database keeps writing for its gates and
444451 // runs; starting ours beside it stalls the listener behind its write lock
445452 // (#6957). The listener answers meanwhile; a signal now stops what is up.
src/spawn/forge_code.ts added +175
@@ -0,0 +1,175 @@
1 /**
2 * A child forge's own source (#10989): `<org>/forge`, a fork of the
3 * parent's code at the commit the parent holds, with the history behind it.
4 * Three things make it the child's own to work on:
5 *
6 * 1. the child's `forgeRepo` setting names it, so the forge shows that
7 * repository as itself (`core/forge_repo.ts`);
8 * 2. `parent.json`'s `forgeCode` record names the parent's repository as
9 * its upstream: "bring in upstream" (`spawn/take.ts`) and "send a change
10 * back" (`actions/upstream.ts`, a pull on the parent through the
11 * proposal token, docs/ORIGINS.md) both read that record;
12 * 3. the crew is seated on it with the roles it holds elsewhere, so
13 * "improve my forge" has someone to take it.
14 *
15 * Every step is idempotent, and a repository a person already made under
16 * that name is left alone (`backfillForgeCode`).
17 */
18
19 import { rename } from "node:fs/promises";
20 import { join } from "node:path";
21 import type { App } from "../app.ts";
22 import { addCrew, listAgents, listCrewRoles, setCrewRole, type CrewRole } from "../core/agents.ts";
23 import { getOrg, listTeamMembers, listTeams, OWNERS_TEAM } from "../core/orgs.ts";
24 import { createRepo, getRepo, listReposOwnedBy, type Repo } from "../core/repos.ts";
25 import { runGit } from "../git/env.ts";
26 import { repoDir } from "../git/repos.ts";
27 import { err, ok, type Result } from "../kernel/result.ts";
28 import { forgeSource, setForgeSource, storedForgeSource } from "../settings/forge_source.ts";
29 import { chainForChild, DEFAULT_MODE, forgeLinkOf, type FollowMode, type Hop } from "./chain.ts";
30
31 /** The repository's name inside the child's organisation. */
32 export const FORGE_CODE_NAME = "forge";
33
34 export interface ForgeCode {
35 readonly upstream: string;
36 readonly mode: FollowMode;
37 readonly tip: string;
38 readonly chain: readonly Hop[];
39 }
40
41 /** The parent's own code repository as `owner/name`: its `forgeRepo`, else the fork it was born with. */
42 export async function forgeCodeRef(app: App): Promise<string | undefined> {
43 const ref = forgeSource(app.db, app.config.forgeRepo) ?? (await forgeLinkOf(app))?.repo;
44 const parts = ref?.split("/") ?? [];
45 return parts.length === 2 && parts[0] && parts[1] ? ref : undefined;
46 }
47
48 /**
49 * Fork the parent's code into the child's organisation with its full
50 * history; undefined when the forge names no code or it is not there (a
51 * child is still made, just without the fork). Git carries the history; the
52 * upstream stays reachable through `parent.json`.
53 */
54 export async function forkForgeCode(
55 app: App,
56 child: App,
57 orgName: string,
58 mode: FollowMode,
59 ): Promise<Result<ForgeCode | undefined, string>> {
60 const ref = await forgeCodeRef(app);
61 if (ref === undefined) return ok(undefined);
62 const [o, n] = ref.split("/") as [string, string];
63 const parent = getRepo(app.db, o, n);
64 const org = getOrg(child.db, orgName);
65 if (!parent || !org) return ok(undefined);
66 let repo = getRepo(child.db, orgName, FORGE_CODE_NAME);
67 if (!repo) {
68 const made = await createRepo(child.db, child.reposRoot, {
69 ownerId: org.id,
70 name: FORGE_CODE_NAME,
71 visibility: "public",
72 description: `A fork of ${o}/${n}: the forge's own code, following its parent.`,
73 defaultBranch: parent.defaultBranch,
74 });
75 if (!made.ok) return err(`could not make the child's forge repo: ${made.error}`);
76 repo = made.value;
77 }
78 const fetched = await runGit(
79 [
80 "fetch",
81 "--no-write-fetch-head",
82 repoDir(app.reposRoot, o, n),
83 "+refs/heads/*:refs/heads/*",
84 "+refs/tags/*:refs/tags/*",
85 ],
86 { cwd: repoDir(child.reposRoot, orgName, FORGE_CODE_NAME), timeoutMs: 600_000 },
87 );
88 if (!fetched.ok) return err(`could not copy the forge's code: ${fetched.error.stderr}`);
89 const born = await chainForChild(app, `${o}/${n}`);
90 return ok({ upstream: `${o}/${n}`, mode, tip: born?.tip ?? "", chain: born?.chain ?? [] });
91 }
92
93 /**
94 * Make the fork the child's own source: its `forgeRepo` names it (an admin's
95 * earlier choice stands) and every crew member is seated on it, with the
96 * role it holds on the child's other repositories, if any.
97 */
98 export function adoptForgeCode(child: App, orgName: string, actorId: string): Repo | undefined {
99 const repo = getRepo(child.db, orgName, FORGE_CODE_NAME);
100 if (!repo) return undefined;
101 if (storedForgeSource(child.db) === undefined)
102 setForgeSource(child.db, `${orgName}/${FORGE_CODE_NAME}`, actorId);
103 const roleOf = new Map<string, CrewRole>();
104 for (const other of listReposOwnedBy(child.db, repo.ownerId).filter((r) => r.id !== repo.id))
105 for (const { agent, role } of listCrewRoles(child.db, other.id))
106 if (!roleOf.has(agent.user.id)) roleOf.set(agent.user.id, role);
107 for (const agent of listAgents(child.db)) {
108 addCrew(child.db, repo.id, agent.user.id, actorId);
109 const role = roleOf.get(agent.user.id);
110 if (role !== undefined) setCrewRole(child.db, repo.id, agent.user.id, role);
111 }
112 return repo;
113 }
114
115 export type Backfilled = "made" | "already" | "left-alone" | "no-code" | "not-a-child";
116
117 /**
118 * Give an existing child its own source (#10989), once: forks the parent's
119 * code into `<org>/forge`, records it in the child's `parent.json`, names it
120 * as `forgeRepo`, and seats the crew. A child that already has it is
121 * `already`; a repository of that name nobody here made is `left-alone`,
122 * untouched. "We made it" is a record, `forgeCodeStarted`, written to
123 * `parent.json` before the repository is created: a run that died after the
124 * repo but before the fetch or the `forgeCode` record finds it on the next
125 * pass and finishes (both steps are idempotent), where a repo without the
126 * record is a person's.
127 */
128 export async function backfillForgeCode(
129 app: App,
130 child: App,
131 childDir: string,
132 mode: FollowMode = DEFAULT_MODE,
133 ): Promise<Result<Backfilled, string>> {
134 const file = join(childDir, "parent.json");
135 let doc: Record<string, unknown>;
136 try {
137 const parsed = await Bun.file(file).json();
138 if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return ok("not-a-child");
139 doc = parsed as Record<string, unknown>;
140 } catch {
141 return ok("not-a-child");
142 }
143 const orgName = doc["org"];
144 const org = typeof orgName === "string" ? getOrg(child.db, orgName) : undefined;
145 if (org === undefined || typeof orgName !== "string") return ok("not-a-child");
146 const ownersTeam = listTeams(child.db, org.id).find((t) => t.name === OWNERS_TEAM);
147 const owner = ownersTeam === undefined ? undefined : listTeamMembers(child.db, ownersTeam.id)[0];
148 const actorId = owner?.id ?? org.id;
149
150 const have = doc["forgeCode"] !== undefined && doc["forgeCode"] !== null;
151 const name = `${orgName}/${FORGE_CODE_NAME}`;
152 const unfinished = doc["forgeCodeStarted"] === name;
153 if (!have && !unfinished && getRepo(child.db, orgName, FORGE_CODE_NAME) !== undefined) return ok("left-alone");
154 let made = false;
155 if (!have) {
156 const write = async (next: Record<string, unknown>): Promise<void> => {
157 await Bun.write(`${file}.tmp`, JSON.stringify(next));
outdated

A repo left by an unfinished earlier run (created, fetch failed, parent.json unwritten) lands here as 'left-alone' for good. Distinguish it and test it.

158 await rename(`${file}.tmp`, file);
159 };
160 // Only a parent that has code to give starts the record: otherwise a
161 // person's later `forge` repo would read as ours.
162 const ref = await forgeCodeRef(app);
163 const [o, n] = ref?.split("/") ?? [];
164 if (ref === undefined || !o || !n || getRepo(app.db, o, n) === undefined) return ok("no-code");
165 if (!unfinished) await write({ ...doc, forgeCodeStarted: name });
166 const forked = await forkForgeCode(app, child, orgName, mode);
167 if (!forked.ok) return err(forked.error);
168 if (forked.value === undefined) return ok("no-code");
169 const { forgeCodeStarted: _started, ...rest } = doc;
170 await write({ ...rest, forgeCode: { repo: name, ...forked.value } });
171 made = true;
172 }
173 adoptForgeCode(child, orgName, actorId);
174 return ok(made ? "made" : "already");
175 }
outdated

Hardcoded child names in source: a one-off migration that stays forever. Make it a recorded one-shot.

src/spawn/forge_code_backfill.ts added +89
@@ -0,0 +1,89 @@
1 /**
2 * The one-shot backfill of #10989: the children that exist the first time
3 * this runs, and were born before every child came with its own source, get
4 * `<org>/forge` too. The run is recorded in `forge-code-backfill.json` under
5 * the parent's data directory: the children it covers, and what became of
6 * each. A later boot reads that one file and, once every child is settled,
7 * opens nothing. Children born afterwards are never in it (they are made
8 * with their source).
9 *
10 * Opening a child's database writes (migrations, WAL), so a child that is
11 * awake, open here or running in a cell, is not touched: it stays unsettled
12 * and is tried on the next boot. A child whose `parent.json` already records
13 * its source is settled without opening its database.
14 */
15
16 import { rename } from "node:fs/promises";
17 import { join } from "node:path";
18 import type { App } from "../app.ts";
19 import { createApp } from "../app.ts";
20 import { childDirOf, listChildren } from "../actions/spawn.ts";
21 import { backfillForgeCode, type Backfilled } from "./forge_code.ts";
22 import { childAwake } from "./host.ts";
23
24 export type Outcome = Backfilled | { error: string };
25 interface Record_ {
26 readonly considered: string[];
27 readonly done: Record<string, Outcome>;
28 }
29
30 const recordPath = (app: App): string => join(app.config.dataDir, "forge-code-backfill.json");
31
32 async function readRecord(app: App): Promise<Record_ | undefined> {
33 try {
34 const doc = (await Bun.file(recordPath(app)).json()) as Partial<Record_>;
35 if (!Array.isArray(doc.considered) || typeof doc.done !== "object" || doc.done === null) return undefined;
outdated

Trailing blank line. Also this opens live child databases on every boot; justify or guard it.

36 return { considered: doc.considered.filter((n) => typeof n === "string"), done: doc.done };
37 } catch {
38 return undefined;
39 }
40 }
41
42 async function writeRecord(app: App, rec: Record_): Promise<void> {
43 const tmp = `${recordPath(app)}.tmp`;
44 await Bun.write(tmp, JSON.stringify(rec));
45 await rename(tmp, recordPath(app));
46 }
47
48 const settled = (o: Outcome | undefined): boolean => o !== undefined && typeof o === "string";
49
50 async function recordsSource(dir: string): Promise<boolean> {
51 try {
52 const doc = (await Bun.file(join(dir, "parent.json")).json()) as Record<string, unknown>;
53 return doc["forgeCode"] !== undefined && doc["forgeCode"] !== null;
54 } catch {
55 return false;
56 }
57 }
58
59 /** Returns what happened to the children it tried this boot. */
60 export async function backfillChildren(app: App): Promise<Record<string, Outcome>> {
61 const children = (await listChildren(app)).filter((c) => c.done);
62 let rec = await readRecord(app);
63 if (rec === undefined) {
64 rec = { considered: children.map((c) => c.name), done: {} };
65 await writeRecord(app, rec);
66 }
67 const tried: Record<string, Outcome> = {};
68 for (const c of children) {
69 if (!rec.considered.includes(c.name) || settled(rec.done[c.name])) continue;
70 const dir = childDirOf(app, c.bodyId);
71 if (await recordsSource(dir)) {
72 rec.done[c.name] = "already";
73 continue;
74 }
75 if (childAwake(app, c.name)) continue;
76 let child: App | undefined;
77 try {
78 child = createApp({ dataDir: dir });
79 const r = await backfillForgeCode(app, child, dir);
80 tried[c.name] = rec.done[c.name] = r.ok ? r.value : { error: r.error };
81 } catch (e) {
82 tried[c.name] = rec.done[c.name] = { error: String(e) };
83 } finally {
84 child?.close();
85 }
86 }
87 await writeRecord(app, rec);
88 return tried;
89 }
test/forge-code-own-source.test.ts added +173
@@ -0,0 +1,173 @@
1 /**
2 * Every new forge comes with its own source (#10989): `<org>/forge`, named as
3 * its `forgeRepo`, upstream wired to the parent's code; existing children get
4 * it by an idempotent backfill that leaves a person's own repo alone.
5 */
6
7 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
8 import { mkdtemp, rm } from "node:fs/promises";
9 import { tmpdir } from "node:os";
10 import { join } from "node:path";
11 import { createRepo } from "../src/actions/repos.ts";
12 import { childDirOf, spawnForge } from "../src/actions/spawn.ts";
13 import { createApp, type App } from "../src/app.ts";
14 import { addCrew, createAgentRow, listCrewRoles } from "../src/core/agents.ts";
15 import { getOrg } from "../src/core/orgs.ts";
16 import { createRepo as createRepoCore, getRepo } from "../src/core/repos.ts";
17 import { createUser, getUserByName, type User } from "../src/core/users.ts";
18 import { runGit } from "../src/git/env.ts";
19 import { repoDir } from "../src/git/repos.ts";
20 import { commitFiles } from "../src/git/write.ts";
21 import { forgeLinkOf } from "../src/spawn/chain.ts";
22 import { backfillForgeCode } from "../src/spawn/forge_code.ts";
23 import { backfillChildren } from "../src/spawn/forge_code_backfill.ts";
24 import { hostedPlacement } from "../src/spawn/placement.ts";
25 import { setForgeSource, storedForgeSource } from "../src/settings/forge_source.ts";
26 import { quietLogs } from "./helpers/globals.ts";
27
28 quietLogs();
29
30 const source = { catalog: [], briefs: [], subscriptions: [] };
31 const spawn = (app: App, owner: User, name: string) =>
32 spawnForge(
33 app,
34 owner,
35 { name, org: name, apps: [], carryBriefs: false, carrySubscriptions: false },
36 source,
37 {
38 placement: hostedPlacement({ host: "root.test" }),
39 request: new Request("https://root.test/x", { method: "POST", headers: { origin: "https://root.test" } }),
40 url: new URL("https://root.test/x"),
41 },
42 );
43
44 describe("a forge's own source", () => {
45 let dir = "";
46 let root: App;
47 let alice: User;
48 const open: App[] = [];
49 let n = 0;
50
51 /** A child born of a parent that named no code, so it has no `forge` repo: how the children before this began. */
52 async function bornBare(name: string): Promise<{ child: App; childDir: string; parent: App }> {
53 const parent = createApp({ dataDir: join(dir, `bare-${++n}`), url: `https://bare${n}.test`, port: 0 });
54 open.push(parent);
55 const u = await createUser(parent.db, { origin: "seed", trusted: true, username: "alice", email: "[email protected]", isAdmin: true });
56 if (!u.ok) throw new Error("user");
57 const born = await spawn(parent, u.value, name);
58 if (!born.ok) throw new Error(JSON.stringify(born.error));
59 const childDir = childDirOf(parent, born.value.bodyId);
60 const child = createApp({ dataDir: childDir, port: 0 });
61 open.push(child);
62 expect(getRepo(child.db, name, "forge")).toBeUndefined();
63 return { child, childDir, parent };
64 }
65
66 beforeAll(async () => {
67 dir = await mkdtemp(join(tmpdir(), "forge-own-"));
68 root = createApp({ dataDir: dir, url: "https://root.test", port: 0, forgeRepo: "alice/forge-code" });
69 const u = await createUser(root.db, { origin: "seed", trusted: true, username: "alice", email: "[email protected]", isAdmin: true });
70 if (!u.ok) throw new Error("user");
71 alice = u.value;
72 const made = await createRepo(root, alice, { name: "forge-code", visibility: "public" });
73 if (!made.ok) throw new Error("repo");
74 const c = await commitFiles(repoDir(root.reposRoot, "alice", "forge-code"), {
75 branch: "main",
76 message: "forge v1",
77 files: [{ path: "README.md", content: "# forge\n" }],
78 author: { name: "a", email: "[email protected]" },
79 });
80 if (!c.ok) throw new Error("commit");
81 }, 120_000);
82
83 afterAll(async () => {
84 for (const a of open) a.close();
85 root?.close();
86 await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
87 });
88
89 test("a spawned child has <admin>/forge, its forgeRepo setting and the parent as origin", async () => {
90 const r = await spawn(root, alice, "kid");
91 if (!r.ok) throw new Error(JSON.stringify(r.error));
92 const child = createApp({ dataDir: childDirOf(root, r.value.bodyId), port: 0 });
93 open.push(child);
94 expect(getRepo(child.db, "kid", "forge")).toBeDefined();
95 expect(storedForgeSource(child.db)).toBe("kid/forge");
96 const link = await forgeLinkOf(child);
97 expect(link?.repo).toBe("kid/forge");
98 expect(link?.upstream).toBe("alice/forge-code");
99 }, 240_000);
100
101 test("backfill gives an existing child its source once", async () => {
102 const { child, childDir } = await bornBare("old");
103 expect((await backfillForgeCode(root, child, childDir)).ok).toBe(true);
104 expect(getRepo(child.db, "old", "forge")).toBeDefined();
105 expect(storedForgeSource(child.db)).toBe("old/forge");
106 expect((await forgeLinkOf(child))?.upstream).toBe("alice/forge-code");
107 const again = await backfillForgeCode(root, child, childDir);
108 expect(again.ok && again.value).toBe("already");
109 }, 240_000);
110
111 test("a repo a person made under that name stays theirs", async () => {
112 const { child, childDir } = await bornBare("mine");
113 const theirs = await createRepoCore(child.db, child.reposRoot, { ownerId: getOrg(child.db, "mine")!.id, name: "forge", visibility: "private" });
114 if (!theirs.ok) throw new Error("theirs");
115 const left = await backfillForgeCode(root, child, childDir);
116 expect(left.ok && left.value).toBe("left-alone");
117 expect(storedForgeSource(child.db)).toBeUndefined();
118 expect((await Bun.file(join(childDir, "parent.json")).json()).forgeCodeStarted).toBeUndefined();
119 }, 240_000);
120
121 test("a repo our own earlier run left unfinished is finished, not left alone", async () => {
122 const { child, childDir } = await bornBare("half");
123 // The run died after creating the repo and recording that it began: no refs, no forgeCode.
124 const empty = await createRepoCore(child.db, child.reposRoot, { ownerId: getOrg(child.db, "half")!.id, name: "forge", visibility: "public" });
125 if (!empty.ok) throw new Error("empty");
126 const file = join(childDir, "parent.json");
127 await Bun.write(file, JSON.stringify({ ...(await Bun.file(file).json()), forgeCodeStarted: "half/forge" }));
128 const r = await backfillForgeCode(root, child, childDir);
129 expect(r.ok && r.value).toBe("made");
130 const doc = await Bun.file(file).json();
131 expect(doc.forgeCode.repo).toBe("half/forge");
132 expect(doc.forgeCodeStarted).toBeUndefined();
133 const head = await runGit(["rev-parse", "--verify", "refs/heads/main"], { cwd: repoDir(child.reposRoot, "half", "forge") });
134 expect(head.ok).toBe(true);
135 expect(storedForgeSource(child.db)).toBe("half/forge");
136 }, 240_000);
137
138 test("the crew is seated on it with the role held on the child's other repos, and an earlier forgeRepo stands", async () => {
139 const { child, childDir } = await bornBare("crewed");
140 const mk = async (username: string) => {
141 const a = await createUser(child.db, { username, email: `${username}@x.test`, agent: true, origin: "agent" });
142 if (!a.ok) throw new Error("agent");
143 createAgentRow(child.db, { userId: a.value.id, roles: ["builder", "planner"] });
144 return a.value;
145 };
146 const pia = await mk("pia");
147 await mk("bo");
148 const other = await createRepoCore(child.db, child.reposRoot, { ownerId: getOrg(child.db, "crewed")!.id, name: "other", visibility: "private" });
149 if (!other.ok) throw new Error("other");
150 addCrew(child.db, other.value.id, pia.id, undefined, "builder");
151 // An admin already chose a source: it stands.
152 setForgeSource(child.db, "crewed/custom", pia.id);
153
154 expect((await backfillForgeCode(root, child, childDir)).ok).toBe(true);
155 const repo = getRepo(child.db, "crewed", "forge")!;
156 const seats = new Map(listCrewRoles(child.db, repo.id).map((s) => [s.agent.user.username, s.role]));
157 expect(seats.get("pia")).toBe("builder"); // copied, not the first role she declares (planner)
158 expect(seats.get("bo")).toBe("planner"); // no seat elsewhere: the first role declared
159 expect(storedForgeSource(child.db)).toBe("crewed/custom");
160 }, 240_000);
161
162 test("the backfill of all children is a recorded one-shot", async () => {
163 const { parent, child } = await bornBare("later");
164 expect(Object.keys(await backfillChildren(parent))).toEqual(["later"]);
165 // Recorded as settled: the next boot opens nothing, and a child born afterwards is not swept in.
166 expect(getRepo(child.db, "later", "forge")).toBeUndefined(); // the bare parent names no code
167 expect(await Bun.file(join(parent.config.dataDir, "forge-code-backfill.json")).exists()).toBe(true);
168 const u = getUserByName(parent.db, "alice")!;
169 const born = await spawn(parent, u, "newer");
170 if (!born.ok) throw new Error(JSON.stringify(born.error));
171 expect(await backfillChildren(parent)).toEqual({});
172 }, 240_000);
173 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs