1// @effect-diagnostics nodeBuiltinImport:off - Drives the real shell installer through a PTY and a gated HTTP fixture.
2import * as HostProcess from "@t3tools/shared/HostProcess";
3import * as NodeChildProcess from "node:child_process";
4import * as NodeCrypto from "node:crypto";
5import * as NodeFSP from "node:fs/promises";
6import * as NodeHttp from "node:http";
7import * as NodeOS from "node:os";
8import * as NodePath from "node:path";
9import { describe, expect, it } from "vite-plus/test";
10
11// util-linux's script gives the real installer a terminal without a browser or extra packages.
12describe.skipIf(HostProcess.Platform.defaultValue() !== "linux")("installer terminal", () => {
13 it.each([false, true])(
14 "preserves download and install behavior (HTTP failure: %s)",
15 async (fail) => {
16 const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-install-progress-"));
17 const version = "1.2.3";
18 const stem = `t3-${version}-linux-${HostProcess.Architecture.defaultValue()}`;
19 const archiveName = `${stem}.tar.gz`;
20 let resumeDownload: (() => void) | undefined;
21 let sawPartialProgress = false;
22 let output = "";
23 await NodeFSP.mkdir(NodePath.join(root, stem));
24 await NodeFSP.writeFile(NodePath.join(root, stem, "t3"), "#!/bin/sh\necho 't3 v1.2.3'\n", {
25 mode: 0o755,
26 });
27 await NodeFSP.writeFile(
28 NodePath.join(root, stem, "payload"),
29 NodeCrypto.randomBytes(64 * 1024),
30 );
31 NodeChildProcess.execFileSync("tar", [
32 "-czf",
33 NodePath.join(root, archiveName),
34 "-C",
35 root,
36 stem,
37 ]);
38 const archive = await NodeFSP.readFile(NodePath.join(root, archiveName));
39 const checksum = NodeCrypto.createHash("sha256").update(archive).digest("hex");
40 const server = NodeHttp.createServer((request, response) => {
41 if (request.url?.endsWith("/SHA256SUMS")) {
42 response.end(`${checksum} ${archiveName}\n`);
43 } else if (fail) {
44 response.writeHead(500).end();
45 } else {
46 response.writeHead(200, { "Content-Length": archive.length });
47 resumeDownload = () => response.end(archive.subarray(Math.floor(archive.length / 2)));
48 response.write(archive.subarray(0, Math.floor(archive.length / 2)));
49 }
50 });
51 await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
52 const address = server.address();
53 if (!address || typeof address === "string") throw new Error("Expected a TCP listener");
54 const installer = NodePath.resolve(import.meta.dirname, "install.sh").replaceAll(
55 "'",
56 "'\\''",
57 );
58 const child = NodeChildProcess.spawn("script", ["-qec", `sh '${installer}'`, "/dev/null"], {
59 env: {
60 ...process.env,
61 TERM: "xterm",
62 NO_COLOR: "1",
63 T3CODE_VERSION: version,
64 T3CODE_HOME: NodePath.join(root, "home"),
65 T3CODE_INSTALL_BIN_DIR: NodePath.join(root, "bin"),
66 T3CODE_RELEASE_BASE_URL: `http://127.0.0.1:${address.port}`,
67 },
68 stdio: ["ignore", "pipe", "pipe"],
69 });
70 const collect = (chunk: Buffer) => {
71 output += chunk.toString();
72 if (!sawPartialProgress && /\b[1-9]\d?%/.test(output)) {
73 sawPartialProgress = true;
74 resumeDownload?.();
75 }
76 };
77 child.stdout.on("data", collect);
78 child.stderr.on("data", collect);
79 try {
80 const code = await new Promise<number | null>((resolve, reject) => {
81 child.on("error", reject);
82 child.on("close", resolve);
83 });
84 const versions = NodePath.join(root, "home/runtime/versions");
85 if (fail) {
86 expect(code).not.toBe(0);
87 expect(output).toContain("500");
88 expect(output).not.toContain("100%");
89 expect(output).not.toContain("Installed T3 Code");
90 expect(await NodeFSP.readdir(versions)).toEqual([]);
91 } else {
92 expect(code).toBe(0);
93 expect(sawPartialProgress).toBe(true);
94 expect(output).toContain("100%");
95 expect(output).toContain("0.1 / 0.1 MB");
96 expect(output).toContain("Installed T3 Code 1.2.3");
97 expect(
98 await NodeFSP.readFile(NodePath.join(versions, version, ".install-complete"), "utf8"),
99 ).toBe("1.2.3\n");
100 expect(
101 NodeChildProcess.execFileSync(NodePath.join(root, "bin/t3"), ["--version"], {
102 encoding: "utf8",
103 }).trim(),
104 ).toBe("t3 v1.2.3");
105 expect(await NodeFSP.readdir(versions)).toEqual([version]);
106 }
107 } finally {
108 if (child.exitCode === null) child.kill();
109 server.closeAllConnections();
110 await new Promise<void>((resolve) => server.close(() => resolve()));
111 await NodeFSP.rm(root, { recursive: true, force: true });
112 }
113 },
114 );
115});