authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-04 13:33:20-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-04 20:33:20+00:00
logc251e41b5e5287b786cda44ddc263b8e6a74859c
tree85d55508f2b295ffff54bb950ed57df42fe3768f
parent5c6c1d67d82bb81bddaee6d83e54e8aecd96ec7d
signature Signed by PGP key B5690EEEBB952194

test(desktop): make path fixtures and timeouts host-portable (#9568)

Co-authored-by: Claude Code <noreply@anthropic.com>

6 files changed, 134 insertions(+), 88 deletions(-)

apps/desktop/scripts/electron-launcher.mjs+8-4
...@@ -165,10 +165,14 @@ function registerMacLauncherBundle(appBundlePath) {...@@ -165,10 +165,14 @@ function registerMacLauncherBundle(appBundlePath) {
165 }165 }
166}166}
167167
168// Bundle-internal paths are macOS paths whatever host builds them.
168export function resolveMacLauncherIconPaths(runtimeDir, development = isDevelopment) {169export function resolveMacLauncherIconPaths(runtimeDir, development = isDevelopment) {
169 return {170 return {
170 sourceIconPath: development ? developmentMacIconPngPath : productionMacIconPngPath,171 sourceIconPath: development ? developmentMacIconPngPath : productionMacIconPngPath,
171 generatedIconPath: NodePath.join(runtimeDir, development ? "icon-dev.icns" : "icon-prod.icns"),172 generatedIconPath: NodePath.posix.join(
173 runtimeDir,
174 development ? "icon-dev.icns" : "icon-prod.icns",
175 ),
172 };176 };
173}177}
174178
...@@ -280,12 +284,12 @@ function readJson(path) {...@@ -280,12 +284,12 @@ function readJson(path) {
280}284}
281285
282export function resolveMacLauncherPaths(appBundlePath, displayName = APP_DISPLAY_NAME) {286export function resolveMacLauncherPaths(appBundlePath, displayName = APP_DISPLAY_NAME) {
283 const executableDir = NodePath.join(appBundlePath, "Contents", "MacOS");287 const executableDir = NodePath.posix.join(appBundlePath, "Contents", "MacOS");
284 const launcherExecutableName = `${displayName} Launcher`;288 const launcherExecutableName = `${displayName} Launcher`;
285 return {289 return {
286 launcherExecutableName,290 launcherExecutableName,
287 launcherBinaryPath: NodePath.join(executableDir, launcherExecutableName),291 launcherBinaryPath: NodePath.posix.join(executableDir, launcherExecutableName),
288 runtimeElectronBinaryPath: NodePath.join(executableDir, "Electron"),292 runtimeElectronBinaryPath: NodePath.posix.join(executableDir, "Electron"),
289 };293 };
290}294}
291295
apps/desktop/scripts/electron-launcher.test.mjs+3-2
...@@ -84,9 +84,10 @@ describe("electron development launcher", () => {...@@ -84,9 +84,10 @@ describe("electron development launcher", () => {
84 const development = resolveMacLauncherIconPaths("/runtime", true);84 const development = resolveMacLauncherIconPaths("/runtime", true);
85 const production = resolveMacLauncherIconPaths("/runtime", false);85 const production = resolveMacLauncherIconPaths("/runtime", false);
8686
87 assert.match(development.sourceIconPath, /assets\/dev\/blueprint-macos-1024\.png$/);87 // The source icons are real repo paths, joined for the host.
88 assert.match(development.sourceIconPath, /assets[\\/]dev[\\/]blueprint-macos-1024\.png$/);
88 assert.equal(development.generatedIconPath, "/runtime/icon-dev.icns");89 assert.equal(development.generatedIconPath, "/runtime/icon-dev.icns");
89 assert.match(production.sourceIconPath, /assets\/prod\/black-macos-1024\.png$/);90 assert.match(production.sourceIconPath, /assets[\\/]prod[\\/]black-macos-1024\.png$/);
90 assert.equal(production.generatedIconPath, "/runtime/icon-prod.icns");91 assert.equal(production.generatedIconPath, "/runtime/icon-prod.icns");
91 });92 });
92});93});
apps/desktop/src/app/DesktopConnectionCatalogStore.test.ts+19-12
...@@ -4,6 +4,7 @@ import { ConnectionCatalogDocument } from "@t3tools/client-runtime/platform";...@@ -4,6 +4,7 @@ import { ConnectionCatalogDocument } from "@t3tools/client-runtime/platform";
4import { EnvironmentId, type PersistedSavedEnvironmentRecord } from "@t3tools/contracts";4import { EnvironmentId, type PersistedSavedEnvironmentRecord } from "@t3tools/contracts";
5import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
6import * as FileSystem from "effect/FileSystem";6import * as FileSystem from "effect/FileSystem";
7import * as Path from "effect/Path";
7import * as Layer from "effect/Layer";8import * as Layer from "effect/Layer";
8import * as Option from "effect/Option";9import * as Option from "effect/Option";
9import * as PlatformError from "effect/PlatformError";10import * as PlatformError from "effect/PlatformError";
...@@ -226,10 +227,11 @@ describe("DesktopConnectionCatalogStore", () => {...@@ -226,10 +227,11 @@ describe("DesktopConnectionCatalogStore", () => {
226 it.effect("surfaces malformed catalog documents without deleting them", () =>227 it.effect("surfaces malformed catalog documents without deleting them", () =>
227 withStore(228 withStore(
228 Effect.gen(function* () {229 Effect.gen(function* () {
230 const path = yield* Path.Path;
229 const environment = yield* DesktopEnvironment.DesktopEnvironment;231 const environment = yield* DesktopEnvironment.DesktopEnvironment;
230 const fileSystem = yield* FileSystem.FileSystem;232 const fileSystem = yield* FileSystem.FileSystem;
231 const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore;233 const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore;
232 const catalogPath = `${environment.stateDir}/connection-catalog.json`;234 const catalogPath = path.join(environment.stateDir, "connection-catalog.json");
233 yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true });235 yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true });
234 yield* fileSystem.writeFileString(catalogPath, "{not-json");236 yield* fileSystem.writeFileString(catalogPath, "{not-json");
235237
...@@ -247,6 +249,7 @@ describe("DesktopConnectionCatalogStore", () => {...@@ -247,6 +249,7 @@ describe("DesktopConnectionCatalogStore", () => {
247249
248 it.effect("surfaces catalog filesystem failures instead of treating them as missing", () =>250 it.effect("surfaces catalog filesystem failures instead of treating them as missing", () =>
249 Effect.gen(function* () {251 Effect.gen(function* () {
252 const path = yield* Path.Path;
250 const baseFileSystem = yield* FileSystem.FileSystem;253 const baseFileSystem = yield* FileSystem.FileSystem;
251 const baseDir = yield* baseFileSystem.makeTempDirectoryScoped({254 const baseDir = yield* baseFileSystem.makeTempDirectoryScoped({
252 prefix: "t3-desktop-connection-catalog-test-",255 prefix: "t3-desktop-connection-catalog-test-",
...@@ -255,7 +258,7 @@ describe("DesktopConnectionCatalogStore", () => {...@@ -255,7 +258,7 @@ describe("DesktopConnectionCatalogStore", () => {
255 _tag: "PermissionDenied",258 _tag: "PermissionDenied",
256 module: "FileSystem",259 module: "FileSystem",
257 method: "readFileString",260 method: "readFileString",
258 pathOrDescriptor: `${baseDir}/userdata/connection-catalog.json`,261 pathOrDescriptor: path.join(baseDir, "userdata", "connection-catalog.json"),
259 });262 });
260 const fileSystemLayer = Layer.succeed(263 const fileSystemLayer = Layer.succeed(
261 FileSystem.FileSystem,264 FileSystem.FileSystem,
...@@ -272,11 +275,11 @@ describe("DesktopConnectionCatalogStore", () => {...@@ -272,11 +275,11 @@ describe("DesktopConnectionCatalogStore", () => {
272 error,275 error,
273 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreReadError,276 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreReadError,
274 );277 );
275 assert.equal(error.catalogPath, `${baseDir}/userdata/connection-catalog.json`);278 assert.equal(error.catalogPath, path.join(baseDir, "userdata", "connection-catalog.json"));
276 assert.strictEqual(error.cause, permissionError);279 assert.strictEqual(error.cause, permissionError);
277 assert.equal(280 assert.equal(
278 error.message,281 error.message,
279 `Failed to read the desktop connection catalog at ${baseDir}/userdata/connection-catalog.json.`,282 `Failed to read the desktop connection catalog at ${path.join(baseDir, "userdata", "connection-catalog.json")}.`,
280 );283 );
281 assert.notEqual(error.message, permissionError.message);284 assert.notEqual(error.message, permissionError.message);
282 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),285 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
...@@ -285,6 +288,7 @@ describe("DesktopConnectionCatalogStore", () => {...@@ -285,6 +288,7 @@ describe("DesktopConnectionCatalogStore", () => {
285 it.effect("reports the failed catalog write operation and path", () =>288 it.effect("reports the failed catalog write operation and path", () =>
286 Effect.gen(function* () {289 Effect.gen(function* () {
287 const baseFileSystem = yield* FileSystem.FileSystem;290 const baseFileSystem = yield* FileSystem.FileSystem;
291 const path = yield* Path.Path;
288 const baseDir = yield* baseFileSystem.makeTempDirectoryScoped({292 const baseDir = yield* baseFileSystem.makeTempDirectoryScoped({
289 prefix: "t3-desktop-connection-catalog-test-",293 prefix: "t3-desktop-connection-catalog-test-",
290 });294 });
...@@ -292,7 +296,7 @@ describe("DesktopConnectionCatalogStore", () => {...@@ -292,7 +296,7 @@ describe("DesktopConnectionCatalogStore", () => {
292 _tag: "PermissionDenied",296 _tag: "PermissionDenied",
293 module: "FileSystem",297 module: "FileSystem",
294 method: "makeDirectory",298 method: "makeDirectory",
295 pathOrDescriptor: `${baseDir}/userdata`,299 pathOrDescriptor: path.join(baseDir, "userdata"),
296 });300 });
297 const fileSystemLayer = Layer.succeed(301 const fileSystemLayer = Layer.succeed(
298 FileSystem.FileSystem,302 FileSystem.FileSystem,
...@@ -310,11 +314,11 @@ describe("DesktopConnectionCatalogStore", () => {...@@ -310,11 +314,11 @@ describe("DesktopConnectionCatalogStore", () => {
310 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreWriteError,314 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreWriteError,
311 );315 );
312 assert.equal(error.operation, "create-directory");316 assert.equal(error.operation, "create-directory");
313 assert.equal(error.path, `${baseDir}/userdata`);317 assert.equal(error.path, path.join(baseDir, "userdata"));
314 assert.strictEqual(error.cause, permissionError);318 assert.strictEqual(error.cause, permissionError);
315 assert.equal(319 assert.equal(
316 error.message,320 error.message,
317 `Desktop connection catalog write failed during create-directory at ${baseDir}/userdata.`,321 `Desktop connection catalog write failed during create-directory at ${path.join(baseDir, "userdata")}.`,
318 );322 );
319 assert.notEqual(error.message, permissionError.message);323 assert.notEqual(error.message, permissionError.message);
320 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),324 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
...@@ -323,6 +327,7 @@ describe("DesktopConnectionCatalogStore", () => {...@@ -323,6 +327,7 @@ describe("DesktopConnectionCatalogStore", () => {
323 it.effect("reports the legacy migration stage", () =>327 it.effect("reports the legacy migration stage", () =>
324 withStore(328 withStore(
325 Effect.gen(function* () {329 Effect.gen(function* () {
330 const path = yield* Path.Path;
326 const environment = yield* DesktopEnvironment.DesktopEnvironment;331 const environment = yield* DesktopEnvironment.DesktopEnvironment;
327 const fileSystem = yield* FileSystem.FileSystem;332 const fileSystem = yield* FileSystem.FileSystem;
328 const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore;333 const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore;
...@@ -335,7 +340,7 @@ describe("DesktopConnectionCatalogStore", () => {...@@ -335,7 +340,7 @@ describe("DesktopConnectionCatalogStore", () => {
335 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreMigrationError,340 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreMigrationError,
336 );341 );
337 assert.equal(error.operation, "read-legacy-registry");342 assert.equal(error.operation, "read-legacy-registry");
338 assert.equal(error.catalogPath, `${environment.stateDir}/connection-catalog.json`);343 assert.equal(error.catalogPath, path.join(environment.stateDir, "connection-catalog.json"));
339 assert.instanceOf(344 assert.instanceOf(
340 error.cause,345 error.cause,
341 DesktopSavedEnvironments.DesktopSavedEnvironmentsDocumentDecodeError,346 DesktopSavedEnvironments.DesktopSavedEnvironmentsDocumentDecodeError,
...@@ -345,7 +350,7 @@ describe("DesktopConnectionCatalogStore", () => {...@@ -345,7 +350,7 @@ describe("DesktopConnectionCatalogStore", () => {
345 assert.exists(registryError.cause);350 assert.exists(registryError.cause);
346 assert.equal(351 assert.equal(
347 error.message,352 error.message,
348 `Legacy desktop saved-environment migration failed during read-legacy-registry into ${environment.stateDir}/connection-catalog.json.`,353 `Legacy desktop saved-environment migration failed during read-legacy-registry into ${path.join(environment.stateDir, "connection-catalog.json")}.`,
349 );354 );
350 assert.notEqual(error.message, registryError.message);355 assert.notEqual(error.message, registryError.message);
351 }),356 }),
...@@ -355,10 +360,11 @@ describe("DesktopConnectionCatalogStore", () => {...@@ -355,10 +360,11 @@ describe("DesktopConnectionCatalogStore", () => {
355 it.effect("reports invalid encrypted catalog data without exposing it", () =>360 it.effect("reports invalid encrypted catalog data without exposing it", () =>
356 withStore(361 withStore(
357 Effect.gen(function* () {362 Effect.gen(function* () {
363 const path = yield* Path.Path;
358 const environment = yield* DesktopEnvironment.DesktopEnvironment;364 const environment = yield* DesktopEnvironment.DesktopEnvironment;
359 const fileSystem = yield* FileSystem.FileSystem;365 const fileSystem = yield* FileSystem.FileSystem;
360 const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore;366 const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore;
361 const catalogPath = `${environment.stateDir}/connection-catalog.json`;367 const catalogPath = path.join(environment.stateDir, "connection-catalog.json");
362 yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true });368 yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true });
363 yield* fileSystem.writeFileString(catalogPath, '{"version":1,"encryptedCatalog":"%%%"}\n');369 yield* fileSystem.writeFileString(catalogPath, '{"version":1,"encryptedCatalog":"%%%"}\n');
364370
...@@ -381,6 +387,7 @@ describe("DesktopConnectionCatalogStore", () => {...@@ -381,6 +387,7 @@ describe("DesktopConnectionCatalogStore", () => {
381387
382 it.effect("surfaces a catalog that can no longer be decrypted without deleting it", () =>388 it.effect("surfaces a catalog that can no longer be decrypted without deleting it", () =>
383 Effect.gen(function* () {389 Effect.gen(function* () {
390 const path = yield* Path.Path;
384 const fileSystem = yield* FileSystem.FileSystem;391 const fileSystem = yield* FileSystem.FileSystem;
385 const baseDir = yield* fileSystem.makeTempDirectoryScoped({392 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
386 prefix: "t3-desktop-connection-catalog-test-",393 prefix: "t3-desktop-connection-catalog-test-",
...@@ -399,14 +406,14 @@ describe("DesktopConnectionCatalogStore", () => {...@@ -399,14 +406,14 @@ describe("DesktopConnectionCatalogStore", () => {
399 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreProtectionError,406 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreProtectionError,
400 );407 );
401 assert.equal(error.operation, "decrypt-catalog");408 assert.equal(error.operation, "decrypt-catalog");
402 assert.equal(error.catalogPath, `${baseDir}/userdata/connection-catalog.json`);409 assert.equal(error.catalogPath, path.join(baseDir, "userdata", "connection-catalog.json"));
403 assert.instanceOf(error.cause, ElectronSafeStorage.ElectronSafeStorageDecryptError);410 assert.instanceOf(error.cause, ElectronSafeStorage.ElectronSafeStorageDecryptError);
404 const decryptError = error.cause as ElectronSafeStorage.ElectronSafeStorageDecryptError;411 const decryptError = error.cause as ElectronSafeStorage.ElectronSafeStorageDecryptError;
405 assert.instanceOf(decryptError.cause, Error);412 assert.instanceOf(decryptError.cause, Error);
406 assert.equal(decryptError.cause.message, "invalid encrypted catalog");413 assert.equal(decryptError.cause.message, "invalid encrypted catalog");
407 assert.equal(414 assert.equal(
408 error.message,415 error.message,
409 `Desktop connection catalog protection failed during decrypt-catalog at ${baseDir}/userdata/connection-catalog.json.`,416 `Desktop connection catalog protection failed during decrypt-catalog at ${path.join(baseDir, "userdata", "connection-catalog.json")}.`,
410 );417 );
411 assert.notEqual(error.message, decryptError.message);418 assert.notEqual(error.message, decryptError.message);
412 yield* Ref.set(failDecrypt, false);419 yield* Ref.set(failDecrypt, false);
apps/desktop/src/preview/BrowserImport/Sources.test.ts+92-66
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1// @effect-diagnostics nodeBuiltinImport:off - Builds a Chromium-shaped cookie1// @effect-diagnostics nodeBuiltinImport:off - Builds a Chromium-shaped cookie
2// table with the same native bindings the source reads.2// table with the same native bindings the source reads.
3import * as NodePath from "node:path";
3import * as NodeServices from "@effect/platform-node/NodeServices";4import * as NodeServices from "@effect/platform-node/NodeServices";
4import { assert, describe, it } from "@effect/vitest";5import { assert, describe, it } from "@effect/vitest";
5import {6import {
...@@ -531,7 +532,7 @@ Path=Profiles/wxyz.empty...@@ -531,7 +532,7 @@ Path=Profiles/wxyz.empty
531 yield* fileSystem.makeDirectory(`${root}/Profiles/wxyz.empty`, { recursive: true });532 yield* fileSystem.makeDirectory(`${root}/Profiles/wxyz.empty`, { recursive: true });
532533
533 assert.deepEqual(yield* listSourceProfiles(firefox, context), [534 assert.deepEqual(yield* listSourceProfiles(firefox, context), [
534 { directory: "Profiles/abcd.default-release", name: "original" },535 { directory: context.path.join("Profiles", "abcd.default-release"), name: "original" },
535 ]);536 ]);
536 }),537 }),
537 ),538 ),
...@@ -612,10 +613,16 @@ describe("cookieDatabaseCandidatePaths", () => {...@@ -612,10 +613,16 @@ describe("cookieDatabaseCandidatePaths", () => {
612 run(613 run(
613 Effect.gen(function* () {614 Effect.gen(function* () {
614 const context = yield* withSourceHome();615 const context = yield* withSourceHome();
615 const profile = `${context.home}/Library/Application Support/net.imput.helium/Profile 1`;616 const profile = context.path.join(
617 context.home,
618 "Library",
619 "Application Support",
620 "net.imput.helium",
621 "Profile 1",
622 );
616 assert.deepEqual(cookieDatabaseCandidatePaths(helium, context, "Profile 1"), [623 assert.deepEqual(cookieDatabaseCandidatePaths(helium, context, "Profile 1"), [
617 `${profile}/Network/Cookies`,624 context.path.join(profile, "Network", "Cookies"),
618 `${profile}/Cookies`,625 context.path.join(profile, "Cookies"),
619 ]);626 ]);
620 }),627 }),
621 ),628 ),
...@@ -626,7 +633,7 @@ describe("cookieDatabaseCandidatePaths", () => {...@@ -626,7 +633,7 @@ describe("cookieDatabaseCandidatePaths", () => {
626 Effect.gen(function* () {633 Effect.gen(function* () {
627 const fileSystem = yield* FileSystem.FileSystem;634 const fileSystem = yield* FileSystem.FileSystem;
628 const context = yield* withSourceHome();635 const context = yield* withSourceHome();
629 const root = helium.userDataDirectory(context);636 const root = userDataDirectory(context);
630 // Chromium 96+ keeps sessions in Network/; a root Cookies left behind637 // Chromium 96+ keeps sessions in Network/; a root Cookies left behind
631 // by the move is stale and must not be the one imported.638 // by the move is stale and must not be the one imported.
632 yield* fileSystem.makeDirectory(`${root}/Default/Network`, { recursive: true });639 yield* fileSystem.makeDirectory(`${root}/Default/Network`, { recursive: true });
...@@ -635,7 +642,7 @@ describe("cookieDatabaseCandidatePaths", () => {...@@ -635,7 +642,7 @@ describe("cookieDatabaseCandidatePaths", () => {
635642
636 assert.equal(643 assert.equal(
637 yield* resolveCookieDatabase(helium, context, "Default"),644 yield* resolveCookieDatabase(helium, context, "Default"),
638 `${root}/Default/Network/Cookies`,645 context.path.join(root, "Default", "Network", "Cookies"),
639 );646 );
640 // A fresh install with only the Network/ jar is installed, not hidden.647 // A fresh install with only the Network/ jar is installed, not hidden.
641 yield* fileSystem.remove(`${root}/Default/Cookies`);648 yield* fileSystem.remove(`${root}/Default/Cookies`);
...@@ -680,8 +687,8 @@ describe("Firefox Snap profiles", () => {...@@ -680,8 +687,8 @@ describe("Firefox Snap profiles", () => {
680 Effect.provideService(HostProcessEnvironment, { HOME: home }),687 Effect.provideService(HostProcessEnvironment, { HOME: home }),
681 Effect.provideService(HostProcessPlatform, "linux"),688 Effect.provideService(HostProcessPlatform, "linux"),
682 );689 );
683 const root = `${home}/snap/firefox/common/.mozilla/firefox`;690 const root = context.path.join(home, "snap", "firefox", "common", ".mozilla", "firefox");
684 const directory = `${root}/abcd.default`;691 const directory = context.path.join(root, "abcd.default");
685 yield* fileSystem.makeDirectory(directory, { recursive: true });692 yield* fileSystem.makeDirectory(directory, { recursive: true });
686 yield* writeFirefoxCookieDatabase(`${directory}/cookies.sqlite`, 2, 1);693 yield* writeFirefoxCookieDatabase(`${directory}/cookies.sqlite`, 2, 1);
687 yield* fileSystem.writeFileString(694 yield* fileSystem.writeFileString(
...@@ -695,7 +702,7 @@ describe("Firefox Snap profiles", () => {...@@ -695,7 +702,7 @@ describe("Firefox Snap profiles", () => {
695 ]);702 ]);
696 assert.equal(703 assert.equal(
697 yield* resolveCookieDatabase(firefox, context, directory),704 yield* resolveCookieDatabase(firefox, context, directory),
698 `${directory}/cookies.sqlite`,705 context.path.join(directory, "cookies.sqlite"),
699 );706 );
700 assert.isFalse(yield* isSourceRunning(firefox, context));707 assert.isFalse(yield* isSourceRunning(firefox, context));
701 yield* fileSystem.symlink("foreign-host:+4242", `${directory}/lock`);708 yield* fileSystem.symlink("foreign-host:+4242", `${directory}/lock`);
...@@ -720,8 +727,8 @@ describe("Firefox Snap profiles", () => {...@@ -720,8 +727,8 @@ describe("Firefox Snap profiles", () => {
720 Effect.provideService(HostProcessEnvironment, { HOME: home }),727 Effect.provideService(HostProcessEnvironment, { HOME: home }),
721 Effect.provideService(HostProcessPlatform, "linux"),728 Effect.provideService(HostProcessPlatform, "linux"),
722 );729 );
723 const native = `${home}/.mozilla/firefox`;730 const native = context.path.join(home, ".mozilla", "firefox");
724 const snap = `${home}/snap/firefox/common/.mozilla/firefox`;731 const snap = context.path.join(home, "snap", "firefox", "common", ".mozilla", "firefox");
725 for (const root of [native, snap]) {732 for (const root of [native, snap]) {
726 yield* fileSystem.makeDirectory(`${root}/abcd.default`, { recursive: true });733 yield* fileSystem.makeDirectory(`${root}/abcd.default`, { recursive: true });
727 yield* writeFirefoxCookieDatabase(`${root}/abcd.default/cookies.sqlite`, 1, 0);734 yield* writeFirefoxCookieDatabase(`${root}/abcd.default/cookies.sqlite`, 1, 0);
...@@ -735,14 +742,14 @@ describe("Firefox Snap profiles", () => {...@@ -735,14 +742,14 @@ describe("Firefox Snap profiles", () => {
735 const profiles = yield* listSourceProfiles(firefox, context);742 const profiles = yield* listSourceProfiles(firefox, context);
736 assert.deepEqual(743 assert.deepEqual(
737 profiles.map((profile) => profile.directory),744 profiles.map((profile) => profile.directory),
738 ["abcd.default", `${snap}/abcd.default`],745 ["abcd.default", context.path.join(snap, "abcd.default")],
739 );746 );
740 const databases = yield* Effect.forEach(profiles, (profile) =>747 const databases = yield* Effect.forEach(profiles, (profile) =>
741 resolveCookieDatabase(firefox, context, profile.directory),748 resolveCookieDatabase(firefox, context, profile.directory),
742 );749 );
743 assert.deepEqual(databases, [750 assert.deepEqual(databases, [
744 `${native}/abcd.default/cookies.sqlite`,751 context.path.join(native, "abcd.default", "cookies.sqlite"),
745 `${snap}/abcd.default/cookies.sqlite`,752 context.path.join(snap, "abcd.default", "cookies.sqlite"),
746 ]);753 ]);
747 }),754 }),
748 ),755 ),
...@@ -752,8 +759,8 @@ describe("Firefox Snap profiles", () => {...@@ -752,8 +759,8 @@ describe("Firefox Snap profiles", () => {
752describe("listSourceProfiles Firefox fallback", () => {759describe("listSourceProfiles Firefox fallback", () => {
753 const cases = [760 const cases = [
754 { platform: "linux" as const, profileDirectory: "linux.default" },761 { platform: "linux" as const, profileDirectory: "linux.default" },
755 { platform: "darwin" as const, profileDirectory: "Profiles/macos.default" },762 { platform: "darwin" as const, profileDirectory: NodePath.join("Profiles", "macos.default") },
756 { platform: "win32" as const, profileDirectory: "Profiles/windows.default" },763 { platform: "win32" as const, profileDirectory: NodePath.join("Profiles", "windows.default") },
757 ];764 ];
758765
759 for (const { platform, profileDirectory } of cases) {766 for (const { platform, profileDirectory } of cases) {
...@@ -824,7 +831,11 @@ describe("listSourceProfiles Firefox fallback", () => {...@@ -824,7 +831,11 @@ describe("listSourceProfiles Firefox fallback", () => {
824831
825 // Returning the empty declared list would hide the browser entirely.832 // Returning the empty declared list would hide the browser entirely.
826 assert.deepEqual(yield* listSourceProfiles(firefox, context), [833 assert.deepEqual(yield* listSourceProfiles(firefox, context), [
827 { directory: "Profiles/real.default", name: "real.default", cookieCount: 3 },834 {
835 directory: path.join("Profiles", "real.default"),
836 name: "real.default",
837 cookieCount: 3,
838 },
828 ]);839 ]);
829 assert.isTrue(yield* isSourceInstalled(firefox, context));840 assert.isTrue(yield* isSourceInstalled(firefox, context));
830 }),841 }),
...@@ -855,7 +866,11 @@ describe("listSourceProfiles Firefox fallback", () => {...@@ -855,7 +866,11 @@ describe("listSourceProfiles Firefox fallback", () => {
855 );866 );
856867
857 assert.deepEqual(yield* listSourceProfiles(firefox, context), [868 assert.deepEqual(yield* listSourceProfiles(firefox, context), [
858 { directory: "Profiles/declared.default", name: "Declared", cookieCount: 2 },869 {
870 directory: path.join("Profiles", "declared.default"),
871 name: "Declared",
872 cookieCount: 2,
873 },
859 ]);874 ]);
860875
861 yield* fileSystem.remove(path.join(root, "profiles.ini"));876 yield* fileSystem.remove(path.join(root, "profiles.ini"));
...@@ -864,8 +879,16 @@ describe("listSourceProfiles Firefox fallback", () => {...@@ -864,8 +879,16 @@ describe("listSourceProfiles Firefox fallback", () => {
864 yield* writeFirefoxCookieDatabase(path.join(fallbackDirectory, "cookies.sqlite"), 1, 4);879 yield* writeFirefoxCookieDatabase(path.join(fallbackDirectory, "cookies.sqlite"), 1, 4);
865880
866 assert.deepEqual(yield* listSourceProfiles(firefox, context), [881 assert.deepEqual(yield* listSourceProfiles(firefox, context), [
867 { directory: "Profiles/declared.default", name: "declared.default", cookieCount: 2 },882 {
868 { directory: "Profiles/fallback.default", name: "fallback.default", cookieCount: 1 },883 directory: path.join("Profiles", "declared.default"),
884 name: "declared.default",
885 cookieCount: 2,
886 },
887 {
888 directory: path.join("Profiles", "fallback.default"),
889 name: "fallback.default",
890 cookieCount: 1,
891 },
869 ]);892 ]);
870 }),893 }),
871 ),894 ),
...@@ -928,53 +951,56 @@ describe("isSourceRunning for Firefox", () => {...@@ -928,53 +951,56 @@ describe("isSourceRunning for Firefox", () => {
928 ),951 ),
929 );952 );
930953
931 it.effect("detects a live fcntl lock on .parentlock, as macOS Firefox leaves it", () =>954 // Holds the lock with python3's fcntl, which does not exist on Windows.
932 run(955 it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32")(
933 Effect.gen(function* () {956 "detects a live fcntl lock on .parentlock, as macOS Firefox leaves it",
934 const fileSystem = yield* FileSystem.FileSystem;957 () =>
935 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;958 run(
936 const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-firefox-" });959 Effect.gen(function* () {
937 const context = yield* sourcePathContext.pipe(960 const fileSystem = yield* FileSystem.FileSystem;
938 Effect.provideService(HostProcessEnvironment, { HOME: home }),961 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
939 Effect.provideService(HostProcessPlatform, "darwin"),962 const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-firefox-" });
940 );963 const context = yield* sourcePathContext.pipe(
941 const root = firefox.userDataDirectory(context)!;964 Effect.provideService(HostProcessEnvironment, { HOME: home }),
942 const profile = `${root}/Profiles/abcd.default-release`;965 Effect.provideService(HostProcessPlatform, "darwin"),
943 yield* fileSystem.makeDirectory(profile, { recursive: true });966 );
944 yield* fileSystem.writeFileString(`${profile}/cookies.sqlite`, "db");967 const root = firefox.userDataDirectory(context)!;
945 const parentLock = `${profile}/.parentlock`;968 const profile = `${root}/Profiles/abcd.default-release`;
946 yield* fileSystem.writeFileString(parentLock, "");969 yield* fileSystem.makeDirectory(profile, { recursive: true });
947970 yield* fileSystem.writeFileString(`${profile}/cookies.sqlite`, "db");
948 // Hold the lock from a child the way Firefox does (F_SETLK, write),971 const parentLock = `${profile}/.parentlock`;
949 // and keep it until the scope closes.972 yield* fileSystem.writeFileString(parentLock, "");
950 const holder = yield* spawner.spawn(973
951 ChildProcess.make(974 // Hold the lock from a child the way Firefox does (F_SETLK, write),
952 "python3",975 // and keep it until the scope closes.
953 [976 const holder = yield* spawner.spawn(
954 "-c",977 ChildProcess.make(
955 "import fcntl,os,sys,time\n" +978 "python3",
956 "fd=os.open(sys.argv[1],os.O_WRONLY)\n" +979 [
957 "fcntl.lockf(fd,fcntl.LOCK_EX|fcntl.LOCK_NB)\n" +980 "-c",
958 "print('locked',flush=True)\n" +981 "import fcntl,os,sys,time\n" +
959 "time.sleep(30)",982 "fd=os.open(sys.argv[1],os.O_WRONLY)\n" +
960 parentLock,983 "fcntl.lockf(fd,fcntl.LOCK_EX|fcntl.LOCK_NB)\n" +
961 ],984 "print('locked',flush=True)\n" +
962 { stdin: "ignore" },985 "time.sleep(30)",
963 ),986 parentLock,
964 );987 ],
965 // Wait for the child to confirm it holds the lock before probing.988 { stdin: "ignore" },
966 yield* holder.stdout.pipe(989 ),
967 Stream.decodeText(),990 );
968 Stream.splitLines,991 // Wait for the child to confirm it holds the lock before probing.
969 Stream.filter((line) => line.trim() === "locked"),992 yield* holder.stdout.pipe(
970 Stream.take(1),993 Stream.decodeText(),
971 Stream.runDrain,994 Stream.splitLines,
972 );995 Stream.filter((line) => line.trim() === "locked"),
996 Stream.take(1),
997 Stream.runDrain,
998 );
973999
974 assert.isTrue(yield* isSourceRunning(firefox, context));1000 assert.isTrue(yield* isSourceRunning(firefox, context));
975 yield* holder.kill();1001 yield* holder.kill();
976 }),1002 }),
977 ),1003 ),
978 );1004 );
9791005
980 it.effect("reads a Firefox lock symlink's pid to tell live from crashed", () =>1006 it.effect("reads a Firefox lock symlink's pid to tell live from crashed", () =>
apps/desktop/src/settings/DesktopSavedEnvironments.test.ts+7-4
...@@ -3,6 +3,7 @@ import { assert, describe, it } from "@effect/vitest";...@@ -3,6 +3,7 @@ import { assert, describe, it } from "@effect/vitest";
3import { EnvironmentId, type PersistedSavedEnvironmentRecord } from "@t3tools/contracts";3import { EnvironmentId, type PersistedSavedEnvironmentRecord } from "@t3tools/contracts";
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";5import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";
6import * as Layer from "effect/Layer";7import * as Layer from "effect/Layer";
7import * as Option from "effect/Option";8import * as Option from "effect/Option";
8import * as PlatformError from "effect/PlatformError";9import * as PlatformError from "effect/PlatformError";
...@@ -400,11 +401,12 @@ describe("DesktopSavedEnvironments", () => {...@@ -400,11 +401,12 @@ describe("DesktopSavedEnvironments", () => {
400401
401 it.effect("reports saved environment filesystem reads separately from document decoding", () =>402 it.effect("reports saved environment filesystem reads separately from document decoding", () =>
402 Effect.gen(function* () {403 Effect.gen(function* () {
404 const path = yield* Path.Path;
403 const baseFileSystem = yield* FileSystem.FileSystem;405 const baseFileSystem = yield* FileSystem.FileSystem;
404 const baseDir = yield* baseFileSystem.makeTempDirectoryScoped({406 const baseDir = yield* baseFileSystem.makeTempDirectoryScoped({
405 prefix: "t3-desktop-saved-environments-test-",407 prefix: "t3-desktop-saved-environments-test-",
406 });408 });
407 const registryPath = `${baseDir}/userdata/saved-environments.json`;409 const registryPath = path.join(baseDir, "userdata", "saved-environments.json");
408 const permissionError = PlatformError.systemError({410 const permissionError = PlatformError.systemError({
409 _tag: "PermissionDenied",411 _tag: "PermissionDenied",
410 module: "FileSystem",412 module: "FileSystem",
...@@ -433,6 +435,7 @@ describe("DesktopSavedEnvironments", () => {...@@ -433,6 +435,7 @@ describe("DesktopSavedEnvironments", () => {
433 it.effect("reports the failed saved environment write operation and path", () =>435 it.effect("reports the failed saved environment write operation and path", () =>
434 Effect.gen(function* () {436 Effect.gen(function* () {
435 const baseFileSystem = yield* FileSystem.FileSystem;437 const baseFileSystem = yield* FileSystem.FileSystem;
438 const path = yield* Path.Path;
436 const baseDir = yield* baseFileSystem.makeTempDirectoryScoped({439 const baseDir = yield* baseFileSystem.makeTempDirectoryScoped({
437 prefix: "t3-desktop-saved-environments-test-",440 prefix: "t3-desktop-saved-environments-test-",
438 });441 });
...@@ -440,7 +443,7 @@ describe("DesktopSavedEnvironments", () => {...@@ -440,7 +443,7 @@ describe("DesktopSavedEnvironments", () => {
440 _tag: "PermissionDenied",443 _tag: "PermissionDenied",
441 module: "FileSystem",444 module: "FileSystem",
442 method: "makeDirectory",445 method: "makeDirectory",
443 pathOrDescriptor: `${baseDir}/userdata`,446 pathOrDescriptor: path.join(baseDir, "userdata"),
444 });447 });
445 const fileSystemLayer = Layer.succeed(448 const fileSystemLayer = Layer.succeed(
446 FileSystem.FileSystem,449 FileSystem.FileSystem,
...@@ -456,11 +459,11 @@ describe("DesktopSavedEnvironments", () => {...@@ -456,11 +459,11 @@ describe("DesktopSavedEnvironments", () => {
456 const error = yield* savedEnvironments.setRegistry([savedRegistryRecord]).pipe(Effect.flip);459 const error = yield* savedEnvironments.setRegistry([savedRegistryRecord]).pipe(Effect.flip);
457 assert.instanceOf(error, DesktopSavedEnvironments.DesktopSavedEnvironmentsWriteError);460 assert.instanceOf(error, DesktopSavedEnvironments.DesktopSavedEnvironmentsWriteError);
458 assert.equal(error.operation, "create-directory");461 assert.equal(error.operation, "create-directory");
459 assert.equal(error.path, `${baseDir}/userdata`);462 assert.equal(error.path, path.join(baseDir, "userdata"));
460 assert.strictEqual(error.cause, permissionError);463 assert.strictEqual(error.cause, permissionError);
461 assert.equal(464 assert.equal(
462 error.message,465 error.message,
463 `Desktop saved-environment write failed during create-directory at ${baseDir}/userdata.`,466 `Desktop saved-environment write failed during create-directory at ${path.join(baseDir, "userdata")}.`,
464 );467 );
465 assert.notEqual(error.message, permissionError.message);468 assert.notEqual(error.message, permissionError.message);
466 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),469 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
apps/desktop/vite.config.ts+5
...@@ -83,4 +83,9 @@ export default defineConfig({...@@ -83,4 +83,9 @@ export default defineConfig({
83 entry: ["src/preview-pip-preload.ts"],83 entry: ["src/preview-pip-preload.ts"],
84 },84 },
85 ],85 ],
86 test: {
87 // The Windows lane runs workspace suites concurrently; filesystem-heavy
88 // desktop integration tests can exceed Vitest's 5 second default there.
89 testTimeout: 15_000,
90 },
86});91});