1import { assert, describe, it } from "@effect/vitest"
2import { Effect, FileSystem, Layer, Path, PlatformError, Redacted, Schema, Stdio } from "effect"
3import { Primitive } from "effect/cli"
4import { ChildProcessSpawner } from "effect/process"
5import { TestConsole } from "effect/testing"
6import * as MockTerminal from "./services/MockTerminal.ts"
7
8const ConsoleLayer = TestConsole.layer
9const FileSystemLayer = FileSystem.layerNoop({})
10const PathLayer = Path.layer
11const TerminalLayer = MockTerminal.layer
12const StdioLayer = Stdio.layerTest({})
13const ChildProcessSpawnerLayer = Layer.succeed(
14 ChildProcessSpawner.ChildProcessSpawner,
15 ChildProcessSpawner.make(() => Effect.die("Not implemented"))
16)
17
18const TestLayer = Layer.mergeAll(
19 ConsoleLayer,
20 FileSystemLayer,
21 PathLayer,
22 TerminalLayer,
23 StdioLayer,
24 ChildProcessSpawnerLayer
25)
26
27// Helper functions to reduce repetition
28const expectValidValues = <A>(
29 primitive: Primitive.Primitive<A>,
30 cases: Array<[string, A]>
31) =>
32 Effect.gen(function*() {
33 for (const [input, expected] of cases) {
34 const result = yield* primitive.parse(input)
35 assert.strictEqual(result, expected)
36 }
37 })
38
39const expectInvalidValues = <A>(
40 primitive: Primitive.Primitive<A>,
41 inputs: ReadonlyArray<string>,
42 messages: ReadonlyArray<string>
43) =>
44 Effect.gen(function*() {
45 for (let i = 0; i < inputs.length; i++) {
46 const error = yield* Effect.flip(primitive.parse(inputs[i]))
47 assert.strictEqual(error, messages[i])
48 }
49 })
50
51const expectValidDates = (
52 primitive: Primitive.Primitive<Date>,
53 cases: Array<[string, (date: Date) => void]>
54) =>
55 Effect.gen(function*() {
56 for (const [input, validator] of cases) {
57 const result = yield* primitive.parse(input)
58 assert.isTrue(result instanceof Date)
59 validator(result)
60 }
61 }) as Effect.Effect<void, string, never>
62
63describe("Primitive", () => {
64 describe("Boolean", () => {
65 it.layer(TestLayer)((it) => {
66 it.effect("should parse true values correctly", () =>
67 expectValidValues(Primitive.Boolean, [
68 ["true", true],
69 ["1", true],
70 ["y", true],
71 ["yes", true],
72 ["on", true]
73 ]))
74
75 it.effect("should parse false values correctly", () =>
76 expectValidValues(Primitive.Boolean, [
77 ["false", false],
78 ["0", false],
79 ["n", false],
80 ["no", false],
81 ["off", false]
82 ]))
83
84 it.effect("should fail for invalid values", () =>
85 expectInvalidValues(
86 Primitive.Boolean,
87 ["invalid"],
88 [`Expected "true" | "yes" | "on" | "1" | "y" | "false" | "no" | "off" | "0" | "n"`]
89 ))
90
91 it("should have correct _tag", () => {
92 assert.strictEqual(Primitive.Boolean._tag, "Boolean")
93 })
94 })
95 })
96
97 describe("Finite", () => {
98 it.layer(TestLayer)((it) => {
99 it.effect("should parse valid finite numbers", () =>
100 expectValidValues(Primitive.Finite, [
101 ["42", 42],
102 ["3.14", 3.14],
103 ["-42.5", -42.5],
104 ["0", 0],
105 ["1e3", 1000]
106 ]))
107
108 it.effect("should fail for invalid values", () =>
109 expectInvalidValues(Primitive.Finite, ["not-a-number"], [
110 `Expected a string representing a finite number`
111 ]))
112
113 it.effect("should reject non-finite numbers and overflow", () =>
114 Effect.gen(function*() {
115 for (const input of ["NaN", "Infinity", "-Infinity", "1e309"]) {
116 const error = yield* Effect.flip(Primitive.Finite.parse(input))
117 assert.include(error, "finite number")
118 }
119 }))
120
121 it("should have correct _tag", () => {
122 assert.strictEqual(Primitive.Finite._tag, "Finite")
123 })
124 })
125 })
126
127 describe("Date", () => {
128 it.layer(TestLayer)((it) => {
129 it.effect("should parse valid date values", () =>
130 expectValidDates(Primitive.Date, [
131 // ISO date
132 [
133 "2024-01-15",
134 (date) => {
135 assert.strictEqual(date.toISOString().slice(0, 10), "2024-01-15")
136 }
137 ],
138 // Full ISO datetime
139 [
140 "2024-01-15T12:30:45.123Z",
141 (date) => {
142 assert.strictEqual(date.toISOString(), "2024-01-15T12:30:45.123Z")
143 }
144 ],
145 // With timezone offset
146 [
147 "2024-01-15T12:30:45+02:00",
148 (date) => {
149 assert.strictEqual(date.getUTCHours(), 10)
150 assert.strictEqual(date.getUTCMinutes(), 30)
151 }
152 ]
153 ]))
154
155 it.effect("should fail for invalid values", () =>
156 expectInvalidValues(Primitive.Date, ["not-a-date"], [`Expected a valid Date`]))
157
158 it("should have correct _tag", () => {
159 assert.strictEqual(Primitive.Date._tag, "Date")
160 })
161 })
162 })
163
164 describe("Int", () => {
165 it.layer(TestLayer)((it) => {
166 it.effect("should parse valid integer values", () =>
167 expectValidValues(Primitive.Int, [
168 ["42", 42],
169 ["-123", -123],
170 ["0", 0],
171 ["9007199254740991", 9007199254740991],
172 ["1e3", 1000]
173 ]))
174
175 it.effect("should fail for invalid values", () =>
176 expectInvalidValues(
177 Primitive.Int,
178 ["3.14", "not-a-number"],
179 [`Expected an integer`, `Expected a string representing a finite number`]
180 ))
181
182 it("should have correct _tag", () => {
183 assert.strictEqual(Primitive.Int._tag, "Int")
184 })
185 })
186 })
187
188 describe("String", () => {
189 it.layer(TestLayer)((it) => {
190 it.effect("should parse string values", () =>
191 expectValidValues(Primitive.String, [
192 ["hello", "hello"],
193 ["", ""],
194 [" spaces ", " spaces "],
195 ["123", "123"],
196 ["special!@#$%", "special!@#$%"]
197 ]))
198
199 it("should have correct _tag", () => {
200 assert.strictEqual(Primitive.String._tag, "String")
201 })
202 })
203 })
204
205 describe("Choice", () => {
206 const colorChoice = Primitive.Choice([
207 ["red", "RED"],
208 ["green", "GREEN"],
209 ["blue", "BLUE"]
210 ])
211
212 it.layer(TestLayer)((it) => {
213 it.effect("should parse valid choices", () =>
214 expectValidValues(colorChoice, [
215 ["red", "RED"],
216 ["green", "GREEN"],
217 ["blue", "BLUE"]
218 ]))
219
220 it.effect("should fail for invalid choices", () =>
221 expectInvalidValues(
222 colorChoice,
223 ["yellow", "purple", ""],
224 [
225 `"red" | "green" | "blue"`,
226 `"red" | "green" | "blue"`,
227 `"red" | "green" | "blue"`
228 ]
229 ))
230
231 it("should have correct _tag", () => {
232 assert.strictEqual(colorChoice._tag, "Choice")
233 })
234
235 const numberChoice = Primitive.Choice([
236 ["one", 1],
237 ["two", 2],
238 ["three", 3]
239 ])
240
241 it.effect("should work with different value types", () =>
242 expectValidValues(numberChoice, [
243 ["one", 1],
244 ["two", 2],
245 ["three", 3]
246 ]))
247 })
248 })
249
250 describe("Path", () => {
251 it.layer(TestLayer)((it) => {
252 it.effect("should resolve paths without requiring existence", () =>
253 Effect.gen(function*() {
254 const pathPrimitive = Primitive.Path("either")
255 const result1 = yield* pathPrimitive.parse("./test.txt")
256 const result2 = yield* pathPrimitive.parse("/absolute/path")
257 const result3 = yield* pathPrimitive.parse("relative/path")
258
259 // Results should be absolute paths
260 assert.isTrue(result1.includes("test.txt"))
261 assert.isTrue(result2 === "/absolute/path")
262 assert.isTrue(result3.includes("relative/path"))
263 }))
264
265 it("should have correct _tag", () => {
266 assert.strictEqual(Primitive.Path("either")._tag, "Path")
267 })
268
269 it.effect("should fail when a required file path does not exist", () =>
270 Effect.gen(function*() {
271 const filePath = Primitive.Path("file", true)
272
273 // Test non-existent file - should fail validation
274 const error = yield* Effect.flip(
275 filePath.parse("/non/existent/file.txt")
276 )
277
278 assert.strictEqual(error, "Path does not exist: /non/existent/file.txt")
279 }).pipe(
280 Effect.provide(
281 FileSystem.layerNoop({
282 stat: (path) =>
283 Effect.fail(
284 PlatformError.badArgument({
285 module: "",
286 method: "",
287 description: `The specified path ${path} does not exist`
288 })
289 )
290 })
291 )
292 ))
293
294 it.effect("should fail when a required directory path does not exist", () =>
295 Effect.gen(function*() {
296 const dirPath = Primitive.Path("directory", true)
297
298 // Test non-existent directory - should fail validation
299 const error = yield* Effect.flip(
300 dirPath.parse("/non/existent/directory")
301 )
302
303 assert.strictEqual(error, "Path does not exist: /non/existent/directory")
304 }).pipe(
305 Effect.provide(
306 FileSystem.layerNoop({
307 stat: (path) =>
308 Effect.fail(
309 PlatformError.badArgument({
310 module: "",
311 method: "",
312 description: `The specified path ${path} does not exist`
313 })
314 )
315 })
316 )
317 ))
318 })
319 })
320
321 describe("Redacted", () => {
322 it.layer(TestLayer)((it) => {
323 it.effect("should parse and redact values", () =>
324 Effect.gen(function*() {
325 const result = yield* Primitive.Redacted.parse("secret123")
326 // Check if it's a Redacted value
327 assert.isTrue(Redacted.isRedacted(result))
328 // The toString method should return a redacted representation
329 assert.strictEqual(String(result), "<redacted>")
330 }))
331
332 it("should have correct _tag", () => {
333 assert.strictEqual(Primitive.Redacted._tag, "Redacted")
334 })
335
336 it.effect("should handle empty strings", () =>
337 Effect.gen(function*() {
338 const result = yield* Primitive.Redacted.parse("")
339 assert.isTrue(Redacted.isRedacted(result))
340 }))
341 })
342 })
343
344 describe("KeyValuePair", () => {
345 it.layer(TestLayer)((it) => {
346 it.effect("should preserve '=' in URL query parameters", () =>
347 Effect.gen(function*() {
348 const url = yield* Primitive.KeyValuePair.parse(
349 "DATABASE_URL=postgres://user:pass@host:5432/db?sslmode=require"
350 )
351 assert.deepStrictEqual(url, {
352 DATABASE_URL: "postgres://user:pass@host:5432/db?sslmode=require"
353 })
354 }))
355
356 it.effect("should preserve trailing '=' in padded values", () =>
357 Effect.gen(function*() {
358 const padded = yield* Primitive.KeyValuePair.parse("TOKEN=YWJjZA==")
359 assert.deepStrictEqual(padded, { TOKEN: "YWJjZA==" })
360 }))
361
362 it.effect("should fail when the input is malformed", () =>
363 expectInvalidValues(
364 Primitive.KeyValuePair,
365 ["invalid", "=value", "key="],
366 [
367 "Invalid key=value format. Expected format: key=value, got: invalid",
368 "Invalid key=value format. Both key and value must be non-empty. Got: =value",
369 "Invalid key=value format. Both key and value must be non-empty. Got: key="
370 ]
371 ))
372 })
373 })
374
375 it.effect("file constructors preserve text, parsed content, and schema decoding", () =>
376 Effect.gen(function*() {
377 assert.strictEqual(yield* Primitive.FileText.parse("/config.json"), "{\"enabled\":true}")
378 assert.deepStrictEqual(yield* Primitive.FileParse().parse("/config.json"), { enabled: true })
379 assert.deepStrictEqual(
380 yield* Primitive.FileSchema(Schema.Struct({ enabled: Schema.Boolean })).parse("/config.json"),
381 { enabled: true }
382 )
383 const error = yield* Effect.flip(Primitive.FileSchema(Schema.String).parse("/config.json"))
384 assert.include(error, "string")
385 }).pipe(
386 Effect.provide(FileSystem.layerNoop({
387 exists: () => Effect.succeed(true),
388 stat: () => Effect.succeed({ type: "File" } as FileSystem.File.Info),
389 readFileString: () => Effect.succeed("{\"enabled\":true}")
390 })),
391 Effect.provide(TestLayer)
392 ))
393
394 it("Never has the matching primitive tag", () => {
395 assert.strictEqual(Primitive.Never._tag, "Never")
396 })
397
398 it.effect("Never remains an always-failing sentinel", () =>
399 expectInvalidValues(Primitive.Never, ["value"], ["This option does not accept values"]).pipe(
400 Effect.provide(TestLayer)
401 ))
402})