authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 15:55:38-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 15:55:38-07:00
logad2cb1dd57dbdb028f6544ad780df4616ed41437
tree082458c6696e77f8e2b129029c824b8833a0098d
parent674590e0098e397c44c1be50adedc875635209a7
signature Signed by PGP key B5690EEEBB952194

[codex] Diagnose desktop client settings read failures (#3432)

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 146 insertions(+), 2 deletions(-)

apps/desktop/src/settings/DesktopClientSettings.diagnostics.test.ts created+129
......@@ -0,0 +1,129 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, describe, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";
6import * as Logger from "effect/Logger";
7import * as Option from "effect/Option";
8import * as PlatformError from "effect/PlatformError";
9import * as References from "effect/References";
10
11import * as DesktopConfig from "../app/DesktopConfig.ts";
12import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
13import * as DesktopClientSettings from "./DesktopClientSettings.ts";
14
15interface LogRecord {
16 readonly message: unknown;
17 readonly annotations: Readonly<Record<string, unknown>>;
18}
19
20const baseDir = "/virtual-home";
21
22function makeLayer(fileSystemLayer: Layer.Layer<FileSystem.FileSystem>) {
23 const environmentLayer = DesktopEnvironment.layer({
24 dirname: "/repo/apps/desktop/src",
25 homeDirectory: baseDir,
26 platform: "darwin",
27 processArch: "x64",
28 appVersion: "1.2.3",
29 appPath: "/repo",
30 isPackaged: true,
31 resourcesPath: "/missing/resources",
32 runningUnderArm64Translation: false,
33 }).pipe(
34 Layer.provide(
35 Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({ T3CODE_HOME: baseDir })),
36 ),
37 );
38
39 return DesktopClientSettings.layer.pipe(
40 Layer.provideMerge(Layer.mergeAll(environmentLayer, NodeServices.layer, fileSystemLayer)),
41 );
42}
43
44const readWithLogs = (fileSystemLayer: Layer.Layer<FileSystem.FileSystem>) => {
45 const records: Array<LogRecord> = [];
46 const logger = Logger.make(({ fiber, message }) => {
47 records.push({
48 message,
49 annotations: { ...fiber.getRef(References.CurrentLogAnnotations) },
50 });
51 });
52
53 return Effect.gen(function* () {
54 const environment = yield* DesktopEnvironment.DesktopEnvironment;
55 const settings = yield* DesktopClientSettings.DesktopClientSettings;
56 return {
57 result: yield* settings.get,
58 settingsPath: environment.clientSettingsPath,
59 records,
60 };
61 }).pipe(
62 Effect.provide(
63 Layer.mergeAll(
64 makeLayer(fileSystemLayer),
65 Logger.layer([logger], { mergeWithExisting: false }),
66 ),
67 ),
68 );
69};
70
71describe("DesktopClientSettings diagnostics", () => {
72 it.effect("treats a missing settings file as expected without warning", () =>
73 Effect.gen(function* () {
74 const result = yield* readWithLogs(FileSystem.layerNoop({}));
75
76 assert.isTrue(Option.isNone(result.result));
77 assert.deepEqual(result.records, []);
78 }),
79 );
80
81 it.effect("logs non-missing filesystem failures with the settings path", () => {
82 const permissionError = PlatformError.systemError({
83 _tag: "PermissionDenied",
84 module: "FileSystem",
85 method: "readFileString",
86 pathOrDescriptor: `${baseDir}/userdata/client-settings.json`,
87 });
88
89 return Effect.gen(function* () {
90 const result = yield* readWithLogs(
91 FileSystem.layerNoop({
92 readFileString: () => Effect.fail(permissionError),
93 }),
94 );
95
96 assert.isTrue(Option.isNone(result.result));
97 assert.equal(result.records.length, 1);
98 assert.deepEqual(result.records[0]?.message, [
99 "Could not read desktop client settings.",
100 permissionError,
101 ]);
102 assert.equal(result.records[0]?.annotations.settingsPath, result.settingsPath);
103 });
104 });
105
106 it.effect("logs malformed settings documents with the settings path", () =>
107 Effect.gen(function* () {
108 const result = yield* readWithLogs(
109 FileSystem.layerNoop({
110 readFileString: () => Effect.succeed("{not-json"),
111 }),
112 );
113
114 assert.isTrue(Option.isNone(result.result));
115 assert.equal(result.records.length, 1);
116 const message = result.records[0]?.message;
117 if (!Array.isArray(message)) {
118 return assert.fail("expected structured warning arguments");
119 }
120 assert.equal(message[0], "Could not decode desktop client settings.");
121 const schemaError = message[1];
122 if (schemaError === null || typeof schemaError !== "object") {
123 return assert.fail("expected the schema error in the warning");
124 }
125 assert.equal("_tag" in schemaError ? schemaError._tag : undefined, "SchemaError");
126 assert.equal(result.records[0]?.annotations.settingsPath, result.settingsPath);
127 }),
128 );
129});
apps/desktop/src/settings/DesktopClientSettings.ts+17-2
......@@ -67,14 +67,29 @@ const readClientSettings = (
6767 settingsPath: string,
6868): Effect.Effect<Option.Option<ClientSettings>> =>
6969 fileSystem.readFileString(settingsPath).pipe(
70 Effect.option,
70 Effect.map(Option.some),
71 Effect.catchTags({
72 PlatformError: (cause) =>
73 cause.reason._tag === "NotFound"
74 ? Effect.succeed(Option.none<string>())
75 : Effect.logWarning("Could not read desktop client settings.", cause).pipe(
76 Effect.annotateLogs({ settingsPath }),
77 Effect.as(Option.none<string>()),
78 ),
79 }),
7180 Effect.flatMap(
7281 Option.match({
7382 onNone: () => Effect.succeed(Option.none<ClientSettings>()),
7483 onSome: (raw) =>
7584 decodeClientSettingsJson(raw).pipe(
7685 Effect.map((settings) => Option.some(settings)),
77 Effect.orElseSucceed(() => Option.none<ClientSettings>()),
86 Effect.catchTags({
87 SchemaError: (cause) =>
88 Effect.logWarning("Could not decode desktop client settings.", cause).pipe(
89 Effect.annotateLogs({ settingsPath }),
90 Effect.as(Option.none<ClientSettings>()),
91 ),
92 }),
7893 ),
7994 }),
8095 ),