Skip to content
Source
Sign inSign up

bootError.test.ts

133 lines5.6 KB

1import type { DesktopBridge, DesktopCliCommandState, DesktopUpdateState } from "@t3tools/contracts";
2import { describe, expect, it, vi } from "vite-plus/test";
3​
4import {
5 type BootErrorEnvironment,
6 buildErrorReport,
7 resolveTriageCommand,
8 updateActionFor,
9} from "./bootError";
10​
11const desktopWith = (getState: () => Promise<DesktopCliCommandState>) =>
12 ({ cliCommand: { getState } }) as unknown as DesktopBridge;
13​
14const notOnPath = (launcherPath: string | null): DesktopCliCommandState => ({
15 supported: launcherPath !== null,
16 installedPath: null,
17 onPath: false,
18 launcherPath,
19});
20​
21describe("resolveTriageCommand", () => {
22 it("prefers t3 on PATH, then the desktop launcher", async () => {
23 const onPath = { ...notOnPath("/home/me/.t3/bin/t3"), onPath: true };
24 await expect(resolveTriageCommand(desktopWith(async () => onPath))).resolves.toBe("t3 triage");
25 await expect(
26 resolveTriageCommand(desktopWith(async () => notOnPath("/home/me/.t3/bin/t3"))),
27 ).resolves.toBe("/home/me/.t3/bin/t3 triage");
28 });
29​
30 it("quotes launcher paths for the shell that runs them", async () => {
31 await expect(
32 resolveTriageCommand(desktopWith(async () => notOnPath("/Users/O'Neil/.t3/bin/t3"))),
33 ).resolves.toBe(`'/Users/O'"'"'Neil/.t3/bin/t3' triage`);
34 await expect(
35 resolveTriageCommand(desktopWith(async () => notOnPath("C:\\Users\\Me\\.t3\\bin\\t3.cmd"))),
36 ).resolves.toBe("C:\\Users\\Me\\.t3\\bin\\t3.cmd triage");
37 await expect(
38 resolveTriageCommand(
39 desktopWith(async () => notOnPath("C:\\Users\\Jo O'Neil\\.t3\\bin\\t3.cmd")),
40 ),
41 ).resolves.toBe("& 'C:\\Users\\Jo O''Neil\\.t3\\bin\\t3.cmd' triage");
42 });
43​
44 it("falls back to npx without a usable desktop launcher", async () => {
45 using _warn = vi.spyOn(console, "warn").mockImplementation(() => {});
46 await expect(resolveTriageCommand(undefined)).resolves.toBe("npx t3 triage");
47 await expect(resolveTriageCommand({} as DesktopBridge)).resolves.toBe("npx t3 triage");
48 await expect(resolveTriageCommand(desktopWith(async () => notOnPath(null)))).resolves.toBe(
49 "npx t3 triage",
50 );
51 const staleWindowsPathEntry = { ...notOnPath(null), supported: true, onPath: true };
52 await expect(
53 resolveTriageCommand(desktopWith(async () => staleWindowsPathEntry)),
54 ).resolves.toBe("npx t3 triage");
55 await expect(
56 resolveTriageCommand(desktopWith(() => Promise.reject(new Error("ipc closed")))),
57 ).resolves.toBe("npx t3 triage");
58 });
59});
60​
61const environment: BootErrorEnvironment = {
62 version: "0.1.3-nightly.20261026.3707",
63 channel: "nightly",
64 surface: "web",
65 platform: "MacIntel",
66 userAgent: "Test",
67 time: "2026-10-26T00:00:00.000Z",
68};
69​
70describe("buildErrorReport", () => {
71 it("drops hosts and tokens from URLs in the error", () => {
72 const error = new TypeError(
73 "Failed to fetch dynamically imported module: http://studio.tail1234.ts.net:3773/assets/a.js?pair=SECRET#token=SECRET",
74 {
75 cause: new Error(
76 "ws://studio.tail1234.ts.net:3773/ws?token=SECRET closed, main-a.js?sig=SECRET",
77 ),
78 },
79 );
80 error.stack = `TypeError: ${error.message}\n at http://studio.tail1234.ts.net:3773/src/main.tsx?t=1:10:5`;
81 const report = buildErrorReport(error, environment);
82 expect(report).not.toContain("SECRET");
83 expect(report).not.toContain("tail1234");
84 expect(report).toContain("module: /assets/a.js\n at /src/main.tsx:10:5");
85 expect(report).toContain("Caused by: Error: /ws closed, main-a.js");
86 expect(report).toContain("Version: 0.1.3-nightly.20261026.3707");
87 });
88​
89 it("keeps the header when a Firefox stack frame starts with the error name", () => {
90 const error = new Error("boot failed");
91 error.stack = "ErrorBoundary@/assets/main.js:1:2";
92 expect(buildErrorReport(error, environment)).toMatch(
93 /^Error: boot failed\nErrorBoundary@\/assets\/main.js:1:2\n/,
94 );
95 const unnamed = new Error();
96 unnamed.stack = "ErrorBoundary@/assets/main.js:1:2";
97 expect(buildErrorReport(unnamed, environment)).toMatch(/^Error\nErrorBoundary@/);
98 });
99​
100 it("stops at cause cycles and survives values that are not errors", () => {
101 const loop = new Error("outer");
102 loop.cause = loop;
103 expect(buildErrorReport(loop, environment).match(/outer/g)).toHaveLength(1);
104 expect(buildErrorReport("plain text", environment)).toMatch(/^plain text\n/);
105 expect(buildErrorReport({ code: 42 }, environment)).toMatch(/^\{"code":42\}\n/);
106 expect(buildErrorReport(Object.create(null), environment)).toMatch(/^\{\}\n/);
107 expect(buildErrorReport(undefined, environment)).toMatch(/^\(no error message\)\n/);
108 });
109​
110 it("cuts long errors but keeps the environment", () => {
111 const report = buildErrorReport("x".repeat(20_000), environment);
112 expect(report).toContain("[truncated]");
113 expect(report.length).toBeLessThan(7_000);
114 expect(report).toContain("Time: 2026-10-26T00:00:00.000Z");
115 });
116});
117​
118describe("updateActionFor", () => {
119 it("offers an action only when a newer build exists", () => {
120 const state = (status: DesktopUpdateState["status"], downloadPercent: number | null = null) =>
121 ({ status, downloadPercent }) as DesktopUpdateState;
122 expect(updateActionFor(state("available"))).toEqual({ kind: "download" });
123 expect(updateActionFor(state("downloading", 41.6))).toEqual({
124 kind: "downloading",
125 percent: 42,
126 });
127 expect(updateActionFor(state("downloaded"))).toEqual({ kind: "install" });
128 for (const status of ["disabled", "idle", "checking", "up-to-date", "error"] as const) {
129 expect(updateActionFor(state(status))).toBeNull();
130 }
131 expect(updateActionFor(undefined)).toBeNull();
132 });
133});