diff --git a/src/commands/signal-install.test.ts b/src/commands/signal-install.test.ts new file mode 100644 index 000000000..018caf6c9 --- /dev/null +++ b/src/commands/signal-install.test.ts @@ -0,0 +1,128 @@ +import { describe, expect, it } from "vitest"; +import type { ReleaseAsset } from "./signal-install.js"; +import { looksLikeArchive, pickAsset } from "./signal-install.js"; + +// Realistic asset list modelled after an actual signal-cli GitHub release. +const SAMPLE_ASSETS: ReleaseAsset[] = [ + { + name: "signal-cli-0.13.14-Linux-native.tar.gz", + browser_download_url: "https://example.com/linux-native.tar.gz", + }, + { + name: "signal-cli-0.13.14-Linux-native.tar.gz.asc", + browser_download_url: "https://example.com/linux-native.tar.gz.asc", + }, + { + name: "signal-cli-0.13.14-macOS-native.tar.gz", + browser_download_url: "https://example.com/macos-native.tar.gz", + }, + { + name: "signal-cli-0.13.14-macOS-native.tar.gz.asc", + browser_download_url: "https://example.com/macos-native.tar.gz.asc", + }, + { + name: "signal-cli-0.13.14-Windows-native.zip", + browser_download_url: "https://example.com/windows-native.zip", + }, + { + name: "signal-cli-0.13.14-Windows-native.zip.asc", + browser_download_url: "https://example.com/windows-native.zip.asc", + }, + { name: "signal-cli-0.13.14.tar.gz", browser_download_url: "https://example.com/jvm.tar.gz" }, + { + name: "signal-cli-0.13.14.tar.gz.asc", + browser_download_url: "https://example.com/jvm.tar.gz.asc", + }, +]; + +describe("looksLikeArchive", () => { + it("recognises .tar.gz", () => { + expect(looksLikeArchive("foo.tar.gz")).toBe(true); + }); + + it("recognises .tgz", () => { + expect(looksLikeArchive("foo.tgz")).toBe(true); + }); + + it("recognises .zip", () => { + expect(looksLikeArchive("foo.zip")).toBe(true); + }); + + it("rejects signature files", () => { + expect(looksLikeArchive("foo.tar.gz.asc")).toBe(false); + }); + + it("rejects unrelated files", () => { + expect(looksLikeArchive("README.md")).toBe(false); + }); +}); + +describe("pickAsset", () => { + describe("linux", () => { + it("selects the Linux-native asset on x64", () => { + const result = pickAsset(SAMPLE_ASSETS, "linux", "x64"); + expect(result).toBeDefined(); + expect(result!.name).toContain("Linux-native"); + expect(result!.name).toMatch(/\.tar\.gz$/); + }); + + it("returns undefined on arm64 (triggers brew fallback)", () => { + const result = pickAsset(SAMPLE_ASSETS, "linux", "arm64"); + expect(result).toBeUndefined(); + }); + + it("returns undefined on arm (32-bit)", () => { + const result = pickAsset(SAMPLE_ASSETS, "linux", "arm"); + expect(result).toBeUndefined(); + }); + }); + + describe("darwin", () => { + it("selects the macOS-native asset", () => { + const result = pickAsset(SAMPLE_ASSETS, "darwin", "arm64"); + expect(result).toBeDefined(); + expect(result!.name).toContain("macOS-native"); + }); + + it("selects the macOS-native asset on x64", () => { + const result = pickAsset(SAMPLE_ASSETS, "darwin", "x64"); + expect(result).toBeDefined(); + expect(result!.name).toContain("macOS-native"); + }); + }); + + describe("win32", () => { + it("selects the Windows-native asset", () => { + const result = pickAsset(SAMPLE_ASSETS, "win32", "x64"); + expect(result).toBeDefined(); + expect(result!.name).toContain("Windows-native"); + expect(result!.name).toMatch(/\.zip$/); + }); + }); + + describe("edge cases", () => { + it("returns undefined for an empty asset list", () => { + expect(pickAsset([], "linux", "x64")).toBeUndefined(); + }); + + it("skips assets with missing name or url", () => { + const partial: ReleaseAsset[] = [ + { name: "signal-cli.tar.gz" }, + { browser_download_url: "https://example.com/file.tar.gz" }, + ]; + expect(pickAsset(partial, "linux", "x64")).toBeUndefined(); + }); + + it("falls back to first archive for unknown platform", () => { + const result = pickAsset(SAMPLE_ASSETS, "freebsd" as NodeJS.Platform, "x64"); + expect(result).toBeDefined(); + expect(result!.name).toMatch(/\.tar\.gz$/); + }); + + it("never selects .asc signature files", () => { + const result = pickAsset(SAMPLE_ASSETS, "linux", "x64"); + expect(result).toBeDefined(); + expect(result!.name).not.toMatch(/\.asc$/); + }); + }); +}); diff --git a/src/commands/signal-install.ts b/src/commands/signal-install.ts index 1f168cd9f..4e68a8131 100644 --- a/src/commands/signal-install.ts +++ b/src/commands/signal-install.ts @@ -9,12 +9,12 @@ import { resolveBrewExecutable } from "../infra/brew.js"; import { runCommandWithTimeout } from "../process/exec.js"; import { CONFIG_DIR } from "../utils.js"; -type ReleaseAsset = { +export type ReleaseAsset = { name?: string; browser_download_url?: string; }; -type NamedAsset = { +export type NamedAsset = { name: string; browser_download_url: string; }; @@ -31,7 +31,8 @@ export type SignalInstallResult = { error?: string; }; -function looksLikeArchive(name: string): boolean { +/** @internal Exported for testing. */ +export function looksLikeArchive(name: string): boolean { return name.endsWith(".tar.gz") || name.endsWith(".tgz") || name.endsWith(".zip"); } @@ -43,7 +44,8 @@ function looksLikeArchive(name: string): boolean { * returns `undefined` so the caller can fall back to a different install * strategy (e.g. Homebrew). */ -function pickAsset( +/** @internal Exported for testing. */ +export function pickAsset( assets: ReleaseAsset[], platform: NodeJS.Platform, arch: string,