| 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,10 +165,14 @@ function registerMacLauncherBundle(appBundlePath) { |
| 165 | } | 165 | } |
| 166 | } | 166 | } |
| 167 | 167 | ||
| 168 | // Bundle-internal paths are macOS paths whatever host builds them. | ||
| 168 | export function resolveMacLauncherIconPaths(runtimeDir, development = isDevelopment) { | 169 | export 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 | } |
| 174 | 178 | ||
| ... | @@ -280,12 +284,12 @@ function readJson(path) { | ... | @@ -280,12 +284,12 @@ function readJson(path) { |
| 280 | } | 284 | } |
| 281 | 285 | ||
| 282 | export function resolveMacLauncherPaths(appBundlePath, displayName = APP_DISPLAY_NAME) { | 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 | 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 | } |
| 291 | 295 |
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); |
| 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 | 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"; |
| 4 | import { EnvironmentId, type PersistedSavedEnvironmentRecord } from "@t3tools/contracts"; | 4 | import { EnvironmentId, type PersistedSavedEnvironmentRecord } from "@t3tools/contracts"; |
| 5 | import * as Effect from "effect/Effect"; | 5 | import * as Effect from "effect/Effect"; |
| 6 | import * as FileSystem from "effect/FileSystem"; | 6 | import * as FileSystem from "effect/FileSystem"; |
| 7 | import * as Path from "effect/Path"; | ||
| 7 | import * as Layer from "effect/Layer"; | 8 | import * as Layer from "effect/Layer"; |
| 8 | import * as Option from "effect/Option"; | 9 | import * as Option from "effect/Option"; |
| 9 | import * as PlatformError from "effect/PlatformError"; | 10 | import * 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"); |
| 235 | 237 | ||
| ... | @@ -247,6 +249,7 @@ describe("DesktopConnectionCatalogStore", () => { | ... | @@ -247,6 +249,7 @@ describe("DesktopConnectionCatalogStore", () => { |
| 247 | 249 | ||
| 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'); |
| 364 | 370 | ||
| ... | @@ -381,6 +387,7 @@ describe("DesktopConnectionCatalogStore", () => { | ... | @@ -381,6 +387,7 @@ describe("DesktopConnectionCatalogStore", () => { |
| 381 | 387 | ||
| 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 cookie | 1 | // @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. |
| 3 | import * as NodePath from "node:path"; | ||
| 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 4 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 4 | import { assert, describe, it } from "@effect/vitest"; | 5 | import { assert, describe, it } from "@effect/vitest"; |
| 5 | import { | 6 | import { |
| ... | @@ -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 }); |
| 532 | 533 | ||
| 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 behind | 637 | // 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", () => { |
| 635 | 642 | ||
| 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", () => { |
| 752 | describe("listSourceProfiles Firefox fallback", () => { | 759 | describe("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 | ]; |
| 758 | 765 | ||
| 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", () => { |
| 824 | 831 | ||
| 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 | ); |
| 856 | 867 | ||
| 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 | ]); |
| 860 | 875 | ||
| 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); |
| 865 | 880 | ||
| 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 | ); |
| 930 | 953 | ||
| 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 }); |
| 947 | 970 | 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 | ); | ||
| 973 | 999 | ||
| 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 | ); |
| 979 | 1005 | ||
| 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"; |
| 3 | import { EnvironmentId, type PersistedSavedEnvironmentRecord } from "@t3tools/contracts"; | 3 | import { EnvironmentId, type PersistedSavedEnvironmentRecord } from "@t3tools/contracts"; |
| 4 | import * as Effect from "effect/Effect"; | 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as FileSystem from "effect/FileSystem"; | 5 | import * as FileSystem from "effect/FileSystem"; |
| 6 | import * as Path from "effect/Path"; | ||
| 6 | import * as Layer from "effect/Layer"; | 7 | import * as Layer from "effect/Layer"; |
| 7 | import * as Option from "effect/Option"; | 8 | import * as Option from "effect/Option"; |
| 8 | import * as PlatformError from "effect/PlatformError"; | 9 | import * as PlatformError from "effect/PlatformError"; |
| ... | @@ -400,11 +401,12 @@ describe("DesktopSavedEnvironments", () => { | ... | @@ -400,11 +401,12 @@ describe("DesktopSavedEnvironments", () => { |
| 400 | 401 | ||
| 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 | }); |