| author | |
| committer | |
| log | c251e41b5e5287b786cda44ddc263b8e6a74859c |
| tree | 85d55508f2b295ffff54bb950ed57df42fe3768f |
| parent | 5c6c1d67d82bb81bddaee6d83e54e8aecd96ec7d |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 165 | } |
| 166 | 166 | } |
| 167 | 167 | |
| 168 | // Bundle-internal paths are macOS paths whatever host builds them. | |
| 168 | 169 | export function resolveMacLauncherIconPaths(runtimeDir, development = isDevelopment) { |
| 169 | 170 | return { |
| 170 | 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 | } |
| 174 | 178 | |
| ... | ... | @@ -280,12 +284,12 @@ function readJson(path) { |
| 280 | 284 | } |
| 281 | 285 | |
| 282 | 286 | export 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 | 288 | const launcherExecutableName = `${displayName} Launcher`; |
| 285 | 289 | return { |
| 286 | 290 | launcherExecutableName, |
| 287 | launcherBinaryPath: NodePath.join(executableDir, launcherExecutableName), | |
| 288 | runtimeElectronBinaryPath: NodePath.join(executableDir, "Electron"), | |
| 291 | launcherBinaryPath: NodePath.posix.join(executableDir, launcherExecutableName), | |
| 292 | runtimeElectronBinaryPath: NodePath.posix.join(executableDir, "Electron"), | |
| 289 | 293 | }; |
| 290 | 294 | } |
| 291 | 295 |
apps/desktop/scripts/electron-launcher.test.mjs+3-2| ... | ... | @@ -84,9 +84,10 @@ describe("electron development launcher", () => { |
| 84 | 84 | const development = resolveMacLauncherIconPaths("/runtime", true); |
| 85 | 85 | const production = resolveMacLauncherIconPaths("/runtime", false); |
| 86 | 86 | |
| 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 | 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 | 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 | 4 | import { EnvironmentId, type PersistedSavedEnvironmentRecord } from "@t3tools/contracts"; |
| 5 | 5 | import * as Effect from "effect/Effect"; |
| 6 | 6 | import * as FileSystem from "effect/FileSystem"; |
| 7 | import * as Path from "effect/Path"; | |
| 7 | 8 | import * as Layer from "effect/Layer"; |
| 8 | 9 | import * as Option from "effect/Option"; |
| 9 | 10 | import * as PlatformError from "effect/PlatformError"; |
| ... | ... | @@ -226,10 +227,11 @@ describe("DesktopConnectionCatalogStore", () => { |
| 226 | 227 | it.effect("surfaces malformed catalog documents without deleting them", () => |
| 227 | 228 | withStore( |
| 228 | 229 | Effect.gen(function* () { |
| 230 | const path = yield* Path.Path; | |
| 229 | 231 | const environment = yield* DesktopEnvironment.DesktopEnvironment; |
| 230 | 232 | const fileSystem = yield* FileSystem.FileSystem; |
| 231 | 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 | 235 | yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true }); |
| 234 | 236 | yield* fileSystem.writeFileString(catalogPath, "{not-json"); |
| 235 | 237 | |
| ... | ... | @@ -247,6 +249,7 @@ describe("DesktopConnectionCatalogStore", () => { |
| 247 | 249 | |
| 248 | 250 | it.effect("surfaces catalog filesystem failures instead of treating them as missing", () => |
| 249 | 251 | Effect.gen(function* () { |
| 252 | const path = yield* Path.Path; | |
| 250 | 253 | const baseFileSystem = yield* FileSystem.FileSystem; |
| 251 | 254 | const baseDir = yield* baseFileSystem.makeTempDirectoryScoped({ |
| 252 | 255 | prefix: "t3-desktop-connection-catalog-test-", |
| ... | ... | @@ -255,7 +258,7 @@ describe("DesktopConnectionCatalogStore", () => { |
| 255 | 258 | _tag: "PermissionDenied", |
| 256 | 259 | module: "FileSystem", |
| 257 | 260 | method: "readFileString", |
| 258 | pathOrDescriptor: `${baseDir}/userdata/connection-catalog.json`, | |
| 261 | pathOrDescriptor: path.join(baseDir, "userdata", "connection-catalog.json"), | |
| 259 | 262 | }); |
| 260 | 263 | const fileSystemLayer = Layer.succeed( |
| 261 | 264 | FileSystem.FileSystem, |
| ... | ... | @@ -272,11 +275,11 @@ describe("DesktopConnectionCatalogStore", () => { |
| 272 | 275 | error, |
| 273 | 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 | 279 | assert.strictEqual(error.cause, permissionError); |
| 277 | 280 | assert.equal( |
| 278 | 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 | 284 | assert.notEqual(error.message, permissionError.message); |
| 282 | 285 | }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), |
| ... | ... | @@ -285,6 +288,7 @@ describe("DesktopConnectionCatalogStore", () => { |
| 285 | 288 | it.effect("reports the failed catalog write operation and path", () => |
| 286 | 289 | Effect.gen(function* () { |
| 287 | 290 | const baseFileSystem = yield* FileSystem.FileSystem; |
| 291 | const path = yield* Path.Path; | |
| 288 | 292 | const baseDir = yield* baseFileSystem.makeTempDirectoryScoped({ |
| 289 | 293 | prefix: "t3-desktop-connection-catalog-test-", |
| 290 | 294 | }); |
| ... | ... | @@ -292,7 +296,7 @@ describe("DesktopConnectionCatalogStore", () => { |
| 292 | 296 | _tag: "PermissionDenied", |
| 293 | 297 | module: "FileSystem", |
| 294 | 298 | method: "makeDirectory", |
| 295 | pathOrDescriptor: `${baseDir}/userdata`, | |
| 299 | pathOrDescriptor: path.join(baseDir, "userdata"), | |
| 296 | 300 | }); |
| 297 | 301 | const fileSystemLayer = Layer.succeed( |
| 298 | 302 | FileSystem.FileSystem, |
| ... | ... | @@ -310,11 +314,11 @@ describe("DesktopConnectionCatalogStore", () => { |
| 310 | 314 | DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreWriteError, |
| 311 | 315 | ); |
| 312 | 316 | assert.equal(error.operation, "create-directory"); |
| 313 | assert.equal(error.path, `${baseDir}/userdata`); | |
| 317 | assert.equal(error.path, path.join(baseDir, "userdata")); | |
| 314 | 318 | assert.strictEqual(error.cause, permissionError); |
| 315 | 319 | assert.equal( |
| 316 | 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 | 323 | assert.notEqual(error.message, permissionError.message); |
| 320 | 324 | }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), |
| ... | ... | @@ -323,6 +327,7 @@ describe("DesktopConnectionCatalogStore", () => { |
| 323 | 327 | it.effect("reports the legacy migration stage", () => |
| 324 | 328 | withStore( |
| 325 | 329 | Effect.gen(function* () { |
| 330 | const path = yield* Path.Path; | |
| 326 | 331 | const environment = yield* DesktopEnvironment.DesktopEnvironment; |
| 327 | 332 | const fileSystem = yield* FileSystem.FileSystem; |
| 328 | 333 | const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore; |
| ... | ... | @@ -335,7 +340,7 @@ describe("DesktopConnectionCatalogStore", () => { |
| 335 | 340 | DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreMigrationError, |
| 336 | 341 | ); |
| 337 | 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 | 344 | assert.instanceOf( |
| 340 | 345 | error.cause, |
| 341 | 346 | DesktopSavedEnvironments.DesktopSavedEnvironmentsDocumentDecodeError, |
| ... | ... | @@ -345,7 +350,7 @@ describe("DesktopConnectionCatalogStore", () => { |
| 345 | 350 | assert.exists(registryError.cause); |
| 346 | 351 | assert.equal( |
| 347 | 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 | 355 | assert.notEqual(error.message, registryError.message); |
| 351 | 356 | }), |
| ... | ... | @@ -355,10 +360,11 @@ describe("DesktopConnectionCatalogStore", () => { |
| 355 | 360 | it.effect("reports invalid encrypted catalog data without exposing it", () => |
| 356 | 361 | withStore( |
| 357 | 362 | Effect.gen(function* () { |
| 363 | const path = yield* Path.Path; | |
| 358 | 364 | const environment = yield* DesktopEnvironment.DesktopEnvironment; |
| 359 | 365 | const fileSystem = yield* FileSystem.FileSystem; |
| 360 | 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 | 368 | yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true }); |
| 363 | 369 | yield* fileSystem.writeFileString(catalogPath, '{"version":1,"encryptedCatalog":"%%%"}\n'); |
| 364 | 370 | |
| ... | ... | @@ -381,6 +387,7 @@ describe("DesktopConnectionCatalogStore", () => { |
| 381 | 387 | |
| 382 | 388 | it.effect("surfaces a catalog that can no longer be decrypted without deleting it", () => |
| 383 | 389 | Effect.gen(function* () { |
| 390 | const path = yield* Path.Path; | |
| 384 | 391 | const fileSystem = yield* FileSystem.FileSystem; |
| 385 | 392 | const baseDir = yield* fileSystem.makeTempDirectoryScoped({ |
| 386 | 393 | prefix: "t3-desktop-connection-catalog-test-", |
| ... | ... | @@ -399,14 +406,14 @@ describe("DesktopConnectionCatalogStore", () => { |
| 399 | 406 | DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreProtectionError, |
| 400 | 407 | ); |
| 401 | 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 | 410 | assert.instanceOf(error.cause, ElectronSafeStorage.ElectronSafeStorageDecryptError); |
| 404 | 411 | const decryptError = error.cause as ElectronSafeStorage.ElectronSafeStorageDecryptError; |
| 405 | 412 | assert.instanceOf(decryptError.cause, Error); |
| 406 | 413 | assert.equal(decryptError.cause.message, "invalid encrypted catalog"); |
| 407 | 414 | assert.equal( |
| 408 | 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 | 418 | assert.notEqual(error.message, decryptError.message); |
| 412 | 419 | yield* Ref.set(failDecrypt, false); |
apps/desktop/src/preview/BrowserImport/Sources.test.ts+92-66| ... | ... | @@ -1,5 +1,6 @@ |
| 1 | 1 | // @effect-diagnostics nodeBuiltinImport:off - Builds a Chromium-shaped cookie |
| 2 | 2 | // table with the same native bindings the source reads. |
| 3 | import * as NodePath from "node:path"; | |
| 3 | 4 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 4 | 5 | import { assert, describe, it } from "@effect/vitest"; |
| 5 | 6 | import { |
| ... | ... | @@ -531,7 +532,7 @@ Path=Profiles/wxyz.empty |
| 531 | 532 | yield* fileSystem.makeDirectory(`${root}/Profiles/wxyz.empty`, { recursive: true }); |
| 532 | 533 | |
| 533 | 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 | 613 | run( |
| 613 | 614 | Effect.gen(function* () { |
| 614 | 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 | 623 | assert.deepEqual(cookieDatabaseCandidatePaths(helium, context, "Profile 1"), [ |
| 617 | `${profile}/Network/Cookies`, | |
| 618 | `${profile}/Cookies`, | |
| 624 | context.path.join(profile, "Network", "Cookies"), | |
| 625 | context.path.join(profile, "Cookies"), | |
| 619 | 626 | ]); |
| 620 | 627 | }), |
| 621 | 628 | ), |
| ... | ... | @@ -626,7 +633,7 @@ describe("cookieDatabaseCandidatePaths", () => { |
| 626 | 633 | Effect.gen(function* () { |
| 627 | 634 | const fileSystem = yield* FileSystem.FileSystem; |
| 628 | 635 | const context = yield* withSourceHome(); |
| 629 | const root = helium.userDataDirectory(context); | |
| 636 | const root = userDataDirectory(context); | |
| 630 | 637 | // Chromium 96+ keeps sessions in Network/; a root Cookies left behind |
| 631 | 638 | // by the move is stale and must not be the one imported. |
| 632 | 639 | yield* fileSystem.makeDirectory(`${root}/Default/Network`, { recursive: true }); |
| ... | ... | @@ -635,7 +642,7 @@ describe("cookieDatabaseCandidatePaths", () => { |
| 635 | 642 | |
| 636 | 643 | assert.equal( |
| 637 | 644 | yield* resolveCookieDatabase(helium, context, "Default"), |
| 638 | `${root}/Default/Network/Cookies`, | |
| 645 | context.path.join(root, "Default", "Network", "Cookies"), | |
| 639 | 646 | ); |
| 640 | 647 | // A fresh install with only the Network/ jar is installed, not hidden. |
| 641 | 648 | yield* fileSystem.remove(`${root}/Default/Cookies`); |
| ... | ... | @@ -680,8 +687,8 @@ describe("Firefox Snap profiles", () => { |
| 680 | 687 | Effect.provideService(HostProcessEnvironment, { HOME: home }), |
| 681 | 688 | Effect.provideService(HostProcessPlatform, "linux"), |
| 682 | 689 | ); |
| 683 | const root = `${home}/snap/firefox/common/.mozilla/firefox`; | |
| 684 | const directory = `${root}/abcd.default`; | |
| 690 | const root = context.path.join(home, "snap", "firefox", "common", ".mozilla", "firefox"); | |
| 691 | const directory = context.path.join(root, "abcd.default"); | |
| 685 | 692 | yield* fileSystem.makeDirectory(directory, { recursive: true }); |
| 686 | 693 | yield* writeFirefoxCookieDatabase(`${directory}/cookies.sqlite`, 2, 1); |
| 687 | 694 | yield* fileSystem.writeFileString( |
| ... | ... | @@ -695,7 +702,7 @@ describe("Firefox Snap profiles", () => { |
| 695 | 702 | ]); |
| 696 | 703 | assert.equal( |
| 697 | 704 | yield* resolveCookieDatabase(firefox, context, directory), |
| 698 | `${directory}/cookies.sqlite`, | |
| 705 | context.path.join(directory, "cookies.sqlite"), | |
| 699 | 706 | ); |
| 700 | 707 | assert.isFalse(yield* isSourceRunning(firefox, context)); |
| 701 | 708 | yield* fileSystem.symlink("foreign-host:+4242", `${directory}/lock`); |
| ... | ... | @@ -720,8 +727,8 @@ describe("Firefox Snap profiles", () => { |
| 720 | 727 | Effect.provideService(HostProcessEnvironment, { HOME: home }), |
| 721 | 728 | Effect.provideService(HostProcessPlatform, "linux"), |
| 722 | 729 | ); |
| 723 | const native = `${home}/.mozilla/firefox`; | |
| 724 | const snap = `${home}/snap/firefox/common/.mozilla/firefox`; | |
| 730 | const native = context.path.join(home, ".mozilla", "firefox"); | |
| 731 | const snap = context.path.join(home, "snap", "firefox", "common", ".mozilla", "firefox"); | |
| 725 | 732 | for (const root of [native, snap]) { |
| 726 | 733 | yield* fileSystem.makeDirectory(`${root}/abcd.default`, { recursive: true }); |
| 727 | 734 | yield* writeFirefoxCookieDatabase(`${root}/abcd.default/cookies.sqlite`, 1, 0); |
| ... | ... | @@ -735,14 +742,14 @@ describe("Firefox Snap profiles", () => { |
| 735 | 742 | const profiles = yield* listSourceProfiles(firefox, context); |
| 736 | 743 | assert.deepEqual( |
| 737 | 744 | profiles.map((profile) => profile.directory), |
| 738 | ["abcd.default", `${snap}/abcd.default`], | |
| 745 | ["abcd.default", context.path.join(snap, "abcd.default")], | |
| 739 | 746 | ); |
| 740 | 747 | const databases = yield* Effect.forEach(profiles, (profile) => |
| 741 | 748 | resolveCookieDatabase(firefox, context, profile.directory), |
| 742 | 749 | ); |
| 743 | 750 | assert.deepEqual(databases, [ |
| 744 | `${native}/abcd.default/cookies.sqlite`, | |
| 745 | `${snap}/abcd.default/cookies.sqlite`, | |
| 751 | context.path.join(native, "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 | 759 | describe("listSourceProfiles Firefox fallback", () => { |
| 753 | 760 | const cases = [ |
| 754 | 761 | { platform: "linux" as const, profileDirectory: "linux.default" }, |
| 755 | { platform: "darwin" as const, profileDirectory: "Profiles/macos.default" }, | |
| 756 | { platform: "win32" as const, profileDirectory: "Profiles/windows.default" }, | |
| 762 | { platform: "darwin" as const, profileDirectory: NodePath.join("Profiles", "macos.default") }, | |
| 763 | { platform: "win32" as const, profileDirectory: NodePath.join("Profiles", "windows.default") }, | |
| 757 | 764 | ]; |
| 758 | 765 | |
| 759 | 766 | for (const { platform, profileDirectory } of cases) { |
| ... | ... | @@ -824,7 +831,11 @@ describe("listSourceProfiles Firefox fallback", () => { |
| 824 | 831 | |
| 825 | 832 | // Returning the empty declared list would hide the browser entirely. |
| 826 | 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 | 840 | assert.isTrue(yield* isSourceInstalled(firefox, context)); |
| 830 | 841 | }), |
| ... | ... | @@ -855,7 +866,11 @@ describe("listSourceProfiles Firefox fallback", () => { |
| 855 | 866 | ); |
| 856 | 867 | |
| 857 | 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 | ]); |
| 860 | 875 | |
| 861 | 876 | yield* fileSystem.remove(path.join(root, "profiles.ini")); |
| ... | ... | @@ -864,8 +879,16 @@ describe("listSourceProfiles Firefox fallback", () => { |
| 864 | 879 | yield* writeFirefoxCookieDatabase(path.join(fallbackDirectory, "cookies.sqlite"), 1, 4); |
| 865 | 880 | |
| 866 | 881 | assert.deepEqual(yield* listSourceProfiles(firefox, context), [ |
| 867 | { directory: "Profiles/declared.default", name: "declared.default", cookieCount: 2 }, | |
| 868 | { directory: "Profiles/fallback.default", name: "fallback.default", cookieCount: 1 }, | |
| 882 | { | |
| 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 | 951 | ), |
| 929 | 952 | ); |
| 930 | 953 | |
| 931 | it.effect("detects a live fcntl lock on .parentlock, as macOS Firefox leaves it", () => | |
| 932 | run( | |
| 933 | Effect.gen(function* () { | |
| 934 | const fileSystem = yield* FileSystem.FileSystem; | |
| 935 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; | |
| 936 | const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-firefox-" }); | |
| 937 | const context = yield* sourcePathContext.pipe( | |
| 938 | Effect.provideService(HostProcessEnvironment, { HOME: home }), | |
| 939 | Effect.provideService(HostProcessPlatform, "darwin"), | |
| 940 | ); | |
| 941 | const root = firefox.userDataDirectory(context)!; | |
| 942 | const profile = `${root}/Profiles/abcd.default-release`; | |
| 943 | yield* fileSystem.makeDirectory(profile, { recursive: true }); | |
| 944 | yield* fileSystem.writeFileString(`${profile}/cookies.sqlite`, "db"); | |
| 945 | const parentLock = `${profile}/.parentlock`; | |
| 946 | yield* fileSystem.writeFileString(parentLock, ""); | |
| 947 | ||
| 948 | // Hold the lock from a child the way Firefox does (F_SETLK, write), | |
| 949 | // and keep it until the scope closes. | |
| 950 | const holder = yield* spawner.spawn( | |
| 951 | ChildProcess.make( | |
| 952 | "python3", | |
| 953 | [ | |
| 954 | "-c", | |
| 955 | "import fcntl,os,sys,time\n" + | |
| 956 | "fd=os.open(sys.argv[1],os.O_WRONLY)\n" + | |
| 957 | "fcntl.lockf(fd,fcntl.LOCK_EX|fcntl.LOCK_NB)\n" + | |
| 958 | "print('locked',flush=True)\n" + | |
| 959 | "time.sleep(30)", | |
| 960 | parentLock, | |
| 961 | ], | |
| 962 | { stdin: "ignore" }, | |
| 963 | ), | |
| 964 | ); | |
| 965 | // Wait for the child to confirm it holds the lock before probing. | |
| 966 | yield* holder.stdout.pipe( | |
| 967 | Stream.decodeText(), | |
| 968 | Stream.splitLines, | |
| 969 | Stream.filter((line) => line.trim() === "locked"), | |
| 970 | Stream.take(1), | |
| 971 | Stream.runDrain, | |
| 972 | ); | |
| 954 | // Holds the lock with python3's fcntl, which does not exist on Windows. | |
| 955 | it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32")( | |
| 956 | "detects a live fcntl lock on .parentlock, as macOS Firefox leaves it", | |
| 957 | () => | |
| 958 | run( | |
| 959 | Effect.gen(function* () { | |
| 960 | const fileSystem = yield* FileSystem.FileSystem; | |
| 961 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; | |
| 962 | const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-firefox-" }); | |
| 963 | const context = yield* sourcePathContext.pipe( | |
| 964 | Effect.provideService(HostProcessEnvironment, { HOME: home }), | |
| 965 | Effect.provideService(HostProcessPlatform, "darwin"), | |
| 966 | ); | |
| 967 | const root = firefox.userDataDirectory(context)!; | |
| 968 | const profile = `${root}/Profiles/abcd.default-release`; | |
| 969 | yield* fileSystem.makeDirectory(profile, { recursive: true }); | |
| 970 | yield* fileSystem.writeFileString(`${profile}/cookies.sqlite`, "db"); | |
| 971 | const parentLock = `${profile}/.parentlock`; | |
| 972 | yield* fileSystem.writeFileString(parentLock, ""); | |
| 973 | ||
| 974 | // Hold the lock from a child the way Firefox does (F_SETLK, write), | |
| 975 | // and keep it until the scope closes. | |
| 976 | const holder = yield* spawner.spawn( | |
| 977 | ChildProcess.make( | |
| 978 | "python3", | |
| 979 | [ | |
| 980 | "-c", | |
| 981 | "import fcntl,os,sys,time\n" + | |
| 982 | "fd=os.open(sys.argv[1],os.O_WRONLY)\n" + | |
| 983 | "fcntl.lockf(fd,fcntl.LOCK_EX|fcntl.LOCK_NB)\n" + | |
| 984 | "print('locked',flush=True)\n" + | |
| 985 | "time.sleep(30)", | |
| 986 | parentLock, | |
| 987 | ], | |
| 988 | { stdin: "ignore" }, | |
| 989 | ), | |
| 990 | ); | |
| 991 | // Wait for the child to confirm it holds the lock before probing. | |
| 992 | yield* holder.stdout.pipe( | |
| 993 | Stream.decodeText(), | |
| 994 | Stream.splitLines, | |
| 995 | Stream.filter((line) => line.trim() === "locked"), | |
| 996 | Stream.take(1), | |
| 997 | Stream.runDrain, | |
| 998 | ); | |
| 973 | 999 | |
| 974 | assert.isTrue(yield* isSourceRunning(firefox, context)); | |
| 975 | yield* holder.kill(); | |
| 976 | }), | |
| 977 | ), | |
| 1000 | assert.isTrue(yield* isSourceRunning(firefox, context)); | |
| 1001 | yield* holder.kill(); | |
| 1002 | }), | |
| 1003 | ), | |
| 978 | 1004 | ); |
| 979 | 1005 | |
| 980 | 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 | 3 | import { EnvironmentId, type PersistedSavedEnvironmentRecord } from "@t3tools/contracts"; |
| 4 | 4 | import * as Effect from "effect/Effect"; |
| 5 | 5 | import * as FileSystem from "effect/FileSystem"; |
| 6 | import * as Path from "effect/Path"; | |
| 6 | 7 | import * as Layer from "effect/Layer"; |
| 7 | 8 | import * as Option from "effect/Option"; |
| 8 | 9 | import * as PlatformError from "effect/PlatformError"; |
| ... | ... | @@ -400,11 +401,12 @@ describe("DesktopSavedEnvironments", () => { |
| 400 | 401 | |
| 401 | 402 | it.effect("reports saved environment filesystem reads separately from document decoding", () => |
| 402 | 403 | Effect.gen(function* () { |
| 404 | const path = yield* Path.Path; | |
| 403 | 405 | const baseFileSystem = yield* FileSystem.FileSystem; |
| 404 | 406 | const baseDir = yield* baseFileSystem.makeTempDirectoryScoped({ |
| 405 | 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 | 410 | const permissionError = PlatformError.systemError({ |
| 409 | 411 | _tag: "PermissionDenied", |
| 410 | 412 | module: "FileSystem", |
| ... | ... | @@ -433,6 +435,7 @@ describe("DesktopSavedEnvironments", () => { |
| 433 | 435 | it.effect("reports the failed saved environment write operation and path", () => |
| 434 | 436 | Effect.gen(function* () { |
| 435 | 437 | const baseFileSystem = yield* FileSystem.FileSystem; |
| 438 | const path = yield* Path.Path; | |
| 436 | 439 | const baseDir = yield* baseFileSystem.makeTempDirectoryScoped({ |
| 437 | 440 | prefix: "t3-desktop-saved-environments-test-", |
| 438 | 441 | }); |
| ... | ... | @@ -440,7 +443,7 @@ describe("DesktopSavedEnvironments", () => { |
| 440 | 443 | _tag: "PermissionDenied", |
| 441 | 444 | module: "FileSystem", |
| 442 | 445 | method: "makeDirectory", |
| 443 | pathOrDescriptor: `${baseDir}/userdata`, | |
| 446 | pathOrDescriptor: path.join(baseDir, "userdata"), | |
| 444 | 447 | }); |
| 445 | 448 | const fileSystemLayer = Layer.succeed( |
| 446 | 449 | FileSystem.FileSystem, |
| ... | ... | @@ -456,11 +459,11 @@ describe("DesktopSavedEnvironments", () => { |
| 456 | 459 | const error = yield* savedEnvironments.setRegistry([savedRegistryRecord]).pipe(Effect.flip); |
| 457 | 460 | assert.instanceOf(error, DesktopSavedEnvironments.DesktopSavedEnvironmentsWriteError); |
| 458 | 461 | assert.equal(error.operation, "create-directory"); |
| 459 | assert.equal(error.path, `${baseDir}/userdata`); | |
| 462 | assert.equal(error.path, path.join(baseDir, "userdata")); | |
| 460 | 463 | assert.strictEqual(error.cause, permissionError); |
| 461 | 464 | assert.equal( |
| 462 | 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 | 468 | assert.notEqual(error.message, permissionError.message); |
| 466 | 469 | }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), |
apps/desktop/vite.config.ts+5| ... | ... | @@ -83,4 +83,9 @@ export default defineConfig({ |
| 83 | 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 | }); |