1import * as NodeServices from "@effect/platform-node/NodeServices";
2import * as HostProcess from "@t3tools/shared/HostProcess";
3import { assert, it } from "@effect/vitest";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Layer from "effect/Layer";
7import * as Path from "effect/Path";
8import * as PlatformError from "effect/PlatformError";
9import * as Sink from "effect/Sink";
10import * as Stream from "effect/Stream";
11import { ChildProcessSpawner } from "effect/process";
12
13import { collectSources, runCommand } from "./mobile-native-static-check.ts";
14
15const processHandle = (
16 exitCode: Effect.Effect<ChildProcessSpawner.ExitCode, PlatformError.PlatformError>,
17) =>
18 ChildProcessSpawner.makeHandle({
19 pid: ChildProcessSpawner.ProcessId(1),
20 exitCode,
21 isRunning: Effect.succeed(false),
22 kill: () => Effect.void,
23 unref: Effect.succeed(Effect.void),
24 stdin: Sink.drain,
25 stdout: Stream.empty,
26 stderr: Stream.empty,
27 all: Stream.empty,
28 getInputFd: () => Sink.drain,
29 getOutputFd: () => Stream.empty,
30 });
31
32const provideSpawner = (spawn: ChildProcessSpawner.ChildProcessSpawner["Service"]["spawn"]) =>
33 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make(spawn));
34
35const runSwiftLint = runCommand("swiftlint", ["lint", "--strict"], "/repo/apps/mobile").pipe(
36 Effect.provideService(HostProcess.Platform, "linux"),
37);
38
39it.layer(NodeServices.layer)("mobile native source discovery", (it) => {
40 it.effect("preserves the failed discovery operation, path, and exact cause", () =>
41 Effect.gen(function* () {
42 const fs = yield* FileSystem.FileSystem;
43 const path = yield* Path.Path;
44 const root = yield* fs.makeTempDirectoryScoped({ prefix: "mobile-native-static-check-" });
45 const missingDirectory = path.join(root, "missing");
46
47 const error = yield* collectSources(missingDirectory, root).pipe(Effect.flip);
48
49 assert.equal(error._tag, "NativeStaticCheckSourceDiscoveryError");
50 assert.equal(error.operation, "read-directory");
51 assert.equal(error.path, missingDirectory);
52 assert.instanceOf(error.cause, PlatformError.PlatformError);
53 assert.equal(error.message, "Native source discovery operation 'read-directory' failed.");
54 }),
55 );
56});
57
58it.effect("preserves process spawn context and the exact cause", () => {
59 const cause = PlatformError.systemError({
60 _tag: "NotFound",
61 module: "ChildProcess",
62 method: "spawn",
63 description: "swiftlint was not found",
64 });
65
66 return Effect.gen(function* () {
67 const error = yield* runSwiftLint.pipe(
68 Effect.provide(provideSpawner(() => Effect.fail(cause))),
69 Effect.flip,
70 );
71
72 if (error._tag !== "NativeStaticCheckProcessError") {
73 return assert.fail(`Unexpected error: ${error._tag}`);
74 }
75 assert.equal(error.operation, "spawn");
76 assert.equal(error.command, "swiftlint");
77 assert.equal(error.argumentCount, 2);
78 assert.equal(error.cwd, "/repo/apps/mobile");
79 assert.equal(error.shell, false);
80 assert.equal(error.cause, cause);
81 assert.equal(
82 error.message,
83 "Native static check process operation 'spawn' failed for command 'swiftlint'.",
84 );
85 assert.notProperty(error, "args");
86 });
87});
88
89it.effect("preserves process wait context and the exact cause", () => {
90 const cause = PlatformError.systemError({
91 _tag: "Unknown",
92 module: "ChildProcess",
93 method: "exitCode",
94 description: "status unavailable",
95 });
96
97 return Effect.gen(function* () {
98 const error = yield* runSwiftLint.pipe(
99 Effect.provide(provideSpawner(() => Effect.succeed(processHandle(Effect.fail(cause))))),
100 Effect.flip,
101 );
102
103 if (error._tag !== "NativeStaticCheckProcessError") {
104 return assert.fail(`Unexpected error: ${error._tag}`);
105 }
106 assert.equal(error.operation, "wait-for-exit");
107 assert.equal(error.command, "swiftlint");
108 assert.equal(error.argumentCount, 2);
109 assert.equal(error.cwd, "/repo/apps/mobile");
110 assert.equal(error.shell, false);
111 assert.equal(error.cause, cause);
112 assert.equal(
113 error.message,
114 "Native static check process operation 'wait-for-exit' failed for command 'swiftlint'.",
115 );
116 assert.notProperty(error, "args");
117 });
118});
119
120it.effect("reports non-zero exits without manufacturing a cause", () =>
121 Effect.gen(function* () {
122 const error = yield* runSwiftLint.pipe(
123 Effect.provide(
124 provideSpawner(() =>
125 Effect.succeed(processHandle(Effect.succeed(ChildProcessSpawner.ExitCode(2)))),
126 ),
127 ),
128 Effect.flip,
129 );
130
131 if (error._tag !== "NativeStaticCheckCommandError") {
132 return assert.fail(`Unexpected error: ${error._tag}`);
133 }
134 assert.equal(error.command, "swiftlint");
135 assert.equal(error.argumentCount, 2);
136 assert.equal(error.cwd, "/repo/apps/mobile");
137 assert.equal(error.shell, false);
138 assert.equal(error.exitCode, 2);
139 assert.notProperty(error, "cause");
140 assert.notProperty(error, "args");
141 }),
142);