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

An open chain page updates live when its forge is severed, through any door #2224

Merged opened this

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

Merged

scout/forge:scout/2221-an-open-chain-page-updates-live-when-its → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/2221-an-open-chain-page-updates-live-when-its was merged into main as 698a1b7.

Review

Sign in to review this change.
3 files changed +103 −0
src/actions/follow.ts +1
@@ -54,6 +54,7 @@ export async function severForge(
5454 if (!cut.ok) return err("not_following");
5555 app.bus.append({
5656 kind: "forge.severed",
57 repoId: repo.id,
5758 actorId: who.value.id,
5859 payload: { repo: `${repo.ownerName}/${repo.name}`, at: cut.value },
5960 });
src/web/pages/chain.ts +9
@@ -15,6 +15,9 @@ import { html, type Html } from "../html.ts";
1515 import { flashOrJson, page, redirect } from "../layout.ts";
1616 import { btn, card } from "../ui.ts";
1717
18 /** The events that change what the chain page says. */
19 export const CHAIN_LIVE = "forge.severed";

CHAIN_LIVE only lists forge.severed. When the mode-change event exists, extend it here as a comma-separated list.

20
1821 const MODE_WORDS = { "tell-me": "tells when the parent moves", "take-updates": "takes updates" } as const;
1922
2023 function lineRow(l: ChainLine): Html {
@@ -63,6 +66,11 @@ export async function chainPage(ctx: Ctx): Promise<Response> {
6366 ${pageHead("Following", {
6467 sub: "The forges this one's code descends from, root first. Numbers are as of the day each hop was made; the last is counted now.",
6568 })}
69 <div
70 id="live-chain"
71 data-live="${CHAIN_LIVE}"
72 data-live-repo="${repo.ownerName}/${repo.name}"
73 >
6674 ${card(
6775 html`<ul class="list lineage" aria-label="Forges, root first">
6876 ${view.lines.map(lineRow)}
@@ -91,6 +99,7 @@ export async function chainPage(ctx: Ctx): Promise<Response> {
9199 { head: html`<h2>Sever</h2>` },
92100 )
93101 : null}
102 </div>
94103 </div>`,
95104 });
96105 }
test/following.test.ts +93
@@ -12,6 +12,12 @@ import { parentUrlProblem, refreshIdle } from "../src/core/parent_releases.ts";
1212 import { Session } from "../src/tui/session.ts";
1313 import { releasesRow } from "../src/tui/screens/about.ts";
1414 import { createApp, type App } from "../src/app.ts";
15 import { childDirOf, spawnForge } from "../src/actions/spawn.ts";
16 import { severForge } from "../src/actions/follow.ts";
17 import { createSession } from "../src/core/sessions.ts";
18 import { getRepo } from "../src/core/repos.ts";
19 import { commitFiles } from "../src/git/write.ts";
20 import { hostedPlacement } from "../src/spawn/placement.ts";
1521 import { addToCrew, createAgent } from "../src/actions/crew.ts";
1622 import { startChecks, type ChecksHandle } from "../src/checks/service.ts";
1723 import { mergePull, openPull } from "../src/actions/pulls.ts";
@@ -1017,3 +1023,90 @@ describe("a parent URL", () => {
10171023 expect(parentUrlProblem("http://127.0.0.1:1", true)).toBeNull();
10181024 });
10191025 });
1026
1027 describe("an open chain page hears a sever", () => {
1028 let dir = "";
1029 let root: App;
1030 let child: App;
1031 let servers: Servers;
1032 let cookie = "";
1033
1034 beforeAll(async () => {
1035 dir = await mkdtemp(join(tmpdir(), "forge-chain-live-"));
1036 root = createApp({ dataDir: dir, url: "https://root.test", port: 0, forgeRepo: "alice/forge-code" });
1037 const alice = await createUser(root.db, { trusted: true, username: "alice", email: "[email protected]", isAdmin: true });
1038 if (!alice.ok) throw new Error("user");
1039 const made = await createRepo(root, alice.value, { name: "forge-code", visibility: "public" });
1040 if (!made.ok) throw new Error("repo");
1041 const c = await commitFiles(repoDir(root.reposRoot, "alice", "forge-code"), {
1042 branch: "main",
1043 message: "forge v1",
1044 files: [{ path: "README.md", content: "# forge\n" }],
1045 author: { name: "a", email: "[email protected]" },
1046 });
1047 if (!c.ok) throw new Error("commit");
1048 const host = "root.test";
1049 const spawned = await spawnForge(
1050 root,
1051 alice.value,
1052 { name: "kid", org: "kid", apps: [], carryBriefs: false, carrySubscriptions: false },
1053 { catalog: [], briefs: [], subscriptions: [] },
1054 {
1055 placement: hostedPlacement({ host }),
1056 request: new Request(`https://${host}/x`, { method: "POST", headers: { origin: `https://${host}` } }),
1057 url: new URL(`https://${host}/x`),
1058 },
1059 );
1060 if (!spawned.ok) throw new Error(JSON.stringify(spawned.error));
1061 child = createApp({ dataDir: childDirOf(root, spawned.value.bodyId), url: "https://kid.test", port: 0 });
1062 servers = await serve(child);
1063 cookie = `forge_session=${createSession(child.db, getUserByName(child.db, "alice")!.id).token}`;
1064 }, 60_000);
1065
1066 afterAll(async () => {
1067 servers.stop();
1068 child.close();
1069 root.close();
1070 await rm(dir, { recursive: true, force: true });
1071 });
1072
1073 const chainPage = async () =>
1074 (await (await fetch(`${servers.url}/kid/forge/following`, { headers: { cookie } })).text()).replace(/\s+/g, " ");
1075
1076 test("the page carries a live region for forge.severed, scoped to its repository", async () => {
1077 const page = await chainPage();
1078 expect(page).toContain('id="live-chain"');
1079 expect(page).toContain('data-live="forge.severed"');
1080 expect(page).toContain('data-live-repo="kid/forge"');
1081 expect(page).not.toContain("follows nothing since");
1082 });
1083
1084 test("a sever reaches the viewer's stream with the repo the region filters on, and a refetch shows it", async () => {
1085 const ctl = new AbortController();
1086 const res = await fetch(`${servers.url}/events`, {
1087 headers: { cookie },
1088 signal: ctl.signal,
1089 });
1090 const reader = res.body!.getReader();
1091 const owner = getUserByName(child.db, "alice")!;
1092 const cut = await severForge(child, owner, getRepo(child.db, "kid", "forge")!);
1093 expect(cut.ok).toBe(true);
1094
1095 let seen = "";
1096 const dec = new TextDecoder();
1097 while (!seen.includes('"forge.severed"')) {
1098 const { value, done } = await reader.read();
1099 if (done) break;
1100 seen += dec.decode(value);
1101 }
1102 ctl.abort();
1103 const line = seen.split("\n").find((l) => l.startsWith("data:") && l.includes("forge.severed"))!;
1104 const event = JSON.parse(line.slice(5));
1105 expect(event.kind).toBe("forge.severed");
1106 expect(event.repo).toBe("kid/forge");
1107
1108 const page = await chainPage();
1109 expect(page).toContain("follows nothing since");
1110 expect(page).toContain('id="live-chain"');
1111 });
1112 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs