import * as NodeServices from "@effect/platform-node/NodeServices";import { assert, describe, it } from "@effect/vitest";import { ClientSettingsSchema, DEFAULT_CLIENT_SETTINGS, 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 = { ...DEFAULT_CLIENT_SETTINGS, 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, 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, onboardingCompletedAt: null, panelAnimationDurationMs: 0, planModeEnabled: false, proactivePanelsEnabled: true, showSkillsInSlashMenu: false, providerModelPreferences: {}, sidebarProjectGroupingMode: "repository_path", sidebarProjectGroupingOverrides: { "environment-1:/tmp/project-a": "separate", }, sidebarProjectSortOrder: "manual", sidebarThreadSortOrder: "created_at", sidebarThreadPreviewCount: 6, legacySidebarEnabled: false, loadBalancingEnabled: false, loadBalancingWeights: { "environment-1": 75, "environment-2": 0 }, pullRequestMergeMethodOverrides: {}, 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 = <A, E, R>( effect: Effect.Effect<A, E, R | DesktopClientSettings.DesktopClientSettings>,) => 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", ), ); }), ), ); for (const failure of [ { label: "permission", reason: "PermissionDenied" }, { label: "I/O", reason: "Unknown" }, ] as const) { it.effect(`preserves saved preferences across ${failure.label} read failures and retries`, () => withClientSettings( Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; const fileSystem = yield* FileSystem.FileSystem; const settings = yield* DesktopClientSettings.DesktopClientSettings; const savedSettings = { ...clientSettings, onboardingCompletedAt: "2026-09-05T12:00:00.000Z", }; yield* settings.set(savedSettings); const savedContents = yield* fileSystem.readFileString(environment.clientSettingsPath); const cause = PlatformError.systemError({ _tag: failure.reason, module: "FileSystem", method: "readFileString", pathOrDescriptor: environment.clientSettingsPath, }); let failRead = true; const retryableSettings = yield* DesktopClientSettings.make.pipe( Effect.provideService( FileSystem.FileSystem, FileSystem.FileSystem.of({ ...fileSystem, readFileString: (path) => Effect.suspend(() => failRead ? Effect.fail(cause) : fileSystem.readFileString(path), ), }), ), ); const error = yield* retryableSettings.get.pipe(Effect.flip); assert.instanceOf(error, DesktopClientSettings.DesktopClientSettingsReadError); assert.equal(error.operation, "read-file"); assert.equal(error.path, environment.clientSettingsPath); assert.strictEqual(error.cause, cause); assert.equal( yield* fileSystem.readFileString(environment.clientSettingsPath), savedContents, ); failRead = false; assert.deepEqual(yield* retryableSettings.get, Option.some(savedSettings)); }), ), ); } 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("{}"))); }), ), ); for (const document of [ { label: "malformed JSON", contents: "{not-json" }, { label: "invalid direct settings", contents: '{"fontSizeCode":"large"}' }, { label: "invalid legacy settings", contents: '{"settings":{"fontSizeCode":"large"}}' }, ]) { it.effect(`reports ${document.label} without treating the settings file 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, document.contents); const error = yield* settings.get.pipe(Effect.flip); assert.instanceOf(error, DesktopClientSettings.DesktopClientSettingsReadError); assert.equal(error.operation, "decode-document"); assert.equal(error.path, environment.clientSettingsPath); assert.instanceOf(error.cause, Schema.SchemaError); assert.equal( yield* fileSystem.readFileString(environment.clientSettingsPath), document.contents, ); }), ), ); }});
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, describe, it } from "@effect/vitest";
import {
ClientSettingsSchema,
DEFAULT_CLIENT_SETTINGS,
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 = {
...DEFAULT_CLIENT_SETTINGS,
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,
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,
onboardingCompletedAt: null,
panelAnimationDurationMs: 0,
planModeEnabled: false,
proactivePanelsEnabled: true,
showSkillsInSlashMenu: false,
providerModelPreferences: {},
sidebarProjectGroupingMode: "repository_path",
sidebarProjectGroupingOverrides: {
"environment-1:/tmp/project-a": "separate",
},
sidebarProjectSortOrder: "manual",
sidebarThreadSortOrder: "created_at",
sidebarThreadPreviewCount: 6,
legacySidebarEnabled: false,
loadBalancingEnabled: false,
loadBalancingWeights: { "environment-1": 75, "environment-2": 0 },
pullRequestMergeMethodOverrides: {},
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 = <A, E, R>(
effect: Effect.Effect<A, E, R | DesktopClientSettings.DesktopClientSettings>,
) =>
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",
),
);
}),
),
);
for (const failure of [
{ label: "permission", reason: "PermissionDenied" },
{ label: "I/O", reason: "Unknown" },
] as const) {
it.effect(`preserves saved preferences across ${failure.label} read failures and retries`, () =>
withClientSettings(
Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const fileSystem = yield* FileSystem.FileSystem;
const settings = yield* DesktopClientSettings.DesktopClientSettings;
const savedSettings = {
...clientSettings,
onboardingCompletedAt: "2026-09-05T12:00:00.000Z",
};
yield* settings.set(savedSettings);
const savedContents = yield* fileSystem.readFileString(environment.clientSettingsPath);
const cause = PlatformError.systemError({
_tag: failure.reason,
module: "FileSystem",
method: "readFileString",
pathOrDescriptor: environment.clientSettingsPath,
});
let failRead = true;
const retryableSettings = yield* DesktopClientSettings.make.pipe(
Effect.provideService(
FileSystem.FileSystem,
FileSystem.FileSystem.of({
...fileSystem,
readFileString: (path) =>
Effect.suspend(() =>
failRead ? Effect.fail(cause) : fileSystem.readFileString(path),
),
}),
),
);
const error = yield* retryableSettings.get.pipe(Effect.flip);
assert.instanceOf(error, DesktopClientSettings.DesktopClientSettingsReadError);
assert.equal(error.operation, "read-file");
assert.equal(error.path, environment.clientSettingsPath);
assert.strictEqual(error.cause, cause);
assert.equal(
yield* fileSystem.readFileString(environment.clientSettingsPath),
savedContents,
);
failRead = false;
assert.deepEqual(yield* retryableSettings.get, Option.some(savedSettings));
}),
),
);
}
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("{}")));
}),
),
);
for (const document of [
{ label: "malformed JSON", contents: "{not-json" },
{ label: "invalid direct settings", contents: '{"fontSizeCode":"large"}' },
{ label: "invalid legacy settings", contents: '{"settings":{"fontSizeCode":"large"}}' },
]) {
it.effect(`reports ${document.label} without treating the settings file 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, document.contents);
const error = yield* settings.get.pipe(Effect.flip);
assert.instanceOf(error, DesktopClientSettings.DesktopClientSettingsReadError);
assert.equal(error.operation, "decode-document");
assert.equal(error.path, environment.clientSettingsPath);
assert.instanceOf(error.cause, Schema.SchemaError);
assert.equal(
yield* fileSystem.readFileString(environment.clientSettingsPath),
document.contents,
);
}),
),
);
}
});