@redflow/client 0.0.3 → 0.0.5

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.
@@ -52,7 +52,7 @@ test("manual run: succeeds and records steps", async () => {
52
52
  },
53
53
  );
54
54
 
55
- const worker = await startWorker({
55
+ const worker = await startWorker({ app: "test-app",
56
56
  redis,
57
57
  prefix,
58
58
  queues: ["q1"],
@@ -82,7 +82,7 @@ test("runAt: scheduled -> promoted -> executed", async () => {
82
82
  return { ok: true };
83
83
  });
84
84
 
85
- const worker = await startWorker({
85
+ const worker = await startWorker({ app: "test-app",
86
86
  redis,
87
87
  prefix,
88
88
  queues: ["q2"],
@@ -120,7 +120,7 @@ test("runAt: cleared after retry exhaustion final failure", async () => {
120
120
  },
121
121
  );
122
122
 
123
- const worker = await startWorker({
123
+ const worker = await startWorker({ app: "test-app",
124
124
  redis,
125
125
  prefix,
126
126
  queues: ["q2_retry_fail"],
@@ -153,7 +153,7 @@ test("NonRetriableError: skips retries and fails immediately on attempt 1", asyn
153
153
  },
154
154
  );
155
155
 
156
- const worker = await startWorker({
156
+ const worker = await startWorker({ app: "test-app",
157
157
  redis,
158
158
  prefix,
159
159
  queues: ["q_non_retriable"],
@@ -196,7 +196,7 @@ test("onFailure: called after retry exhaustion with correct context", async () =
196
196
  },
197
197
  );
198
198
 
199
- const worker = await startWorker({
199
+ const worker = await startWorker({ app: "test-app",
200
200
  redis,
201
201
  prefix,
202
202
  queues: ["q_on_failure_1"],
@@ -239,7 +239,7 @@ test("onFailure: called immediately with NonRetriableError", async () => {
239
239
  },
240
240
  );
241
241
 
242
- const worker = await startWorker({
242
+ const worker = await startWorker({ app: "test-app",
243
243
  redis,
244
244
  prefix,
245
245
  queues: ["q_on_failure_2"],
@@ -283,7 +283,7 @@ test("onFailure: NOT called on cancellation", async () => {
283
283
  },
284
284
  );
285
285
 
286
- const worker = await startWorker({
286
+ const worker = await startWorker({ app: "test-app",
287
287
  redis,
288
288
  prefix,
289
289
  queues: ["q_on_failure_3"],
@@ -333,7 +333,7 @@ test("cron: creates runs and executes workflow", async () => {
333
333
  },
334
334
  );
335
335
 
336
- const worker = await startWorker({
336
+ const worker = await startWorker({ app: "test-app",
337
337
  redis,
338
338
  prefix,
339
339
  queues: ["q4"],
@@ -352,6 +352,75 @@ test("cron: creates runs and executes workflow", async () => {
352
352
  redis.close();
353
353
  });
354
354
 
355
+ test("cron: skips when workflow already running", async () => {
356
+ const prefix = testPrefix();
357
+ const redis = new RedisClient(redisServer.url);
358
+ const client = createClient({ redis, prefix });
359
+ setDefaultClient(client);
360
+
361
+ const manualCountKey = `${prefix}:t:cronSkipManualCount`;
362
+ const cronCountKey = `${prefix}:t:cronSkipCronCount`;
363
+
364
+ defineWorkflow(
365
+ "cron-skip-running-wf",
366
+ {
367
+ queue: "q4_skip",
368
+ cron: [{ expression: "*/5 * * * * *", input: { source: "cron" } }],
369
+ },
370
+ async ({ input, step }) => {
371
+ await step.run({ name: "record-source" }, async () => {
372
+ const source = (input as { source?: string } | null | undefined)?.source;
373
+ if (source === "cron") {
374
+ await redis.incr(cronCountKey);
375
+ } else {
376
+ await redis.incr(manualCountKey);
377
+ }
378
+ return true;
379
+ });
380
+
381
+ await step.run({ name: "hold" }, async () => {
382
+ await new Promise((resolve) => setTimeout(resolve, 6500));
383
+ return true;
384
+ });
385
+
386
+ return { ok: true };
387
+ },
388
+ );
389
+
390
+ const worker = await startWorker({ app: "test-app",
391
+ redis,
392
+ prefix,
393
+ queues: ["q4_skip"],
394
+ concurrency: 2,
395
+ runtime: { leaseMs: 500 },
396
+ });
397
+
398
+ try {
399
+ const manualHandle = await client.emitWorkflow("cron-skip-running-wf", { source: "manual" });
400
+
401
+ await waitFor(
402
+ async () => Number((await redis.get(manualCountKey)) ?? "0") >= 1,
403
+ { timeoutMs: 5000, label: "manual run started" },
404
+ );
405
+
406
+ // Wait long enough for one cron tick while manual run is still executing.
407
+ await new Promise((resolve) => setTimeout(resolve, 5500));
408
+ const cronWhileManualRunning = Number((await redis.get(cronCountKey)) ?? "0");
409
+ expect(cronWhileManualRunning).toBe(0);
410
+
411
+ await manualHandle.result({ timeoutMs: 15_000 });
412
+
413
+ // Cron should resume once the running instance is gone.
414
+ await waitFor(
415
+ async () => Number((await redis.get(cronCountKey)) ?? "0") >= 1,
416
+ { timeoutMs: 10_000, label: "cron resumed after manual run" },
417
+ );
418
+ } finally {
419
+ await worker.stop();
420
+ redis.close();
421
+ }
422
+ }, { timeout: 30_000 });
423
+
355
424
  test("step.runWorkflow: auto idempotency and override are forwarded to child runs", async () => {
356
425
  const prefix = testPrefix();
357
426
  const redis = new RedisClient(redisServer.url);
@@ -388,7 +457,7 @@ test("step.runWorkflow: auto idempotency and override are forwarded to child run
388
457
  return { auto, custom };
389
458
  });
390
459
 
391
- const worker = await startWorker({
460
+ const worker = await startWorker({ app: "test-app",
392
461
  redis,
393
462
  prefix,
394
463
  queues: ["q_rw_parent", "q_rw_child"],
@@ -451,7 +520,7 @@ test("step.runWorkflow: child workflow executes once across parent retries", asy
451
520
  },
452
521
  );
453
522
 
454
- const worker = await startWorker({
523
+ const worker = await startWorker({ app: "test-app",
455
524
  redis,
456
525
  prefix,
457
526
  queues: ["q_rw_retry_parent", "q_rw_retry_child"],
@@ -508,7 +577,7 @@ test("step.runWorkflow: same queue avoids self-deadlock with concurrency 1", asy
508
577
  return await step.runWorkflow({ name: "call-child" }, child, {});
509
578
  });
510
579
 
511
- const worker = await startWorker({
580
+ const worker = await startWorker({ app: "test-app",
512
581
  redis,
513
582
  prefix,
514
583
  queues: ["q_rw_single"],
@@ -552,7 +621,7 @@ test("step.emitWorkflow: supports workflow name strings", async () => {
552
621
  return { childRunId };
553
622
  });
554
623
 
555
- const worker = await startWorker({
624
+ const worker = await startWorker({ app: "test-app",
556
625
  redis,
557
626
  prefix,
558
627
  queues: ["q_emit_name_parent", "q_emit_name_child"],
@@ -606,7 +675,7 @@ test("retries: step results are cached across attempts", async () => {
606
675
  },
607
676
  );
608
677
 
609
- const worker = await startWorker({
678
+ const worker = await startWorker({ app: "test-app",
610
679
  redis,
611
680
  prefix,
612
681
  queues: ["q5"],
@@ -654,7 +723,7 @@ test("retries: run queued before worker start keeps workflow maxAttempts", async
654
723
  const queuedState = await client.getRun(handle.id);
655
724
  expect(queuedState?.maxAttempts).toBe(2);
656
725
 
657
- const worker = await startWorker({
726
+ const worker = await startWorker({ app: "test-app",
658
727
  redis,
659
728
  prefix,
660
729
  queues: ["q5_before_sync"],
@@ -687,7 +756,7 @@ test("step timeout: run fails and error is recorded", async () => {
687
756
  return { ok: true };
688
757
  });
689
758
 
690
- const worker = await startWorker({
759
+ const worker = await startWorker({ app: "test-app",
691
760
  redis,
692
761
  prefix,
693
762
  queues: ["q6"],
@@ -737,7 +806,7 @@ test("cancellation: scheduled/queued/running", async () => {
737
806
 
738
807
  // Scheduled cancel.
739
808
  {
740
- const worker = await startWorker({
809
+ const worker = await startWorker({ app: "test-app",
741
810
  redis,
742
811
  prefix,
743
812
  queues: ["q7"],
@@ -773,7 +842,7 @@ test("cancellation: scheduled/queued/running", async () => {
773
842
 
774
843
  // Running cancel.
775
844
  {
776
- const worker = await startWorker({
845
+ const worker = await startWorker({ app: "test-app",
777
846
  redis,
778
847
  prefix,
779
848
  queues: ["q7"],
@@ -856,7 +925,7 @@ test("cancel race: queued cancel before start does not execute handler", async (
856
925
 
857
926
  let worker: Awaited<ReturnType<typeof startWorker>> | null = null;
858
927
  try {
859
- worker = await startWorker({
928
+ worker = await startWorker({ app: "test-app",
860
929
  redis,
861
930
  prefix,
862
931
  queues: [queue],
@@ -977,7 +1046,7 @@ test("idempotencyKey: same key returns same run id and executes once", async ()
977
1046
  return { ok: true };
978
1047
  });
979
1048
 
980
- const worker = await startWorker({
1049
+ const worker = await startWorker({ app: "test-app",
981
1050
  redis,
982
1051
  prefix,
983
1052
  queues: ["q9"],
@@ -1015,7 +1084,7 @@ test("idempotencyKey: delayed TTL refresh cannot fork duplicate runs", async ()
1015
1084
  return { ok: true };
1016
1085
  });
1017
1086
 
1018
- const worker = await startWorker({
1087
+ const worker = await startWorker({ app: "test-app",
1019
1088
  redis,
1020
1089
  prefix,
1021
1090
  queues: ["q9_race_fix"],
@@ -1063,7 +1132,7 @@ test("enqueue: producer-side zadd failure does not leave runs orphaned", async (
1063
1132
  setDefaultClient(client);
1064
1133
 
1065
1134
  const wf = defineWorkflow("enqueue-atomic", {queue: "q_enqueue_atomic" }, async () => ({ ok: true }));
1066
- await client.syncRegistry(getDefaultRegistry());
1135
+ await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
1067
1136
 
1068
1137
  const originalZadd = redis.zadd.bind(redis);
1069
1138
  let injected = false;
@@ -1082,7 +1151,7 @@ test("enqueue: producer-side zadd failure does not leave runs orphaned", async (
1082
1151
  redis.zadd = originalZadd;
1083
1152
  }
1084
1153
 
1085
- const worker = await startWorker({
1154
+ const worker = await startWorker({ app: "test-app",
1086
1155
  redis,
1087
1156
  prefix,
1088
1157
  queues: ["q_enqueue_atomic"],
@@ -1107,7 +1176,7 @@ test("idempotencyKey: delimiter-like workflow/key pairs do not collide", async (
1107
1176
  const wfA = defineWorkflow("idem:a", {queue: "q9a" }, async () => ({ workflow: "idem:a" }));
1108
1177
  const wfB = defineWorkflow("idem:a:b", {queue: "q9b" }, async () => ({ workflow: "idem:a:b" }));
1109
1178
 
1110
- const worker = await startWorker({
1179
+ const worker = await startWorker({ app: "test-app",
1111
1180
  redis,
1112
1181
  prefix,
1113
1182
  queues: ["q9a", "q9b"],
@@ -1150,7 +1219,7 @@ test("idempotencyTtl: short TTL expires and allows a new run with same key", asy
1150
1219
  return { ok: true };
1151
1220
  });
1152
1221
 
1153
- const worker = await startWorker({
1222
+ const worker = await startWorker({ app: "test-app",
1154
1223
  redis,
1155
1224
  prefix,
1156
1225
  queues: ["q_idem_ttl"],
@@ -1199,7 +1268,7 @@ test("input validation: invalid input fails once and is not retried", async () =
1199
1268
  },
1200
1269
  );
1201
1270
 
1202
- const worker = await startWorker({
1271
+ const worker = await startWorker({ app: "test-app",
1203
1272
  redis,
1204
1273
  prefix,
1205
1274
  queues: ["q11"],
@@ -1234,7 +1303,7 @@ test("unknown workflow: run fails and is not retried", async () => {
1234
1303
  const redis = new RedisClient(redisServer.url);
1235
1304
  const client = createClient({ redis, prefix });
1236
1305
 
1237
- const worker = await startWorker({
1306
+ const worker = await startWorker({ app: "test-app",
1238
1307
  redis,
1239
1308
  prefix,
1240
1309
  queues: ["qx"],
@@ -1287,7 +1356,7 @@ test("cancel during step: run becomes canceled and step error kind is canceled",
1287
1356
  return { ok: true };
1288
1357
  });
1289
1358
 
1290
- const worker = await startWorker({
1359
+ const worker = await startWorker({ app: "test-app",
1291
1360
  redis,
1292
1361
  prefix,
1293
1362
  queues: ["q12"],
@@ -1345,7 +1414,7 @@ test("terminal run re-queued is ignored (no re-execution)", async () => {
1345
1414
  return { ok: true };
1346
1415
  });
1347
1416
 
1348
- const worker = await startWorker({
1417
+ const worker = await startWorker({ app: "test-app",
1349
1418
  redis,
1350
1419
  prefix,
1351
1420
  queues: [queue],
@@ -1393,7 +1462,7 @@ test("lease+reaper: long running step is not duplicated", async () => {
1393
1462
  return { ok: true };
1394
1463
  });
1395
1464
 
1396
- const worker = await startWorker({
1465
+ const worker = await startWorker({ app: "test-app",
1397
1466
  redis,
1398
1467
  prefix,
1399
1468
  queues: [queue],
@@ -1438,8 +1507,8 @@ test(
1438
1507
  },
1439
1508
  );
1440
1509
 
1441
- const w1 = await startWorker({ redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
1442
- const w2 = await startWorker({ redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
1510
+ const w1 = await startWorker({ app: "test-app", redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
1511
+ const w2 = await startWorker({ app: "test-app", redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
1443
1512
 
1444
1513
  await new Promise((r) => setTimeout(r, 3500));
1445
1514
  const count = Number((await redis.get(counterKey)) ?? "0");
@@ -1505,7 +1574,7 @@ test(
1505
1574
  },
1506
1575
  );
1507
1576
 
1508
- const worker = await startWorker({
1577
+ const worker = await startWorker({ app: "test-app",
1509
1578
  redis,
1510
1579
  prefix,
1511
1580
  queues: ["q_parent", "q_child"],
@@ -1560,7 +1629,7 @@ test(
1560
1629
  const queuedMembers = await redis.zrange(queuedIndex, 0, -1);
1561
1630
  expect(queuedMembers.includes(handle.id)).toBe(true);
1562
1631
 
1563
- const worker = await startWorker({
1632
+ const worker = await startWorker({ app: "test-app",
1564
1633
  redis,
1565
1634
  prefix,
1566
1635
  queues: [queue],
@@ -1606,7 +1675,7 @@ test(
1606
1675
  cron: [{ expression: "*/1 * * * * *" }],
1607
1676
  }, async () => ({ ok: true }));
1608
1677
 
1609
- await client.syncRegistry(getDefaultRegistry());
1678
+ await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
1610
1679
 
1611
1680
  const workflowKey = `${prefix}:workflow:${name}`;
1612
1681
  const cronIdsJson = await redis.hget(workflowKey, "cronIdsJson");
@@ -1624,7 +1693,7 @@ test(
1624
1693
  queue: "q_update",
1625
1694
  }, async () => ({ ok: true }));
1626
1695
 
1627
- await client.syncRegistry(getDefaultRegistry());
1696
+ await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
1628
1697
 
1629
1698
  expect(await redis.hget(`${prefix}:cron:def`, cronId)).toBeNull();
1630
1699
  const cronNext2 = await redis.zrange(`${prefix}:cron:next`, 0, -1);
@@ -1658,7 +1727,7 @@ test(
1658
1727
  },
1659
1728
  );
1660
1729
 
1661
- const worker = await startWorker({
1730
+ const worker = await startWorker({ app: "test-app",
1662
1731
  redis,
1663
1732
  prefix,
1664
1733
  queues: [queue],
@@ -1708,8 +1777,8 @@ test(
1708
1777
  },
1709
1778
  );
1710
1779
 
1711
- const w1 = await startWorker({ redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
1712
- const w2 = await startWorker({ redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
1780
+ const w1 = await startWorker({ app: "test-app", redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
1781
+ const w2 = await startWorker({ app: "test-app", redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
1713
1782
 
1714
1783
  await waitFor(
1715
1784
  async () => Number((await redis.get(counterKey)) ?? "0") >= 1,
@@ -1744,7 +1813,7 @@ test("listRuns: status + workflow filters are applied together", async () => {
1744
1813
  throw new Error("expected failure");
1745
1814
  });
1746
1815
 
1747
- const worker = await startWorker({ redis, prefix, queues: [queue], runtime: { leaseMs: 500 } });
1816
+ const worker = await startWorker({ app: "test-app", redis, prefix, queues: [queue], runtime: { leaseMs: 500 } });
1748
1817
 
1749
1818
  const successHandle = await succeeds.run({});
1750
1819
  const failHandle = await fails.run({});
@@ -1795,7 +1864,7 @@ test("cron trigger ids: same custom id in two workflows does not collide", async
1795
1864
  async () => ({ ok: true }),
1796
1865
  );
1797
1866
 
1798
- await client.syncRegistry(getDefaultRegistry());
1867
+ await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
1799
1868
 
1800
1869
  const idsA = JSON.parse((await redis.hget(`${prefix}:workflow:cron-id-a`, "cronIdsJson")) ?? "[]") as string[];
1801
1870
  const idsB = JSON.parse((await redis.hget(`${prefix}:workflow:cron-id-b`, "cronIdsJson")) ?? "[]") as string[];
@@ -1823,7 +1892,7 @@ test("syncRegistry: cron trigger with explicit null input preserves null payload
1823
1892
  async () => ({ ok: true }),
1824
1893
  );
1825
1894
 
1826
- await client.syncRegistry(getDefaultRegistry());
1895
+ await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
1827
1896
 
1828
1897
  const workflowKey = `${prefix}:workflow:${workflowName}`;
1829
1898
  const cronIds = JSON.parse((await redis.hget(workflowKey, "cronIdsJson")) ?? "[]") as string[];
@@ -1854,7 +1923,7 @@ test("syncRegistry: invalid cron expression removes existing next schedule for s
1854
1923
  async () => ({ ok: true }),
1855
1924
  );
1856
1925
 
1857
- await client.syncRegistry(getDefaultRegistry());
1926
+ await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
1858
1927
 
1859
1928
  const cronIds = JSON.parse((await redis.hget(workflowKey, "cronIdsJson")) ?? "[]") as string[];
1860
1929
  expect(cronIds.length).toBe(1);
@@ -1870,7 +1939,7 @@ test("syncRegistry: invalid cron expression removes existing next schedule for s
1870
1939
  async () => ({ ok: true }),
1871
1940
  );
1872
1941
 
1873
- await client.syncRegistry(getDefaultRegistry());
1942
+ await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
1874
1943
 
1875
1944
  const after = await redis.zrange(cronNextKey, 0, -1);
1876
1945
  expect(after.includes(cronId)).toBe(false);
@@ -1891,7 +1960,7 @@ test("syncRegistry: removes stale workflow metadata (cron)", async () => {
1891
1960
  async () => ({ ok: true }),
1892
1961
  );
1893
1962
 
1894
- await client.syncRegistry(getDefaultRegistry());
1963
+ await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
1895
1964
 
1896
1965
  const workflowKey = `${prefix}:workflow:${workflowName}`;
1897
1966
  const cronIds = JSON.parse((await redis.hget(workflowKey, "cronIdsJson")) ?? "[]") as string[];
@@ -1903,7 +1972,7 @@ test("syncRegistry: removes stale workflow metadata (cron)", async () => {
1903
1972
  // Force stale age beyond grace period and sync an empty registry.
1904
1973
  await redis.hset(workflowKey, { updatedAt: "1" });
1905
1974
  __unstableResetDefaultRegistryForTests();
1906
- await client.syncRegistry(getDefaultRegistry());
1975
+ await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
1907
1976
 
1908
1977
  expect((await redis.smembers(`${prefix}:workflows`)).includes(workflowName)).toBe(false);
1909
1978
  expect(await redis.hget(workflowKey, "queue")).toBeNull();
@@ -1914,37 +1983,37 @@ test("syncRegistry: removes stale workflow metadata (cron)", async () => {
1914
1983
  redis.close();
1915
1984
  });
1916
1985
 
1917
- test("syncRegistry: stale cleanup is isolated by owner", async () => {
1986
+ test("syncRegistry: stale cleanup is isolated by app", async () => {
1918
1987
  const prefix = testPrefix();
1919
1988
  const redis = new RedisClient(redisServer.url);
1920
1989
  const client = createClient({ redis, prefix });
1921
1990
 
1922
1991
  __unstableResetDefaultRegistryForTests();
1923
- defineWorkflow("owner-a-wf", {queue: "qa",
1992
+ defineWorkflow("app-a-wf", {queue: "qa",
1924
1993
  cron: [{ id: "a", expression: "*/30 * * * * *" }],
1925
1994
  },
1926
1995
  async () => ({ ok: true }),
1927
1996
  );
1928
- await client.syncRegistry(getDefaultRegistry(), { owner: "svc-a" });
1997
+ await client.syncRegistry(getDefaultRegistry(), { app: "svc-a" });
1929
1998
 
1930
1999
  __unstableResetDefaultRegistryForTests();
1931
- defineWorkflow("owner-b-wf", {queue: "qb",
2000
+ defineWorkflow("app-b-wf", {queue: "qb",
1932
2001
  cron: [{ id: "b", expression: "*/30 * * * * *" }],
1933
2002
  },
1934
2003
  async () => ({ ok: true }),
1935
2004
  );
1936
- await client.syncRegistry(getDefaultRegistry(), { owner: "svc-b" });
2005
+ await client.syncRegistry(getDefaultRegistry(), { app: "svc-b" });
1937
2006
 
1938
- await redis.hset(`${prefix}:workflow:owner-a-wf`, { updatedAt: "1" });
1939
- await redis.hset(`${prefix}:workflow:owner-b-wf`, { updatedAt: "1" });
2007
+ await redis.hset(`${prefix}:workflow:app-a-wf`, { updatedAt: "1" });
2008
+ await redis.hset(`${prefix}:workflow:app-b-wf`, { updatedAt: "1" });
1940
2009
 
1941
2010
  __unstableResetDefaultRegistryForTests();
1942
- await client.syncRegistry(getDefaultRegistry(), { owner: "svc-a" });
2011
+ await client.syncRegistry(getDefaultRegistry(), { app: "svc-a" });
1943
2012
 
1944
- expect(await redis.hget(`${prefix}:workflow:owner-a-wf`, "queue")).toBeNull();
1945
- expect(await redis.hget(`${prefix}:workflow:owner-b-wf`, "queue")).not.toBeNull();
1946
- expect((await redis.smembers(`${prefix}:workflows`)).includes("owner-a-wf")).toBe(false);
1947
- expect((await redis.smembers(`${prefix}:workflows`)).includes("owner-b-wf")).toBe(true);
2013
+ expect(await redis.hget(`${prefix}:workflow:app-a-wf`, "queue")).toBeNull();
2014
+ expect(await redis.hget(`${prefix}:workflow:app-b-wf`, "queue")).not.toBeNull();
2015
+ expect((await redis.smembers(`${prefix}:workflows`)).includes("app-a-wf")).toBe(false);
2016
+ expect((await redis.smembers(`${prefix}:workflows`)).includes("app-b-wf")).toBe(true);
1948
2017
 
1949
2018
  redis.close();
1950
2019
  });
@@ -1957,7 +2026,7 @@ test("cancelRun: terminal runs are unchanged", async () => {
1957
2026
 
1958
2027
  const wf = defineWorkflow("cancel-terminal", {queue: "q_cancel_terminal" }, async () => ({ ok: true }));
1959
2028
 
1960
- const worker = await startWorker({
2029
+ const worker = await startWorker({ app: "test-app",
1961
2030
  redis,
1962
2031
  prefix,
1963
2032
  queues: ["q_cancel_terminal"],
@@ -1992,7 +2061,7 @@ test("output serialization: non-JSON output fails once and is not stuck", async
1992
2061
  async () => ({ bad: 1n }),
1993
2062
  );
1994
2063
 
1995
- const worker = await startWorker({
2064
+ const worker = await startWorker({ app: "test-app",
1996
2065
  redis,
1997
2066
  prefix,
1998
2067
  queues: ["q_serial"],
@@ -2047,7 +2116,7 @@ test("retry step sequence: new steps keep monotonic order after cached steps", a
2047
2116
  },
2048
2117
  );
2049
2118
 
2050
- const worker = await startWorker({
2119
+ const worker = await startWorker({ app: "test-app",
2051
2120
  redis,
2052
2121
  prefix,
2053
2122
  queues: ["q_seq"],
@@ -2078,7 +2147,7 @@ test("listRuns: negative offset is clamped to zero", async () => {
2078
2147
  setDefaultClient(client);
2079
2148
 
2080
2149
  const wf = defineWorkflow("offset-wf", {queue: "q_offset" }, async () => ({ ok: true }));
2081
- const worker = await startWorker({
2150
+ const worker = await startWorker({ app: "test-app",
2082
2151
  redis,
2083
2152
  prefix,
2084
2153
  queues: ["q_offset"],
@@ -2142,7 +2211,7 @@ test("error serialization: non-serializable thrown values do not wedge a run", a
2142
2211
  },
2143
2212
  );
2144
2213
 
2145
- const worker = await startWorker({
2214
+ const worker = await startWorker({ app: "test-app",
2146
2215
  redis,
2147
2216
  prefix,
2148
2217
  queues: ["q_bigint"],
@@ -2185,7 +2254,7 @@ test("cron trigger ids: delimiter-like custom ids do not collide", async () => {
2185
2254
  async () => ({ ok: true }),
2186
2255
  );
2187
2256
 
2188
- await client.syncRegistry(getDefaultRegistry());
2257
+ await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
2189
2258
 
2190
2259
  const idsA = JSON.parse((await redis.hget(`${prefix}:workflow:wf:a`, "cronIdsJson")) ?? "[]") as string[];
2191
2260
  const idsB = JSON.parse((await redis.hget(`${prefix}:workflow:wf:a:b`, "cronIdsJson")) ?? "[]") as string[];
@@ -2219,7 +2288,7 @@ test("idempotencyKey: stale pointer is recovered instead of returning missing ru
2219
2288
 
2220
2289
  const wf = defineWorkflow(workflowName, {queue }, async () => ({ ok: true }));
2221
2290
 
2222
- const worker = await startWorker({
2291
+ const worker = await startWorker({ app: "test-app",
2223
2292
  redis,
2224
2293
  prefix,
2225
2294
  queues: [queue],
@@ -2262,7 +2331,7 @@ test("idempotencyKey: partial existing run is repaired and executed", async () =
2262
2331
  return { ok: true };
2263
2332
  });
2264
2333
 
2265
- const worker = await startWorker({
2334
+ const worker = await startWorker({ app: "test-app",
2266
2335
  redis,
2267
2336
  prefix,
2268
2337
  queues: [queue],
@@ -2314,7 +2383,7 @@ test("syncRegistry: duplicate custom cron ids in one workflow are rejected", asy
2314
2383
  async () => ({ ok: true }),
2315
2384
  );
2316
2385
 
2317
- await expect(client.syncRegistry(getDefaultRegistry())).rejects.toThrow("Duplicate cron trigger id");
2386
+ await expect(client.syncRegistry(getDefaultRegistry(), { app: "test-app" })).rejects.toThrow("Duplicate cron trigger id");
2318
2387
 
2319
2388
  redis.close();
2320
2389
  });
@@ -2331,7 +2400,7 @@ test("scheduled promoter: stale scheduled entry does not resurrect terminal run"
2331
2400
  return { ok: true };
2332
2401
  });
2333
2402
 
2334
- const worker = await startWorker({
2403
+ const worker = await startWorker({ app: "test-app",
2335
2404
  redis,
2336
2405
  prefix,
2337
2406
  queues: [queue],
@@ -2398,7 +2467,7 @@ test(
2398
2467
 
2399
2468
  let worker: Awaited<ReturnType<typeof startWorker>> | null = null;
2400
2469
  try {
2401
- worker = await startWorker({
2470
+ worker = await startWorker({ app: "test-app",
2402
2471
  redis,
2403
2472
  prefix,
2404
2473
  queues: [queue],
@@ -2449,7 +2518,7 @@ test(
2449
2518
 
2450
2519
  let worker: Awaited<ReturnType<typeof startWorker>> | null = null;
2451
2520
  try {
2452
- worker = await startWorker({
2521
+ worker = await startWorker({ app: "test-app",
2453
2522
  redis,
2454
2523
  prefix,
2455
2524
  queues: [queue],
@@ -2492,7 +2561,7 @@ test("workflow names ending with ':runs' do not collide with workflow run indexe
2492
2561
 
2493
2562
  expect(keys.workflow(prefix, "keyspace-base:runs")).not.toBe(keys.workflowRuns(prefix, "keyspace-base"));
2494
2563
 
2495
- const worker = await startWorker({
2564
+ const worker = await startWorker({ app: "test-app",
2496
2565
  redis,
2497
2566
  prefix,
2498
2567
  queues: [queue],
@@ -2537,7 +2606,7 @@ test("cancelRun race: near-finish cancellation settles as canceled", async () =>
2537
2606
  return await originalFinalizeRun.call(this, runId, args);
2538
2607
  }) as RedflowClient["finalizeRun"];
2539
2608
 
2540
- const worker = await startWorker({
2609
+ const worker = await startWorker({ app: "test-app",
2541
2610
  redis,
2542
2611
  prefix,
2543
2612
  queues: [queue],
@@ -2600,7 +2669,7 @@ test("cancelRun race: cancellation requested during finalize wins over success",
2600
2669
  return await originalFinalizeRun.call(this, runId, args);
2601
2670
  }) as RedflowClient["finalizeRun"];
2602
2671
 
2603
- const worker = await startWorker({
2672
+ const worker = await startWorker({ app: "test-app",
2604
2673
  redis,
2605
2674
  prefix,
2606
2675
  queues: [queue],
@@ -2650,7 +2719,7 @@ test(
2650
2719
  await new Promise<never>(() => {});
2651
2720
  });
2652
2721
 
2653
- const worker = await startWorker({
2722
+ const worker = await startWorker({ app: "test-app",
2654
2723
  redis,
2655
2724
  prefix,
2656
2725
  queues: [queue],