Skip to content
136 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { HandbackPlan, RepoMirror } from "@g1t/contracts";
5
6import { DEFAULT_MIRROR_SETTINGS, TAKE_OVER_AFTER_MINUTES, mirrorWritable } from "../../../../packages/contracts/src/mirrors.ts";
7
8import {
9 clampMinutes,
10 decisionsFrom,
11 handBackReady,
12 mirrorBadge,
13 mirrorBanner,
14 mirrorReason,
15 mirrorSettingsFrom,
16 shortSha,
17 TAKE_OVER_MINUTES,
18 workflowReason,
19 writable,
20} from "./mirror.ts";
21
22const mirror = (state: RepoMirror["state"]): RepoMirror => ({
23 state,
24 remote: "github.com/acme/web",
25 url: "https://github.com/acme/web",
26 since: "2026-10-08T10:00:00Z",
27});
28const repo = (state: RepoMirror["state"] | null) => ({ namespace: "acme", name: "web", mirror: state ? mirror(state) : null });
29const leader = (reachable: boolean) => ({ repoId: "r", role: "leader" as const, name: "github.com/acme/web", state: "standby" as const, reachable });
30const follower = (name: string) => ({ repoId: "r", role: "follower" as const, name, state: "following" as const, reachable: true });
31
32test("the copies agree with the contracts", () => {
33 for (const state of [null, "standby", "ci", "takeover", "handing_back"] as const) {
34 assert.equal(writable(state ? mirror(state) : null), mirrorWritable(state ? mirror(state) : null));
35 }
36 assert.deepEqual([...TAKE_OVER_MINUTES], [...TAKE_OVER_AFTER_MINUTES]);
37});
38
39test("a read-only mirror says why its buttons are off, and where to take over", () => {
40 assert.equal(
41 mirrorReason(repo("standby")),
42 "acme/web is a mirror of github.com/acme/web. Work happens there until someone takes over in Settings → Mirroring.",
43 );
44 assert.equal(mirrorReason(repo("ci")), mirrorReason(repo("standby")));
45 assert.equal(mirrorReason(repo("handing_back")), "acme/web is handing back to github.com/acme/web; it takes changes again when that is done.");
46 assert.equal(mirrorReason(repo("takeover")), null);
47 assert.equal(mirrorReason(repo(null)), null);
48 assert.equal(mirrorReason(null), null);
49});
50
51test("workflows run in CI failover and takeovers, not while standing by or handing back", () => {
52 assert.equal(workflowReason(repo("ci")), null);
53 assert.equal(workflowReason(repo("takeover")), null);
54 assert.ok(workflowReason(repo("standby")));
55 assert.ok(workflowReason(repo("handing_back")));
56 assert.equal(workflowReason(repo(null)), null);
57});
58
59test("the badge says what the repository is to its remotes", () => {
60 assert.deepEqual(mirrorBadge(mirror("standby"), []), { label: "Mirror of github.com/acme/web", tone: "neutral", more: 0 });
61 assert.equal(mirrorBadge(mirror("ci"), [])?.tone, "info");
62 assert.equal(mirrorBadge(mirror("takeover"), [])?.label, "Taken over from github.com/acme/web");
63 assert.equal(mirrorBadge(mirror("handing_back"), [])?.label, "Handing back to github.com/acme/web");
64 assert.deepEqual(mirrorBadge(null, [follower("github.com/acme/web"), follower("git.example.com/web")]), {
65 label: "Mirrored to github.com/acme/web",
66 tone: "accent",
67 more: 1,
68 });
69 assert.equal(mirrorBadge(null, []), null);
70});
71
72test("a mirror standing by whose remote answers needs no banner", () => {
73 assert.equal(mirrorBanner(mirror("standby"), [leader(true)]), null);
74 assert.equal(mirrorBanner(mirror("standby"), []), null);
75 assert.equal(mirrorBanner(mirror("standby"), [leader(false)]), "unreachable");
76 assert.equal(mirrorBanner(mirror("ci"), [leader(false)]), "unreachable");
77 assert.equal(mirrorBanner(mirror("ci"), [leader(true)]), "ci");
78 assert.equal(mirrorBanner(mirror("takeover"), [leader(false)]), "takeover");
79 assert.equal(mirrorBanner(mirror("handing_back"), []), "handing_back");
80 assert.equal(mirrorBanner(null, [follower("x")]), null);
81});
82
83test("hand-back decisions are read from their fields, and only known ones", () => {
84 const form = new FormData();
85 form.set("intent", "hand-back");
86 form.set("decision:main", "keep_ours");
87 form.set("decision:release/1.x", "pull_request");
88 form.set("decision:dev", "delete_everything");
89 assert.deepEqual(decisionsFrom(form.entries()), { main: "keep_ours", "release/1.x": "pull_request" });
90});
91
92test("a hand-back is ready once the remote answers and every diverged branch has a decision", () => {
93 const plan: HandbackPlan = {
94 reachable: true,
95 ready: false,
96 refs: [
97 { ref: "main", base: "a", ours: "b", theirs: "c", action: "diverged", decision: null },
98 { ref: "dev", base: "a", ours: "b", theirs: "a", action: "push", decision: null },
99 ],
100 };
101 assert.equal(handBackReady(plan, {}), false);
102 assert.equal(handBackReady(plan, { main: "keep_theirs" }), true);
103 assert.equal(handBackReady({ ...plan, reachable: false }, { main: "keep_theirs" }), false);
104});
105
106test("settings come back from the form, keeping what it did not carry", () => {
107 const leaderForm = new FormData();
108 leaderForm.set("kind", "leader");
109 leaderForm.set("notify", "inbox");
110 leaderForm.set("autoTakeOver", "on");
111 leaderForm.set("takeOverAfter", "2");
112 leaderForm.set("githubWorkflows", "on");
113 const current = { ...DEFAULT_MIRROR_SETTINGS, remotePushes: "overwrite" as const };
114 assert.deepEqual(mirrorSettingsFrom(leaderForm, current), {
115 notify: "inbox",
116 takeOverAfter: 5,
117 handBack: "ask",
118 keepCiWarm: false,
119 githubWorkflows: true,
120 holdDeploys: false,
121 remotePushes: "overwrite",
122 });
123 const followerForm = new FormData();
124 followerForm.set("kind", "follower");
125 followerForm.set("remotePushes", "overwrite");
126 assert.deepEqual(mirrorSettingsFrom(followerForm, DEFAULT_MIRROR_SETTINGS), { ...DEFAULT_MIRROR_SETTINGS, remotePushes: "overwrite" });
127});
128
129test("minutes and commits are shown within bounds", () => {
130 assert.equal(clampMinutes(3), 5);
131 assert.equal(clampMinutes(90.4), 90);
132 assert.equal(clampMinutes(99999), 1440);
133 assert.equal(clampMinutes(Number.NaN), 5);
134 assert.equal(shortSha("0123456789abcdef"), "0123456");
135 assert.equal(shortSha(null), "—");
136});