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

test/fork-flow.test.ts runs in under 5 s, every test under 1 s, proving what it proves today #10257

Merged opened this

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

Merged

scout/forge:scout/10250-test-fork-flow-test-ts-runs-in-under-5-s → main 2 commits fork

  • Merged. scout/10250-test-fork-flow-test-ts-runs-in-under-5-s was merged into main as e7c926e.

Review

Sign in to review this change.
2 files changed +85 −57
src/origins/fork-route.ts +8 −2
@@ -56,6 +56,12 @@ export const forkDeadline: { ms: number } = { ms: FORK_DEADLINE_MS };
5656 /** Backoff between polls, in ms; the last value repeats until the deadline. */
5757 const FORK_BACKOFF = [250, 500, 1_000, 2_000, 3_000] as const;
5858
59 /**
60 * The backoff a fork poll walks. Mutable like `forkDeadline`: a test that
61 * proves the poll goes round shortens the steps and puts back what it found.
62 */
63 export const forkBackoff: { ms: readonly number[] } = { ms: FORK_BACKOFF };
64
5965 /** Pushing a branch into the fork: the same cap as any push through. */
6066 const PUSH_TIMEOUT_MS = 120_000;
6167
@@ -128,8 +134,8 @@ export async function awaitFork(
128134 ? "origin_unreachable"
129135 : "origin_refused",
130136 );
131 const wait =
132 FORK_BACKOFF[Math.min(attempt, FORK_BACKOFF.length - 1)] ?? 3_000;
137 const steps = forkBackoff.ms;
138 const wait = steps[Math.min(attempt, steps.length - 1)] ?? 3_000;
133139 if (Date.now() + wait > deadline) return err("fork_timeout");
134140 await new Promise((resolve) => setTimeout(resolve, wait));
135141 }
test/fork-flow.test.ts +77 −55
@@ -1,11 +1,12 @@
11 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2 import { mkdtemp, rm } from "node:fs/promises";
2 import { mkdtemp, rm, writeFile } from "node:fs/promises";
33 import { tmpdir } from "node:os";
44 import { join } from "node:path";
55 import { createApp, type App } from "../src/app.ts";
66 import { serve, type Servers } from "../src/http/server.ts";
77 import { listEvents } from "../src/core/events.ts";
88 import { firstFetchRetry } from "../src/origins/mirror.ts";
9 import { forkBackoff } from "../src/origins/fork-route.ts";
910 import { settled } from "../src/origins/jobs.ts";
1011 import {
1112 mirrorAndWait,
@@ -122,13 +123,19 @@ describe("the fork flow", () => {
122123 });
123124 }
124125
125 const fakeFor = (kind: "forgejo" | "github", repos: FakeState["repos"]) => {
126 // The origin does not finish making a fork before it answers: a read of
127 // the new repository 404s before it appears. This is GitHub's real
128 // behaviour, and the reason `forkOrigin` polls at all. One 404 makes the
129 // poll go round; the backoff between polls is shortened to 10 ms, since
130 // what is proved is that it goes round, not how long it waits.
131 const fakeFor = (
132 kind: "forgejo" | "github",
133 repos: FakeState["repos"],
134 forkDelayReads = 1,
135 ) => {
126136 const state = newFakeState(kind, ORIGIN_LOGIN, originToken);
127137 state.repos.push(...repos);
128 // The origin does not finish making a fork before it answers: two reads
129 // of the new repository 404 before it appears. This is GitHub's real
130 // behaviour, and the reason `forkOrigin` polls at all.
131 state.forkDelayReads = 2;
138 state.forkDelayReads = forkDelayReads;
132139 return serveFakeForge(state);
133140 };
134141
@@ -145,32 +152,68 @@ describe("the fork flow", () => {
145152 return (await read<{ id: string }>(connected)).id;
146153 };
147154
155 const foundBackoff = forkBackoff.ms;
156
148157 beforeAll(async () => {
158 forkBackoff.ms = [10];
149159 dataDir = await mkdtemp(join(tmpdir(), "forge-ff-"));
150160 originDataDir = await mkdtemp(join(tmpdir(), "forge-ff-up-"));
151161
162 // Both forges are built at once: nothing on one waits for the other
163 // until the origins are connected.
152164 // ---- the upstream forge: the git host behind every fake clone URL ----
153165 originApp = createApp({ dataDir: originDataDir, port: 0 });
154 originServers = await serve(originApp);
155 await mkAccount(originServers, UPSTREAM_OWNER);
156 originToken = await mkAccount(originServers, ORIGIN_LOGIN);
166 // ---- the local forge ----
167 app = createApp({ dataDir, port: 0 });
168 [originServers, servers] = await Promise.all([serve(originApp), serve(app)]);
169 base = servers.url;
170 [, originToken, aliceToken, bobToken] = await Promise.all([
171 mkAccount(originServers, UPSTREAM_OWNER),
172 mkAccount(originServers, ORIGIN_LOGIN),
173 mkAccount(servers, "alice"),
174 mkAccount(servers, "bob"),
175 ]);
157176 proveAll(originApp);
158177 promote(originApp.db);
178 proveAll(app);
179 promote(app.db);
159180
160 await seedUpstream("gears");
161 await seedUpstream("widgets");
162 // A repo `me` owns outright — the "you can already push here" case.
163 await seedUpstream("own", ORIGIN_LOGIN);
181 await Promise.all([
182 seedUpstream("gears"),
183 seedUpstream("widgets"),
184 // A repo `me` owns outright — the "you can already push here" case.
185 seedUpstream("own", ORIGIN_LOGIN),
186 ]);
164187
165188 // `me` really forks both upstreams, so the fake's clone URL for the fork
166189 // points at a repository that exists and that `me` may push to.
167 for (const name of ["gears", "widgets"]) {
168 const forked = await upstream(
169 `/api/v1/repos/${UPSTREAM_OWNER}/${name}/forks`,
170 { as: ORIGIN_LOGIN, method: "POST", body: "{}" },
171 );
172 expect(forked.status).toBe(201);
173 }
190 const forks = await Promise.all(
191 ["gears", "widgets"].map((name) =>
192 upstream(`/api/v1/repos/${UPSTREAM_OWNER}/${name}/forks`, {
193 as: ORIGIN_LOGIN,
194 method: "POST",
195 body: "{}",
196 }),
197 ),
198 );
199 for (const forked of forks) expect(forked.status).toBe(201);
200
201 // The upstream forge stands in for the origin's git, not for its push
202 // policy: GitHub runs no forge hooks. Each contribution pushes twice into
203 // these three repositories, and each push would spawn two Bun processes
204 // to ask the upstream forge's own rules, about 100 ms a push. A hook
205 // that accepts keeps the pushes real and leaves the local forge's own
206 // hooks, which this file does exercise, as they are.
207 await Promise.all(
208 ["gears", "widgets", "own"].flatMap((name) =>
209 ["pre-receive", "post-receive"].map((hook) =>
210 writeFile(
211 join(originDataDir, "repos", ORIGIN_LOGIN, `${name}.git`, "hooks", hook),
212 "#!/bin/sh\nexit 0\n",
213 ),
214 ),
215 ),
216 );
174217
175218 // The upstream moves on AFTER the fork: a branch started from the fork's
176219 // own copy of main would now be proposing a stale tree.
@@ -216,20 +259,14 @@ describe("the fork flow", () => {
216259 },
217260 ]);
218261
219 // ---- the local forge ----
220 app = createApp({ dataDir, port: 0 });
221 servers = await serve(app);
222 base = servers.url;
223 aliceToken = await mkAccount(servers, "alice");
224 bobToken = await mkAccount(servers, "bob");
225 proveAll(app);
226 promote(app.db);
227
228 forgejoOrigin = await connect(forgejo);
229 githubOrigin = await connect(github, "github");
262 [forgejoOrigin, githubOrigin] = await Promise.all([
263 connect(forgejo),
264 connect(github, "github"),
265 ]);
230266 });
231267
232268 afterAll(async () => {
269 forkBackoff.ms = foundBackoff;
233270 forgejo.stop();
234271 github.stop();
235272 servers.stop();
@@ -375,29 +412,6 @@ describe("the fork flow", () => {
375412 expect(pull?.pull?.headRepo).toBe(`${ORIGIN_LOGIN}/gears`);
376413 });
377414
378 test("a second contribution reuses the fork rather than making another", async () => {
379 const before = forgejo.state.requests.filter(
380 (r) => r.method === "POST" && r.path.endsWith("/gears/forks"),
381 ).length;
382 const response = await api(
383 `/api/v1/user/origins/${forgejoOrigin}/browse/${UPSTREAM_OWNER}/gears/contribute`,
384 {
385 method: "POST",
386 body: JSON.stringify({
387 branch: "second-go",
388 message: "another",
389 files: [{ path: "b.txt", content: "two\n" }],
390 openPull: { title: "another" },
391 }),
392 },
393 );
394 expect(response.status).toBe(201);
395 const after = forgejo.state.requests.filter(
396 (r) => r.method === "POST" && r.path.endsWith("/gears/forks"),
397 ).length;
398 expect(after).toBe(before);
399 });
400
401415 test("push rights mean no fork: the branch lands on the upstream itself", async () => {
402416 const before = forgejo.state.requests.filter(
403417 (r) => r.method === "POST" && r.path.endsWith("/forks"),
@@ -492,7 +506,7 @@ describe("the fork flow", () => {
492506
493507 // ---- contributing from a mirrored upstream ----
494508
495 test("the repo route contributes to the upstream a mirror names", async () => {
509 test("the repo route contributes to the upstream a mirror names, through the fork already made", async () => {
496510 // Mirror the upstream itself, then contribute from its own page.
497511 const job = await mirrorAndWait(
498512 await api(`/api/v1/user/origins/${forgejoOrigin}/mirror`, {
@@ -503,6 +517,11 @@ describe("the fork flow", () => {
503517 expect(job?.stage).toBe("done");
504518 const mirror = job!.repo!;
505519 mirrorPath = `${mirror.ownerName}/${mirror.name}`;
520 const forksAsked = () =>
521 forgejo.state.requests.filter(
522 (r) => r.method === "POST" && r.path.endsWith("/gears/forks"),
523 ).length;
524 const before = forksAsked();
506525
507526 const response = await api(
508527 `/api/v1/repos/${mirror.ownerName}/${mirror.name}/contribute`,
@@ -524,6 +543,9 @@ describe("the fork flow", () => {
524543 }>(response);
525544 expect(result.via).toBe("fork");
526545 expect(result.forkFullName).toBe(`${ORIGIN_LOGIN}/gears`);
546 // A second contribution to gears reuses the fork the first one made
547 // rather than asking the origin for another.
548 expect(forksAsked()).toBe(before);
527549 // The upstream IS mirrored here, so the pull came back as a local row.
528550 expect(result.pull?.local?.number).toBe(result.pull!.number);
529551

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs