authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:10:16-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:10:16-07:00
log61aade9ea4e7df15fd196d9805a00d24ed715f7c
tree06a9e609741801a472b9395c25410872f49f902b
parent04f82ae1f3c50f2c00bbb9c018c768b113b70ae7
signature Signed by PGP key B5690EEEBB952194

[codex] Preserve desktop asset probe failures (#3373)

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

2 files changed, 94 insertions(+), 8 deletions(-)

apps/desktop/src/app/DesktopAssets.test.ts created+57
...@@ -0,0 +1,57 @@
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 PlatformError from "effect/PlatformError";
7
8import * as DesktopAssets from "./DesktopAssets.ts";
9import * as DesktopConfig from "./DesktopConfig.ts";
10import * as DesktopEnvironment from "./DesktopEnvironment.ts";
11
12const environmentLayer = DesktopEnvironment.layer({
13 dirname: "/repo/apps/desktop/dist-electron",
14 homeDirectory: "/Users/alice",
15 platform: "darwin",
16 processArch: "arm64",
17 appVersion: "1.2.3",
18 appPath: "/Applications/T3 Code.app/Contents/Resources/app.asar",
19 isPackaged: true,
20 resourcesPath: "/Applications/T3 Code.app/Contents/Resources",
21 runningUnderArm64Translation: false,
22}).pipe(Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({}))));
23
24describe("DesktopAssets", () => {
25 it.effect("preserves the failed asset candidate and filesystem cause", () =>
26 Effect.gen(function* () {
27 const fileName = "custom.bin";
28 const candidatePath = "/repo/apps/desktop/resources/custom.bin";
29 const cause = PlatformError.systemError({
30 _tag: "PermissionDenied",
31 module: "FileSystem",
32 method: "exists",
33 pathOrDescriptor: candidatePath,
34 description: "private filesystem diagnostic",
35 });
36 const fileSystemLayer = FileSystem.layerNoop({
37 exists: (path) => (path === candidatePath ? Effect.fail(cause) : Effect.succeed(false)),
38 });
39 const assetsLayer = DesktopAssets.layer.pipe(
40 Layer.provide(Layer.merge(fileSystemLayer, environmentLayer)),
41 );
42 const assets = yield* DesktopAssets.DesktopAssets.pipe(Effect.provide(assetsLayer));
43
44 const error = yield* assets.resolveResourcePath(fileName).pipe(Effect.flip);
45
46 assert.instanceOf(error, DesktopAssets.DesktopAssetProbeError);
47 assert.equal(error.fileName, fileName);
48 assert.equal(error.candidatePath, candidatePath);
49 assert.strictEqual(error.cause, cause);
50 assert.equal(
51 error.message,
52 `Failed to probe desktop asset "${fileName}" at ${candidatePath}.`,
53 );
54 assert.notInclude(error.message, "private filesystem diagnostic");
55 }),
56 );
57});
apps/desktop/src/app/DesktopAssets.ts+37-8
...@@ -3,6 +3,7 @@ import * as Effect from "effect/Effect";...@@ -3,6 +3,7 @@ import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";3import * as FileSystem from "effect/FileSystem";
4import * as Layer from "effect/Layer";4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";5import * as Option from "effect/Option";
6import * as Schema from "effect/Schema";
67
7import * as DesktopEnvironment from "./DesktopEnvironment.ts";8import * as DesktopEnvironment from "./DesktopEnvironment.ts";
89
...@@ -12,11 +13,26 @@ export interface DesktopIconPaths {...@@ -12,11 +13,26 @@ export interface DesktopIconPaths {
12 readonly png: Option.Option<string>;13 readonly png: Option.Option<string>;
13}14}
1415
16export class DesktopAssetProbeError extends Schema.TaggedErrorClass<DesktopAssetProbeError>()(
17 "DesktopAssetProbeError",
18 {
19 fileName: Schema.String,
20 candidatePath: Schema.String,
21 cause: Schema.Defect(),
22 },
23) {
24 override get message(): string {
25 return `Failed to probe desktop asset "${this.fileName}" at ${this.candidatePath}.`;
26 }
27}
28
15export class DesktopAssets extends Context.Service<29export class DesktopAssets extends Context.Service<
16 DesktopAssets,30 DesktopAssets,
17 {31 {
18 readonly iconPaths: Effect.Effect<DesktopIconPaths>;32 readonly iconPaths: Effect.Effect<DesktopIconPaths>;
19 readonly resolveResourcePath: (fileName: string) => Effect.Effect<Option.Option<string>>;33 readonly resolveResourcePath: (
34 fileName: string,
35 ) => Effect.Effect<Option.Option<string>, DesktopAssetProbeError>;
20 }36 }
21>()("@t3tools/desktop/app/DesktopAssets") {}37>()("@t3tools/desktop/app/DesktopAssets") {}
2238
...@@ -24,14 +40,20 @@ const resolveResourcePath = Effect.fn("desktop.assets.resolveResourcePath")(func...@@ -24,14 +40,20 @@ const resolveResourcePath = Effect.fn("desktop.assets.resolveResourcePath")(func
24 fileName: string,40 fileName: string,
25): Effect.fn.Return<41): Effect.fn.Return<
26 Option.Option<string>,42 Option.Option<string>,
27 never,43 DesktopAssetProbeError,
28 FileSystem.FileSystem | DesktopEnvironment.DesktopEnvironment44 FileSystem.FileSystem | DesktopEnvironment.DesktopEnvironment
29> {45> {
30 const fileSystem = yield* FileSystem.FileSystem;46 const fileSystem = yield* FileSystem.FileSystem;
31 const environment = yield* DesktopEnvironment.DesktopEnvironment;47 const environment = yield* DesktopEnvironment.DesktopEnvironment;
32 const candidates = environment.resolveResourcePathCandidates(fileName);48 const candidates = environment.resolveResourcePathCandidates(fileName);
33 for (const candidate of candidates) {49 for (const candidate of candidates) {
34 const exists = yield* fileSystem.exists(candidate).pipe(Effect.orElseSucceed(() => false));50 const exists = yield* fileSystem
51 .exists(candidate)
52 .pipe(
53 Effect.mapError(
54 (cause) => new DesktopAssetProbeError({ fileName, candidatePath: candidate, cause }),
55 ),
56 );
35 if (exists) {57 if (exists) {
36 return Option.some(candidate);58 return Option.some(candidate);
37 }59 }
...@@ -43,16 +65,23 @@ const resolveIconPath = Effect.fn("desktop.assets.resolveIconPath")(function* (...@@ -43,16 +65,23 @@ const resolveIconPath = Effect.fn("desktop.assets.resolveIconPath")(function* (
43 ext: keyof DesktopIconPaths,65 ext: keyof DesktopIconPaths,
44): Effect.fn.Return<66): Effect.fn.Return<
45 Option.Option<string>,67 Option.Option<string>,
46 never,68 DesktopAssetProbeError,
47 FileSystem.FileSystem | DesktopEnvironment.DesktopEnvironment69 FileSystem.FileSystem | DesktopEnvironment.DesktopEnvironment
48> {70> {
49 const fileSystem = yield* FileSystem.FileSystem;71 const fileSystem = yield* FileSystem.FileSystem;
50 const environment = yield* DesktopEnvironment.DesktopEnvironment;72 const environment = yield* DesktopEnvironment.DesktopEnvironment;
51 if (environment.isDevelopment && environment.platform === "darwin" && ext === "png") {73 if (environment.isDevelopment && environment.platform === "darwin" && ext === "png") {
52 const developmentDockIconPath = environment.developmentDockIconPath;74 const developmentDockIconPath = environment.developmentDockIconPath;
53 const developmentDockIconExists = yield* fileSystem75 const developmentDockIconExists = yield* fileSystem.exists(developmentDockIconPath).pipe(
54 .exists(developmentDockIconPath)76 Effect.mapError(
55 .pipe(Effect.orElseSucceed(() => false));77 (cause) =>
78 new DesktopAssetProbeError({
79 fileName: "icon.png",
80 candidatePath: developmentDockIconPath,
81 cause,
82 }),
83 ),
84 );
56 if (developmentDockIconExists) {85 if (developmentDockIconExists) {
57 return Option.some(developmentDockIconPath);86 return Option.some(developmentDockIconPath);
58 }87 }
...@@ -61,7 +90,7 @@ const resolveIconPath = Effect.fn("desktop.assets.resolveIconPath")(function* (...@@ -61,7 +90,7 @@ const resolveIconPath = Effect.fn("desktop.assets.resolveIconPath")(function* (
61 return yield* resolveResourcePath(`icon.${ext}`);90 return yield* resolveResourcePath(`icon.${ext}`);
62});91});
6392
64const make = Effect.gen(function* () {93export const make = Effect.gen(function* () {
65 const context = yield* Effect.context<94 const context = yield* Effect.context<
66 FileSystem.FileSystem | DesktopEnvironment.DesktopEnvironment95 FileSystem.FileSystem | DesktopEnvironment.DesktopEnvironment
67 >();96 >();