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

One test proves cross-forge following end to end: parent commits, child checks, take pull opens, owner merges #2662

Merged opened this

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

Merged

scout/forge:scout/2659-one-test-proves-cross-forge-following-en → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/2659-one-test-proves-cross-forge-following-en was merged into main as 67a096a.

Review

Sign in to review this change.
1 file changed +139 −0
test/follow-e2e.test.ts added +139
@@ -0,0 +1,139 @@
1 /**
2 * Following across forges, end to end (#2659): the parent commits, the child
3 * checks by hand, the tell-me card lists the commits, take-updates opens one
4 * pull, the owner merges it through the merge action, and a conflicting
5 * parent commit opens one issue and no pull. Local bare repositories, no
6 * network, no timers.
7 */
8
9 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
10 import { mkdtemp, rm } from "node:fs/promises";
11 import { tmpdir } from "node:os";
12 import { join } from "node:path";
13 import { mergePull } from "../src/actions/pulls.ts";
14 import { commitToBranch, createRepo } from "../src/actions/repos.ts";
15 import { createApp, type App } from "../src/app.ts";
16 import { listAssignedTo, listIssues } from "../src/core/issues.ts";
17 import { listPulls } from "../src/core/pulls.ts";
18 import type { Repo } from "../src/core/repos.ts";
19 import { createUser, type User } from "../src/core/users.ts";
20 import { runGit } from "../src/git/env.ts";
21 import { repoDir } from "../src/git/repos.ts";
22 import { setChainMode } from "../src/spawn/chain.ts";
23 import { checkUpstream, upstreamWords } from "../src/spawn/upstream.ts";
24 import { quietLogs } from "./helpers/globals.ts";
25
26 quietLogs();
27
28 const git = async (cwd: string, ...args: string[]) => {
29 const r = await runGit(["-c", "user.name=t", "-c", "[email protected]", ...args], { cwd });
30 if (!r.ok) throw new Error(r.error.stderr);
31 return r.value.trim();
32 };
33
34 describe("following across forges, end to end", () => {
35 let dir = "";
36 let app: App;
37 let kid: User;
38 let repo: Repo;
39 let child = "";
40 let parent = "";
41 let work = "";
42
43 const theirs = async (path: string, content: string, subject: string) => {
44 await Bun.write(join(work, path), content);
45 await git(work, "add", ".");
46 await git(work, "commit", "-m", subject);
47 await git(work, "push", "-q", parent, "HEAD:refs/heads/main");
48 };
49 const main = () => git(child, "rev-parse", "refs/heads/main");
50 const takes = () => listPulls(app.db, repo.id, { state: "open" }).filter((p) => p.headBranch === "forge/take-updates");
51
52 beforeAll(async () => {
53 dir = await mkdtemp(join(tmpdir(), "forge-follow-e2e-"));
54 app = createApp({ dataDir: join(dir, "data"), url: "https://child.test", port: 0 });
55 const u = await createUser(app.db, {
56 origin: "seed",
57 trusted: true,
58 username: "kid",
59 email: "[email protected]",
60 password: "a-good-passphrase",
61 adminIfFirst: true,
62 });
63 if (!u.ok) throw new Error(u.error);
64 kid = u.value;
65 const made = await createRepo(app, kid, { name: "forge", visibility: "public" });
66 if (!made.ok) throw new Error(String(made.error));
67 repo = made.value;
68 child = repoDir(app.reposRoot, "kid", "forge");
69 const base = await commitToBranch(app, kid, repo, { message: "base", files: [{ path: "a.txt", content: "base\n" }] });
70 if (!base.ok) throw new Error(JSON.stringify(base.error));
71 parent = join(dir, "parent.git");
72 work = join(dir, "work");
73 await git(dir, "clone", "-q", "--bare", child, parent);
74 await git(dir, "clone", "-q", parent, work);
75 await Bun.write(
76 join(app.config.dataDir, "parent.json"),
77 JSON.stringify({
78 upstream: "https://parent.test",
79 forgeCode: { repo: "kid/forge", upstream: "root/forge", mode: "tell-me", tip: "a".repeat(40), chain: [] },
80 }),
81 );
82 }, 30_000);
83
84 afterAll(async () => {
85 app.close();
86 await rm(dir, { recursive: true, force: true });
87 });
88
89 test("the whole path", async () => {
90 const start = await main();
91
92 // 1-2. Two parent commits: behind 2, one card listing both.
93 await theirs("p1.txt", "1\n", "parent one");
94 await theirs("p2.txt", "2\n", "parent two");
95 const told = await checkUpstream(app, { remote: parent });
96 expect(told?.behind).toBe(2);
97 expect(told?.taken).toBeUndefined();
98 const cards = listAssignedTo(app.db, kid.id).filter((i) => i.title === "Your parent forge has 2 new commits");
99 expect(cards).toHaveLength(1);
100 expect(cards[0]!.body).toContain("parent one");
101 expect(cards[0]!.body).toContain("parent two");
102 expect(takes()).toHaveLength(0);
103
104 // 3. take-updates: the next check opens one pull listing both; main stays.
105 expect((await setChainMode(app, "take-updates")).ok).toBe(true);
106 const took = await checkUpstream(app, { remote: parent });
107 expect(took?.taken).toMatchObject({ kind: "pull", commits: 2 });
108 expect(takes()).toHaveLength(1);
109 const pull = takes()[0]!;
110 expect(pull.body).toContain("2 new commits from root/forge");
111 expect(pull.body).toContain("parent one");
112 expect(pull.body).toContain("parent two");
113 expect(await main()).toBe(start);
114
115 // 4. The owner merges it; the next check says up to date.
116 const merged = await mergePull(app, kid, repo, pull, "merge");
117 expect(merged.ok).toBe(true);
118 expect(await main()).not.toBe(start);
119 const after = await checkUpstream(app, { remote: parent });
120 expect(after?.behind).toBe(0);
121 expect(after?.taken).toBeUndefined();
122 expect(upstreamWords(after, after!.checkedAt)).toContain("up to date");
123 expect(takes()).toHaveLength(0);
124
125 // 5. A conflicting parent commit: one issue, no pull, main untouched.
126 const r = await commitToBranch(app, kid, repo, { message: "kid edits", files: [{ path: "a.txt", content: "kid's\n" }] });
127 expect(r.ok).toBe(true);
128 await theirs("a.txt", "parent's\n", "parent edits a");
129 const before = await main();
130 const clash = await checkUpstream(app, { remote: parent });
131 expect(clash?.taken).toMatchObject({ kind: "conflict", files: ["a.txt"] });

The second checkUpstream call proves no duplicate pull. Also assert that the issue count is still 1 right after it, before the final check, so a second issue would be caught at this step.

132 await checkUpstream(app, { remote: parent });
133 expect(takes()).toHaveLength(0);
134 expect(await main()).toBe(before);
135 const issues = listIssues(app.db, repo.id, { sealed: true }).filter((i) => i.title === "Take updates from root/forge");
136 expect(issues).toHaveLength(1);
137 expect(issues[0]!.body).toContain("a.txt");
138 });
139 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs