1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { describe, expect, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Sink from "effect/Sink";
6import * as Stream from "effect/Stream";
7import { ChildProcessSpawner } from "effect/process";
8import * as HostProcess from "@t3tools/shared/HostProcess";
9
10import * as ServerConfig from "../config.ts";
11import * as NativeAppIconResolver from "./NativeAppIconResolver.ts";
12
13function emptyProcessHandle() {
14 return ChildProcessSpawner.makeHandle({
15 pid: ChildProcessSpawner.ProcessId(1),
16 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(0)),
17 isRunning: Effect.succeed(false),
18 kill: () => Effect.void,
19 unref: Effect.succeed(Effect.void),
20 stdin: Sink.drain,
21 stdout: Stream.empty,
22 stderr: Stream.empty,
23 all: Stream.empty,
24 getInputFd: () => Sink.drain,
25 getOutputFd: () => Stream.empty,
26 });
27}
28
29describe("resolveNativeAppIcon", () => {
30 it.effect("escapes Spotlight wildcards and caches misses", () => {
31 const commands: Array<{ readonly command: string; readonly args: ReadonlyArray<string> }> = [];
32 const spawner = ChildProcessSpawner.make((command) =>
33 Effect.sync(() => {
34 const input = command as unknown as {
35 readonly command: string;
36 readonly args: ReadonlyArray<string>;
37 };
38 commands.push(input);
39 return emptyProcessHandle();
40 }),
41 );
42 const layerConfig = ServerConfig.ServerConfig.layerTest(process.cwd(), {
43 prefix: "t3-native-app-icon-test-",
44 });
45 const layerDependencies = Layer.mergeAll(
46 layerConfig,
47 Layer.succeed(HostProcess.Platform, "darwin"),
48 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner),
49 ).pipe(Layer.provideMerge(NodeServices.layer));
50 const layerTest = NativeAppIconResolver.layer.pipe(Layer.provide(layerDependencies));
51 const app = { _tag: "display-name", displayName: "Review * App" } as const;
52
53 return Effect.gen(function* () {
54 const resolver = yield* NativeAppIconResolver.NativeAppIconResolver;
55 expect(yield* resolver.resolve(app)).toBeNull();
56 expect(yield* resolver.resolve(app)).toBeNull();
57
58 expect(commands).toHaveLength(1);
59 expect(commands[0]).toMatchObject({ command: "/usr/bin/mdfind" });
60 expect(commands[0]?.args[0]).toContain("Review \\* App");
61
62 for (let index = 0; index < 256; index += 1) {
63 expect(
64 yield* resolver.resolve({
65 _tag: "display-name",
66 displayName: `Missing Review App ${index}`,
67 }),
68 ).toBeNull();
69 }
70 expect(commands).toHaveLength(257);
71 expect(yield* resolver.resolve(app)).toBeNull();
72 expect(commands).toHaveLength(258);
73 }).pipe(Effect.provide(layerTest));
74 });
75});