|
| 1 | +import { describe, expect, test } from "bun:test"; |
| 2 | + |
| 3 | +import { |
| 4 | + BREW_FORMULA, |
| 5 | + DEB_PACKAGE, |
| 6 | + RELEASES_URL, |
| 7 | + checkForUpgrade, |
| 8 | + compareVersionStrings, |
| 9 | + detectInstallMethod, |
| 10 | + formatUpgradeMessage, |
| 11 | + type InstallProbe, |
| 12 | +} from "./index.js"; |
| 13 | + |
| 14 | +function probe(partial: Partial<InstallProbe> & Pick<InstallProbe, "execPath">): InstallProbe { |
| 15 | + return { |
| 16 | + argv: [], |
| 17 | + platform: "darwin", |
| 18 | + pathExists: () => false, |
| 19 | + env: {}, |
| 20 | + ...partial, |
| 21 | + }; |
| 22 | +} |
| 23 | + |
| 24 | +describe("compareVersionStrings", () => { |
| 25 | + test("orders major.minor.patch and strips a leading v", () => { |
| 26 | + expect(compareVersionStrings("0.2.95", "0.2.94")).toBeGreaterThan(0); |
| 27 | + expect(compareVersionStrings("0.2.94", "0.2.95")).toBeLessThan(0); |
| 28 | + expect(compareVersionStrings("0.2.95", "0.2.95")).toBe(0); |
| 29 | + expect(compareVersionStrings("v0.3.0", "0.2.99")).toBeGreaterThan(0); |
| 30 | + }); |
| 31 | + |
| 32 | + test("returns null for unparseable input", () => { |
| 33 | + expect(compareVersionStrings("not-a-version", "0.1.0")).toBeNull(); |
| 34 | + expect(compareVersionStrings("0.1.0", "")).toBeNull(); |
| 35 | + }); |
| 36 | +}); |
| 37 | + |
| 38 | +describe("detectInstallMethod", () => { |
| 39 | + test("detects Homebrew Cellar installs", () => { |
| 40 | + expect( |
| 41 | + detectInstallMethod( |
| 42 | + probe({ |
| 43 | + execPath: "/opt/homebrew/Cellar/corbits-code/0.2.95/bin/corbits", |
| 44 | + }), |
| 45 | + ), |
| 46 | + ).toBe("homebrew"); |
| 47 | + expect( |
| 48 | + detectInstallMethod( |
| 49 | + probe({ |
| 50 | + execPath: "/usr/local/bin/corbits", |
| 51 | + resolvedPath: "/usr/local/Cellar/corbits-code/0.2.90/bin/corbits", |
| 52 | + }), |
| 53 | + ), |
| 54 | + ).toBe("homebrew"); |
| 55 | + }); |
| 56 | + |
| 57 | + test("detects Homebrew via HOMEBREW_PREFIX when the binary lives under it", () => { |
| 58 | + expect( |
| 59 | + detectInstallMethod( |
| 60 | + probe({ |
| 61 | + execPath: "/opt/homebrew/bin/corbits", |
| 62 | + env: { HOMEBREW_PREFIX: "/opt/homebrew" }, |
| 63 | + }), |
| 64 | + ), |
| 65 | + ).toBe("homebrew"); |
| 66 | + }); |
| 67 | + |
| 68 | + test("detects Debian package installs", () => { |
| 69 | + expect( |
| 70 | + detectInstallMethod( |
| 71 | + probe({ |
| 72 | + execPath: "/usr/bin/corbits", |
| 73 | + platform: "linux", |
| 74 | + pathExists: (p) => p === `/var/lib/dpkg/info/${DEB_PACKAGE}.list`, |
| 75 | + }), |
| 76 | + ), |
| 77 | + ).toBe("deb"); |
| 78 | + expect( |
| 79 | + detectInstallMethod( |
| 80 | + probe({ |
| 81 | + execPath: "/usr/bin/corbits", |
| 82 | + platform: "linux", |
| 83 | + pathExists: (p) => p === `/usr/share/doc/${DEB_PACKAGE}`, |
| 84 | + }), |
| 85 | + ), |
| 86 | + ).toBe("deb"); |
| 87 | + }); |
| 88 | + |
| 89 | + test("detects Bun / from-source runs", () => { |
| 90 | + expect( |
| 91 | + detectInstallMethod( |
| 92 | + probe({ |
| 93 | + execPath: "/Users/dev/.bun/bin/bun", |
| 94 | + argv: ["bun", "/repo/corbits-code/src/index.ts"], |
| 95 | + }), |
| 96 | + ), |
| 97 | + ).toBe("source"); |
| 98 | + expect( |
| 99 | + detectInstallMethod( |
| 100 | + probe({ |
| 101 | + execPath: "/usr/local/bin/bun", |
| 102 | + argv: ["bun", "/repo/dist/index.js"], |
| 103 | + }), |
| 104 | + ), |
| 105 | + ).toBe("source"); |
| 106 | + // brew-installed bun must not look like a brew-installed corbits |
| 107 | + expect( |
| 108 | + detectInstallMethod( |
| 109 | + probe({ |
| 110 | + execPath: "/opt/homebrew/bin/bun", |
| 111 | + argv: ["bun", "/repo/src/index.ts"], |
| 112 | + env: { HOMEBREW_PREFIX: "/opt/homebrew" }, |
| 113 | + }), |
| 114 | + ), |
| 115 | + ).toBe("source"); |
| 116 | + }); |
| 117 | + |
| 118 | + test("detects standalone release binaries", () => { |
| 119 | + expect( |
| 120 | + detectInstallMethod( |
| 121 | + probe({ |
| 122 | + execPath: "/home/user/.local/bin/corbits", |
| 123 | + platform: "linux", |
| 124 | + }), |
| 125 | + ), |
| 126 | + ).toBe("binary"); |
| 127 | + expect( |
| 128 | + detectInstallMethod( |
| 129 | + probe({ |
| 130 | + execPath: "/Users/dev/bin/corbits", |
| 131 | + platform: "darwin", |
| 132 | + }), |
| 133 | + ), |
| 134 | + ).toBe("binary"); |
| 135 | + }); |
| 136 | + |
| 137 | + test("falls back to unknown rather than guessing brew", () => { |
| 138 | + expect( |
| 139 | + detectInstallMethod( |
| 140 | + probe({ |
| 141 | + execPath: "/mysterious/path/agent-runner", |
| 142 | + argv: ["agent-runner"], |
| 143 | + }), |
| 144 | + ), |
| 145 | + ).toBe("unknown"); |
| 146 | + }); |
| 147 | +}); |
| 148 | + |
| 149 | +describe("formatUpgradeMessage", () => { |
| 150 | + const base = { current: "0.2.90", latest: "0.2.95" }; |
| 151 | + |
| 152 | + test("homebrew message uses the live formula upgrade", () => { |
| 153 | + const msg = formatUpgradeMessage({ ...base, method: "homebrew" }); |
| 154 | + expect(msg).toContain("v0.2.90 → v0.2.95"); |
| 155 | + expect(msg).toContain(`brew update && brew upgrade ${BREW_FORMULA}`); |
| 156 | + expect(msg).not.toContain("dpkg"); |
| 157 | + }); |
| 158 | + |
| 159 | + test("source message points at pull + bun rebuild", () => { |
| 160 | + const msg = formatUpgradeMessage({ ...base, method: "source" }); |
| 161 | + expect(msg).toContain("bun install"); |
| 162 | + expect(msg).toContain("bun run start"); |
| 163 | + expect(msg).not.toContain("brew upgrade"); |
| 164 | + }); |
| 165 | + |
| 166 | + test("binary message points at the GitHub releases page", () => { |
| 167 | + const msg = formatUpgradeMessage({ ...base, method: "binary" }); |
| 168 | + expect(msg).toContain(`${RELEASES_URL}/latest`); |
| 169 | + expect(msg).not.toContain("brew upgrade"); |
| 170 | + expect(msg).not.toContain("dpkg"); |
| 171 | + }); |
| 172 | + |
| 173 | + test("deb message points at dpkg install of the release artifact", () => { |
| 174 | + const msg = formatUpgradeMessage({ ...base, method: "deb" }); |
| 175 | + expect(msg).toContain("dpkg -i"); |
| 176 | + expect(msg).toContain(`${DEB_PACKAGE}_0.2.95_`); |
| 177 | + expect(msg).not.toContain("brew upgrade"); |
| 178 | + }); |
| 179 | + |
| 180 | + test("unknown message is generic — no brew or apt command", () => { |
| 181 | + const msg = formatUpgradeMessage({ ...base, method: "unknown" }); |
| 182 | + expect(msg).toContain(RELEASES_URL); |
| 183 | + expect(msg).not.toContain("brew"); |
| 184 | + expect(msg).not.toContain("dpkg"); |
| 185 | + expect(msg).not.toContain("apt"); |
| 186 | + }); |
| 187 | +}); |
| 188 | + |
| 189 | +describe("checkForUpgrade", () => { |
| 190 | + test("reports available when latest is newer", async () => { |
| 191 | + const result = await checkForUpgrade({ |
| 192 | + currentVersion: "0.2.90", |
| 193 | + method: "homebrew", |
| 194 | + fetchLatest: async () => "0.2.95", |
| 195 | + }); |
| 196 | + expect(result.kind).toBe("available"); |
| 197 | + if (result.kind !== "available") return; |
| 198 | + expect(result.notice.current).toBe("0.2.90"); |
| 199 | + expect(result.notice.latest).toBe("0.2.95"); |
| 200 | + expect(result.notice.method).toBe("homebrew"); |
| 201 | + expect(result.notice.message).toContain("brew upgrade"); |
| 202 | + }); |
| 203 | + |
| 204 | + test("reports current when running the latest (or newer)", async () => { |
| 205 | + expect( |
| 206 | + ( |
| 207 | + await checkForUpgrade({ |
| 208 | + currentVersion: "0.2.95", |
| 209 | + fetchLatest: async () => "0.2.95", |
| 210 | + }) |
| 211 | + ).kind, |
| 212 | + ).toBe("current"); |
| 213 | + expect( |
| 214 | + ( |
| 215 | + await checkForUpgrade({ |
| 216 | + currentVersion: "0.3.0", |
| 217 | + fetchLatest: async () => "0.2.95", |
| 218 | + }) |
| 219 | + ).kind, |
| 220 | + ).toBe("current"); |
| 221 | + }); |
| 222 | + |
| 223 | + test("soft-skips when the network probe fails", async () => { |
| 224 | + const result = await checkForUpgrade({ |
| 225 | + currentVersion: "0.2.90", |
| 226 | + fetchLatest: async () => null, |
| 227 | + }); |
| 228 | + expect(result).toEqual({ kind: "skipped", reason: "latest version unavailable" }); |
| 229 | + }); |
| 230 | + |
| 231 | + test("soft-skips when fetchLatest throws", async () => { |
| 232 | + const result = await checkForUpgrade({ |
| 233 | + currentVersion: "0.2.90", |
| 234 | + fetchLatest: async () => { |
| 235 | + throw new Error("offline"); |
| 236 | + }, |
| 237 | + }); |
| 238 | + expect(result.kind).toBe("skipped"); |
| 239 | + }); |
| 240 | + |
| 241 | + test("detects method from probe when not forced", async () => { |
| 242 | + const result = await checkForUpgrade({ |
| 243 | + currentVersion: "0.1.0", |
| 244 | + fetchLatest: async () => "0.2.0", |
| 245 | + probe: probe({ |
| 246 | + execPath: "/Users/dev/.bun/bin/bun", |
| 247 | + argv: ["bun", "/repo/src/index.ts"], |
| 248 | + }), |
| 249 | + }); |
| 250 | + expect(result.kind).toBe("available"); |
| 251 | + if (result.kind !== "available") return; |
| 252 | + expect(result.notice.method).toBe("source"); |
| 253 | + expect(result.notice.message).toContain("bun install"); |
| 254 | + }); |
| 255 | +}); |
0 commit comments