@dudousxd/nestjs-durable-testing 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -399,7 +399,7 @@ async function assertReplayable(register, history) {
399
399
  __name(assertReplayable, "assertReplayable");
400
400
 
401
401
  // src/state-store-conformance.ts
402
- import { SINGLETON_ADMITTED_TAG, WorkflowEngine as WorkflowEngine3 } from "@dudousxd/nestjs-durable-core";
402
+ import { SINGLETON_ADMITTED_TAG, SINGLETON_TRACKED_TAG, WorkflowEngine as WorkflowEngine3 } from "@dudousxd/nestjs-durable-core";
403
403
  import { beforeEach, describe as describe2, expect as expect3, it as it2 } from "vitest";
404
404
 
405
405
  // src/checkpoint-aggregation-contract.ts
@@ -932,7 +932,8 @@ function runStateStoreContract(name, makeStore) {
932
932
  await store.createRun(run({
933
933
  id,
934
934
  tags: [
935
- "singleton:k"
935
+ "singleton:k",
936
+ SINGLETON_TRACKED_TAG
936
937
  ]
937
938
  }));
938
939
  }
@@ -1021,6 +1022,134 @@ function runStateStoreContract(name, makeStore) {
1021
1022
  }));
1022
1023
  expect3(await claim("other-workflow", "singleton:k", "other")).toBe(true);
1023
1024
  });
1025
+ const created = /* @__PURE__ */ __name((offset) => new Date(at.getTime() + offset * 1e3), "created");
1026
+ t("singleton admission counts an untagged pre-0.73 holder (rolling upgrade)", async () => {
1027
+ const claim = /* @__PURE__ */ __name((id, wakeAt) => store.tryAdmitSingleton?.(id, "singleton:k", "checkout", 1, wakeAt), "claim");
1028
+ await store.createRun(run({
1029
+ id: "legacy",
1030
+ tags: [
1031
+ "singleton:k"
1032
+ ],
1033
+ createdAt: created(1)
1034
+ }));
1035
+ await store.createRun(run({
1036
+ id: "new",
1037
+ tags: [
1038
+ "singleton:k",
1039
+ SINGLETON_TRACKED_TAG
1040
+ ],
1041
+ createdAt: created(0)
1042
+ }));
1043
+ expect3(await claim("new", 12e3)).toBe(false);
1044
+ expect3(await store.getRun("new")).toMatchObject({
1045
+ status: "suspended",
1046
+ wakeAt: 12e3
1047
+ });
1048
+ expect3((await store.getRun("new"))?.tags).toContain(SINGLETON_TRACKED_TAG);
1049
+ await store.updateRun("legacy", {
1050
+ status: "suspended",
1051
+ wakeAt: 2e4
1052
+ });
1053
+ expect3(await claim("new")).toBe(false);
1054
+ await store.updateRun("legacy", {
1055
+ status: "completed"
1056
+ });
1057
+ expect3(await claim("new")).toBe(true);
1058
+ expect3((await store.getRun("new"))?.tags).toEqual([
1059
+ "singleton:k",
1060
+ SINGLETON_ADMITTED_TAG
1061
+ ]);
1062
+ await store.updateRun("new", {
1063
+ status: "completed"
1064
+ });
1065
+ expect3((await store.getRun("new"))?.tags).toEqual([
1066
+ "singleton:k"
1067
+ ]);
1068
+ });
1069
+ t("legacy singleton waiters keep their FIFO place across the upgrade", async () => {
1070
+ const claim = /* @__PURE__ */ __name((id) => store.tryAdmitSingleton?.(id, "singleton:k", "checkout", 1), "claim");
1071
+ await store.createRun(run({
1072
+ id: "holder",
1073
+ tags: [
1074
+ "singleton:k"
1075
+ ],
1076
+ createdAt: created(0)
1077
+ }));
1078
+ await store.createRun(run({
1079
+ id: "old-waiter",
1080
+ status: "suspended",
1081
+ wakeAt: 5e3,
1082
+ tags: [
1083
+ "singleton:k"
1084
+ ],
1085
+ createdAt: created(1)
1086
+ }));
1087
+ await store.createRun(run({
1088
+ id: "new-run",
1089
+ tags: [
1090
+ "singleton:k",
1091
+ SINGLETON_TRACKED_TAG
1092
+ ],
1093
+ createdAt: created(2)
1094
+ }));
1095
+ expect3(await claim("new-run")).toBe(false);
1096
+ expect3(await claim("old-waiter")).toBe(false);
1097
+ expect3(await store.tryAdmitSingleton?.("old-waiter", "singleton:k", "checkout", 1, 6e3)).toBe(false);
1098
+ expect3((await store.getRun("old-waiter"))?.tags).toContain(SINGLETON_TRACKED_TAG);
1099
+ await store.updateRun("holder", {
1100
+ status: "completed"
1101
+ });
1102
+ expect3(await claim("new-run")).toBe(false);
1103
+ expect3(await claim("old-waiter")).toBe(true);
1104
+ expect3(await claim("new-run")).toBe(false);
1105
+ await store.updateRun("old-waiter", {
1106
+ status: "completed"
1107
+ });
1108
+ expect3(await claim("new-run")).toBe(true);
1109
+ });
1110
+ t("concurrent claims never over-admit beside an untagged legacy holder", async () => {
1111
+ await store.createRun(run({
1112
+ id: "legacy",
1113
+ tags: [
1114
+ "singleton:k"
1115
+ ],
1116
+ createdAt: created(0)
1117
+ }));
1118
+ const ids2 = [
1119
+ "c1",
1120
+ "c2",
1121
+ "c3",
1122
+ "c4"
1123
+ ];
1124
+ for (const [i, id] of ids2.entries()) {
1125
+ await store.createRun(run({
1126
+ id,
1127
+ tags: [
1128
+ "singleton:k",
1129
+ SINGLETON_TRACKED_TAG
1130
+ ],
1131
+ createdAt: created(i + 1)
1132
+ }));
1133
+ }
1134
+ const results = await Promise.all([
1135
+ ...ids2
1136
+ ].reverse().map((id, i) => (i % 2 ? peerStore : store).tryAdmitSingleton?.(id, "singleton:k", "checkout", 2)));
1137
+ expect3(results).toEqual([
1138
+ false,
1139
+ false,
1140
+ false,
1141
+ true
1142
+ ]);
1143
+ const active = await store.listRuns({
1144
+ workflow: "checkout",
1145
+ statuses: [
1146
+ "running"
1147
+ ]
1148
+ });
1149
+ expect3(active.filter((r) => r.tags?.includes(SINGLETON_ADMITTED_TAG)).map((r) => r.id)).toEqual([
1150
+ "c1"
1151
+ ]);
1152
+ });
1024
1153
  t("creates a run and reads it back with its JSON input", async () => {
1025
1154
  await store.createRun(run());
1026
1155
  const loaded = await store.getRun("r1");
@@ -1501,40 +1630,40 @@ function runStateStoreContract(name, makeStore) {
1501
1630
  t("listOrphanedRuns returns incomplete runs with a free or expired lease, oldest first", async () => {
1502
1631
  expect3(store.listOrphanedRuns).toBeDefined();
1503
1632
  if (!store.listOrphanedRuns) return;
1504
- const created = /* @__PURE__ */ __name((offset) => new Date(at.getTime() + offset * 1e3), "created");
1633
+ const created2 = /* @__PURE__ */ __name((offset) => new Date(at.getTime() + offset * 1e3), "created");
1505
1634
  await store.createRun(run({
1506
1635
  id: "held",
1507
- createdAt: created(1)
1636
+ createdAt: created2(1)
1508
1637
  }));
1509
1638
  expect3(await store.tryLockRun("held", "live", NOW + 3e4, NOW)).toBe(true);
1510
1639
  await store.createRun(run({
1511
1640
  id: "expired",
1512
- createdAt: created(3)
1641
+ createdAt: created2(3)
1513
1642
  }));
1514
1643
  expect3(await store.tryLockRun("expired", "dead", NOW, NOW - 1)).toBe(true);
1515
1644
  await store.createRun(run({
1516
1645
  id: "free",
1517
- createdAt: created(2)
1646
+ createdAt: created2(2)
1518
1647
  }));
1519
1648
  await store.createRun(run({
1520
1649
  id: "compensating",
1521
1650
  status: "cancelling",
1522
- createdAt: created(4)
1651
+ createdAt: created2(4)
1523
1652
  }));
1524
1653
  await store.createRun(run({
1525
1654
  id: "parked",
1526
1655
  status: "suspended",
1527
- createdAt: created(0)
1656
+ createdAt: created2(0)
1528
1657
  }));
1529
1658
  await store.createRun(run({
1530
1659
  id: "done",
1531
1660
  status: "completed",
1532
- createdAt: created(0)
1661
+ createdAt: created2(0)
1533
1662
  }));
1534
1663
  await store.createRun(run({
1535
1664
  id: "tenant",
1536
1665
  namespace: "beta",
1537
- createdAt: created(5)
1666
+ createdAt: created2(5)
1538
1667
  }));
1539
1668
  expect3((await store.listOrphanedRuns(NOW, 10)).map((r) => r.id)).toEqual([
1540
1669
  "free",
@@ -1552,6 +1681,50 @@ function runStateStoreContract(name, makeStore) {
1552
1681
  expect3((await store.listOrphanedRuns(NOW + 3e4, 10)).map((r) => r.id)).toContain("held");
1553
1682
  expect3(await store.listOrphanedRuns(NOW, 0)).toEqual([]);
1554
1683
  });
1684
+ t("listOrphanedRuns also returns the owner's live leases taken before its cutoff", async () => {
1685
+ if (!store.listOrphanedRuns) return;
1686
+ const created2 = /* @__PURE__ */ __name((offset) => new Date(at.getTime() + offset * 1e3), "created");
1687
+ await store.createRun(run({
1688
+ id: "own-old",
1689
+ createdAt: created2(1)
1690
+ }));
1691
+ expect3(await store.tryLockRun("own-old", "pod-a", NOW + 1e4, NOW)).toBe(true);
1692
+ await store.createRun(run({
1693
+ id: "own-new",
1694
+ createdAt: created2(2)
1695
+ }));
1696
+ expect3(await store.tryLockRun("own-new", "pod-a", NOW + 3e4, NOW)).toBe(true);
1697
+ await store.createRun(run({
1698
+ id: "foreign",
1699
+ createdAt: created2(3)
1700
+ }));
1701
+ expect3(await store.tryLockRun("foreign", "pod-b", NOW + 1e4, NOW)).toBe(true);
1702
+ await store.createRun(run({
1703
+ id: "own-parked",
1704
+ status: "suspended",
1705
+ createdAt: created2(4)
1706
+ }));
1707
+ expect3(await store.tryLockRun("own-parked", "pod-a", NOW + 1e4, NOW)).toBe(true);
1708
+ await store.createRun(run({
1709
+ id: "free",
1710
+ createdAt: created2(5)
1711
+ }));
1712
+ const ownedBy = {
1713
+ owner: "pod-a",
1714
+ lockedBefore: NOW + 3e4
1715
+ };
1716
+ expect3((await store.listOrphanedRuns(NOW, 10)).map((r) => r.id)).toEqual([
1717
+ "free"
1718
+ ]);
1719
+ expect3((await store.listOrphanedRuns(NOW, 10, void 0, ownedBy)).map((r) => r.id)).toEqual([
1720
+ "own-old",
1721
+ "free"
1722
+ ]);
1723
+ expect3((await store.listOrphanedRuns(NOW, 1, void 0, ownedBy)).map((r) => r.id)).toEqual([
1724
+ "own-old"
1725
+ ]);
1726
+ expect3((await store.listOrphanedRuns(NOW, 10, "beta", ownedBy)).map((r) => r.id)).toEqual([]);
1727
+ });
1555
1728
  t("updateRunIf writes only over an expected status", async () => {
1556
1729
  expect3(store.updateRunIf).toBeDefined();
1557
1730
  if (!store.updateRunIf) return;
@@ -1614,6 +1787,29 @@ function runStateStoreContract(name, makeStore) {
1614
1787
  })).toBe(false);
1615
1788
  expect3(await store.getRun("missing")).toBeNull();
1616
1789
  });
1790
+ t("updateRunIf with an empty patch is a pure compare: never throws, writes nothing", async () => {
1791
+ expect3(store.updateRunIf).toBeDefined();
1792
+ if (!store.updateRunIf) return;
1793
+ await store.createRun(run({
1794
+ output: {
1795
+ kept: true
1796
+ }
1797
+ }));
1798
+ expect3(await store.updateRunIf("r1", [
1799
+ "running"
1800
+ ], {})).toBe(true);
1801
+ expect3(await store.updateRunIf("r1", [
1802
+ "suspended"
1803
+ ], {})).toBe(false);
1804
+ expect3(await store.updateRunIf("missing", [
1805
+ "running"
1806
+ ], {})).toBe(false);
1807
+ const current = await store.getRun("r1");
1808
+ expect3(current?.status).toBe("running");
1809
+ expect3(current?.output).toEqual({
1810
+ kept: true
1811
+ });
1812
+ });
1617
1813
  t("updateRunIf settling a run terminal drops its singleton admission like updateRun", async () => {
1618
1814
  expect3(store.updateRunIf).toBeDefined();
1619
1815
  if (!store.updateRunIf) return;
@@ -3871,6 +4067,77 @@ function runStateStoreContract(name, makeStore) {
3871
4067
  })).rejects.toThrow(/vwf@9 is not registered/);
3872
4068
  expect3(await store.getRun("ghost-run")).toBeNull();
3873
4069
  });
4070
+ t("times out every unfinished run of its workflow and namespace, except one compensating", async () => {
4071
+ const engine = new WorkflowEngine3({
4072
+ store,
4073
+ namespace: "alpha"
4074
+ });
4075
+ engine.register("t-capped", "1", async () => void 0, {
4076
+ executionTimeout: "1h"
4077
+ });
4078
+ const old = {
4079
+ workflow: "t-capped",
4080
+ namespace: "alpha",
4081
+ createdAt: /* @__PURE__ */ new Date(1e3)
4082
+ };
4083
+ await store.createRun(run({
4084
+ ...old,
4085
+ id: "tc-pending",
4086
+ status: "pending"
4087
+ }));
4088
+ await store.createRun(run({
4089
+ ...old,
4090
+ id: "tc-running",
4091
+ status: "running"
4092
+ }));
4093
+ await store.createRun(run({
4094
+ ...old,
4095
+ id: "tc-suspended",
4096
+ status: "suspended"
4097
+ }));
4098
+ await store.createRun(run({
4099
+ ...old,
4100
+ id: "tc-blocked",
4101
+ status: "blocked"
4102
+ }));
4103
+ await store.createRun(run({
4104
+ ...old,
4105
+ id: "tc-cancelling",
4106
+ status: "cancelling"
4107
+ }));
4108
+ await store.createRun(run({
4109
+ ...old,
4110
+ id: "tc-done",
4111
+ status: "completed"
4112
+ }));
4113
+ await store.createRun(run({
4114
+ ...old,
4115
+ id: "tc-other-tenant",
4116
+ status: "pending",
4117
+ namespace: "beta"
4118
+ }));
4119
+ await store.createRun(run({
4120
+ ...old,
4121
+ id: "tc-other-workflow",
4122
+ status: "pending",
4123
+ workflow: "t-uncapped"
4124
+ }));
4125
+ await engine.sweepTimeouts(1e3 + 37e5);
4126
+ for (const id of [
4127
+ "tc-pending",
4128
+ "tc-running",
4129
+ "tc-suspended",
4130
+ "tc-blocked"
4131
+ ]) {
4132
+ const swept = await store.getRun(id);
4133
+ expect3(swept?.status, id).toBe("cancelled");
4134
+ expect3(swept?.error?.code, id).toBe("execution_timeout");
4135
+ }
4136
+ expect3((await store.getRun("tc-cancelling"))?.status).toBe("cancelling");
4137
+ expect3((await store.getRun("tc-done"))?.status).toBe("completed");
4138
+ expect3((await store.getRun("tc-other-tenant"))?.status).toBe("pending");
4139
+ expect3((await store.getRun("tc-other-workflow"))?.status).toBe("pending");
4140
+ });
3874
4141
  t("cancels a timed-out run\u2019s live child subtree, not just the run itself", async () => {
3875
4142
  const engine = new WorkflowEngine3({
3876
4143
  store