@temporal-contract/contract 0.0.1

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.
@@ -0,0 +1,625 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { defineContract } from "./builder.js";
3
+ import { z } from "zod";
4
+
5
+ describe("Contract Builder", () => {
6
+ describe("defineContract", () => {
7
+ it("should create a contract with workflows", () => {
8
+ const contract = defineContract({
9
+ taskQueue: "test-queue",
10
+ workflows: {
11
+ processOrder: {
12
+ input: z.object({ orderId: z.string() }),
13
+ output: z.object({ status: z.string() }),
14
+ },
15
+ },
16
+ });
17
+
18
+ expect(contract.taskQueue).toBe("test-queue");
19
+ expect(contract.workflows.processOrder).toBeDefined();
20
+ expect(contract.workflows.processOrder.input).toBeDefined();
21
+ expect(contract.workflows.processOrder.output).toBeDefined();
22
+ });
23
+
24
+ it("should create a contract with global activities", () => {
25
+ const contract = defineContract({
26
+ taskQueue: "test-queue",
27
+ workflows: {
28
+ simpleWorkflow: {
29
+ input: z.object({ value: z.string() }),
30
+ output: z.object({ result: z.string() }),
31
+ },
32
+ },
33
+ activities: {
34
+ sendEmail: {
35
+ input: z.object({ to: z.string(), subject: z.string(), body: z.string() }),
36
+ output: z.object({ sent: z.boolean() }),
37
+ },
38
+ },
39
+ });
40
+
41
+ expect(contract.activities).toBeDefined();
42
+ expect(contract.activities?.sendEmail).toBeDefined();
43
+ });
44
+
45
+ it("should create a contract with workflow-specific activities", () => {
46
+ const contract = defineContract({
47
+ taskQueue: "test-queue",
48
+ workflows: {
49
+ processOrder: {
50
+ input: z.object({ orderId: z.string() }),
51
+ output: z.object({ status: z.string() }),
52
+ activities: {
53
+ validateInventory: {
54
+ input: z.object({ orderId: z.string() }),
55
+ output: z.object({ available: z.boolean() }),
56
+ },
57
+ processPayment: {
58
+ input: z.object({ amount: z.number() }),
59
+ output: z.object({ transactionId: z.string() }),
60
+ },
61
+ },
62
+ },
63
+ },
64
+ });
65
+
66
+ expect(contract.workflows.processOrder.activities).toBeDefined();
67
+ expect(contract.workflows.processOrder.activities?.validateInventory).toBeDefined();
68
+ expect(contract.workflows.processOrder.activities?.processPayment).toBeDefined();
69
+ });
70
+
71
+ it("should support signals in workflow definitions", () => {
72
+ const contract = defineContract({
73
+ taskQueue: "test-queue",
74
+ workflows: {
75
+ processOrder: {
76
+ input: z.object({ orderId: z.string() }),
77
+ output: z.object({ status: z.string() }),
78
+ signals: {
79
+ cancel: {
80
+ input: z.object({ reason: z.string() }),
81
+ },
82
+ addItem: {
83
+ input: z.object({ itemId: z.string(), quantity: z.number() }),
84
+ },
85
+ },
86
+ },
87
+ },
88
+ });
89
+
90
+ expect(contract.workflows.processOrder.signals).toBeDefined();
91
+ expect(contract.workflows.processOrder.signals?.cancel).toBeDefined();
92
+ expect(contract.workflows.processOrder.signals?.addItem).toBeDefined();
93
+ });
94
+
95
+ it("should support queries in workflow definitions", () => {
96
+ const contract = defineContract({
97
+ taskQueue: "test-queue",
98
+ workflows: {
99
+ processOrder: {
100
+ input: z.object({ orderId: z.string() }),
101
+ output: z.object({ status: z.string() }),
102
+ queries: {
103
+ getStatus: {
104
+ input: z.object({}),
105
+ output: z.object({ status: z.string(), progress: z.number() }),
106
+ },
107
+ getTotal: {
108
+ input: z.object({}),
109
+ output: z.object({ amount: z.number() }),
110
+ },
111
+ },
112
+ },
113
+ },
114
+ });
115
+
116
+ expect(contract.workflows.processOrder.queries).toBeDefined();
117
+ expect(contract.workflows.processOrder.queries?.getStatus).toBeDefined();
118
+ expect(contract.workflows.processOrder.queries?.getTotal).toBeDefined();
119
+ });
120
+
121
+ it("should support updates in workflow definitions", () => {
122
+ const contract = defineContract({
123
+ taskQueue: "test-queue",
124
+ workflows: {
125
+ processOrder: {
126
+ input: z.object({ orderId: z.string() }),
127
+ output: z.object({ status: z.string() }),
128
+ updates: {
129
+ updateDiscount: {
130
+ input: z.object({ percentage: z.number() }),
131
+ output: z.object({ newTotal: z.number() }),
132
+ },
133
+ updateShippingAddress: {
134
+ input: z.object({ address: z.string() }),
135
+ output: z.object({ updated: z.boolean() }),
136
+ },
137
+ },
138
+ },
139
+ },
140
+ });
141
+
142
+ expect(contract.workflows.processOrder.updates).toBeDefined();
143
+ expect(contract.workflows.processOrder.updates?.updateDiscount).toBeDefined();
144
+ expect(contract.workflows.processOrder.updates?.updateShippingAddress).toBeDefined();
145
+ });
146
+
147
+ it("should support multiple workflows", () => {
148
+ const contract = defineContract({
149
+ taskQueue: "test-queue",
150
+ workflows: {
151
+ processOrder: {
152
+ input: z.object({ orderId: z.string() }),
153
+ output: z.object({ status: z.string() }),
154
+ },
155
+ cancelOrder: {
156
+ input: z.object({ orderId: z.string(), reason: z.string() }),
157
+ output: z.object({ cancelled: z.boolean() }),
158
+ },
159
+ refundOrder: {
160
+ input: z.object({ orderId: z.string(), amount: z.number() }),
161
+ output: z.object({ refunded: z.boolean(), transactionId: z.string() }),
162
+ },
163
+ },
164
+ });
165
+
166
+ expect(Object.keys(contract.workflows)).toHaveLength(3);
167
+ expect(contract.workflows.processOrder).toBeDefined();
168
+ expect(contract.workflows.cancelOrder).toBeDefined();
169
+ expect(contract.workflows.refundOrder).toBeDefined();
170
+ });
171
+
172
+ it("should validate single parameter pattern", () => {
173
+ const contract = defineContract({
174
+ taskQueue: "test-queue",
175
+ workflows: {
176
+ simpleWorkflow: {
177
+ input: z.string(), // Single primitive
178
+ output: z.object({ result: z.string() }),
179
+ },
180
+ complexWorkflow: {
181
+ input: z.object({
182
+ // Object with multiple properties
183
+ orderId: z.string(),
184
+ customerId: z.string(),
185
+ amount: z.number(),
186
+ }),
187
+ output: z.object({ success: z.boolean() }),
188
+ },
189
+ arrayWorkflow: {
190
+ input: z.array(
191
+ z.object({
192
+ // Array of objects
193
+ id: z.string(),
194
+ value: z.number(),
195
+ }),
196
+ ),
197
+ output: z.object({ processed: z.number() }),
198
+ },
199
+ },
200
+ });
201
+
202
+ expect(contract.workflows.simpleWorkflow.input).toBeDefined();
203
+ expect(contract.workflows.complexWorkflow.input).toBeDefined();
204
+ expect(contract.workflows.arrayWorkflow.input).toBeDefined();
205
+ });
206
+
207
+ it("should preserve type information", () => {
208
+ const contract = defineContract({
209
+ taskQueue: "my-queue",
210
+ workflows: {
211
+ myWorkflow: {
212
+ input: z.object({ id: z.string() }),
213
+ output: z.object({ success: z.boolean() }),
214
+ },
215
+ },
216
+ });
217
+
218
+ // Type assertions to verify inference works
219
+ const taskQueue: string = contract.taskQueue;
220
+ expect(taskQueue).toBe("my-queue");
221
+ });
222
+ });
223
+
224
+ describe("defineContract validation", () => {
225
+ it("should throw when taskQueue is empty", () => {
226
+ expect(() =>
227
+ defineContract({
228
+ taskQueue: "",
229
+ workflows: {
230
+ test: {
231
+ input: z.object({}),
232
+ output: z.object({}),
233
+ },
234
+ },
235
+ }),
236
+ ).toThrow("taskQueue cannot be empty");
237
+ });
238
+
239
+ it("should throw when taskQueue is only whitespace", () => {
240
+ expect(() =>
241
+ defineContract({
242
+ taskQueue: " ",
243
+ workflows: {
244
+ test: {
245
+ input: z.object({}),
246
+ output: z.object({}),
247
+ },
248
+ },
249
+ }),
250
+ ).toThrow("Contract error");
251
+ });
252
+
253
+ it("should throw when no workflows are defined", () => {
254
+ expect(() =>
255
+ defineContract({
256
+ taskQueue: "test",
257
+ workflows: {},
258
+ }),
259
+ ).toThrow("at least one workflow is required");
260
+ });
261
+
262
+ it("should throw when workflow name is invalid", () => {
263
+ expect(() =>
264
+ defineContract({
265
+ taskQueue: "test",
266
+ workflows: {
267
+ "invalid-name": {
268
+ input: z.object({}),
269
+ output: z.object({}),
270
+ },
271
+ },
272
+ }),
273
+ ).toThrow("must be a valid JavaScript identifier");
274
+ });
275
+
276
+ it("should throw when global activity name is invalid", () => {
277
+ expect(() =>
278
+ defineContract({
279
+ taskQueue: "test",
280
+ workflows: {
281
+ test: {
282
+ input: z.object({}),
283
+ output: z.object({}),
284
+ },
285
+ },
286
+ activities: {
287
+ "send-email": {
288
+ input: z.object({}),
289
+ output: z.object({}),
290
+ },
291
+ },
292
+ }),
293
+ ).toThrow("must be a valid JavaScript identifier");
294
+ });
295
+
296
+ it("should throw when workflow activity conflicts with global activity", () => {
297
+ expect(() =>
298
+ defineContract({
299
+ taskQueue: "test",
300
+ workflows: {
301
+ processOrder: {
302
+ input: z.object({}),
303
+ output: z.object({}),
304
+ activities: {
305
+ sendEmail: {
306
+ input: z.object({}),
307
+ output: z.object({}),
308
+ },
309
+ },
310
+ },
311
+ },
312
+ activities: {
313
+ sendEmail: {
314
+ input: z.object({}),
315
+ output: z.object({}),
316
+ },
317
+ },
318
+ }),
319
+ ).toThrow(
320
+ 'workflow "processOrder" has activity "sendEmail" that conflicts with a global activity. Consider renaming the workflow-specific activity or removing the global activity "sendEmail".',
321
+ );
322
+ });
323
+
324
+ it("should throw when signal name is invalid", () => {
325
+ expect(() =>
326
+ defineContract({
327
+ taskQueue: "test",
328
+ workflows: {
329
+ test: {
330
+ input: z.object({}),
331
+ output: z.object({}),
332
+ signals: {
333
+ "cancel-order": {
334
+ input: z.object({}),
335
+ },
336
+ },
337
+ },
338
+ },
339
+ }),
340
+ ).toThrow("must be a valid JavaScript identifier");
341
+ });
342
+
343
+ it("should throw when query name is invalid", () => {
344
+ expect(() =>
345
+ defineContract({
346
+ taskQueue: "test",
347
+ workflows: {
348
+ test: {
349
+ input: z.object({}),
350
+ output: z.object({}),
351
+ queries: {
352
+ "get-status": {
353
+ input: z.object({}),
354
+ output: z.object({}),
355
+ },
356
+ },
357
+ },
358
+ },
359
+ }),
360
+ ).toThrow("must be a valid JavaScript identifier");
361
+ });
362
+
363
+ it("should throw when update name is invalid", () => {
364
+ expect(() =>
365
+ defineContract({
366
+ taskQueue: "test",
367
+ workflows: {
368
+ test: {
369
+ input: z.object({}),
370
+ output: z.object({}),
371
+ updates: {
372
+ "update-amount": {
373
+ input: z.object({}),
374
+ output: z.object({}),
375
+ },
376
+ },
377
+ },
378
+ },
379
+ }),
380
+ ).toThrow("must be a valid JavaScript identifier");
381
+ });
382
+
383
+ it("should allow valid camelCase names", () => {
384
+ expect(() =>
385
+ defineContract({
386
+ taskQueue: "test-queue",
387
+ workflows: {
388
+ processOrder: {
389
+ input: z.object({}),
390
+ output: z.object({}),
391
+ activities: {
392
+ sendEmail: {
393
+ input: z.object({}),
394
+ output: z.object({}),
395
+ },
396
+ },
397
+ signals: {
398
+ cancelOrder: {
399
+ input: z.object({}),
400
+ },
401
+ },
402
+ queries: {
403
+ getStatus: {
404
+ input: z.object({}),
405
+ output: z.object({}),
406
+ },
407
+ },
408
+ updates: {
409
+ updateAmount: {
410
+ input: z.object({}),
411
+ output: z.object({}),
412
+ },
413
+ },
414
+ },
415
+ },
416
+ }),
417
+ ).not.toThrow();
418
+ });
419
+
420
+ it("should allow names with underscores and dollar signs", () => {
421
+ expect(() =>
422
+ defineContract({
423
+ taskQueue: "test",
424
+ workflows: {
425
+ process_order: {
426
+ input: z.object({}),
427
+ output: z.object({}),
428
+ },
429
+ $process: {
430
+ input: z.object({}),
431
+ output: z.object({}),
432
+ },
433
+ },
434
+ activities: {
435
+ send_email: {
436
+ input: z.object({}),
437
+ output: z.object({}),
438
+ },
439
+ $send: {
440
+ input: z.object({}),
441
+ output: z.object({}),
442
+ },
443
+ },
444
+ }),
445
+ ).not.toThrow();
446
+ });
447
+ });
448
+
449
+ describe("Edge Cases", () => {
450
+ it("should handle empty object schemas", () => {
451
+ const contract = defineContract({
452
+ taskQueue: "test",
453
+ workflows: {
454
+ empty: {
455
+ input: z.object({}),
456
+ output: z.object({}),
457
+ },
458
+ },
459
+ });
460
+ expect(contract).toBeDefined();
461
+ expect(contract.workflows.empty.input).toBeDefined();
462
+ });
463
+
464
+ it("should handle void input", () => {
465
+ const contract = defineContract({
466
+ taskQueue: "test",
467
+ workflows: {
468
+ noInput: {
469
+ input: z.void(),
470
+ output: z.object({ result: z.string() }),
471
+ },
472
+ },
473
+ });
474
+ expect(contract).toBeDefined();
475
+ });
476
+
477
+ it("should handle workflows without activities, signals, queries, or updates", () => {
478
+ const contract = defineContract({
479
+ taskQueue: "test",
480
+ workflows: {
481
+ simple: {
482
+ input: z.string(),
483
+ output: z.string(),
484
+ },
485
+ },
486
+ });
487
+ const workflow = contract.workflows.simple;
488
+ expect("activities" in workflow).toBe(false);
489
+ expect("signals" in workflow).toBe(false);
490
+ expect("queries" in workflow).toBe(false);
491
+ expect("updates" in workflow).toBe(false);
492
+ });
493
+
494
+ it("should handle contract without global activities", () => {
495
+ const contract = defineContract({
496
+ taskQueue: "test",
497
+ workflows: {
498
+ test: {
499
+ input: z.object({}),
500
+ output: z.object({}),
501
+ },
502
+ },
503
+ });
504
+ expect("activities" in contract).toBe(false);
505
+ });
506
+
507
+ it("should throw when workflow is missing input", () => {
508
+ expect(() =>
509
+ defineContract({
510
+ taskQueue: "test",
511
+ workflows: {
512
+ // @ts-expect-error - Testing validation with missing input
513
+ test: {
514
+ output: z.object({}),
515
+ },
516
+ },
517
+ }),
518
+ ).toThrow("Contract error");
519
+ });
520
+
521
+ it("should throw when workflow is missing output", () => {
522
+ expect(() =>
523
+ defineContract({
524
+ taskQueue: "test",
525
+ workflows: {
526
+ // @ts-expect-error - Testing validation with missing output
527
+ test: {
528
+ input: z.object({}),
529
+ },
530
+ },
531
+ }),
532
+ ).toThrow("Contract error");
533
+ });
534
+
535
+ it("should throw when activity is missing input", () => {
536
+ expect(() =>
537
+ defineContract({
538
+ taskQueue: "test",
539
+ workflows: {
540
+ test: {
541
+ input: z.object({}),
542
+ output: z.object({}),
543
+ },
544
+ },
545
+ activities: {
546
+ // @ts-expect-error - Testing validation with missing input
547
+ test: {
548
+ output: z.object({}),
549
+ },
550
+ },
551
+ }),
552
+ ).toThrow("Contract error");
553
+ });
554
+
555
+ it("should throw when activity is missing output", () => {
556
+ expect(() =>
557
+ defineContract({
558
+ taskQueue: "test",
559
+ workflows: {
560
+ test: {
561
+ input: z.object({}),
562
+ output: z.object({}),
563
+ },
564
+ },
565
+ activities: {
566
+ // @ts-expect-error - Testing validation with missing output
567
+ test: {
568
+ input: z.object({}),
569
+ },
570
+ },
571
+ }),
572
+ ).toThrow("Contract error");
573
+ });
574
+
575
+ it("should throw when workflow activity name is invalid", () => {
576
+ expect(() =>
577
+ defineContract({
578
+ taskQueue: "test",
579
+ workflows: {
580
+ test: {
581
+ input: z.object({}),
582
+ output: z.object({}),
583
+ activities: {
584
+ "invalid-name": {
585
+ input: z.object({}),
586
+ output: z.object({}),
587
+ },
588
+ },
589
+ },
590
+ },
591
+ }),
592
+ ).toThrow("must be a valid JavaScript identifier");
593
+ });
594
+
595
+ it("should throw when input is not a Zod schema", () => {
596
+ expect(() =>
597
+ defineContract({
598
+ taskQueue: "test",
599
+ workflows: {
600
+ test: {
601
+ // @ts-expect-error - Testing validation with invalid input type
602
+ input: "not a schema",
603
+ output: z.object({}),
604
+ },
605
+ },
606
+ }),
607
+ ).toThrow("Contract error");
608
+ });
609
+
610
+ it("should throw when output is not a Zod schema", () => {
611
+ expect(() =>
612
+ defineContract({
613
+ taskQueue: "test",
614
+ workflows: {
615
+ test: {
616
+ input: z.object({}),
617
+ // @ts-expect-error - Testing validation with invalid output type
618
+ output: { invalid: true },
619
+ },
620
+ },
621
+ }),
622
+ ).toThrow("Contract error");
623
+ });
624
+ });
625
+ });