1// @effect-diagnostics nodeBuiltinImport:off - tests use POSIX path joining to match the Linux startup boundary.
2import * as NodePath from "node:path";
3import { assert, describe, it } from "@effect/vitest";
4
5import {
6 resolveEarlyLinuxElectronOptions,
7 resolveEarlyLinuxPasswordStorePreference,
8 resolveLinuxDisableFeaturesSwitch,
9} from "./DesktopEarlyElectronStartup.ts";
10
11describe("DesktopEarlyElectronStartup", () => {
12 const joinPath = NodePath.posix.join;
13
14 it("reads the persisted linux password-store preference before Electron is ready", () => {
15 const preference = resolveEarlyLinuxPasswordStorePreference({
16 env: { T3CODE_HOME: "/home/user/.t3-test" },
17 homeDirectory: "/home/user",
18 joinPath,
19 readFileString: (path) => {
20 assert.equal(path, "/home/user/.t3-test/userdata/desktop-settings.json");
21 return JSON.stringify({ linuxPasswordStore: "kwallet6" });
22 },
23 });
24
25 assert.equal(preference, "kwallet6");
26 });
27
28 it("accepts JSONC in the early desktop settings file", () => {
29 const preference = resolveEarlyLinuxPasswordStorePreference({
30 env: { T3CODE_HOME: "/home/user/.t3-test" },
31 homeDirectory: "/home/user",
32 joinPath,
33 readFileString: () => `{
34 // manually edited setting
35 "linuxPasswordStore": "gnome-libsecret",
36 }`,
37 });
38
39 assert.equal(preference, "gnome-libsecret");
40 });
41
42 it("falls back to auto when the early settings document is missing or invalid", () => {
43 const preference = resolveEarlyLinuxPasswordStorePreference({
44 env: {},
45 homeDirectory: "/home/user",
46 joinPath,
47 readFileString: () => {
48 throw new Error("missing");
49 },
50 });
51
52 assert.equal(preference, "auto");
53 });
54
55 it("preserves absolute root paths when resolving early settings", () => {
56 const preference = resolveEarlyLinuxPasswordStorePreference({
57 env: { T3CODE_HOME: "/" },
58 homeDirectory: "/home/user",
59 joinPath,
60 readFileString: (path) => {
61 assert.equal(path, "/userdata/desktop-settings.json");
62 return JSON.stringify({ linuxPasswordStore: "kwallet6" });
63 },
64 });
65
66 assert.equal(preference, "kwallet6");
67 });
68
69 it("resolves the early linux Electron switches", () => {
70 const options = resolveEarlyLinuxElectronOptions({
71 env: {
72 T3CODE_HOME: "/home/user/.t3-test",
73 XDG_CURRENT_DESKTOP: "niri",
74 VITE_DEV_SERVER_URL: "http://127.0.0.1:5173",
75 },
76 homeDirectory: "/home/user",
77 joinPath,
78 readFileString: (path) => {
79 assert.equal(path, "/home/user/.t3-test/userdata/desktop-settings.json");
80 return JSON.stringify({ linuxPasswordStore: "auto" });
81 },
82 });
83
84 assert.deepEqual(options, {
85 isDevelopment: true,
86 linuxWmClass: "t3code-dev",
87 linuxDesktopEntryName: "com.t3tools.T3Code.Development.desktop",
88 passwordStore: "gnome-libsecret",
89 });
90 });
91
92 it("keeps implicit development state under ~/.t3/dev when T3CODE_HOME is unset", () => {
93 const preference = resolveEarlyLinuxPasswordStorePreference({
94 env: {
95 VITE_DEV_SERVER_URL: "http://127.0.0.1:5173",
96 },
97 homeDirectory: "/home/user",
98 joinPath,
99 readFileString: (path) => {
100 assert.equal(path, "/home/user/.t3/dev/desktop-settings.json");
101 return JSON.stringify({ linuxPasswordStore: "kwallet" });
102 },
103 });
104
105 assert.equal(preference, "kwallet");
106 });
107
108 it("treats whitespace-only T3CODE_HOME as unconfigured in development", () => {
109 const preference = resolveEarlyLinuxPasswordStorePreference({
110 env: {
111 T3CODE_HOME: " ",
112 VITE_DEV_SERVER_URL: "http://127.0.0.1:5173",
113 },
114 homeDirectory: "/home/user",
115 joinPath,
116 readFileString: (path) => {
117 assert.equal(path, "/home/user/.t3/dev/desktop-settings.json");
118 return JSON.stringify({ linuxPasswordStore: "gnome-libsecret" });
119 },
120 });
121
122 assert.equal(preference, "gnome-libsecret");
123 });
124
125 describe("resolveLinuxDisableFeaturesSwitch", () => {
126 const hyprland = { XDG_CURRENT_DESKTOP: "Hyprland", XDG_SESSION_TYPE: "wayland" };
127 const releaseCommits: Record<string, string> = {
128 "0.48.0": "5ee35f914f921e5696030698e74fb5566a804768",
129 "0.48.1": "29e2e59fdbab8ed2cc23a20e3c6043d5decb5cdc",
130 "0.49.0": "9958d297641b5c84dcff93f9039d80a5ad37ab00",
131 "0.50.0": "c4a4c341568944bd4fb9cd503558b2de602c0213",
132 "0.50.1": "4e242d086e20b32951fdc0ebcbfb4d41b5be8dcc",
133 };
134 // Shape of `hyprctl version -j` from a release tarball, trimmed to the fields we read.
135 const hyprctlVersion =
136 (version: string, overrides: Record<string, unknown> = {}) =>
137 () =>
138 JSON.stringify({
139 commit: releaseCommits[version] ?? "0123456789abcdef0123456789abcdef01234567",
140 version,
141 dirty: false,
142 tag: `v${version}`,
143 flags: [],
144 ...overrides,
145 });
146 const brokenHyprland = {
147 env: hyprland,
148 ozonePlatform: "wayland",
149 enableFeatures: null,
150 disableFeatures: null,
151 readHyprctlVersionJson: hyprctlVersion("0.48.1"),
152 };
153
154 it.each(["0.48.0", "0.48.1", "0.49.0", "0.50.0", "0.50.1"])(
155 "disables the color manager on Hyprland %s",
156 (version) => {
157 assert.equal(
158 resolveLinuxDisableFeaturesSwitch({
159 ...brokenHyprland,
160 readHyprctlVersionJson: hyprctlVersion(version),
161 }),
162 "WaylandWpColorManagerV1",
163 );
164 },
165 );
166
167 it.each([
168 { label: "case-insensitive desktop tokens", env: { XDG_CURRENT_DESKTOP: "foo:HYPRLAND" } },
169 {
170 label: "XDG_SESSION_DESKTOP when XDG_CURRENT_DESKTOP is unset",
171 env: { XDG_SESSION_DESKTOP: "hyprland" },
172 },
173 ])("detects Hyprland via $label", ({ env }) => {
174 assert.equal(
175 resolveLinuxDisableFeaturesSwitch({ ...brokenHyprland, env }),
176 "WaylandWpColorManagerV1",
177 );
178 });
179
180 it("keeps existing disabled features", () => {
181 assert.equal(
182 resolveLinuxDisableFeaturesSwitch({
183 ...brokenHyprland,
184 disableFeatures: "Vulkan,WaylandWpColorManagerV1Extra",
185 }),
186 "Vulkan,WaylandWpColorManagerV1Extra,WaylandWpColorManagerV1",
187 );
188 });
189
190 it.each([
191 { label: "Hyprland 0.47.2", readHyprctlVersionJson: hyprctlVersion("0.47.2") },
192 { label: "Hyprland 0.51.0", readHyprctlVersionJson: hyprctlVersion("0.51.0") },
193 { label: "Hyprland 0.56.2", readHyprctlVersionJson: hyprctlVersion("0.56.2") },
194 { label: "an unrecognized version", readHyprctlVersionJson: hyprctlVersion("1.0.0") },
195 {
196 label: "a malformed version",
197 readHyprctlVersionJson: hyprctlVersion("0.48.not-a-version"),
198 },
199 { label: "a two-part version", readHyprctlVersionJson: hyprctlVersion("0.48") },
200 { label: "a prerelease version", readHyprctlVersionJson: hyprctlVersion("0.49.0-rc1") },
201 {
202 label: "a development build after an affected release",
203 readHyprctlVersionJson: hyprctlVersion("0.50.1", { tag: "v0.50.1-87-g4e8875b5" }),
204 },
205 {
206 label: "a later Nix build reporting an affected release tag",
207 readHyprctlVersionJson: hyprctlVersion("0.50.1", {
208 commit: "4e8875b5e9700c81ca4e169dc7b85bb5b3c8cb7a",
209 }),
210 },
211 {
212 label: "an affected version with another release's commit",
213 readHyprctlVersionJson: hyprctlVersion("0.48.0", { commit: releaseCommits["0.48.1"] }),
214 },
215 { label: "an unknown patch release", readHyprctlVersionJson: hyprctlVersion("0.50.2") },
216 {
217 label: "missing commit metadata",
218 readHyprctlVersionJson: () =>
219 JSON.stringify({ version: "0.48.1", tag: "v0.48.1", dirty: false }),
220 },
221 {
222 label: "a build without git metadata",
223 readHyprctlVersionJson: hyprctlVersion("0.48.1", { tag: "" }),
224 },
225 {
226 label: "a dirty build",
227 readHyprctlVersionJson: hyprctlVersion("0.48.1", { dirty: true }),
228 },
229 {
230 label: "output without build metadata",
231 readHyprctlVersionJson: () => JSON.stringify({ version: "0.48.1" }),
232 },
233 { label: "unparseable hyprctl output", readHyprctlVersionJson: () => "ok" },
234 {
235 label: "a missing hyprctl",
236 readHyprctlVersionJson: () => {
237 throw new Error("spawnSync hyprctl ENOENT");
238 },
239 },
240 ])("preserves color management for $label", (overrides) => {
241 assert.isNull(resolveLinuxDisableFeaturesSwitch({ ...brokenHyprland, ...overrides }));
242 });
243
244 it.each([
245 {
246 label: "a stale Hyprland signature in KDE",
247 env: { XDG_CURRENT_DESKTOP: "KDE", HYPRLAND_INSTANCE_SIGNATURE: "abc_123" },
248 },
249 {
250 label: "resolved X11 in a Wayland session",
251 ozonePlatform: "x11",
252 env: { ...hyprland, WAYLAND_DISPLAY: "wayland-1" },
253 },
254 { label: "a missing platform", ozonePlatform: null },
255 { label: "an unrelated platform", ozonePlatform: "headless" },
256 { label: "already disabled features", disableFeatures: "Vulkan, WaylandWpColorManagerV1" },
257 { label: "an explicitly enabled feature", enableFeatures: "Vulkan,WaylandWpColorManagerV1" },
258 {
259 label: "a feature enabled with trial parameters",
260 enableFeatures: "*WaylandWpColorManagerV1<Study.Group:mode/strict",
261 },
262 ])("skips the Hyprland probe for $label", (overrides) => {
263 assert.isNull(
264 resolveLinuxDisableFeaturesSwitch({
265 ...brokenHyprland,
266 ...overrides,
267 readHyprctlVersionJson: () => assert.fail("hyprctl should not run"),
268 }),
269 );
270 });
271 });
272});