| 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | import { describe, expect, it } from "@effect/vitest"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Layer from "effect/Layer"; |
| 5 | import * as Sink from "effect/Sink"; |
| 6 | import * as Stream from "effect/Stream"; |
| 7 | import { ChildProcessSpawner } from "effect/process"; |
| 8 | import * as HostProcess from "@t3tools/shared/HostProcess"; |
| 9 | |
| 10 | import * as ServerConfig from "../config.ts"; |
| 11 | import * as NativeAppIconResolver from "./NativeAppIconResolver.ts"; |
| 12 | |
| 13 | function 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 | |
| 29 | describe("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 | }); |