返回 CodeWhale
public-installer-contract.test.ts
根目录 / web / lib / public-installer-contract.test.ts
1 import { createHash } from "node:crypto";
2 import { spawnSync } from "node:child_process";
3 import {
4 chmodSync,
5 existsSync,
6 mkdirSync,
7 mkdtempSync,
8 readFileSync,
9 rmSync,
10 writeFileSync,
11 } from "node:fs";
12 import { tmpdir } from "node:os";
13 import path from "node:path";
14 import { fileURLToPath } from "node:url";
15 import { afterEach, describe, expect, it } from "vitest";
16
17 const installer = fileURLToPath(new URL("../public/install.sh", import.meta.url));
18 const fixtureRoots: string[] = [];
19
20 function executable(contents: string): Buffer {
21 return Buffer.from(`#!/bin/sh\nprintf '%s\\n' '${contents}'\n`, "utf8");
22 }
23
24 function sha256(contents: Buffer): string {
25 return createHash("sha256").update(contents).digest("hex");
26 }
27
28 function installFixture(
29 withLegacyTui: boolean,
30 options: {
31 os?: "Darwin" | "Linux";
32 arch?: "x86_64" | "aarch64";
33 version?: string;
34 glibc?: string;
35 expectedStatus?: number;
36 } = {},
37 ) {
38 const {
39 os = "Darwin",
40 arch = "x86_64",
41 version,
42 glibc = "2.39",
43 expectedStatus = 0,
44 } = options;
45 const root = mkdtempSync(path.join(tmpdir(), "codewhale-web-installer-"));
46 fixtureRoots.push(root);
47 const releaseDir = path.join(root, "release");
48 const installDir = path.join(root, "install");
49 const fakeBin = path.join(root, "fake-bin");
50 mkdirSync(releaseDir, { recursive: true });
51 mkdirSync(installDir, { recursive: true });
52 mkdirSync(fakeBin, { recursive: true });
53
54 const runtime = executable("codewhale 0.9.5 (fixture)");
55 const target = `${os === "Darwin" ? "macos" : "linux"}-${arch === "aarch64" ? "arm64" : "x64"}`;
56 const assets = [`codewhale-${target}`, `codew-${target}`];
57 for (const asset of assets) {
58 writeFileSync(path.join(releaseDir, asset), runtime);
59 }
60 writeFileSync(
61 path.join(releaseDir, "codewhale-artifacts-sha256.txt"),
62 assets.map((asset) => `${sha256(runtime)} ${asset}`).join("\n") + "\n",
63 );
64
65 const fakeUname = [
66 "#!/bin/sh",
67 'case "${1:-}" in',
68 ` -s) printf '%s\\n' ${os} ;;`,
69 ` -m) printf '%s\\n' ${arch} ;;`,
70 ` *) printf '%s\\n' ${os} ;;`,
71 "esac",
72 "",
73 ].join("\n");
74 const fakeCurl = [
75 "#!/bin/sh",
76 'out=""',
77 'url=""',
78 'while [ "$#" -gt 0 ]; do',
79 ' case "$1" in',
80 ' -o) shift; out="$1" ;;',
81 " -*) ;;",
82 ' *) url="$1" ;;',
83 " esac",
84 " shift",
85 "done",
86 'cp "$FAKE_RELEASE_DIR/${url##*/}" "$out"',
87 "",
88 ].join("\n");
89 const fakeGetconf = [
90 "#!/bin/sh",
91 `printf '%s\\n' 'glibc ${glibc}'`,
92 "",
93 ].join("\n");
94 for (const [name, contents] of [
95 ["uname", fakeUname],
96 ["curl", fakeCurl],
97 ["getconf", fakeGetconf],
98 ]) {
99 const destination = path.join(fakeBin, name);
100 writeFileSync(destination, contents);
101 chmodSync(destination, 0o755);
102 }
103
104 const legacyPath = path.join(installDir, "codewhale-tui");
105 if (withLegacyTui) {
106 writeFileSync(legacyPath, executable("codewhale-tui 0.9.4 (legacy fixture)"));
107 chmodSync(legacyPath, 0o755);
108 }
109
110 const result = spawnSync("/bin/sh", [installer], {
111 encoding: "utf8",
112 env: {
113 ...process.env,
114 CODEWHALE_INSTALL_DIR: installDir,
115 CODEWHALE_RELEASE_BASE_URL: "https://fixtures.invalid/download",
116 ...(version ? { CODEWHALE_VERSION: version } : {}),
117 FAKE_RELEASE_DIR: releaseDir,
118 HOME: path.join(root, "home"),
119 PATH: `${fakeBin}:${process.env.PATH ?? "/usr/bin:/bin"}`,
120 },
121 });
122 expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(expectedStatus);
123
124 return { installDir, legacyPath, result, runtime };
125 }
126
127 afterEach(() => {
128 for (const root of fixtureRoots.splice(0)) {
129 rmSync(root, { recursive: true, force: true });
130 }
131 });
132
133 describe.skipIf(process.platform === "win32")("public installer compatibility contract", () => {
134 it("preserves a different legacy TUI command and directs migration to a fresh directory", () => {
135 const { installDir, legacyPath, result } = installFixture(true, { expectedStatus: 1 });
136
137 expect(existsSync(path.join(installDir, "codewhale"))).toBe(false);
138 expect(existsSync(path.join(installDir, "codew"))).toBe(false);
139 expect(readFileSync(legacyPath)).toEqual(executable("codewhale-tui 0.9.4 (legacy fixture)"));
140 expect(result.stderr).toContain(legacyPath);
141 expect(result.stderr).toContain("mktemp -d");
142 expect(result.stderr).toContain("No existing file was changed");
143 });
144
145 it("does not create the retired TUI command for a clean v0.9.5 install", () => {
146 const { installDir, legacyPath, result, runtime } = installFixture(false);
147
148 expect(readFileSync(path.join(installDir, "codewhale"))).toEqual(runtime);
149 expect(readFileSync(path.join(installDir, "codew"))).toEqual(runtime);
150 expect(existsSync(legacyPath)).toBe(false);
151 expect(result.stdout).not.toContain("Refreshed legacy compatibility command:");
152 });
153
154 it.each([undefined, "v0.9.6", "v0.9.11"])(
155 "does not apply a glibc floor to static Linux arm64 version %s",
156 (version) => {
157 const { installDir } = installFixture(false, {
158 os: "Linux",
159 arch: "aarch64",
160 version,
161 glibc: "2.17",
162 });
163
164 expect(existsSync(path.join(installDir, "codewhale"))).toBe(true);
165 expect(existsSync(path.join(installDir, "codew"))).toBe(true);
166 },
167 );
168
169 it("keeps the truthful glibc preflight for an explicitly requested older Linux arm64 release", () => {
170 const { result } = installFixture(false, {
171 os: "Linux",
172 arch: "aarch64",
173 version: "v0.9.5",
174 glibc: "2.35",
175 expectedStatus: 1,
176 });
177
178 expect(result.stderr).toContain(
179 "Codewhale v0.9.5 linux-arm64 assets require glibc 2.39 or newer",
180 );
181 expect(result.stderr).toContain(
182 "Current v0.9.6+ assets are static musl builds",
183 );
184 });
185 });
186
186 lines TYPESCRIPT