@canonical/summon 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +439 -0
- package/generators/example/hello/index.ts +132 -0
- package/generators/example/hello/templates/README.md.ejs +20 -0
- package/generators/example/hello/templates/index.ts.ejs +9 -0
- package/generators/example/webapp/index.ts +509 -0
- package/generators/example/webapp/templates/ARCHITECTURE.md.ejs +180 -0
- package/generators/example/webapp/templates/App.tsx.ejs +86 -0
- package/generators/example/webapp/templates/README.md.ejs +154 -0
- package/generators/example/webapp/templates/app.test.ts.ejs +63 -0
- package/generators/example/webapp/templates/app.ts.ejs +132 -0
- package/generators/example/webapp/templates/feature.ts.ejs +264 -0
- package/generators/example/webapp/templates/index.html.ejs +20 -0
- package/generators/example/webapp/templates/main.tsx.ejs +43 -0
- package/generators/example/webapp/templates/styles.css.ejs +135 -0
- package/generators/init/index.ts +124 -0
- package/generators/init/templates/generator.ts.ejs +85 -0
- package/generators/init/templates/template-index.ts.ejs +9 -0
- package/generators/init/templates/template-test.ts.ejs +8 -0
- package/package.json +64 -0
- package/src/__tests__/combinators.test.ts +895 -0
- package/src/__tests__/dry-run.test.ts +927 -0
- package/src/__tests__/effect.test.ts +816 -0
- package/src/__tests__/interpreter.test.ts +673 -0
- package/src/__tests__/primitives.test.ts +970 -0
- package/src/__tests__/task.test.ts +929 -0
- package/src/__tests__/template.test.ts +666 -0
- package/src/cli-format.ts +165 -0
- package/src/cli-types.ts +53 -0
- package/src/cli.tsx +1322 -0
- package/src/combinators.ts +294 -0
- package/src/completion.ts +488 -0
- package/src/components/App.tsx +960 -0
- package/src/components/ExecutionProgress.tsx +205 -0
- package/src/components/FileTreePreview.tsx +97 -0
- package/src/components/PromptSequence.tsx +483 -0
- package/src/components/Spinner.tsx +36 -0
- package/src/components/index.ts +16 -0
- package/src/dry-run.ts +434 -0
- package/src/effect.ts +224 -0
- package/src/index.ts +266 -0
- package/src/interpreter.ts +463 -0
- package/src/primitives.ts +442 -0
- package/src/task.ts +245 -0
- package/src/template.ts +537 -0
- package/src/types.ts +453 -0
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
ap,
|
|
4
|
+
effect,
|
|
5
|
+
fail,
|
|
6
|
+
failWith,
|
|
7
|
+
flatMap,
|
|
8
|
+
hasEffects,
|
|
9
|
+
isFailed,
|
|
10
|
+
isPure,
|
|
11
|
+
map,
|
|
12
|
+
mapError,
|
|
13
|
+
of,
|
|
14
|
+
pure,
|
|
15
|
+
recover,
|
|
16
|
+
task,
|
|
17
|
+
} from "../task.js";
|
|
18
|
+
import type { Effect, Task, TaskError } from "../types.js";
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// Core Constructors
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
describe("Task Monad - Core Constructors", () => {
|
|
25
|
+
describe("pure", () => {
|
|
26
|
+
it("creates a Pure task with the given value", () => {
|
|
27
|
+
const t = pure(42);
|
|
28
|
+
expect(t._tag).toBe("Pure");
|
|
29
|
+
expect((t as { _tag: "Pure"; value: number }).value).toBe(42);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("works with string values", () => {
|
|
33
|
+
const t = pure("hello");
|
|
34
|
+
expect(t._tag).toBe("Pure");
|
|
35
|
+
expect((t as { _tag: "Pure"; value: string }).value).toBe("hello");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("works with object values", () => {
|
|
39
|
+
const obj = { a: 1, b: "test" };
|
|
40
|
+
const t = pure(obj);
|
|
41
|
+
expect(t._tag).toBe("Pure");
|
|
42
|
+
expect((t as { _tag: "Pure"; value: typeof obj }).value).toEqual(obj);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("works with array values", () => {
|
|
46
|
+
const arr = [1, 2, 3];
|
|
47
|
+
const t = pure(arr);
|
|
48
|
+
expect(t._tag).toBe("Pure");
|
|
49
|
+
expect((t as { _tag: "Pure"; value: number[] }).value).toEqual(arr);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("works with null value", () => {
|
|
53
|
+
const t = pure(null);
|
|
54
|
+
expect(t._tag).toBe("Pure");
|
|
55
|
+
expect((t as { _tag: "Pure"; value: null }).value).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("works with undefined value", () => {
|
|
59
|
+
const t = pure(undefined);
|
|
60
|
+
expect(t._tag).toBe("Pure");
|
|
61
|
+
expect((t as { _tag: "Pure"; value: undefined }).value).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("works with boolean values", () => {
|
|
65
|
+
expect((pure(true) as { value: boolean }).value).toBe(true);
|
|
66
|
+
expect((pure(false) as { value: boolean }).value).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("works with nested objects", () => {
|
|
70
|
+
const nested = { outer: { inner: { value: 42 } } };
|
|
71
|
+
const t = pure(nested);
|
|
72
|
+
expect((t as { value: typeof nested }).value).toEqual(nested);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("preserves referential equality", () => {
|
|
76
|
+
const obj = { a: 1 };
|
|
77
|
+
const t = pure(obj);
|
|
78
|
+
expect((t as { value: typeof obj }).value).toBe(obj);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("effect", () => {
|
|
83
|
+
it("creates an Effect task with the given effect", () => {
|
|
84
|
+
const eff: Effect = { _tag: "ReadFile", path: "/test.txt" };
|
|
85
|
+
const t = effect<string>(eff);
|
|
86
|
+
|
|
87
|
+
expect(t._tag).toBe("Effect");
|
|
88
|
+
expect((t as { effect: Effect }).effect).toEqual(eff);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("has a continuation that wraps result in Pure", () => {
|
|
92
|
+
const eff: Effect = { _tag: "ReadFile", path: "/test.txt" };
|
|
93
|
+
const t = effect<string>(eff);
|
|
94
|
+
|
|
95
|
+
const effectTask = t as {
|
|
96
|
+
_tag: "Effect";
|
|
97
|
+
effect: Effect;
|
|
98
|
+
cont: (result: unknown) => Task<string>;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const continued = effectTask.cont("file content");
|
|
102
|
+
expect(continued._tag).toBe("Pure");
|
|
103
|
+
expect((continued as { value: string }).value).toBe("file content");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("creates WriteFile effect correctly", () => {
|
|
107
|
+
const eff: Effect = {
|
|
108
|
+
_tag: "WriteFile",
|
|
109
|
+
path: "/output.txt",
|
|
110
|
+
content: "hello",
|
|
111
|
+
};
|
|
112
|
+
const t = effect<void>(eff);
|
|
113
|
+
|
|
114
|
+
expect((t as { effect: Effect }).effect._tag).toBe("WriteFile");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("creates Exec effect correctly", () => {
|
|
118
|
+
const eff: Effect = {
|
|
119
|
+
_tag: "Exec",
|
|
120
|
+
command: "npm",
|
|
121
|
+
args: ["install"],
|
|
122
|
+
cwd: "/project",
|
|
123
|
+
};
|
|
124
|
+
const t = effect(eff);
|
|
125
|
+
|
|
126
|
+
const effectTask = t as { effect: Effect };
|
|
127
|
+
expect(effectTask.effect._tag).toBe("Exec");
|
|
128
|
+
expect((effectTask.effect as { command: string }).command).toBe("npm");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("fail", () => {
|
|
133
|
+
it("creates a Fail task with the given error", () => {
|
|
134
|
+
const error: TaskError = { code: "TEST_ERROR", message: "Test error" };
|
|
135
|
+
const t = fail(error);
|
|
136
|
+
|
|
137
|
+
expect(t._tag).toBe("Fail");
|
|
138
|
+
expect((t as { _tag: "Fail"; error: TaskError }).error).toEqual(error);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("preserves error code", () => {
|
|
142
|
+
const error: TaskError = { code: "ERR_NOT_FOUND", message: "Not found" };
|
|
143
|
+
const t = fail(error);
|
|
144
|
+
expect((t as { error: TaskError }).error.code).toBe("ERR_NOT_FOUND");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("preserves error message", () => {
|
|
148
|
+
const error: TaskError = { code: "ERR", message: "Something went wrong" };
|
|
149
|
+
const t = fail(error);
|
|
150
|
+
expect((t as { error: TaskError }).error.message).toBe(
|
|
151
|
+
"Something went wrong",
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("preserves error cause", () => {
|
|
156
|
+
const cause = new Error("Original error");
|
|
157
|
+
const error: TaskError = { code: "WRAPPED", message: "Wrapped", cause };
|
|
158
|
+
const t = fail(error);
|
|
159
|
+
expect((t as { error: TaskError }).error.cause).toBe(cause);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("preserves error context", () => {
|
|
163
|
+
const context = { userId: 123, action: "delete" };
|
|
164
|
+
const error: TaskError = { code: "ERR", message: "Error", context };
|
|
165
|
+
const t = fail(error);
|
|
166
|
+
expect((t as { error: TaskError }).error.context).toEqual(context);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("preserves error stack", () => {
|
|
170
|
+
const error: TaskError = {
|
|
171
|
+
code: "ERR",
|
|
172
|
+
message: "Error",
|
|
173
|
+
stack: "Error: test\n at test.ts:1:1",
|
|
174
|
+
};
|
|
175
|
+
const t = fail(error);
|
|
176
|
+
expect((t as { error: TaskError }).error.stack).toContain("Error: test");
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("failWith", () => {
|
|
181
|
+
it("creates a Fail task from code and message", () => {
|
|
182
|
+
const t = failWith("ERR_CODE", "Error message");
|
|
183
|
+
|
|
184
|
+
expect(t._tag).toBe("Fail");
|
|
185
|
+
const failTask = t as { _tag: "Fail"; error: TaskError };
|
|
186
|
+
expect(failTask.error.code).toBe("ERR_CODE");
|
|
187
|
+
expect(failTask.error.message).toBe("Error message");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("works with empty message", () => {
|
|
191
|
+
const t = failWith("ERR", "");
|
|
192
|
+
expect((t as { error: TaskError }).error.message).toBe("");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("works with special characters in message", () => {
|
|
196
|
+
const t = failWith("ERR", 'Special chars: @#$%^&*()[]{}|\\;"<>');
|
|
197
|
+
expect((t as { error: TaskError }).error.message).toContain("Special");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("works with unicode in message", () => {
|
|
201
|
+
const t = failWith("ERR", "Unicode: \u{1F600} \u{1F4A5}");
|
|
202
|
+
expect((t as { error: TaskError }).error.message).toContain("Unicode");
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// =============================================================================
|
|
208
|
+
// Monad Operations
|
|
209
|
+
// =============================================================================
|
|
210
|
+
|
|
211
|
+
describe("Task Monad - Monad Operations", () => {
|
|
212
|
+
describe("flatMap", () => {
|
|
213
|
+
it("chains Pure tasks", () => {
|
|
214
|
+
const t = flatMap(pure(5), (x) => pure(x + 3));
|
|
215
|
+
expect(t._tag).toBe("Pure");
|
|
216
|
+
expect((t as { value: number }).value).toBe(8);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("propagates Fail tasks without calling continuation", () => {
|
|
220
|
+
const error: TaskError = { code: "ERR", message: "error" };
|
|
221
|
+
let called = false;
|
|
222
|
+
const t = flatMap(fail<number>(error), (_x) => {
|
|
223
|
+
called = true;
|
|
224
|
+
return pure(0);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(t._tag).toBe("Fail");
|
|
228
|
+
expect(called).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("returns Fail if continuation fails", () => {
|
|
232
|
+
const error: TaskError = {
|
|
233
|
+
code: "CONT_ERR",
|
|
234
|
+
message: "continuation error",
|
|
235
|
+
};
|
|
236
|
+
const t = flatMap(pure(5), () => fail<number>(error));
|
|
237
|
+
expect(t._tag).toBe("Fail");
|
|
238
|
+
expect((t as { error: TaskError }).error.code).toBe("CONT_ERR");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("chains through Effect tasks", () => {
|
|
242
|
+
const eff: Effect = { _tag: "ReadFile", path: "/test.txt" };
|
|
243
|
+
const t = flatMap(effect<string>(eff), (content) => pure(content.length));
|
|
244
|
+
|
|
245
|
+
expect(t._tag).toBe("Effect");
|
|
246
|
+
const effectTask = t as {
|
|
247
|
+
_tag: "Effect";
|
|
248
|
+
cont: (result: unknown) => Task<number>;
|
|
249
|
+
};
|
|
250
|
+
const continued = effectTask.cont("hello");
|
|
251
|
+
expect((continued as { value: number }).value).toBe(5);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("chains multiple operations", () => {
|
|
255
|
+
const t = flatMap(
|
|
256
|
+
flatMap(pure(2), (x) => pure(x * 3)),
|
|
257
|
+
(x) => pure(x + 1),
|
|
258
|
+
);
|
|
259
|
+
expect((t as { value: number }).value).toBe(7);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("handles nested flatMaps", () => {
|
|
263
|
+
const t = flatMap(pure(1), (a) =>
|
|
264
|
+
flatMap(pure(2), (b) => flatMap(pure(3), (c) => pure(a + b + c))),
|
|
265
|
+
);
|
|
266
|
+
expect((t as { value: number }).value).toBe(6);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe("map", () => {
|
|
271
|
+
it("transforms the value of a Pure task", () => {
|
|
272
|
+
const t = map(pure(10), (x) => x * 2);
|
|
273
|
+
expect(t._tag).toBe("Pure");
|
|
274
|
+
expect((t as { value: number }).value).toBe(20);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("propagates Fail tasks without calling function", () => {
|
|
278
|
+
const error: TaskError = { code: "ERR", message: "error" };
|
|
279
|
+
let called = false;
|
|
280
|
+
const t = map(fail<number>(error), (_x) => {
|
|
281
|
+
called = true;
|
|
282
|
+
return 0;
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
expect(t._tag).toBe("Fail");
|
|
286
|
+
expect(called).toBe(false);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("maps through Effect tasks", () => {
|
|
290
|
+
const eff: Effect = { _tag: "ReadFile", path: "/test.txt" };
|
|
291
|
+
const t = map(effect<string>(eff), (s) => s.toUpperCase());
|
|
292
|
+
|
|
293
|
+
expect(t._tag).toBe("Effect");
|
|
294
|
+
const effectTask = t as {
|
|
295
|
+
cont: (result: unknown) => Task<string>;
|
|
296
|
+
};
|
|
297
|
+
const continued = effectTask.cont("hello");
|
|
298
|
+
expect((continued as { value: string }).value).toBe("HELLO");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("can transform types", () => {
|
|
302
|
+
const t = map(pure(42), (n) => String(n));
|
|
303
|
+
expect((t as { value: string }).value).toBe("42");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("can transform to complex types", () => {
|
|
307
|
+
const t = map(pure("hello"), (s) => ({
|
|
308
|
+
length: s.length,
|
|
309
|
+
upper: s.toUpperCase(),
|
|
310
|
+
}));
|
|
311
|
+
expect((t as { value: { length: number; upper: string } }).value).toEqual(
|
|
312
|
+
{
|
|
313
|
+
length: 5,
|
|
314
|
+
upper: "HELLO",
|
|
315
|
+
},
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("handles identity function", () => {
|
|
320
|
+
const t = map(pure(42), (x) => x);
|
|
321
|
+
expect((t as { value: number }).value).toBe(42);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("handles constant function", () => {
|
|
325
|
+
const t = map(pure(42), () => "constant");
|
|
326
|
+
expect((t as { value: string }).value).toBe("constant");
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe("ap", () => {
|
|
331
|
+
it("applies a function in a task to a value in a task", () => {
|
|
332
|
+
const fnTask = pure((x: number) => x * 2);
|
|
333
|
+
const valTask = pure(21);
|
|
334
|
+
const t = ap(fnTask, valTask);
|
|
335
|
+
|
|
336
|
+
expect(t._tag).toBe("Pure");
|
|
337
|
+
expect((t as { value: number }).value).toBe(42);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("propagates failure from function task", () => {
|
|
341
|
+
const error: TaskError = { code: "FN_ERR", message: "function error" };
|
|
342
|
+
const fnTask = fail<(x: number) => number>(error);
|
|
343
|
+
const valTask = pure(21);
|
|
344
|
+
const t = ap(fnTask, valTask);
|
|
345
|
+
|
|
346
|
+
expect(t._tag).toBe("Fail");
|
|
347
|
+
expect((t as { error: TaskError }).error.code).toBe("FN_ERR");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("propagates failure from value task", () => {
|
|
351
|
+
const error: TaskError = { code: "VAL_ERR", message: "value error" };
|
|
352
|
+
const fnTask = pure((x: number) => x * 2);
|
|
353
|
+
const valTask = fail<number>(error);
|
|
354
|
+
const t = ap(fnTask, valTask);
|
|
355
|
+
|
|
356
|
+
expect(t._tag).toBe("Fail");
|
|
357
|
+
expect((t as { error: TaskError }).error.code).toBe("VAL_ERR");
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("works with multi-argument functions via currying", () => {
|
|
361
|
+
const add = (a: number) => (b: number) => a + b;
|
|
362
|
+
const t1 = ap(pure(add), pure(10));
|
|
363
|
+
const t2 = ap(t1, pure(5));
|
|
364
|
+
|
|
365
|
+
expect((t2 as { value: number }).value).toBe(15);
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
describe("recover", () => {
|
|
370
|
+
it("does not affect Pure tasks", () => {
|
|
371
|
+
const t = recover(pure(42), () => pure(0));
|
|
372
|
+
expect(t._tag).toBe("Pure");
|
|
373
|
+
expect((t as { value: number }).value).toBe(42);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("recovers from Fail tasks", () => {
|
|
377
|
+
const error: TaskError = { code: "ERR", message: "error" };
|
|
378
|
+
const t = recover(fail<number>(error), () => pure(99));
|
|
379
|
+
|
|
380
|
+
expect(t._tag).toBe("Pure");
|
|
381
|
+
expect((t as { value: number }).value).toBe(99);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("provides the error to the handler", () => {
|
|
385
|
+
const error: TaskError = { code: "ERR_42", message: "error 42" };
|
|
386
|
+
const t = recover(fail<string>(error), (e) =>
|
|
387
|
+
pure(`recovered: ${e.code}`),
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
expect((t as { value: string }).value).toBe("recovered: ERR_42");
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("can return another Fail from handler", () => {
|
|
394
|
+
const error1: TaskError = { code: "ERR1", message: "first" };
|
|
395
|
+
const error2: TaskError = { code: "ERR2", message: "second" };
|
|
396
|
+
const t = recover(fail<number>(error1), () => fail<number>(error2));
|
|
397
|
+
|
|
398
|
+
expect(t._tag).toBe("Fail");
|
|
399
|
+
expect((t as { error: TaskError }).error.code).toBe("ERR2");
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("propagates through Effect tasks", () => {
|
|
403
|
+
const eff: Effect = { _tag: "ReadFile", path: "/test.txt" };
|
|
404
|
+
const t = recover(effect<string>(eff), () => pure("recovered"));
|
|
405
|
+
|
|
406
|
+
expect(t._tag).toBe("Effect");
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("handles nested recover calls", () => {
|
|
410
|
+
const error: TaskError = { code: "ERR", message: "error" };
|
|
411
|
+
const t = recover(
|
|
412
|
+
recover(fail<number>(error), () =>
|
|
413
|
+
fail<number>({ code: "ERR2", message: "second" }),
|
|
414
|
+
),
|
|
415
|
+
() => pure(42),
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
expect((t as { value: number }).value).toBe(42);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe("mapError", () => {
|
|
423
|
+
it("does not affect Pure tasks", () => {
|
|
424
|
+
const t = mapError(pure(42), (e) => ({ ...e, code: "MODIFIED" }));
|
|
425
|
+
expect(t._tag).toBe("Pure");
|
|
426
|
+
expect((t as { value: number }).value).toBe(42);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it("transforms the error of Fail tasks", () => {
|
|
430
|
+
const error: TaskError = { code: "ORIGINAL", message: "original" };
|
|
431
|
+
const t = mapError(fail<number>(error), (e) => ({
|
|
432
|
+
...e,
|
|
433
|
+
code: "MODIFIED",
|
|
434
|
+
message: `wrapped: ${e.message}`,
|
|
435
|
+
}));
|
|
436
|
+
|
|
437
|
+
expect(t._tag).toBe("Fail");
|
|
438
|
+
const failTask = t as { error: TaskError };
|
|
439
|
+
expect(failTask.error.code).toBe("MODIFIED");
|
|
440
|
+
expect(failTask.error.message).toBe("wrapped: original");
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it("propagates through Effect tasks", () => {
|
|
444
|
+
const eff: Effect = { _tag: "ReadFile", path: "/test.txt" };
|
|
445
|
+
const t = mapError(effect<string>(eff), (e) => ({
|
|
446
|
+
...e,
|
|
447
|
+
code: "MODIFIED",
|
|
448
|
+
}));
|
|
449
|
+
|
|
450
|
+
expect(t._tag).toBe("Effect");
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("can add context to errors", () => {
|
|
454
|
+
const error: TaskError = { code: "ERR", message: "error" };
|
|
455
|
+
const t = mapError(fail<number>(error), (e) => ({
|
|
456
|
+
...e,
|
|
457
|
+
context: { operation: "test", timestamp: 12345 },
|
|
458
|
+
}));
|
|
459
|
+
|
|
460
|
+
const failTask = t as { error: TaskError };
|
|
461
|
+
expect(failTask.error.context).toEqual({
|
|
462
|
+
operation: "test",
|
|
463
|
+
timestamp: 12345,
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// =============================================================================
|
|
470
|
+
// TaskBuilder (Fluent API)
|
|
471
|
+
// =============================================================================
|
|
472
|
+
|
|
473
|
+
describe("Task Monad - TaskBuilder", () => {
|
|
474
|
+
describe("map method", () => {
|
|
475
|
+
it("transforms value fluently", () => {
|
|
476
|
+
const result = of(10)
|
|
477
|
+
.map((x) => x * 2)
|
|
478
|
+
.unwrap();
|
|
479
|
+
expect((result as { value: number }).value).toBe(20);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it("chains multiple maps", () => {
|
|
483
|
+
const result = of(5)
|
|
484
|
+
.map((x) => x + 1)
|
|
485
|
+
.map((x) => x * 2)
|
|
486
|
+
.map((x) => String(x))
|
|
487
|
+
.unwrap();
|
|
488
|
+
|
|
489
|
+
expect((result as { value: string }).value).toBe("12");
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
describe("flatMap method", () => {
|
|
494
|
+
it("chains with task-returning function", () => {
|
|
495
|
+
const result = of(10)
|
|
496
|
+
.flatMap((x) => pure(x * 2))
|
|
497
|
+
.unwrap();
|
|
498
|
+
|
|
499
|
+
expect((result as { value: number }).value).toBe(20);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("allows mixing map and flatMap", () => {
|
|
503
|
+
const result = of(5)
|
|
504
|
+
.map((x) => x + 1)
|
|
505
|
+
.flatMap((x) => pure(x * 2))
|
|
506
|
+
.map((x) => x + 3)
|
|
507
|
+
.unwrap();
|
|
508
|
+
|
|
509
|
+
expect((result as { value: number }).value).toBe(15);
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
describe("chain method", () => {
|
|
514
|
+
it("chains with TaskBuilder-returning function", () => {
|
|
515
|
+
const result = of(5)
|
|
516
|
+
.chain((x) => of(x * 3))
|
|
517
|
+
.chain((x) => of(x + 1))
|
|
518
|
+
.unwrap();
|
|
519
|
+
|
|
520
|
+
expect((result as { value: number }).value).toBe(16);
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
describe("recover method", () => {
|
|
525
|
+
it("recovers from failure", () => {
|
|
526
|
+
const error: TaskError = { code: "ERR", message: "error" };
|
|
527
|
+
const result = task(fail<number>(error))
|
|
528
|
+
.recover(() => pure(42))
|
|
529
|
+
.unwrap();
|
|
530
|
+
|
|
531
|
+
expect((result as { value: number }).value).toBe(42);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it("does not affect successful tasks", () => {
|
|
535
|
+
const result = of(100)
|
|
536
|
+
.recover(() => pure(0))
|
|
537
|
+
.unwrap();
|
|
538
|
+
|
|
539
|
+
expect((result as { value: number }).value).toBe(100);
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
describe("mapError method", () => {
|
|
544
|
+
it("transforms errors", () => {
|
|
545
|
+
const error: TaskError = { code: "ORIG", message: "original" };
|
|
546
|
+
const result = task(fail<number>(error))
|
|
547
|
+
.mapError((e) => ({ ...e, code: "MODIFIED" }))
|
|
548
|
+
.unwrap();
|
|
549
|
+
|
|
550
|
+
expect(result._tag).toBe("Fail");
|
|
551
|
+
expect((result as { error: TaskError }).error.code).toBe("MODIFIED");
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
describe("tap method", () => {
|
|
556
|
+
it("executes side effect without changing value", () => {
|
|
557
|
+
let _sideEffect = 0;
|
|
558
|
+
const result = of(42)
|
|
559
|
+
.tap((x) => {
|
|
560
|
+
_sideEffect = x;
|
|
561
|
+
return pure(undefined);
|
|
562
|
+
})
|
|
563
|
+
.unwrap();
|
|
564
|
+
|
|
565
|
+
expect((result as { value: number }).value).toBe(42);
|
|
566
|
+
// Note: side effect happens during dry-run/execution, not task construction
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
describe("andThen method", () => {
|
|
571
|
+
it("sequences tasks discarding first result", () => {
|
|
572
|
+
const result = of(1).andThen(pure(2)).andThen(pure(3)).unwrap();
|
|
573
|
+
|
|
574
|
+
expect((result as { value: number }).value).toBe(3);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it("propagates failure", () => {
|
|
578
|
+
const error: TaskError = { code: "ERR", message: "error" };
|
|
579
|
+
const result = of(1)
|
|
580
|
+
.andThen(fail<number>(error))
|
|
581
|
+
.andThen(pure(3))
|
|
582
|
+
.unwrap();
|
|
583
|
+
|
|
584
|
+
expect(result._tag).toBe("Fail");
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
describe("unwrap method", () => {
|
|
589
|
+
it("extracts the underlying task", () => {
|
|
590
|
+
const builder = of(42);
|
|
591
|
+
const task = builder.unwrap();
|
|
592
|
+
|
|
593
|
+
expect(task._tag).toBe("Pure");
|
|
594
|
+
expect((task as { value: number }).value).toBe(42);
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
describe("task function", () => {
|
|
599
|
+
it("wraps existing task in builder", () => {
|
|
600
|
+
const original = pure(42);
|
|
601
|
+
const builder = task(original);
|
|
602
|
+
|
|
603
|
+
expect(builder.unwrap()).toBe(original);
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
describe("of function", () => {
|
|
608
|
+
it("creates TaskBuilder from pure value", () => {
|
|
609
|
+
const builder = of(42);
|
|
610
|
+
const result = builder.unwrap();
|
|
611
|
+
|
|
612
|
+
expect(result._tag).toBe("Pure");
|
|
613
|
+
expect((result as { value: number }).value).toBe(42);
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// =============================================================================
|
|
619
|
+
// Type Guards
|
|
620
|
+
// =============================================================================
|
|
621
|
+
|
|
622
|
+
describe("Task Monad - Type Guards", () => {
|
|
623
|
+
describe("isPure", () => {
|
|
624
|
+
it("returns true for Pure tasks", () => {
|
|
625
|
+
expect(isPure(pure(42))).toBe(true);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it("returns false for Fail tasks", () => {
|
|
629
|
+
expect(isPure(fail({ code: "ERR", message: "error" }))).toBe(false);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it("returns false for Effect tasks", () => {
|
|
633
|
+
const eff: Effect = { _tag: "ReadFile", path: "/test.txt" };
|
|
634
|
+
expect(isPure(effect(eff))).toBe(false);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it("narrows type correctly", () => {
|
|
638
|
+
const t = pure(42);
|
|
639
|
+
if (isPure(t)) {
|
|
640
|
+
// TypeScript should allow access to value
|
|
641
|
+
expect(t.value).toBe(42);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
describe("isFailed", () => {
|
|
647
|
+
it("returns true for Fail tasks", () => {
|
|
648
|
+
expect(isFailed(fail({ code: "ERR", message: "error" }))).toBe(true);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it("returns false for Pure tasks", () => {
|
|
652
|
+
expect(isFailed(pure(42))).toBe(false);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it("returns false for Effect tasks", () => {
|
|
656
|
+
const eff: Effect = { _tag: "ReadFile", path: "/test.txt" };
|
|
657
|
+
expect(isFailed(effect(eff))).toBe(false);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it("narrows type correctly", () => {
|
|
661
|
+
const t = fail({ code: "ERR", message: "error" });
|
|
662
|
+
if (isFailed(t)) {
|
|
663
|
+
// TypeScript should allow access to error
|
|
664
|
+
expect(t.error.code).toBe("ERR");
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
describe("hasEffects", () => {
|
|
670
|
+
it("returns true for Effect tasks", () => {
|
|
671
|
+
const eff: Effect = { _tag: "ReadFile", path: "/test.txt" };
|
|
672
|
+
expect(hasEffects(effect(eff))).toBe(true);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it("returns false for Pure tasks", () => {
|
|
676
|
+
expect(hasEffects(pure(42))).toBe(false);
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it("returns false for Fail tasks", () => {
|
|
680
|
+
expect(hasEffects(fail({ code: "ERR", message: "error" }))).toBe(false);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it("narrows type correctly", () => {
|
|
684
|
+
const eff: Effect = { _tag: "ReadFile", path: "/test.txt" };
|
|
685
|
+
const t = effect<string>(eff);
|
|
686
|
+
if (hasEffects(t)) {
|
|
687
|
+
// TypeScript should allow access to effect and cont
|
|
688
|
+
expect(t.effect._tag).toBe("ReadFile");
|
|
689
|
+
expect(typeof t.cont).toBe("function");
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
// =============================================================================
|
|
696
|
+
// Monad Laws
|
|
697
|
+
// =============================================================================
|
|
698
|
+
|
|
699
|
+
describe("Task Monad - Monad Laws", () => {
|
|
700
|
+
// Left identity: pure(a) >>= f ≡ f(a)
|
|
701
|
+
describe("Left Identity", () => {
|
|
702
|
+
it("pure(a) >>= f ≡ f(a) for numbers", () => {
|
|
703
|
+
const f = (x: number) => pure(x * 2);
|
|
704
|
+
const a = 21;
|
|
705
|
+
|
|
706
|
+
const left = flatMap(pure(a), f);
|
|
707
|
+
const right = f(a);
|
|
708
|
+
|
|
709
|
+
expect((left as { value: number }).value).toBe(
|
|
710
|
+
(right as { value: number }).value,
|
|
711
|
+
);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it("pure(a) >>= f ≡ f(a) for strings", () => {
|
|
715
|
+
const f = (x: string) => pure(x.toUpperCase());
|
|
716
|
+
const a = "hello";
|
|
717
|
+
|
|
718
|
+
const left = flatMap(pure(a), f);
|
|
719
|
+
const right = f(a);
|
|
720
|
+
|
|
721
|
+
expect((left as { value: string }).value).toBe(
|
|
722
|
+
(right as { value: string }).value,
|
|
723
|
+
);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it("pure(a) >>= f ≡ f(a) when f returns fail", () => {
|
|
727
|
+
const f = (_x: number) => fail<number>({ code: "ERR", message: "error" });
|
|
728
|
+
const a = 42;
|
|
729
|
+
|
|
730
|
+
const left = flatMap(pure(a), f);
|
|
731
|
+
const right = f(a);
|
|
732
|
+
|
|
733
|
+
expect(left._tag).toBe(right._tag);
|
|
734
|
+
expect((left as { error: TaskError }).error.code).toBe(
|
|
735
|
+
(right as { error: TaskError }).error.code,
|
|
736
|
+
);
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
// Right identity: m >>= pure ≡ m
|
|
741
|
+
describe("Right Identity", () => {
|
|
742
|
+
it("m >>= pure ≡ m for Pure tasks", () => {
|
|
743
|
+
const m = pure(42);
|
|
744
|
+
const result = flatMap(m, pure);
|
|
745
|
+
|
|
746
|
+
expect((result as { value: number }).value).toBe(
|
|
747
|
+
(m as { value: number }).value,
|
|
748
|
+
);
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it("m >>= pure ≡ m for Fail tasks", () => {
|
|
752
|
+
const error: TaskError = { code: "ERR", message: "error" };
|
|
753
|
+
const m = fail<number>(error);
|
|
754
|
+
const result = flatMap(m, pure);
|
|
755
|
+
|
|
756
|
+
expect(result._tag).toBe("Fail");
|
|
757
|
+
expect((result as { error: TaskError }).error.code).toBe(error.code);
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
// Associativity: (m >>= f) >>= g ≡ m >>= (\x -> f(x) >>= g)
|
|
762
|
+
describe("Associativity", () => {
|
|
763
|
+
it("(m >>= f) >>= g ≡ m >>= (x => f(x) >>= g) for Pure tasks", () => {
|
|
764
|
+
const m = pure(10);
|
|
765
|
+
const f = (x: number) => pure(x + 5);
|
|
766
|
+
const g = (x: number) => pure(x * 2);
|
|
767
|
+
|
|
768
|
+
const left = flatMap(flatMap(m, f), g);
|
|
769
|
+
const right = flatMap(m, (x) => flatMap(f(x), g));
|
|
770
|
+
|
|
771
|
+
expect((left as { value: number }).value).toBe(
|
|
772
|
+
(right as { value: number }).value,
|
|
773
|
+
);
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
it("associativity holds with failure in first function", () => {
|
|
777
|
+
const m = pure(10);
|
|
778
|
+
const f = (_x: number) => fail<number>({ code: "ERR", message: "error" });
|
|
779
|
+
const g = (x: number) => pure(x * 2);
|
|
780
|
+
|
|
781
|
+
const left = flatMap(flatMap(m, f), g);
|
|
782
|
+
const right = flatMap(m, (x) => flatMap(f(x), g));
|
|
783
|
+
|
|
784
|
+
expect(left._tag).toBe(right._tag);
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
it("associativity holds with failure in second function", () => {
|
|
788
|
+
const m = pure(10);
|
|
789
|
+
const f = (x: number) => pure(x + 5);
|
|
790
|
+
const g = (_x: number) => fail<number>({ code: "ERR", message: "error" });
|
|
791
|
+
|
|
792
|
+
const left = flatMap(flatMap(m, f), g);
|
|
793
|
+
const right = flatMap(m, (x) => flatMap(f(x), g));
|
|
794
|
+
|
|
795
|
+
expect(left._tag).toBe(right._tag);
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
// =============================================================================
|
|
801
|
+
// Functor Laws
|
|
802
|
+
// =============================================================================
|
|
803
|
+
|
|
804
|
+
describe("Task Monad - Functor Laws", () => {
|
|
805
|
+
// Identity: map(id) ≡ id
|
|
806
|
+
describe("Identity", () => {
|
|
807
|
+
it("map(id) ≡ id for Pure tasks", () => {
|
|
808
|
+
const m = pure(42);
|
|
809
|
+
const result = map(m, (x) => x);
|
|
810
|
+
|
|
811
|
+
expect((result as { value: number }).value).toBe(
|
|
812
|
+
(m as { value: number }).value,
|
|
813
|
+
);
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
it("map(id) ≡ id for Fail tasks", () => {
|
|
817
|
+
const error: TaskError = { code: "ERR", message: "error" };
|
|
818
|
+
const m = fail<number>(error);
|
|
819
|
+
const result = map(m, (x) => x);
|
|
820
|
+
|
|
821
|
+
expect(result._tag).toBe("Fail");
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
// Composition: map(f . g) ≡ map(f) . map(g)
|
|
826
|
+
describe("Composition", () => {
|
|
827
|
+
it("map(f . g) ≡ map(f) . map(g)", () => {
|
|
828
|
+
const m = pure(10);
|
|
829
|
+
const f = (x: number) => x + 5;
|
|
830
|
+
const g = (x: number) => x * 2;
|
|
831
|
+
|
|
832
|
+
const left = map(m, (x) => f(g(x)));
|
|
833
|
+
const right = map(map(m, g), f);
|
|
834
|
+
|
|
835
|
+
expect((left as { value: number }).value).toBe(
|
|
836
|
+
(right as { value: number }).value,
|
|
837
|
+
);
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
// =============================================================================
|
|
843
|
+
// Edge Cases
|
|
844
|
+
// =============================================================================
|
|
845
|
+
|
|
846
|
+
describe("Task Monad - Edge Cases", () => {
|
|
847
|
+
describe("Empty and null handling", () => {
|
|
848
|
+
it("handles empty string", () => {
|
|
849
|
+
const t = pure("");
|
|
850
|
+
expect((t as { value: string }).value).toBe("");
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it("handles empty array", () => {
|
|
854
|
+
const t = pure([]);
|
|
855
|
+
expect((t as { value: unknown[] }).value).toEqual([]);
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
it("handles empty object", () => {
|
|
859
|
+
const t = pure({});
|
|
860
|
+
expect((t as { value: object }).value).toEqual({});
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
it("handles zero", () => {
|
|
864
|
+
const t = pure(0);
|
|
865
|
+
expect((t as { value: number }).value).toBe(0);
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
it("handles negative numbers", () => {
|
|
869
|
+
const t = pure(-42);
|
|
870
|
+
expect((t as { value: number }).value).toBe(-42);
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
it("handles Infinity", () => {
|
|
874
|
+
const t = pure(Infinity);
|
|
875
|
+
expect((t as { value: number }).value).toBe(Infinity);
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
it("handles NaN", () => {
|
|
879
|
+
const t = pure(NaN);
|
|
880
|
+
expect((t as { value: number }).value).toBeNaN();
|
|
881
|
+
});
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
describe("Deep nesting", () => {
|
|
885
|
+
it("handles deeply nested flatMap", () => {
|
|
886
|
+
let t: Task<number> = pure(0);
|
|
887
|
+
for (let i = 0; i < 100; i++) {
|
|
888
|
+
t = flatMap(t, (x) => pure(x + 1));
|
|
889
|
+
}
|
|
890
|
+
expect((t as { value: number }).value).toBe(100);
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
it("handles deeply nested map", () => {
|
|
894
|
+
let t: Task<number> = pure(0);
|
|
895
|
+
for (let i = 0; i < 100; i++) {
|
|
896
|
+
t = map(t, (x) => x + 1);
|
|
897
|
+
}
|
|
898
|
+
expect((t as { value: number }).value).toBe(100);
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
it("handles deeply nested TaskBuilder chains", () => {
|
|
902
|
+
let builder = of(0);
|
|
903
|
+
for (let i = 0; i < 100; i++) {
|
|
904
|
+
builder = builder.map((x) => x + 1);
|
|
905
|
+
}
|
|
906
|
+
expect((builder.unwrap() as { value: number }).value).toBe(100);
|
|
907
|
+
});
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
describe("Type coercion", () => {
|
|
911
|
+
it("preserves type through transformations", () => {
|
|
912
|
+
const t = map(pure({ x: 1 }), (obj) => obj.x);
|
|
913
|
+
expect((t as { value: number }).value).toBe(1);
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
it("handles union types", () => {
|
|
917
|
+
const t = pure<string | number>(42);
|
|
918
|
+
expect((t as { value: string | number }).value).toBe(42);
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
it("handles generic types", () => {
|
|
922
|
+
const createTask = <T>(value: T) => pure(value);
|
|
923
|
+
const t = createTask({ nested: { value: 42 } });
|
|
924
|
+
expect(
|
|
925
|
+
(t as { value: { nested: { value: number } } }).value.nested.value,
|
|
926
|
+
).toBe(42);
|
|
927
|
+
});
|
|
928
|
+
});
|
|
929
|
+
});
|