@voyant-travel/workflows-orchestrator 0.107.10

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.
Files changed (61) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +52 -0
  3. package/README.md +76 -0
  4. package/dist/abort-registry.d.ts +6 -0
  5. package/dist/abort-registry.d.ts.map +1 -0
  6. package/dist/abort-registry.js +37 -0
  7. package/dist/concurrency.d.ts +31 -0
  8. package/dist/concurrency.d.ts.map +1 -0
  9. package/dist/concurrency.js +145 -0
  10. package/dist/drive.d.ts +67 -0
  11. package/dist/drive.d.ts.map +1 -0
  12. package/dist/drive.js +373 -0
  13. package/dist/driver-inmemory.d.ts +30 -0
  14. package/dist/driver-inmemory.d.ts.map +1 -0
  15. package/dist/driver-inmemory.js +394 -0
  16. package/dist/event-router.d.ts +51 -0
  17. package/dist/event-router.d.ts.map +1 -0
  18. package/dist/event-router.js +68 -0
  19. package/dist/http-step-handler.d.ts +25 -0
  20. package/dist/http-step-handler.d.ts.map +1 -0
  21. package/dist/http-step-handler.js +78 -0
  22. package/dist/in-memory-store.d.ts +5 -0
  23. package/dist/in-memory-store.d.ts.map +1 -0
  24. package/dist/in-memory-store.js +41 -0
  25. package/dist/index.d.ts +13 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +22 -0
  28. package/dist/journal-helpers.d.ts +3 -0
  29. package/dist/journal-helpers.d.ts.map +1 -0
  30. package/dist/journal-helpers.js +9 -0
  31. package/dist/orchestrator.d.ts +116 -0
  32. package/dist/orchestrator.d.ts.map +1 -0
  33. package/dist/orchestrator.js +411 -0
  34. package/dist/resume-run.d.ts +40 -0
  35. package/dist/resume-run.d.ts.map +1 -0
  36. package/dist/resume-run.js +119 -0
  37. package/dist/schedule.d.ts +51 -0
  38. package/dist/schedule.d.ts.map +1 -0
  39. package/dist/schedule.js +243 -0
  40. package/dist/testing/driver-compliance.d.ts +58 -0
  41. package/dist/testing/driver-compliance.d.ts.map +1 -0
  42. package/dist/testing/driver-compliance.js +667 -0
  43. package/dist/types.d.ts +182 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +4 -0
  46. package/package.json +51 -0
  47. package/src/__tests__/orchestrator-test-support.ts +18 -0
  48. package/src/abort-registry.ts +41 -0
  49. package/src/concurrency.ts +217 -0
  50. package/src/drive.ts +477 -0
  51. package/src/driver-inmemory.ts +511 -0
  52. package/src/event-router.ts +120 -0
  53. package/src/http-step-handler.ts +112 -0
  54. package/src/in-memory-store.ts +44 -0
  55. package/src/index.ts +73 -0
  56. package/src/journal-helpers.ts +11 -0
  57. package/src/orchestrator.ts +527 -0
  58. package/src/resume-run.ts +162 -0
  59. package/src/schedule.ts +310 -0
  60. package/src/testing/driver-compliance.ts +800 -0
  61. package/src/types.ts +201 -0
@@ -0,0 +1,800 @@
1
+ // Driver compliance suite — the contract every WorkflowDriver must satisfy.
2
+ //
3
+ // `runDriverComplianceSuite(name, makeDriver)` is parameterized over a
4
+ // driver factory. It runs identical assertions against every implementation
5
+ // we ship: InMemory, Mode 2 / Postgres, Mode 1 / CF edge.
6
+ //
7
+ // Importable from a regular `.ts` file so downstream packages
8
+ // (`@voyant-travel/workflows-orchestrator-node`, `-cloudflare`) can run the
9
+ // same suite against their own driver factories without duplicating the
10
+ // assertions. Vitest globals are imported explicitly because this file
11
+ // isn't a `.test.ts` (no auto-injection).
12
+ //
13
+ // Tests that depend on machinery not yet wired (filter matching,
14
+ // time-wheel resume of DATETIME waitpoints) are added as those
15
+ // capabilities land. PR1 covers: register/get manifest, trigger,
16
+ // idempotency dedup, ingestEvent's manifest-not-registered + no-filters
17
+ // paths, ctx.services, basic admin reads, shutdown.
18
+ //
19
+ // Architecture: docs/architecture/workflows-runtime-architecture.md §6.4.
20
+
21
+ import { __resetRegistry, workflow } from "@voyant-travel/workflows"
22
+ import type {
23
+ DriverFactory,
24
+ DriverFactoryDeps,
25
+ ServiceResolver,
26
+ } from "@voyant-travel/workflows/driver"
27
+ // agent-quality: file-size exception -- Shared driver compliance suite intentionally keeps cross-driver behavioral cases together so all drivers run the same contract.
28
+ import type { WorkflowManifest } from "@voyant-travel/workflows/protocol"
29
+ import { beforeEach, describe, expect, test, vi } from "vitest"
30
+
31
+ import { WorkflowConcurrencyRejectedError } from "../concurrency.js"
32
+
33
+ /**
34
+ * Tiny in-memory ServiceResolver builder for compliance tests. Lets a test
35
+ * register named services then assert workflow bodies can resolve them via
36
+ * `ctx.services.resolve(...)`.
37
+ */
38
+ export function makeServiceResolver(entries: Record<string, unknown> = {}): ServiceResolver {
39
+ const map = new Map(Object.entries(entries))
40
+ return {
41
+ resolve<T>(name: string): T {
42
+ if (!map.has(name)) {
43
+ throw new Error(`compliance harness: no service registered under "${name}"`)
44
+ }
45
+ return map.get(name) as T
46
+ },
47
+ has(name: string): boolean {
48
+ return map.has(name)
49
+ },
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Build a `DriverFactoryDeps` value suitable for compliance tests. Captures
55
+ * log lines so individual tests can assert on them when needed. Pass
56
+ * `services` to register specific entries; defaults to an empty resolver.
57
+ */
58
+ export function testFactoryDeps(
59
+ services: ServiceResolver = makeServiceResolver(),
60
+ ): DriverFactoryDeps & { logs: Array<[string, string, object?]> } {
61
+ const logs: Array<[string, string, object?]> = []
62
+ return {
63
+ services,
64
+ logger: (level, msg, data) => logs.push([level, msg, data]),
65
+ logs,
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Build a minimal manifest for tests. The shape matches `WorkflowManifest`;
71
+ * filter-related fields stay empty until the event-router (PR2) lands.
72
+ */
73
+ export function buildTestManifest(versionId = "v_test_001"): WorkflowManifest {
74
+ return {
75
+ schemaVersion: 1,
76
+ projectId: "default",
77
+ versionId,
78
+ builtAt: 1_700_000_000_000,
79
+ builderVersion: "test-0.0.0",
80
+ capabilities: testReleaseCapabilities(),
81
+ workflows: [],
82
+ eventFilters: [],
83
+ diagnostics: [],
84
+ bindings: {},
85
+ environments: { production: {}, preview: {}, development: {} },
86
+ }
87
+ }
88
+
89
+ function testReleaseCapabilities(): WorkflowManifest["capabilities"] {
90
+ return {
91
+ trigger: true,
92
+ events: true,
93
+ schedules: true,
94
+ rerun: true,
95
+ resume: true,
96
+ cancel: true,
97
+ humanApproval: false,
98
+ stepRerun: false,
99
+ }
100
+ }
101
+
102
+ // Per-test counter so workflow ids stay unique across tests in a suite —
103
+ // some persistent stores (Mode 2's Postgres) carry state between tests in
104
+ // the same run, and a duplicate id would HMR-warn at minimum and
105
+ // pollute results at worst.
106
+ let suiteCounter = 0
107
+ function uniqueId(prefix: string): string {
108
+ return `${prefix}-${++suiteCounter}`
109
+ }
110
+
111
+ const DATETIME_WAKEUP_TEST_TIMEOUT_MS = 5_000
112
+
113
+ // ---- Suite options ----
114
+
115
+ /**
116
+ * Opt-in capability flags for drivers that don't share a process with
117
+ * step bodies. Default to `true` because in-process drivers (InMemory,
118
+ * Mode 2) satisfy every contract. Out-of-process drivers (Mode 1 / CF
119
+ * edge — orchestrator and tenant live in separate Worker isolates) opt
120
+ * out of in-process-only assertions like `ctx.services` threading.
121
+ */
122
+ export interface DriverComplianceCapabilities {
123
+ /**
124
+ * When true, the framework's `ModuleContainer` is plumbed to step
125
+ * bodies via `ctx.services`. False for Mode 1, where the orchestrator
126
+ * and tenant are separate Workers and a per-tenant container would
127
+ * have to ship across a serialization boundary.
128
+ */
129
+ servicesThreading?: boolean
130
+ /**
131
+ * When true, `admin.listRuns(...)` returns runs the driver knows about.
132
+ * False for self-host Mode 1, which has no native cross-run query
133
+ * layer (per architecture doc §8.3) — `listRuns` exists but returns
134
+ * an empty page; voyant-cloud provides an index in its repo.
135
+ */
136
+ crossRunQueries?: boolean
137
+ /**
138
+ * When true, the driver under test runs an in-process DATETIME wakeup
139
+ * loop/timer during compliance tests. False for drivers whose compliance
140
+ * harness intentionally disables or fakes the time wheel.
141
+ */
142
+ autoDatetimeWakeups?: boolean
143
+ /**
144
+ * When true, workflow-level `WorkflowConfig.concurrency` is enforced
145
+ * for in-process workflow definitions. False for Mode 1 / Cloudflare
146
+ * until it grows a cross-run coordination DO.
147
+ */
148
+ workflowConcurrency?: boolean
149
+ }
150
+
151
+ // ---- The parameterized contract ----
152
+
153
+ export function runDriverComplianceSuite(
154
+ name: string,
155
+ makeFactory: () => DriverFactory,
156
+ capabilities: DriverComplianceCapabilities = {},
157
+ ): void {
158
+ const servicesThreading = capabilities.servicesThreading ?? true
159
+ const crossRunQueries = capabilities.crossRunQueries ?? true
160
+ const autoDatetimeWakeups = capabilities.autoDatetimeWakeups ?? false
161
+ const workflowConcurrency = capabilities.workflowConcurrency ?? true
162
+ describe(`${name} driver compliance`, () => {
163
+ beforeEach(() => {
164
+ __resetRegistry()
165
+ })
166
+
167
+ describe("registerManifest / getManifest", () => {
168
+ test("returns versionId from the supplied manifest", async () => {
169
+ const driver = makeFactory()(testFactoryDeps())
170
+ const manifest = buildTestManifest(uniqueId("v"))
171
+ const result = await driver.registerManifest({
172
+ environment: "production",
173
+ manifest,
174
+ })
175
+ expect(result.versionId).toBe(manifest.versionId)
176
+ })
177
+
178
+ test("getManifest returns the registered manifest for the right environment", async () => {
179
+ const driver = makeFactory()(testFactoryDeps())
180
+ const manifest = buildTestManifest(uniqueId("v"))
181
+ await driver.registerManifest({ environment: "production", manifest })
182
+
183
+ const got = await driver.getManifest({ environment: "production" })
184
+ expect(got).toBeTruthy()
185
+ expect(got?.versionId).toBe(manifest.versionId)
186
+ })
187
+
188
+ test("getManifest returns null when no manifest is registered for an environment", async () => {
189
+ const driver = makeFactory()(testFactoryDeps())
190
+ const got = await driver.getManifest({ environment: "preview" })
191
+ expect(got).toBeNull()
192
+ })
193
+
194
+ test("registerManifest is idempotent: same versionId returns the same value", async () => {
195
+ const driver = makeFactory()(testFactoryDeps())
196
+ const manifest = buildTestManifest(uniqueId("v"))
197
+ const first = await driver.registerManifest({ environment: "production", manifest })
198
+ const second = await driver.registerManifest({ environment: "production", manifest })
199
+ expect(first.versionId).toBe(second.versionId)
200
+ expect(first.versionId).toBe(manifest.versionId)
201
+ })
202
+
203
+ test("manifests are environment-scoped (production vs preview don't bleed)", async () => {
204
+ const driver = makeFactory()(testFactoryDeps())
205
+ const prodManifest = buildTestManifest(uniqueId("v_prod"))
206
+ const prevManifest = buildTestManifest(uniqueId("v_prev"))
207
+ await driver.registerManifest({ environment: "production", manifest: prodManifest })
208
+ await driver.registerManifest({ environment: "preview", manifest: prevManifest })
209
+
210
+ const prod = await driver.getManifest({ environment: "production" })
211
+ const prev = await driver.getManifest({ environment: "preview" })
212
+ expect(prod?.versionId).toBe(prodManifest.versionId)
213
+ expect(prev?.versionId).toBe(prevManifest.versionId)
214
+ })
215
+ })
216
+
217
+ describe("trigger", () => {
218
+ test("creates a run that completes for a trivial workflow body", async () => {
219
+ const driver = makeFactory()(testFactoryDeps())
220
+ const wfId = uniqueId("compliance-double")
221
+ const wf = workflow<{ n: number }, { doubled: number }>({
222
+ id: wfId,
223
+ async run(input) {
224
+ return { doubled: input.n * 2 }
225
+ },
226
+ })
227
+
228
+ const run = await driver.trigger(wf, { n: 21 })
229
+ expect(run.workflowId).toBe(wfId)
230
+ expect(run.status).toBe("completed")
231
+ })
232
+
233
+ test("respects environment from TriggerOptions", async () => {
234
+ const driver = makeFactory()(testFactoryDeps())
235
+ const wf = workflow<Record<string, never>, void>({
236
+ id: uniqueId("compliance-env"),
237
+ async run() {},
238
+ })
239
+
240
+ const run = await driver.trigger(wf, {}, { environment: "preview" })
241
+ expect(run.status).toBe("completed")
242
+ })
243
+
244
+ test("delay parks the run instead of invoking the workflow immediately", async () => {
245
+ const driver = makeFactory()(testFactoryDeps())
246
+ let invocationCount = 0
247
+ const wf = workflow<Record<string, never>, void>({
248
+ id: uniqueId("compliance-delay"),
249
+ async run() {
250
+ invocationCount++
251
+ },
252
+ })
253
+
254
+ const run = await driver.trigger(wf, {}, { delay: "1s" })
255
+ expect(run.status).toBe("waiting")
256
+ expect(invocationCount).toBe(0)
257
+ })
258
+
259
+ test.runIf(autoDatetimeWakeups)(
260
+ "delay continues scheduling later DATETIME waits after the trigger delay fires",
261
+ async () => {
262
+ const driver = makeFactory()(testFactoryDeps())
263
+ const completed = vi.fn()
264
+ const wf = workflow<Record<string, never>, void>({
265
+ id: uniqueId("compliance-delay-then-sleep"),
266
+ async run(_, ctx) {
267
+ await ctx.sleep("10ms")
268
+ completed()
269
+ },
270
+ })
271
+
272
+ const run = await driver.trigger(wf, {}, { delay: "10ms" })
273
+ expect(run.status).toBe("waiting")
274
+
275
+ await vi.waitFor(() => expect(completed).toHaveBeenCalledTimes(1), {
276
+ timeout: DATETIME_WAKEUP_TEST_TIMEOUT_MS,
277
+ })
278
+ },
279
+ )
280
+
281
+ test("idempotencyKey produces a stable run across retries (same key → same run)", async () => {
282
+ const driver = makeFactory()(testFactoryDeps())
283
+ const wf = workflow<{ n: number }, { n: number }>({
284
+ id: uniqueId("compliance-idem"),
285
+ async run(input) {
286
+ return { n: input.n }
287
+ },
288
+ })
289
+
290
+ const key = `key-${suiteCounter}`
291
+ const a = await driver.trigger(wf, { n: 1 }, { idempotencyKey: key })
292
+ const b = await driver.trigger(wf, { n: 999 }, { idempotencyKey: key })
293
+ expect(b.id).toBe(a.id)
294
+ })
295
+
296
+ test("concurrent triggers with the same idempotencyKey only run once", async () => {
297
+ // Closes the get-then-save race window. A counter inside the
298
+ // workflow body lets us assert exactly-once side effects across
299
+ // 8 parallel triggers — without `tryInsert`'s atomicity, the
300
+ // counter would tick more than once.
301
+ const driver = makeFactory()(testFactoryDeps())
302
+ let invocationCount = 0
303
+ const wf = workflow<{ tag: string }, { invoked: number }>({
304
+ id: uniqueId("compliance-race"),
305
+ async run() {
306
+ invocationCount++
307
+ return { invoked: invocationCount }
308
+ },
309
+ })
310
+
311
+ const key = `race-${suiteCounter}`
312
+ const triggers = Array.from({ length: 8 }, (_, i) =>
313
+ driver.trigger(wf, { tag: `caller-${i}` }, { idempotencyKey: key }),
314
+ )
315
+ const results = await Promise.all(triggers)
316
+
317
+ // All 8 callers receive the same runId.
318
+ const ids = new Set(results.map((r) => r.id))
319
+ expect(ids.size).toBe(1)
320
+
321
+ // The body runs at most once. (The first writer wins; later
322
+ // callers return the existing record without re-driving.)
323
+ expect(invocationCount).toBeLessThanOrEqual(1)
324
+ })
325
+
326
+ test.skipIf(!workflowConcurrency)(
327
+ "concurrency queue strategy serializes triggers with the same key",
328
+ async () => {
329
+ const driver = makeFactory()(testFactoryDeps())
330
+ const gate = deferred<void>()
331
+ const started: string[] = []
332
+ const wf = workflow<{ key: string }, string>({
333
+ id: uniqueId("compliance-concurrency-queue"),
334
+ concurrency: {
335
+ key: (input) => input.key,
336
+ limit: 1,
337
+ strategy: "queue",
338
+ },
339
+ async run(input) {
340
+ started.push(input.key)
341
+ if (started.length === 1) await gate.promise
342
+ return input.key
343
+ },
344
+ })
345
+
346
+ const first = driver.trigger(wf, { key: "same" })
347
+ await vi.waitFor(() => expect(started).toEqual(["same"]))
348
+ const second = driver.trigger(wf, { key: "same" })
349
+ await Promise.resolve()
350
+ expect(started).toEqual(["same"])
351
+
352
+ gate.resolve()
353
+ await Promise.all([first, second])
354
+ expect(started).toEqual(["same", "same"])
355
+ },
356
+ )
357
+
358
+ test.skipIf(!workflowConcurrency)(
359
+ "concurrency cancel-newest strategy rejects overflow triggers",
360
+ async () => {
361
+ const driver = makeFactory()(testFactoryDeps())
362
+ const gate = deferred<void>()
363
+ const wf = workflow<{ n: number }, number>({
364
+ id: uniqueId("compliance-concurrency-cancel-newest"),
365
+ concurrency: {
366
+ key: "shared",
367
+ limit: 1,
368
+ strategy: "cancel-newest",
369
+ },
370
+ async run(input) {
371
+ await gate.promise
372
+ return input.n
373
+ },
374
+ })
375
+
376
+ const first = driver.trigger(wf, { n: 1 })
377
+ await expect(driver.trigger(wf, { n: 2 })).rejects.toBeInstanceOf(
378
+ WorkflowConcurrencyRejectedError,
379
+ )
380
+
381
+ gate.resolve()
382
+ await expect(first).resolves.toMatchObject({ status: "completed" })
383
+ },
384
+ )
385
+ })
386
+
387
+ describe("ingestEvent", () => {
388
+ test("returns ok=false when no manifest is registered", async () => {
389
+ const driver = makeFactory()(testFactoryDeps())
390
+ const result = await driver.ingestEvent({
391
+ environment: "production",
392
+ envelope: {
393
+ name: "promotion.changed",
394
+ data: { kind: "all" },
395
+ metadata: { eventId: `evt_${uniqueId("not-registered")}` },
396
+ emittedAt: new Date(1_700_000_000_000).toISOString(),
397
+ },
398
+ })
399
+ // Drivers that registered a manifest earlier in the suite may
400
+ // accept this — gate on the fresh-driver case below instead.
401
+ if (result.ok) {
402
+ expect(result.matches).toEqual([])
403
+ } else {
404
+ expect(result.reason).toBe("manifest_not_registered")
405
+ }
406
+ })
407
+
408
+ test("returns ok=true matches=[] when the manifest has no event filters", async () => {
409
+ const driver = makeFactory()(testFactoryDeps())
410
+ await driver.registerManifest({
411
+ environment: "production",
412
+ manifest: buildTestManifest(uniqueId("v_empty")),
413
+ })
414
+ const eventId = `evt_${uniqueId("none")}`
415
+ const result = await driver.ingestEvent({
416
+ environment: "production",
417
+ envelope: {
418
+ name: "promotion.changed",
419
+ data: {},
420
+ metadata: { eventId },
421
+ emittedAt: new Date(1_700_000_000_000).toISOString(),
422
+ },
423
+ })
424
+ expect(result.ok).toBe(true)
425
+ if (result.ok) {
426
+ expect(result.eventId).toBe(eventId)
427
+ expect(result.matches).toEqual([])
428
+ }
429
+ })
430
+
431
+ test("derives eventId when metadata.eventId is absent", async () => {
432
+ const driver = makeFactory()(testFactoryDeps())
433
+ await driver.registerManifest({
434
+ environment: "production",
435
+ manifest: buildTestManifest(uniqueId("v_noid")),
436
+ })
437
+ const result = await driver.ingestEvent({
438
+ environment: "production",
439
+ envelope: {
440
+ name: "promotion.changed",
441
+ data: {},
442
+ emittedAt: new Date(1_700_000_000_000).toISOString(),
443
+ },
444
+ })
445
+ expect(result.ok).toBe(true)
446
+ if (result.ok) {
447
+ expect(result.eventId).toMatch(/^evt_/)
448
+ }
449
+ })
450
+
451
+ test("eventId fallback is stable across calls (content-derived)", async () => {
452
+ // Same envelope content → same derived eventId. External HTTP
453
+ // retries that don't stamp metadata.eventId still dedupe via the
454
+ // driver's `${filterId}:${eventId}` idempotency key.
455
+ const driver = makeFactory()(testFactoryDeps())
456
+ const wfId = uniqueId("compliance-stable-id")
457
+ const wf = workflow<unknown, void>({
458
+ id: wfId,
459
+ async run() {},
460
+ })
461
+ const filterId = `ef_${uniqueId("ef-stable")}`
462
+ const manifest = {
463
+ ...buildTestManifest(uniqueId("v_stable")),
464
+ eventFilters: [
465
+ {
466
+ id: filterId,
467
+ eventType: "evt.stable",
468
+ payloadHash: filterId,
469
+ targetWorkflowId: wfId,
470
+ },
471
+ ],
472
+ }
473
+ await driver.registerManifest({ environment: "production", manifest })
474
+
475
+ const envelope = {
476
+ name: "evt.stable",
477
+ data: { k: "v" },
478
+ emittedAt: new Date(1_700_000_000_000).toISOString(),
479
+ }
480
+ const a = await driver.ingestEvent({ environment: "production", envelope })
481
+ const b = await driver.ingestEvent({ environment: "production", envelope })
482
+ if (!a.ok || !b.ok) throw new Error("expected ok=true on both")
483
+ expect(a.eventId).toBe(b.eventId)
484
+ const ma = a.matches[0]
485
+ const mb = b.matches[0]
486
+ if (ma?.status !== "queued" || mb?.status !== "queued") {
487
+ throw new Error("expected queued matches")
488
+ }
489
+ // Same eventId → same derived idempotencyKey → same run.
490
+ expect(mb.runId).toBe(ma.runId)
491
+ void wf
492
+ })
493
+
494
+ test("matches a where predicate and triggers a run", async () => {
495
+ const driver = makeFactory()(testFactoryDeps())
496
+ const wfId = uniqueId("compliance-ingest-match")
497
+ const wf = workflow<unknown, void>({
498
+ id: wfId,
499
+ async run() {},
500
+ })
501
+ const filterId = `ef_${uniqueId("ef")}`
502
+ const manifest: WorkflowManifest = {
503
+ ...buildTestManifest(uniqueId("v_match")),
504
+ eventFilters: [
505
+ {
506
+ id: filterId,
507
+ eventType: "promotion.changed",
508
+ where: { eq: [{ path: "data.kind" }, { lit: "all" }] },
509
+ payloadHash: filterId,
510
+ targetWorkflowId: wfId,
511
+ },
512
+ ],
513
+ }
514
+ await driver.registerManifest({ environment: "production", manifest })
515
+
516
+ const result = await driver.ingestEvent({
517
+ environment: "production",
518
+ envelope: {
519
+ name: "promotion.changed",
520
+ data: { kind: "all" },
521
+ metadata: { eventId: `evt_${uniqueId("match")}` },
522
+ emittedAt: new Date(1_700_000_000_000).toISOString(),
523
+ },
524
+ })
525
+ expect(result.ok).toBe(true)
526
+ if (!result.ok) return
527
+ expect(result.matches).toHaveLength(1)
528
+ const m = result.matches[0]
529
+ expect(m?.status).toBe("queued")
530
+ if (m?.status === "queued") {
531
+ expect(m.filterId).toBe(filterId)
532
+ expect(m.targetWorkflowId).toBe(wfId)
533
+ const detail = await driver.admin?.getRun?.(m.runId)
534
+ expect(detail?.workflowId).toBe(wfId)
535
+ }
536
+ void wf
537
+ })
538
+
539
+ test("predicate failure on one filter doesn't block another", async () => {
540
+ const driver = makeFactory()(testFactoryDeps())
541
+ const wfId = uniqueId("compliance-ingest-mixed")
542
+ const wf = workflow<unknown, void>({
543
+ id: wfId,
544
+ async run() {},
545
+ })
546
+ const goodId = `ef_${uniqueId("ok")}`
547
+ const manifest: WorkflowManifest = {
548
+ ...buildTestManifest(uniqueId("v_mixed")),
549
+ eventFilters: [
550
+ {
551
+ id: `ef_${uniqueId("bad")}`,
552
+ eventType: "evt.x",
553
+ where: { wat: "huh" } as never,
554
+ payloadHash: "h",
555
+ targetWorkflowId: wfId,
556
+ },
557
+ {
558
+ id: goodId,
559
+ eventType: "evt.x",
560
+ where: { eq: [{ path: "data.k" }, { lit: "v" }] },
561
+ payloadHash: "h",
562
+ targetWorkflowId: wfId,
563
+ },
564
+ ],
565
+ }
566
+ await driver.registerManifest({ environment: "production", manifest })
567
+
568
+ const result = await driver.ingestEvent({
569
+ environment: "production",
570
+ envelope: {
571
+ name: "evt.x",
572
+ data: { k: "v" },
573
+ metadata: { eventId: `evt_${uniqueId("mixed")}` },
574
+ emittedAt: new Date(1_700_000_000_000).toISOString(),
575
+ },
576
+ })
577
+ expect(result.ok).toBe(true)
578
+ if (!result.ok) return
579
+ const skipped = result.matches.find((m) => m.status === "skipped")
580
+ const queued = result.matches.find((m) => m.status === "queued")
581
+ expect(skipped?.status).toBe("skipped")
582
+ if (skipped?.status === "skipped") {
583
+ expect(skipped.reason).toBe("where_eval_error")
584
+ }
585
+ expect(queued?.status).toBe("queued")
586
+ void wf
587
+ })
588
+
589
+ test("input mapper projects correctly through to the workflow", async () => {
590
+ const driver = makeFactory()(testFactoryDeps())
591
+ const wfId = uniqueId("compliance-ingest-input")
592
+ const wf = workflow<{ kind: string; offer: string }, { kind: string; offer: string }>({
593
+ id: wfId,
594
+ async run(input) {
595
+ return input
596
+ },
597
+ })
598
+ const filterId = `ef_${uniqueId("ef-input")}`
599
+ const manifest: WorkflowManifest = {
600
+ ...buildTestManifest(uniqueId("v_input")),
601
+ eventFilters: [
602
+ {
603
+ id: filterId,
604
+ eventType: "promotion.changed",
605
+ input: {
606
+ object: {
607
+ kind: { path: "data.affected.kind" },
608
+ offer: { path: "data.offerId" },
609
+ },
610
+ },
611
+ payloadHash: filterId,
612
+ targetWorkflowId: wfId,
613
+ },
614
+ ],
615
+ }
616
+ await driver.registerManifest({ environment: "production", manifest })
617
+
618
+ const result = await driver.ingestEvent({
619
+ environment: "production",
620
+ envelope: {
621
+ name: "promotion.changed",
622
+ data: { affected: { kind: "all" }, offerId: "pofr_42" },
623
+ metadata: { eventId: `evt_${uniqueId("input")}` },
624
+ emittedAt: new Date(1_700_000_000_000).toISOString(),
625
+ },
626
+ })
627
+ expect(result.ok).toBe(true)
628
+ if (!result.ok) return
629
+ const m = result.matches[0]
630
+ if (m?.status !== "queued") throw new Error("expected queued match")
631
+ const detail = await driver.admin?.getRun?.(m.runId)
632
+ expect(detail?.output).toEqual({ kind: "all", offer: "pofr_42" })
633
+ void wf
634
+ })
635
+
636
+ test("metadata.eventId dedupes across retries (same event → same run)", async () => {
637
+ const driver = makeFactory()(testFactoryDeps())
638
+ const wfId = uniqueId("compliance-ingest-dedup")
639
+ const wf = workflow<unknown, void>({
640
+ id: wfId,
641
+ async run() {},
642
+ })
643
+ const filterId = `ef_${uniqueId("ef-dedup")}`
644
+ const manifest: WorkflowManifest = {
645
+ ...buildTestManifest(uniqueId("v_dedup")),
646
+ eventFilters: [
647
+ {
648
+ id: filterId,
649
+ eventType: "evt.dedup",
650
+ payloadHash: filterId,
651
+ targetWorkflowId: wfId,
652
+ },
653
+ ],
654
+ }
655
+ await driver.registerManifest({ environment: "production", manifest })
656
+
657
+ const sharedEventId = `evt_${uniqueId("shared")}`
658
+ const envelope = {
659
+ name: "evt.dedup",
660
+ data: {},
661
+ metadata: { eventId: sharedEventId },
662
+ emittedAt: new Date(1_700_000_000_000).toISOString(),
663
+ }
664
+ const a = await driver.ingestEvent({ environment: "production", envelope })
665
+ const b = await driver.ingestEvent({ environment: "production", envelope })
666
+ if (!a.ok || !b.ok) throw new Error("expected ok=true")
667
+ const ma = a.matches[0]
668
+ const mb = b.matches[0]
669
+ if (ma?.status !== "queued" || mb?.status !== "queued") {
670
+ throw new Error("expected queued matches")
671
+ }
672
+ expect(mb.runId).toBe(ma.runId)
673
+ void wf
674
+ })
675
+ })
676
+
677
+ describe("admin (when implemented)", () => {
678
+ test("getRun returns a run by id, null for missing", async () => {
679
+ const driver = makeFactory()(testFactoryDeps())
680
+ const wfId = uniqueId("compliance-admin")
681
+ const wf = workflow<Record<string, never>, void>({
682
+ id: wfId,
683
+ async run() {},
684
+ })
685
+ const created = await driver.trigger(wf, {})
686
+
687
+ const detail = await driver.admin?.getRun?.(created.id)
688
+ expect(detail?.id).toBe(created.id)
689
+ expect(detail?.workflowId).toBe(wfId)
690
+
691
+ const missing = await driver.admin?.getRun?.("nonexistent_run_id")
692
+ expect(missing).toBeNull()
693
+ })
694
+
695
+ test.skipIf(!crossRunQueries)(
696
+ "listRuns surfaces created runs (admin probes its own existence)",
697
+ async () => {
698
+ const driver = makeFactory()(testFactoryDeps())
699
+ if (!driver.admin?.listRuns) return
700
+
701
+ const wfId = uniqueId("compliance-list")
702
+ const wf = workflow<Record<string, never>, void>({
703
+ id: wfId,
704
+ async run() {},
705
+ })
706
+ await driver.trigger(wf, {})
707
+ await driver.trigger(wf, {})
708
+
709
+ const result = await driver.admin.listRuns({ workflowId: wfId })
710
+ expect(result.runs.length).toBeGreaterThanOrEqual(2)
711
+ },
712
+ )
713
+ })
714
+
715
+ describe("shutdown", () => {
716
+ test("subsequent operations after shutdown are refused", async () => {
717
+ const driver = makeFactory()(testFactoryDeps())
718
+ if (!driver.shutdown) return
719
+
720
+ await driver.shutdown()
721
+ const wf = workflow<Record<string, never>, void>({
722
+ id: uniqueId("compliance-shutdown"),
723
+ async run() {},
724
+ })
725
+ await expect(driver.trigger(wf, {})).rejects.toThrow()
726
+ })
727
+ })
728
+
729
+ describe.skipIf(!servicesThreading)("ctx.services (container threading)", () => {
730
+ test("workflow body resolves a service registered on the harness", async () => {
731
+ interface Greeter {
732
+ hello(name: string): string
733
+ }
734
+ const greeter: Greeter = { hello: (n) => `hi ${n}` }
735
+ const services = makeServiceResolver({ greeter })
736
+ const driver = makeFactory()(testFactoryDeps(services))
737
+
738
+ const wf = workflow<{ name: string }, { greeting: string }>({
739
+ id: uniqueId("compliance-services-resolve"),
740
+ async run(input, ctx) {
741
+ const g = ctx.services.resolve<Greeter>("greeter")
742
+ return { greeting: g.hello(input.name) }
743
+ },
744
+ })
745
+
746
+ const run = await driver.trigger(wf, { name: "world" })
747
+ const detail = await driver.admin?.getRun?.(run.id)
748
+ expect(detail?.output).toEqual({ greeting: "hi world" })
749
+ })
750
+
751
+ test("ctx.services.has returns true for registered, false for missing", async () => {
752
+ const services = makeServiceResolver({ db: {} })
753
+ const driver = makeFactory()(testFactoryDeps(services))
754
+
755
+ const wf = workflow<Record<string, never>, { hasDb: boolean; hasMissing: boolean }>({
756
+ id: uniqueId("compliance-services-has"),
757
+ async run(_input, ctx) {
758
+ return {
759
+ hasDb: ctx.services.has("db"),
760
+ hasMissing: ctx.services.has("missing"),
761
+ }
762
+ },
763
+ })
764
+
765
+ const run = await driver.trigger(wf, {})
766
+ const detail = await driver.admin?.getRun?.(run.id)
767
+ expect(detail?.output).toEqual({ hasDb: true, hasMissing: false })
768
+ })
769
+
770
+ test("ctx.services.resolve on an unregistered key surfaces a step error", async () => {
771
+ const driver = makeFactory()(testFactoryDeps())
772
+
773
+ const wf = workflow<Record<string, never>, void>({
774
+ id: uniqueId("compliance-services-missing"),
775
+ async run(_input, ctx) {
776
+ ctx.services.resolve("nope")
777
+ },
778
+ })
779
+
780
+ const run = await driver.trigger(wf, {})
781
+ const detail = await driver.admin?.getRun?.(run.id)
782
+ expect(detail?.status).toBe("failed")
783
+ })
784
+ })
785
+ })
786
+ }
787
+
788
+ function deferred<T>(): {
789
+ promise: Promise<T>
790
+ resolve(value: T): void
791
+ reject(error: unknown): void
792
+ } {
793
+ let resolve!: (value: T) => void
794
+ let reject!: (error: unknown) => void
795
+ const promise = new Promise<T>((res, rej) => {
796
+ resolve = res
797
+ reject = rej
798
+ })
799
+ return { promise, resolve, reject }
800
+ }