@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 +277 -10
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
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:
|
|
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:
|
|
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:
|
|
1646
|
+
createdAt: created2(2)
|
|
1518
1647
|
}));
|
|
1519
1648
|
await store.createRun(run({
|
|
1520
1649
|
id: "compensating",
|
|
1521
1650
|
status: "cancelling",
|
|
1522
|
-
createdAt:
|
|
1651
|
+
createdAt: created2(4)
|
|
1523
1652
|
}));
|
|
1524
1653
|
await store.createRun(run({
|
|
1525
1654
|
id: "parked",
|
|
1526
1655
|
status: "suspended",
|
|
1527
|
-
createdAt:
|
|
1656
|
+
createdAt: created2(0)
|
|
1528
1657
|
}));
|
|
1529
1658
|
await store.createRun(run({
|
|
1530
1659
|
id: "done",
|
|
1531
1660
|
status: "completed",
|
|
1532
|
-
createdAt:
|
|
1661
|
+
createdAt: created2(0)
|
|
1533
1662
|
}));
|
|
1534
1663
|
await store.createRun(run({
|
|
1535
1664
|
id: "tenant",
|
|
1536
1665
|
namespace: "beta",
|
|
1537
|
-
createdAt:
|
|
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
|