@iterativeflow/conformance 2.7.0 → 3.0.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.
Files changed (2) hide show
  1. package/dist/index.mjs +541 -25
  2. package/package.json +2 -2
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { describe, expect, it } from "vitest";
2
- import { builder, cancelRun, defineFlow, reconcile, registry, runTick, serverlessTick, signalRun, signalType, submit, tickOnce } from "@iterativeflow/core";
1
+ import { assert, describe, expect, it } from "vitest";
2
+ import { cancelRun, defineFlow, reconcile, registry, runTick, serverlessTick, signalRun, submit, tickOnce } from "@iterativeflow/core";
3
3
  import { isTerminal } from "@iterativeflow/core/backend";
4
4
  //#region src/store.ts
5
5
  /**
@@ -234,10 +234,23 @@ const storeConformance = (label, makeStore) => {
234
234
  result: { at },
235
235
  attempts: 1
236
236
  });
237
- const memo = (await s.loadRun(runId))?.steps.get("a")?.result;
238
- expect(memo.at).toBe("2030-01-01T00:00:00.000Z");
237
+ expect((await s.loadRun(runId))?.steps.get("a")?.result).toEqual({ at: "2030-01-01T00:00:00.000Z" });
239
238
  });
240
- it("checkpointStep round-trips the shape tag (drift-guard evidence)", async () => {
239
+ it("a run's output round-trips through JSON — a Date comes back as a string on every backend", async () => {
240
+ const s = await makeStore();
241
+ const { runId } = await s.startRun({
242
+ name: "f",
243
+ version: 1,
244
+ input: {}
245
+ });
246
+ const at = /* @__PURE__ */ new Date("2030-01-01T00:00:00.000Z");
247
+ await s.markTerminal(runId, {
248
+ status: "done",
249
+ output: { at }
250
+ });
251
+ expect((await s.loadRunRow(runId))?.output).toEqual({ at: "2030-01-01T00:00:00.000Z" });
252
+ });
253
+ it("checkpointStep round-trips the call that wrote it (drift-guard evidence)", async () => {
241
254
  const s = await makeStore();
242
255
  const { runId } = await s.startRun({
243
256
  name: "f",
@@ -250,9 +263,9 @@ const storeConformance = (label, makeStore) => {
250
263
  status: "ok",
251
264
  result: 1,
252
265
  attempts: 1,
253
- shape: "step:charge"
266
+ call: "step:charge"
254
267
  });
255
- expect((await s.loadRun(runId))?.steps.get("a")?.shape).toBe("step:charge");
268
+ expect((await s.loadRun(runId))?.steps.get("a")?.call).toBe("step:charge");
256
269
  });
257
270
  it("checkpointStep is first-writer-wins — a second write does NOT overwrite (exactly-once memo)", async () => {
258
271
  const s = await makeStore();
@@ -443,8 +456,9 @@ const storeConformance = (label, makeStore) => {
443
456
  otherName,
444
457
  live
445
458
  ]) expect(await s.loadRunRow(spared)).toBeDefined();
459
+ const fromTheWire = JSON.parse(`{"status":"pending"}`);
446
460
  expect(await s.deleteRuns({
447
- status: "pending",
461
+ ...fromTheWire,
448
462
  before: /* @__PURE__ */ new Date()
449
463
  }, 100)).toBe(0);
450
464
  expect((await s.loadRunRow(live))?.status).toBe("pending");
@@ -506,12 +520,17 @@ const storeConformance = (label, makeStore) => {
506
520
  attempts: 1
507
521
  });
508
522
  const snap = await s.loadRun(runId);
509
- snap.run.input.n = 999;
510
- snap.steps.get("a").result.v = 999;
523
+ const input = snap?.run.input;
524
+ const result = snap?.steps.get("a")?.result;
525
+ assert(input instanceof Object && "n" in input);
526
+ assert(result instanceof Object && "v" in result);
527
+ assert(snap?.steps instanceof Map);
528
+ input.n = 999;
529
+ result.v = 999;
511
530
  snap.steps.clear();
512
531
  const again = await s.loadRun(runId);
513
- expect(again.run.input.n).toBe(1);
514
- expect(again.steps.get("a").result.v).toBe(1);
532
+ expect(again?.run.input).toEqual({ n: 1 });
533
+ expect(again?.steps.get("a")?.result).toEqual({ v: 1 });
515
534
  });
516
535
  it("caller mutating the input after startRun can't reach the stored run", async () => {
517
536
  const s = await makeStore();
@@ -654,6 +673,44 @@ const storeConformance = (label, makeStore) => {
654
673
  expect((await s.loadRunRow(runId))?.attempts).toBe(0);
655
674
  expect(await s.markRunning(runId)).toBe(1);
656
675
  });
676
+ it("markTerminal never overwrites a terminal run — a late cancel keeps the outcome", async () => {
677
+ const s = await makeStore();
678
+ const done = (await s.startRun({
679
+ name: "f",
680
+ version: 1,
681
+ input: {}
682
+ })).runId;
683
+ const failed = (await s.startRun({
684
+ name: "f",
685
+ version: 1,
686
+ input: {}
687
+ })).runId;
688
+ await s.markTerminal(done, {
689
+ status: "done",
690
+ output: { total: 42 }
691
+ });
692
+ await s.markTerminal(failed, {
693
+ status: "failed",
694
+ error: {
695
+ code: "X",
696
+ message: "boom"
697
+ }
698
+ });
699
+ await s.markTerminal(done, { status: "canceled" });
700
+ await s.markTerminal(failed, { status: "canceled" });
701
+ expect(await s.loadRunRow(done)).toMatchObject({
702
+ status: "done",
703
+ output: { total: 42 }
704
+ });
705
+ expect(await s.loadRunRow(failed)).toMatchObject({
706
+ status: "failed",
707
+ error: {
708
+ code: "X",
709
+ message: "boom"
710
+ }
711
+ });
712
+ expect((await s.retryRun(failed)).retried).toBe(true);
713
+ });
657
714
  it("cancelRuns cancels the live matched set and never a terminal run", async () => {
658
715
  const s = await makeStore();
659
716
  const mk = async (name) => (await s.startRun({
@@ -677,6 +734,34 @@ const storeConformance = (label, makeStore) => {
677
734
  expect(await s.cancelRuns({ name: "f" }, 100)).toBe(0);
678
735
  await expect(s.cancelRuns({}, 100)).rejects.toThrow();
679
736
  });
737
+ it("cancelRuns and retryRuns narrow by tag, never widening to untagged runs", async () => {
738
+ const s = await makeStore();
739
+ const mk = async (tags) => (await s.startRun({
740
+ name: "f",
741
+ version: 1,
742
+ input: {},
743
+ tags
744
+ })).runId;
745
+ const tenant = await mk(["tenant:42"]);
746
+ const neighbour = await mk(["tenant:7"]);
747
+ const untagged = await mk();
748
+ expect(await s.cancelRuns({ tag: "tenant:42" }, 100)).toBe(1);
749
+ expect((await s.loadRunRow(tenant))?.status).toBe("canceled");
750
+ expect((await s.loadRunRow(neighbour))?.status).toBe("pending");
751
+ expect((await s.loadRunRow(untagged))?.status).toBe("pending");
752
+ const failedTenant = await mk(["tenant:42"]);
753
+ const failedNeighbour = await mk(["tenant:7"]);
754
+ for (const id of [failedTenant, failedNeighbour]) await s.markTerminal(id, {
755
+ status: "failed",
756
+ error: {
757
+ code: "X",
758
+ message: "x"
759
+ }
760
+ });
761
+ expect(await s.retryRuns({ tag: "tenant:42" }, 100)).toBe(1);
762
+ expect((await s.loadRunRow(failedTenant))?.status).toBe("pending");
763
+ expect((await s.loadRunRow(failedNeighbour))?.status).toBe("failed");
764
+ });
680
765
  it("retryRuns re-drives the failed matched set, clearing the spent attempt budget", async () => {
681
766
  const s = await makeStore();
682
767
  const mk = async (name) => (await s.startRun({
@@ -1021,6 +1106,26 @@ const queueConformance = (label, makeQueue) => {
1021
1106
  });
1022
1107
  expect(first.runId).toBe("high");
1023
1108
  });
1109
+ it("negative priorities order numerically, before zero", async () => {
1110
+ const q = await makeQueue();
1111
+ await q.enqueue("zero", { priority: 0 });
1112
+ await q.enqueue("minus3", { priority: -3 });
1113
+ await q.enqueue("minus5", { priority: -5 });
1114
+ const order = [];
1115
+ for (let i = 0; i < 3; i++) {
1116
+ const [l] = await q.claim({
1117
+ limit: 1,
1118
+ leaseMs: 6e4,
1119
+ now: at(0)
1120
+ });
1121
+ order.push(l.runId);
1122
+ }
1123
+ expect(order).toEqual([
1124
+ "minus5",
1125
+ "minus3",
1126
+ "zero"
1127
+ ]);
1128
+ });
1024
1129
  it("re-enqueue is an upsert keyed by runId — claimed once", async () => {
1025
1130
  const q = await makeQueue();
1026
1131
  await q.enqueue("r1");
@@ -1032,6 +1137,98 @@ const queueConformance = (label, makeQueue) => {
1032
1137
  });
1033
1138
  expect(leases.map((l) => l.runId)).toEqual(["r1"]);
1034
1139
  });
1140
+ it("enqueueMany enqueues every run, honouring per-request runAt and priority", async () => {
1141
+ const q = await makeQueue();
1142
+ await q.enqueueMany([
1143
+ {
1144
+ runId: "low",
1145
+ opts: { priority: 10 }
1146
+ },
1147
+ {
1148
+ runId: "high",
1149
+ opts: { priority: 1 }
1150
+ },
1151
+ {
1152
+ runId: "later",
1153
+ opts: { runAt: at(5e3) }
1154
+ }
1155
+ ]);
1156
+ expect(await q.depth(at(0))).toMatchObject({
1157
+ claimable: 2,
1158
+ leased: 0
1159
+ });
1160
+ const [first] = await q.claim({
1161
+ limit: 1,
1162
+ leaseMs: 1e3,
1163
+ now: at(0)
1164
+ });
1165
+ expect(first.runId).toBe("high");
1166
+ const rest = await q.claim({
1167
+ limit: 10,
1168
+ leaseMs: 1e3,
1169
+ now: at(0)
1170
+ });
1171
+ expect(rest.map((l) => l.runId)).toEqual(["low"]);
1172
+ });
1173
+ it("enqueueMany on an empty list is a no-op", async () => {
1174
+ const q = await makeQueue();
1175
+ await q.enqueueMany([]);
1176
+ expect(await q.claim({
1177
+ limit: 10,
1178
+ leaseMs: 1e3,
1179
+ now: at(0)
1180
+ })).toEqual([]);
1181
+ });
1182
+ it("enqueueMany collapses duplicate runIds into one upsert, bumping version once", async () => {
1183
+ const q = await makeQueue();
1184
+ await q.enqueueMany([{
1185
+ runId: "r1",
1186
+ opts: { priority: 9 }
1187
+ }, {
1188
+ runId: "r1",
1189
+ opts: { priority: 2 }
1190
+ }]);
1191
+ const leases = await q.claim({
1192
+ limit: 10,
1193
+ leaseMs: 1e3,
1194
+ now: at(0)
1195
+ });
1196
+ expect(leases.map((l) => l.runId)).toEqual(["r1"]);
1197
+ expect(leases[0].version).toBe(1);
1198
+ });
1199
+ it("enqueueMany does not yank an active lease, and its wake survives the ack", async () => {
1200
+ const q = await makeQueue();
1201
+ await q.enqueue("r1");
1202
+ const [lease] = await q.claim({
1203
+ limit: 10,
1204
+ leaseMs: 1e3,
1205
+ now: at(0)
1206
+ });
1207
+ await q.enqueueMany([{ runId: "r1" }]);
1208
+ expect(await q.claim({
1209
+ limit: 10,
1210
+ leaseMs: 1e3,
1211
+ now: at(500)
1212
+ })).toEqual([]);
1213
+ await q.ack(lease, { now: at(600) });
1214
+ const again = await q.claim({
1215
+ limit: 10,
1216
+ leaseMs: 1e3,
1217
+ now: at(601)
1218
+ });
1219
+ expect(again.map((l) => l.runId)).toEqual(["r1"]);
1220
+ });
1221
+ it("enqueueMany re-enqueues an existing job rather than duplicating it", async () => {
1222
+ const q = await makeQueue();
1223
+ await q.enqueue("r1", { runAt: at(9e3) });
1224
+ await q.enqueueMany([{ runId: "r1" }]);
1225
+ const leases = await q.claim({
1226
+ limit: 10,
1227
+ leaseMs: 1e3,
1228
+ now: at(0)
1229
+ });
1230
+ expect(leases.map((l) => l.runId)).toEqual(["r1"]);
1231
+ });
1035
1232
  });
1036
1233
  };
1037
1234
  //#endregion
@@ -1805,6 +2002,16 @@ const cronConformance = (label, makeStore) => {
1805
2002
  };
1806
2003
  //#endregion
1807
2004
  //#region src/engine.ts
2005
+ const okSchema = { "~standard": {
2006
+ version: 1,
2007
+ vendor: "conformance",
2008
+ validate: (v) => v instanceof Object && "ok" in v ? { value: { ok: v.ok === true } } : { issues: [{ message: "ok is required" }] }
2009
+ } };
2010
+ const textSchema = { "~standard": {
2011
+ version: 1,
2012
+ vendor: "conformance",
2013
+ validate: (v) => v === String(v) ? { value: String(v) } : { issues: [{ message: "not text" }] }
2014
+ } };
1808
2015
  /**
1809
2016
  * Engine-behavior conformance: the composed durable behaviors (retry/dead-letter, signal
1810
2017
  * resume, cancel cascade, drift) run against every backend, not just memory — so a real
@@ -1852,15 +2059,20 @@ const engineConformance = (label, makeBackend) => {
1852
2059
  output: "settled"
1853
2060
  });
1854
2061
  });
1855
- it("runs a builder flow with a durable sleep to completion", async () => {
2062
+ it("a flow with a durable sleep between steps resumes to completion", async () => {
1856
2063
  const backend = await makeBackend();
1857
- const flow = builder(`${label}-sleep`, 1).step("doubled", (acc) => acc.input.x * 2).step("nap", async (_acc, ctx) => {
1858
- await ctx.sleep(5e3);
1859
- return "rested";
1860
- }).output((acc) => ({
1861
- doubled: acc.doubled,
1862
- nap: acc.nap
1863
- }));
2064
+ const flow = defineFlow({
2065
+ name: `${label}-sleep`,
2066
+ version: 1,
2067
+ run: async (ctx, input) => {
2068
+ const doubled = await ctx.step("doubled", () => input.x * 2);
2069
+ await ctx.sleep(5e3);
2070
+ return {
2071
+ doubled,
2072
+ nap: await ctx.step("nap", () => "rested")
2073
+ };
2074
+ }
2075
+ });
1864
2076
  const runId = await submit(backend, flow, { x: 21 });
1865
2077
  const run = await drive(backend, registry([flow]), runId);
1866
2078
  expect(run).toMatchObject({
@@ -1915,7 +2127,7 @@ const engineConformance = (label, makeBackend) => {
1915
2127
  const flow = defineFlow({
1916
2128
  name: "approve",
1917
2129
  version: 1,
1918
- signals: { go: signalType() },
2130
+ signals: { go: okSchema },
1919
2131
  run: async (ctx) => {
1920
2132
  return (await ctx.signal("go")).ok ? "shipped" : "held";
1921
2133
  }
@@ -2209,6 +2421,310 @@ const engineConformance = (label, makeBackend) => {
2209
2421
  expect(kids.every((k) => isTerminal(k.status))).toBe(true);
2210
2422
  expect(kids.some((k) => k.status === "canceled")).toBe(true);
2211
2423
  });
2424
+ it("a settled fan-out returns every child's result and cancels no sibling", async () => {
2425
+ const backend = await makeBackend();
2426
+ const ok = defineFlow({
2427
+ name: "settle-ok",
2428
+ version: 1,
2429
+ run: async (_ctx, n) => n
2430
+ });
2431
+ const boom = defineFlow({
2432
+ name: "settle-boom",
2433
+ version: 1,
2434
+ run: async () => {
2435
+ throw new Error("boom");
2436
+ }
2437
+ });
2438
+ const parent = defineFlow({
2439
+ name: "settle-fan",
2440
+ version: 1,
2441
+ run: async (ctx) => ctx.invoke([
2442
+ {
2443
+ flow: ok,
2444
+ input: 1
2445
+ },
2446
+ {
2447
+ flow: boom,
2448
+ input: {}
2449
+ },
2450
+ {
2451
+ flow: ok,
2452
+ input: 2
2453
+ }
2454
+ ], { onChildFailure: "settle" })
2455
+ });
2456
+ const runId = await submit(backend, parent, {});
2457
+ const run = await drive(backend, registry([
2458
+ parent,
2459
+ ok,
2460
+ boom
2461
+ ]), runId, {
2462
+ maxAttempts: 1,
2463
+ baseDelayMs: 1,
2464
+ maxDelayMs: 1
2465
+ });
2466
+ expect(run).toMatchObject({
2467
+ status: "done",
2468
+ output: [
2469
+ {
2470
+ status: "done",
2471
+ output: 1
2472
+ },
2473
+ {
2474
+ status: "failed",
2475
+ error: { message: "boom" }
2476
+ },
2477
+ {
2478
+ status: "done",
2479
+ output: 2
2480
+ }
2481
+ ]
2482
+ });
2483
+ const kids = await backend.store.childrenOf(runId);
2484
+ expect(kids.filter((k) => k.status === "canceled")).toHaveLength(0);
2485
+ });
2486
+ const waiter = defineFlow({
2487
+ name: "waits-forever",
2488
+ version: 1,
2489
+ run: async (ctx) => {
2490
+ await ctx.signal("never");
2491
+ return 1;
2492
+ }
2493
+ });
2494
+ const parkOnChild = async (backend, parent) => {
2495
+ const flows = registry([parent, waiter]);
2496
+ const runId = await submit(backend, parent, {});
2497
+ const now = () => /* @__PURE__ */ new Date("2030-01-01T00:00:00Z");
2498
+ await tickOnce(backend, flows, {
2499
+ ...base,
2500
+ now
2501
+ });
2502
+ await tickOnce(backend, flows, {
2503
+ ...base,
2504
+ now
2505
+ });
2506
+ expect((await backend.store.loadRunRow(runId))?.status).toBe("awaiting_child");
2507
+ const [child] = await backend.store.childrenOf(runId);
2508
+ return {
2509
+ runId,
2510
+ childId: child.id,
2511
+ tick: () => tickOnce(backend, flows, {
2512
+ ...base,
2513
+ now
2514
+ })
2515
+ };
2516
+ };
2517
+ it("canceling a child wakes its fail-fast parent without waiting for reconcile", async () => {
2518
+ const backend = await makeBackend();
2519
+ const parent = defineFlow({
2520
+ name: "waits-on-child",
2521
+ version: 1,
2522
+ run: async (ctx) => ctx.invoke(waiter, {})
2523
+ });
2524
+ const { runId, childId, tick } = await parkOnChild(backend, parent);
2525
+ await cancelRun(backend, childId);
2526
+ await tick();
2527
+ expect((await backend.store.loadRunRow(runId))?.status).toBe("failed");
2528
+ });
2529
+ it("a settled parent reports a canceled child as canceled and completes", async () => {
2530
+ const backend = await makeBackend();
2531
+ const parent = defineFlow({
2532
+ name: "settles-on-child",
2533
+ version: 1,
2534
+ run: async (ctx) => ctx.invoke(waiter, {}, { onChildFailure: "settle" })
2535
+ });
2536
+ const { runId, childId, tick } = await parkOnChild(backend, parent);
2537
+ await cancelRun(backend, childId);
2538
+ await tick();
2539
+ expect(await backend.store.loadRunRow(runId)).toMatchObject({
2540
+ status: "done",
2541
+ output: { status: "canceled" }
2542
+ });
2543
+ });
2544
+ const wide = (inc) => defineFlow({
2545
+ name: "wide-chunks",
2546
+ version: 1,
2547
+ run: async (ctx) => ctx.invoke(Array.from({ length: 45 }, (_v, n) => ({
2548
+ flow: inc,
2549
+ input: n
2550
+ })))
2551
+ });
2552
+ const inc = defineFlow({
2553
+ name: "inc-chunk",
2554
+ version: 1,
2555
+ run: async (_ctx, n) => n + 1
2556
+ });
2557
+ const secondChunk = (call) => call?.startsWith("invokeAll:1:") ?? false;
2558
+ it("a crash right after a spawn chunk commits spawns no child twice", async () => {
2559
+ const real = await makeBackend();
2560
+ let crashed = false;
2561
+ const backend = {
2562
+ ...real,
2563
+ store: {
2564
+ ...real.store,
2565
+ checkpointStep: async (c, fx) => {
2566
+ const stored = await real.store.checkpointStep(c, fx);
2567
+ if (!crashed && secondChunk(c.call)) {
2568
+ crashed = true;
2569
+ throw new Error("worker died after the commit");
2570
+ }
2571
+ return stored;
2572
+ }
2573
+ }
2574
+ };
2575
+ const parent = wide(inc);
2576
+ const runId = await submit(backend, parent, {});
2577
+ const run = await drive(backend, registry([parent, inc]), runId);
2578
+ expect(crashed).toBe(true);
2579
+ expect(run.output).toEqual(Array.from({ length: 45 }, (_v, n) => n + 1));
2580
+ expect(await backend.store.childrenOf(runId)).toHaveLength(45);
2581
+ });
2582
+ it("a parent canceled between spawn chunks leaves no child running", async () => {
2583
+ const real = await makeBackend();
2584
+ const parent = wide(inc);
2585
+ const runId = await submit(real, parent, {});
2586
+ let canceled = false;
2587
+ const backend = {
2588
+ ...real,
2589
+ store: {
2590
+ ...real.store,
2591
+ checkpointStep: async (c, fx) => {
2592
+ if (!canceled && secondChunk(c.call)) {
2593
+ canceled = true;
2594
+ await cancelRun(real, runId);
2595
+ }
2596
+ return real.store.checkpointStep(c, fx);
2597
+ }
2598
+ }
2599
+ };
2600
+ const flows = registry([parent, inc]);
2601
+ const now = () => /* @__PURE__ */ new Date("2030-01-01T00:00:00Z");
2602
+ for (let i = 0; i < 10; i++) await tickOnce(backend, flows, {
2603
+ ...base,
2604
+ now
2605
+ });
2606
+ expect(canceled).toBe(true);
2607
+ expect((await backend.store.loadRunRow(runId))?.status).toBe("canceled");
2608
+ const kids = await backend.store.childrenOf(runId);
2609
+ expect(kids.length).toBeGreaterThan(0);
2610
+ expect(kids.every((k) => k.status === "canceled")).toBe(true);
2611
+ });
2612
+ it("a child that finishes while its parent is mid-replay still wakes the parent", async () => {
2613
+ const real = await makeBackend();
2614
+ const quick = defineFlow({
2615
+ name: "quick-child",
2616
+ version: 1,
2617
+ run: async () => 7
2618
+ });
2619
+ const parent = defineFlow({
2620
+ name: "replaying-parent",
2621
+ version: 1,
2622
+ run: async (ctx) => ctx.invoke(quick, {})
2623
+ });
2624
+ const flows = registry([parent, quick]);
2625
+ const now = () => /* @__PURE__ */ new Date("2030-01-01T00:00:00Z");
2626
+ let raced = false;
2627
+ const backend = {
2628
+ ...real,
2629
+ store: {
2630
+ ...real.store,
2631
+ loadRunRow: async (id) => {
2632
+ const row = await real.store.loadRunRow(id);
2633
+ if (!raced && row?.name === "quick-child") {
2634
+ raced = true;
2635
+ await tickOnce(real, flows, {
2636
+ ...base,
2637
+ now,
2638
+ names: ["quick-child"]
2639
+ });
2640
+ }
2641
+ return row;
2642
+ }
2643
+ }
2644
+ };
2645
+ const runId = await submit(backend, parent, {});
2646
+ await tickOnce(backend, flows, {
2647
+ ...base,
2648
+ now,
2649
+ names: ["replaying-parent"]
2650
+ });
2651
+ expect(raced).toBe(true);
2652
+ expect((await real.store.loadRunRow(runId))?.status).toBe("awaiting_child");
2653
+ const run = await drive(real, flows, runId);
2654
+ expect(run).toMatchObject({
2655
+ status: "done",
2656
+ output: 7
2657
+ });
2658
+ });
2659
+ it("a re-enqueue with no priority keeps the run's own priority", async () => {
2660
+ const backend = await makeBackend();
2661
+ const at = (ms) => new Date(Date.UTC(2030, 0, 1) + ms);
2662
+ const low = (await backend.store.startRun({
2663
+ name: "p",
2664
+ version: 1,
2665
+ input: {},
2666
+ priority: 10
2667
+ })).runId;
2668
+ const urgent = (await backend.store.startRun({
2669
+ name: "p",
2670
+ version: 1,
2671
+ input: {},
2672
+ priority: -10
2673
+ })).runId;
2674
+ await backend.queue.enqueueMany([{
2675
+ runId: low,
2676
+ opts: { runAt: at(0) }
2677
+ }, {
2678
+ runId: urgent,
2679
+ opts: { runAt: at(1e3) }
2680
+ }]);
2681
+ const [first] = await backend.queue.claim({
2682
+ limit: 1,
2683
+ leaseMs: 6e4,
2684
+ now: at(2e3)
2685
+ });
2686
+ expect(first.runId).toBe(urgent);
2687
+ });
2688
+ it("a signal delivery, a retry and a bulk retry keep the run's own priority", async () => {
2689
+ const backend = await makeBackend();
2690
+ const start = async (priority) => (await backend.store.startRun({
2691
+ name: "p",
2692
+ version: 1,
2693
+ input: {},
2694
+ priority
2695
+ })).runId;
2696
+ const claimFirst = async () => (await backend.queue.claim({
2697
+ limit: 1,
2698
+ leaseMs: 6e4
2699
+ }))[0]?.runId;
2700
+ const rival = async () => backend.queue.enqueue(await start(), { priority: -5 });
2701
+ const signalled = await start(-10);
2702
+ await rival();
2703
+ await backend.store.postSignal(signalled, "go", {});
2704
+ expect(await claimFirst()).toBe(signalled);
2705
+ const retried = await start(-10);
2706
+ await backend.store.markTerminal(retried, {
2707
+ status: "failed",
2708
+ error: {
2709
+ code: "X",
2710
+ message: "x"
2711
+ }
2712
+ });
2713
+ await rival();
2714
+ await backend.store.retryRun(retried);
2715
+ expect(await claimFirst()).toBe(retried);
2716
+ const bulk = await start(-10);
2717
+ await backend.store.markTerminal(bulk, {
2718
+ status: "failed",
2719
+ error: {
2720
+ code: "X",
2721
+ message: "x"
2722
+ }
2723
+ });
2724
+ await rival();
2725
+ await backend.store.retryRuns({ status: "failed" }, 100);
2726
+ expect(await claimFirst()).toBe(bulk);
2727
+ });
2212
2728
  it("a claimed run for an unregistered flow version parks and resumes once that version deploys", async () => {
2213
2729
  const backend = await makeBackend();
2214
2730
  const v1 = defineFlow({
@@ -2234,7 +2750,7 @@ const engineConformance = (label, makeBackend) => {
2234
2750
  now,
2235
2751
  retry: fast
2236
2752
  });
2237
- expect((await backend.store.loadRunRow(runId))?.status).toBe("retrying");
2753
+ expect((await backend.store.loadRunRow(runId))?.status).toBe("parked");
2238
2754
  for (let i = 0; i < 4; i++) {
2239
2755
  clock = new Date(clock.getTime() + 6e4);
2240
2756
  await tickOnce(backend, registry([v1]), {
@@ -2242,7 +2758,7 @@ const engineConformance = (label, makeBackend) => {
2242
2758
  now,
2243
2759
  retry: fast
2244
2760
  });
2245
- expect((await backend.store.loadRunRow(runId))?.status).toBe("retrying");
2761
+ expect((await backend.store.loadRunRow(runId))?.status).toBe("parked");
2246
2762
  }
2247
2763
  const run = await drive(backend, registry([v2]), runId, fast, clock);
2248
2764
  expect(run).toMatchObject({
@@ -2255,7 +2771,7 @@ const engineConformance = (label, makeBackend) => {
2255
2771
  const flow = defineFlow({
2256
2772
  name: "raced",
2257
2773
  version: 1,
2258
- signals: { go: signalType() },
2774
+ signals: { go: textSchema },
2259
2775
  run: async (ctx) => {
2260
2776
  const r = await ctx.signal("go", { timeoutMs: 5e3 });
2261
2777
  return r.received ? `signal:${r.payload}` : "timeout";
@@ -2282,7 +2798,7 @@ const engineConformance = (label, makeBackend) => {
2282
2798
  const flow = defineFlow({
2283
2799
  name: "waits-out",
2284
2800
  version: 1,
2285
- signals: { go: signalType() },
2801
+ signals: { go: textSchema },
2286
2802
  run: async (ctx) => {
2287
2803
  return (await ctx.signal("go", { timeoutMs: 5e3 })).received ? "signal" : "timeout";
2288
2804
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iterativeflow/conformance",
3
- "version": "2.7.0",
3
+ "version": "3.0.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,7 +25,7 @@
25
25
  "access": "public"
26
26
  },
27
27
  "dependencies": {
28
- "@iterativeflow/core": "2.7.0"
28
+ "@iterativeflow/core": "3.0.0"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "vitest": ">=3"