authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:11:43-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:11:43-07:00
log53a477c2e91d206057adc5ac4c28cbdaddfbc07c
treee8819f30f603bd4334426068dbe774e5b648ec31
parentd84ebe831922167d556e16cb40a238a904e0f14a
signature Signed by PGP key B5690EEEBB952194

[codex] Structure desktop backend settings read errors (#3379)

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

2 files changed, 98 insertions(+), 15 deletions(-)

apps/desktop/src/backend/DesktopBackendConfiguration.test.ts+62
......@@ -3,6 +3,8 @@ import { assert, describe, it } from "@effect/vitest";
33import * as Effect from "effect/Effect";
44import * as FileSystem from "effect/FileSystem";
55import * as Layer from "effect/Layer";
6import * as Logger from "effect/Logger";
7import * as PlatformError from "effect/PlatformError";
68import * as Schema from "effect/Schema";
79
810import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
......@@ -21,6 +23,10 @@ const encodePersistedServerObservabilitySettingsDocument = Schema.encodeEffect(
2123 Schema.fromJsonString(PersistedServerObservabilitySettingsDocument),
2224);
2325
26const isDesktopBackendObservabilitySettingsReadError = Schema.is(
27 DesktopBackendConfiguration.DesktopBackendObservabilitySettingsReadError,
28);
29
2430const serverExposureLayer = Layer.succeed(DesktopServerExposure.DesktopServerExposure, {
2531 getState: Effect.die("unexpected getState"),
2632 backendConfig: Effect.succeed({
......@@ -166,6 +172,62 @@ describe("DesktopBackendConfiguration", () => {
166172 ),
167173 );
168174
175 it.effect("logs structured context when persisted observability settings cannot be read", () =>
176 Effect.gen(function* () {
177 const fileSystem = yield* FileSystem.FileSystem;
178 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
179 prefix: "t3-desktop-backend-config-test-",
180 });
181 const settingsPath = `${baseDir}/userdata/settings.json`;
182 const cause = PlatformError.systemError({
183 _tag: "PermissionDenied",
184 module: "FileSystem",
185 method: "readFileString",
186 pathOrDescriptor: settingsPath,
187 });
188 const messages: Array<unknown> = [];
189 const logger = Logger.make(({ message }) => {
190 messages.push(message);
191 });
192 const failingFileSystemLayer = Layer.succeed(
193 FileSystem.FileSystem,
194 FileSystem.makeNoop({
195 readFileString: () => Effect.fail(cause),
196 }),
197 );
198
199 const config = yield* Effect.gen(function* () {
200 const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration;
201 return yield* configuration.resolve;
202 }).pipe(
203 Effect.provide(
204 Layer.mergeAll(
205 DesktopBackendConfiguration.layer.pipe(
206 Layer.provideMerge(serverExposureLayer),
207 Layer.provideMerge(makeEnvironmentLayer(baseDir)),
208 Layer.provideMerge(failingFileSystemLayer),
209 ),
210 Logger.layer([logger], { mergeWithExisting: false }),
211 ),
212 ),
213 );
214
215 assert.isUndefined(config.bootstrap.otlpTracesUrl);
216 assert.isUndefined(config.bootstrap.otlpMetricsUrl);
217
218 const error = messages
219 .flatMap((message) => (Array.isArray(message) ? message : [message]))
220 .find(isDesktopBackendObservabilitySettingsReadError);
221 assert.isDefined(error);
222 assert.equal(error.settingsPath, settingsPath);
223 assert.equal(error.cause, cause);
224 assert.equal(
225 error.message,
226 `Failed to read persisted backend observability settings at ${settingsPath}.`,
227 );
228 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
229 );
230
169231 it.effect("captures backend output in development so child process logs can be persisted", () =>
170232 Effect.gen(function* () {
171233 const fileSystem = yield* FileSystem.FileSystem;
apps/desktop/src/backend/DesktopBackendConfiguration.ts+36-15
......@@ -8,12 +8,24 @@ import * as Layer from "effect/Layer";
88import * as Option from "effect/Option";
99import * as PlatformError from "effect/PlatformError";
1010import * as Ref from "effect/Ref";
11import * as Schema from "effect/Schema";
1112
1213import * as DesktopBackendManager from "./DesktopBackendManager.ts";
1314import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
14import * as DesktopObservability from "../app/DesktopObservability.ts";
1515import * as DesktopServerExposure from "./DesktopServerExposure.ts";
1616
17export class DesktopBackendObservabilitySettingsReadError extends Schema.TaggedErrorClass<DesktopBackendObservabilitySettingsReadError>()(
18 "DesktopBackendObservabilitySettingsReadError",
19 {
20 settingsPath: Schema.String,
21 cause: Schema.Defect(),
22 },
23) {
24 override get message(): string {
25 return `Failed to read persisted backend observability settings at ${this.settingsPath}.`;
26 }
27}
28
1729export class DesktopBackendConfiguration extends Context.Service<
1830 DesktopBackendConfiguration,
1931 {
......@@ -50,25 +62,34 @@ const DESKTOP_BACKEND_ENV_NAMES = [
5062const backendChildEnvPatch = (): Record<string, string | undefined> =>
5163 Object.fromEntries(DESKTOP_BACKEND_ENV_NAMES.map((name) => [name, undefined]));
5264
53const { logWarning: logBackendConfigurationWarning } = DesktopObservability.makeComponentLogger(
54 "desktop-backend-configuration",
55);
65const logBackendObservabilitySettingsReadFailure = (
66 settingsPath: string,
67 cause: PlatformError.PlatformError,
68) => {
69 const error = new DesktopBackendObservabilitySettingsReadError({ settingsPath, cause });
70 return Effect.logWarning(error).pipe(
71 Effect.annotateLogs({
72 component: "desktop-backend-configuration",
73 error,
74 }),
75 );
76};
5677
5778const readPersistedBackendObservabilitySettings = Effect.gen(function* () {
5879 const fileSystem = yield* FileSystem.FileSystem;
5980 const environment = yield* DesktopEnvironment.DesktopEnvironment;
60 const exists = yield* fileSystem
61 .exists(environment.serverSettingsPath)
62 .pipe(Effect.orElseSucceed(() => false));
63 if (!exists) {
64 return emptyBackendObservabilitySettings;
65 }
66
67 const raw = yield* fileSystem.readFileString(environment.serverSettingsPath).pipe(Effect.option);
81 const raw = yield* fileSystem.readFileString(environment.serverSettingsPath).pipe(
82 Effect.map(Option.some),
83 Effect.catchTags({
84 PlatformError: (cause) =>
85 cause.reason._tag === "NotFound"
86 ? Effect.succeed(Option.none())
87 : logBackendObservabilitySettingsReadFailure(environment.serverSettingsPath, cause).pipe(
88 Effect.as(Option.none()),
89 ),
90 }),
91 );
6892 if (Option.isNone(raw)) {
69 yield* logBackendConfigurationWarning(
70 "failed to read persisted backend observability settings",
71 );
7293 return emptyBackendObservabilitySettings;
7394 }
7495