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