import * as NodePath from "@effect/platform-node/NodePath"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as PlatformError from "effect/PlatformError"; import type * as Electron from "electron"; import * as ElectronApp from "../electron/ElectronApp.ts"; import * as DesktopAppIdentity from "./DesktopAppIdentity.ts"; import * as DesktopAssets from "./DesktopAssets.ts"; import * as DesktopConfig from "./DesktopConfig.ts"; import * as DesktopEnvironment from "./DesktopEnvironment.ts"; const defaultEnvironmentInput = { dirname: "/repo/apps/desktop/dist-electron", homeDirectory: "/Users/alice", platform: "darwin", processArch: "arm64", appVersion: "1.2.3", appPath: "/Applications/T3 Code.app/Contents/Resources/app.asar", isPackaged: true, resourcesPath: "/Applications/T3 Code.app/Contents/Resources", runningUnderArm64Translation: false, } satisfies DesktopEnvironment.MakeDesktopEnvironmentInput; type TestEnvironmentInput = Partial & { readonly env?: Record; }; interface ElectronAppCalls { readonly setAboutPanelOptions: Array; readonly setDockIcon: string[]; readonly setName: string[]; } const makeElectronAppLayer = (calls: ElectronAppCalls) => Layer.succeed(ElectronApp.ElectronApp, { metadata: Effect.die("unexpected metadata read"), name: Effect.succeed("T3 Code"), systemLocale: Effect.succeed("en-US"), whenReady: Effect.void, quit: Effect.void, exit: () => Effect.void, relaunch: () => Effect.void, setPath: () => Effect.void, setName: (name) => Effect.sync(() => { calls.setName.push(name); }), setAboutPanelOptions: (options) => Effect.sync(() => { calls.setAboutPanelOptions.push(options); }), setAppUserModelId: () => Effect.void, getAppMetrics: Effect.succeed([]), isDefaultProtocolClient: () => Effect.succeed(false), setAsDefaultProtocolClient: () => Effect.succeed(true), setDesktopName: () => Effect.void, setDockIcon: (iconPath) => Effect.sync(() => { calls.setDockIcon.push(iconPath); }), appendCommandLineSwitch: () => Effect.void, onBeforeQuitForUpdate: () => Effect.void, removeCommandLineSwitch: () => Effect.void, on: () => Effect.void, } satisfies ElectronApp.ElectronApp["Service"]); const makeAssetsLayer = (png: Option.Option) => Layer.succeed(DesktopAssets.DesktopAssets, { iconPaths: Effect.succeed({ ico: Option.none(), icns: Option.none(), png, }), resolveResourcePath: () => Effect.succeed(Option.none()), } satisfies DesktopAssets.DesktopAssets["Service"]); const makeEnvironmentLayer = (overrides: TestEnvironmentInput = {}) => { const { env, ...environmentOverrides } = overrides; return DesktopEnvironment.layer({ ...defaultEnvironmentInput, ...environmentOverrides, }).pipe( Layer.provide( Layer.mergeAll( NodeServices.layer, NodePath.layerPosix, DesktopConfig.layerTest({ ...env, }), ), ), ); }; const withIdentity = ( effect: Effect.Effect< A, E, | R | DesktopAppIdentity.DesktopAppIdentity | DesktopEnvironment.DesktopEnvironment | FileSystem.FileSystem >, input: { readonly calls?: ElectronAppCalls; readonly environment?: TestEnvironmentInput; readonly legacyPathExists?: boolean; readonly legacyPathProbeError?: PlatformError.PlatformError; readonly packageJson?: string; readonly pngIconPath?: Option.Option; } = {}, ) => { const calls: ElectronAppCalls = input.calls ?? { setAboutPanelOptions: [], setDockIcon: [], setName: [], }; return effect.pipe( Effect.provide( DesktopAppIdentity.layer.pipe( Layer.provideMerge( FileSystem.layerNoop({ exists: (path) => input.legacyPathProbeError ? Effect.fail(input.legacyPathProbeError) : Effect.succeed( input.legacyPathExists === true && path.includes("T3 Code (Alpha)"), ), readFileString: () => Effect.succeed(input.packageJson ?? '{"t3codeCommitHash":"abcdef1234567890"}'), }), ), Layer.provideMerge(makeAssetsLayer(input.pngIconPath ?? Option.none())), Layer.provideMerge(makeElectronAppLayer(calls)), Layer.provideMerge(makeEnvironmentLayer(input.environment)), ), ), ); }; describe("DesktopAppIdentity", () => { it.effect("keeps using the legacy userData path when it already exists", () => withIdentity( Effect.gen(function* () { const identity = yield* DesktopAppIdentity.DesktopAppIdentity; const userDataPath = yield* identity.resolveUserDataPath; assert.equal(userDataPath, "/Users/alice/Library/Application Support/T3 Code (Alpha)"); }), { legacyPathExists: true }, ), ); it.effect("preserves failures while inspecting the legacy userData path", () => { const legacyPath = "/Users/alice/Library/Application Support/T3 Code (Alpha)"; const cause = PlatformError.systemError({ _tag: "PermissionDenied", module: "FileSystem", method: "exists", description: "permission denied", pathOrDescriptor: legacyPath, }); return withIdentity( Effect.gen(function* () { const identity = yield* DesktopAppIdentity.DesktopAppIdentity; const error = yield* identity.resolveUserDataPath.pipe(Effect.flip); assert.instanceOf(error, DesktopAppIdentity.DesktopUserDataPathResolutionError); assert.equal(error.legacyPath, legacyPath); assert.strictEqual(error.cause, cause); assert.equal( error.message, `Failed to inspect legacy desktop user-data path at "${legacyPath}".`, ); }), { legacyPathProbeError: cause }, ); }); it.effect("configures app identity from the environment commit override", () => { const calls: ElectronAppCalls = { setAboutPanelOptions: [], setDockIcon: [], setName: [], }; return withIdentity( Effect.gen(function* () { const identity = yield* DesktopAppIdentity.DesktopAppIdentity; yield* identity.configure; assert.deepEqual(calls.setName, ["T3 Code (Alpha)"]); assert.equal(calls.setAboutPanelOptions[0]?.applicationName, "T3 Code (Alpha)"); assert.equal(calls.setAboutPanelOptions[0]?.applicationVersion, "1.2.3"); assert.equal(calls.setAboutPanelOptions[0]?.version, "0123456789ab"); // Packaged: the bundle's own icon stands, so a custom one the user // attached survives. assert.deepEqual(calls.setDockIcon, []); }), { calls, environment: { env: { T3CODE_COMMIT_HASH: "0123456789abcdef", }, }, pngIconPath: Option.some("/icon.png"), }, ); }); it.effect("sets the dock icon only when running unpackaged", () => { const calls: ElectronAppCalls = { setAboutPanelOptions: [], setDockIcon: [], setName: [], }; return withIdentity( Effect.gen(function* () { const identity = yield* DesktopAppIdentity.DesktopAppIdentity; yield* identity.configure; // Electron shows a generic icon for an unpackaged run, which is the // reason this call exists at all. assert.deepEqual(calls.setDockIcon, ["/icon.png"]); }), { calls, environment: { isPackaged: false }, pngIconPath: Option.some("/icon.png"), }, ); }); });