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 layer(fileSystemLayer: Layer.Layer<FileSystem.FileSystem>) {
23 const layerEnvironment = 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(layerEnvironment, 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* Effect.result(settings.get),
58 settingsPath: environment.clientSettingsPath,
59 records,
60 };
61 }).pipe(
62 Effect.provide(
63 Layer.mergeAll(layer(fileSystemLayer), Logger.layer([logger], { mergeWithExisting: false })),
64 ),
65 );
66};
67
68describe("DesktopClientSettings diagnostics", () => {
69 it.effect("treats a missing settings file as expected without warning", () =>
70 Effect.gen(function* () {
71 const result = yield* readWithLogs(FileSystem.layerNoop({}));
72
73 if (result.result._tag !== "Success") return assert.fail("expected a successful read");
74 assert.isTrue(Option.isNone(result.result.success));
75 assert.deepEqual(result.records, []);
76 }),
77 );
78
79 it.effect("reports non-missing filesystem failures and logs the settings path", () => {
80 const permissionError = PlatformError.systemError({
81 _tag: "PermissionDenied",
82 module: "FileSystem",
83 method: "readFileString",
84 pathOrDescriptor: `${baseDir}/userdata/client-settings.json`,
85 });
86
87 return Effect.gen(function* () {
88 const result = yield* readWithLogs(
89 FileSystem.layerNoop({
90 readFileString: () => Effect.fail(permissionError),
91 }),
92 );
93
94 if (result.result._tag !== "Failure") return assert.fail("expected a read failure");
95 assert.instanceOf(
96 result.result.failure,
97 DesktopClientSettings.DesktopClientSettingsReadError,
98 );
99 assert.strictEqual(result.result.failure.cause, permissionError);
100 assert.equal(result.records.length, 1);
101 assert.deepEqual(result.records[0]?.message, [
102 "Could not read desktop client settings.",
103 permissionError,
104 ]);
105 assert.equal(result.records[0]?.annotations.settingsPath, result.settingsPath);
106 });
107 });
108
109 it.effect("reports malformed settings documents and logs the settings path", () =>
110 Effect.gen(function* () {
111 const result = yield* readWithLogs(
112 FileSystem.layerNoop({
113 readFileString: () => Effect.succeed("{not-json"),
114 }),
115 );
116
117 if (result.result._tag !== "Failure") return assert.fail("expected a decode failure");
118 assert.instanceOf(
119 result.result.failure,
120 DesktopClientSettings.DesktopClientSettingsReadError,
121 );
122 assert.equal(result.result.failure.operation, "decode-document");
123 assert.equal(result.records.length, 1);
124 const message = result.records[0]?.message;
125 if (!Array.isArray(message)) {
126 return assert.fail("expected structured warning arguments");
127 }
128 assert.equal(message[0], "Could not decode desktop client settings.");
129 const schemaError = message[1];
130 if (schemaError === null || typeof schemaError !== "object") {
131 return assert.fail("expected the schema error in the warning");
132 }
133 assert.equal("_tag" in schemaError ? schemaError._tag : undefined, "SchemaError");
134 assert.equal(result.records[0]?.annotations.settingsPath, result.settingsPath);
135 }),
136 );
137});