import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; import { ClientSettingsSchema, type ClientSettings } from "@t3tools/contracts"; 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 * as Schema from "effect/Schema"; import * as DesktopConfig from "../app/DesktopConfig.ts"; import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; import * as DesktopClientSettings from "./DesktopClientSettings.ts"; const clientSettings: ClientSettings = { appearanceContrast: 100, browserDefaultViewport: { _tag: "preset", width: 1024, height: 600, presetId: "nest-hub" }, browserDefaultZoomFactor: 1.25, browserDefaultAppearance: "dark", browserRecordingFrameRate: 60, browserLinkTarget: "app", browserAutoShowFloatingPreview: false, browserProfiles: [{ id: "work", name: "Work", kind: "persistent" }], browserDefaultProfileId: "work", confirmQuit: "double-click", confirmThreadArchive: true, confirmThreadDelete: false, confirmThreadUnpin: false, contextWindowMeterEnabled: false, composerCollapseOnBlur: false, composerCollapseOnScroll: true, dismissedProviderUpdateNotificationKeys: [], diffIgnoreWhitespace: true, diffLayout: "stacked", environmentIdentificationMode: "artwork", favorites: [], fontFamilyCode: "", fontFamilyComposer: "", fontFamilySans: "", fontFamilyTerminal: "", fontSizeCode: 13, fontSizeInterface: 16, fontSizePrompt: 14, fontSizeTerminal: 12, fontSmoothing: true, glassOpacity: 80, panelAnimationDurationMs: 0, planModeEnabled: false, proactivePanelsEnabled: true, showSkillsInSlashMenu: false, persistComposerContextStrip: true, providerModelPreferences: {}, sidebarProjectGroupingMode: "repository_path", sidebarProjectGroupingOverrides: { "environment-1:/tmp/project-a": "separate", }, sidebarProjectSortOrder: "manual", sidebarThreadSortOrder: "created_at", sidebarThreadPreviewCount: 6, legacySidebarEnabled: false, timestampFormat: "24-hour", wordWrap: true, }; const decodeClientSettingsJson = Schema.decodeEffect(Schema.fromJsonString(ClientSettingsSchema)); const decodeRecordJson = Schema.decodeEffect( Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown)), ); function makeLayer(baseDir: string) { const environmentLayer = DesktopEnvironment.layer({ dirname: "/repo/apps/desktop/src", homeDirectory: baseDir, platform: "darwin", processArch: "x64", appVersion: "1.2.3", appPath: "/repo", isPackaged: true, resourcesPath: "/missing/resources", runningUnderArm64Translation: false, }).pipe( Layer.provide( Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({ T3CODE_HOME: baseDir })), ), ); return DesktopClientSettings.layer.pipe( Layer.provideMerge(environmentLayer), Layer.provideMerge(NodeServices.layer), ); } const withClientSettings = ( effect: Effect.Effect, ) => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-desktop-client-settings-test-", }); return yield* effect.pipe(Effect.provide(makeLayer(baseDir))); }).pipe(Effect.provide(NodeServices.layer), Effect.scoped); describe("DesktopClientSettings", () => { it.effect("returns none when no client settings file exists", () => withClientSettings( Effect.gen(function* () { const settings = yield* DesktopClientSettings.DesktopClientSettings; assert.isTrue(Option.isNone(yield* settings.get)); }), ), ); it.effect("persists and reloads client settings", () => withClientSettings( Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; const fileSystem = yield* FileSystem.FileSystem; const settings = yield* DesktopClientSettings.DesktopClientSettings; yield* settings.set(clientSettings); assert.deepEqual(yield* settings.get, Option.some(clientSettings)); assert.deepEqual( yield* decodeClientSettingsJson( yield* fileSystem.readFileString(environment.clientSettingsPath), ), clientSettings, ); assert.isFalse( Object.hasOwn( yield* decodeRecordJson( yield* fileSystem.readFileString(environment.clientSettingsPath), ), "settings", ), ); }), ), ); it.effect("reports the failed client settings write operation and path", () => withClientSettings( Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; const fileSystem = yield* FileSystem.FileSystem; const settings = yield* DesktopClientSettings.DesktopClientSettings; yield* fileSystem.makeDirectory(environment.clientSettingsPath, { recursive: true }); const error = yield* settings.set(clientSettings).pipe(Effect.flip); assert.instanceOf(error, DesktopClientSettings.DesktopClientSettingsWriteError); assert.equal(error.operation, "replace-settings-file"); assert.equal(error.path, environment.clientSettingsPath); assert.instanceOf(error.cause, PlatformError.PlatformError); assert.isString(error.cause.stack); assert.equal( error.message, `Desktop client settings write failed during replace-settings-file at ${environment.clientSettingsPath}.`, ); assert.notInclude(error.message, error.cause.message); }), ), ); it.effect("loads lenient direct client settings documents", () => withClientSettings( Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; const fileSystem = yield* FileSystem.FileSystem; const settings = yield* DesktopClientSettings.DesktopClientSettings; yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true }); yield* fileSystem.writeFileString( environment.clientSettingsPath, `{ // Matches server settings parsing. "timestampFormat": "24-hour", }\n`, ); const persisted = yield* settings.get; assert.isTrue(Option.isSome(persisted)); if (Option.isSome(persisted)) { assert.equal(persisted.value.timestampFormat, "24-hour"); } }), ), ); it.effect("loads legacy wrapped client settings documents", () => withClientSettings( Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; const fileSystem = yield* FileSystem.FileSystem; const settings = yield* DesktopClientSettings.DesktopClientSettings; yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true }); yield* fileSystem.writeFileString( environment.clientSettingsPath, `{ "settings": { "timestampFormat": "12-hour" } }\n`, ); const persisted = yield* settings.get; assert.isTrue(Option.isSome(persisted)); if (Option.isSome(persisted)) { assert.equal(persisted.value.timestampFormat, "12-hour"); } }), ), ); it.effect("loads defaults from empty client settings documents", () => withClientSettings( Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; const fileSystem = yield* FileSystem.FileSystem; const settings = yield* DesktopClientSettings.DesktopClientSettings; yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true }); yield* fileSystem.writeFileString(environment.clientSettingsPath, "{}\n"); assert.deepEqual(yield* settings.get, Option.some(yield* decodeClientSettingsJson("{}"))); }), ), ); it.effect("treats malformed client settings documents as absent", () => withClientSettings( Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; const fileSystem = yield* FileSystem.FileSystem; const settings = yield* DesktopClientSettings.DesktopClientSettings; yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true }); yield* fileSystem.writeFileString(environment.clientSettingsPath, "{not-json"); assert.isTrue(Option.isNone(yield* settings.get)); }), ), ); });