@classytic/arc 2.25.0 → 2.30.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/README.md +2 -0
- package/dist/{BaseController--M8EMzt0.mjs → BaseController-B5DwufbB.mjs} +15 -11
- package/dist/EventTransport-C6YA8xY9.mjs +103 -0
- package/dist/{HookSystem-C1vB_Gc9.mjs → HookSystem-WkIougo6.mjs} +15 -35
- package/dist/applyPermissionResult-Cf6zUCvb.mjs +257 -0
- package/dist/audit/index.d.mts +2 -2
- package/dist/audit/index.mjs +1 -1
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.mjs +1 -1
- package/dist/{buildHandler-Dtwd8fdz.mjs → buildHandler-BYS3Vkfz.mjs} +32 -6
- package/dist/cleanup/index.d.mts +313 -0
- package/dist/cleanup/index.mjs +1267 -0
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/docs.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +5 -3
- package/dist/core-poalJLvp.mjs +175 -0
- package/dist/{createActionRouter-B6GvAuAX.mjs → createActionRouter-BxuywVRB.mjs} +2 -2
- package/dist/{createAggregationRouter-wzwFa_dW.mjs → createAggregationRouter-BiY-lGdY.mjs} +7 -20
- package/dist/{preloadResources-DfyngIN9.mjs → createApp-CXrKFciH.mjs} +25 -76
- package/dist/defineAggregation-ufWjUwml.mjs +47 -0
- package/dist/{core-EUNu0u07.mjs → defineResource-CxWqDCAy.mjs} +36 -261
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +1 -1
- package/dist/{eventPlugin-DrQ_FesK.mjs → eventPlugin-D2NNR4py.mjs} +2 -103
- package/dist/events/index.d.mts +28 -2
- package/dist/events/index.mjs +24 -343
- package/dist/factory/index.d.mts +4 -3
- package/dist/factory/index.mjs +3 -1
- package/dist/{fastify-augmentation-dPNBVeIj.d.mts → fastify-augmentation-0WhPnUH7.d.mts} +3 -2
- package/dist/{applyPermissionResult-Tw4iXtlf.mjs → fields-CQ_7KQWw.mjs} +1 -97
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.mjs +1 -1
- package/dist/index-CCzVR-N3.d.mts +107 -0
- package/dist/{index-BMnbzvtO.d.mts → index-Ch16aAah.d.mts} +201 -21
- package/dist/{index-f4PTrk3B.d.mts → index-Cx7bro4R.d.mts} +4 -4
- package/dist/{index-DPRH0Cme.d.mts → index-Q5gzEIWY.d.mts} +97 -66
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +8 -6
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +6 -6
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/{mcpPlugin-Cb7olw0j.mjs → mcpPlugin-nRzm5g2s.mjs} +36 -43
- package/dist/middleware/index.d.mts +1 -1
- package/dist/migrations/index.d.mts +0 -3
- package/dist/migrations/index.mjs +0 -3
- package/dist/module-_-M7C5ES.mjs +29 -0
- package/dist/{openapi-DrLtog_B.mjs → openapi-B8Vkbxbn.mjs} +1 -1
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +4 -3
- package/dist/{permissions-BrtiBKGs.mjs → permissions-Bfu_8y1U.mjs} +318 -289
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/{pipeline-BN5G05rd.mjs → pipeline-BJnpkY9i.mjs} +4 -18
- package/dist/plugins/index.d.mts +4 -4
- package/dist/plugins/index.mjs +2 -2
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{preloadResources-0oe-JSSS.d.mts → preloadResources-DCfa2egz.d.mts} +3 -2
- package/dist/preloadResources-DSY5HGpS.mjs +45 -0
- package/dist/presets/filesUpload.d.mts +2 -2
- package/dist/presets/filesUpload.mjs +1 -1
- package/dist/presets/index.d.mts +14 -2
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-DwFDVKP0.mjs → presets-CJOEXLuk.mjs} +25 -9
- package/dist/projection-UBDgfMwu.mjs +17 -0
- package/dist/{realtime-Cuw_deA-.mjs → realtime-B9nmIY5V.mjs} +2 -1
- package/dist/{registerRoute-v0Wb6Plt.mjs → registerRoute-JkxIEBJq.mjs} +3 -1
- package/dist/registry/index.d.mts +2 -2
- package/dist/registry/index.mjs +1 -1
- package/dist/{registry-vcOCtPij.mjs → registry-BYXUUg21.mjs} +16 -1
- package/dist/repositoryFilter-Dz8k3S4P.mjs +44 -0
- package/dist/subscribe-helpers-DfzsivM6.mjs +339 -0
- package/dist/testing/cleanupStoreContract.d.mts +10 -0
- package/dist/testing/cleanupStoreContract.mjs +375 -0
- package/dist/testing/index.d.mts +34 -5
- package/dist/testing/index.mjs +316 -211
- package/dist/testing/outboxStoreContract.d.mts +13 -0
- package/dist/testing/outboxStoreContract.mjs +247 -0
- package/dist/types/index.d.mts +3 -3
- package/dist/{types-QFGc3Cjv.d.mts → types-Bmmn-fO0.d.mts} +1 -1
- package/dist/types-C_ZZRcD6.d.mts +450 -0
- package/dist/{types-Dmw-6Qeo.d.mts → types-ChvvVNP8.d.mts} +93 -88
- package/dist/{index-KUoxjIq1.d.mts → types-D_n8L6VY.d.mts} +33 -106
- package/dist/usage/index.d.mts +1 -1
- package/dist/usage/index.mjs +0 -0
- package/dist/utils/index.d.mts +2 -5
- package/package.json +539 -524
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
//#region src/testing/cleanupStoreContract.ts
|
|
3
|
+
/**
|
|
4
|
+
* Cleanup Run-Store Contract Suite
|
|
5
|
+
*
|
|
6
|
+
* `CleanupRunStore` is the durability substrate of the Data Cleanup Center: its
|
|
7
|
+
* atomic guarantees are the only thing standing between a crash and a
|
|
8
|
+
* *double-executed destructive operation*. The semantics are subtle —
|
|
9
|
+
* conditional insert by `concurrencyKey`, EXCLUSIVE lease claim with crash
|
|
10
|
+
* recovery, lease-guarded CAS, admission-guarded re-arm — and a plausible-looking
|
|
11
|
+
* adapter can get them wrong in ways no ordinary test catches.
|
|
12
|
+
*
|
|
13
|
+
* Any implementation can import this suite and run it against a live instance.
|
|
14
|
+
* Passing it IS the contract.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* import { runCleanupRunStoreContract } from '@classytic/arc/testing/cleanup';
|
|
19
|
+
* import { mongoCleanupRunStore } from '../src/cleanup/mongo-run-store.js';
|
|
20
|
+
*
|
|
21
|
+
* runCleanupRunStoreContract('mongoCleanupRunStore', async () => {
|
|
22
|
+
* const store = mongoCleanupRunStore(connection);
|
|
23
|
+
* return { store, teardown: async () => connection.dropCollection('cleanup_runs') };
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* Time is exercised through lease `expiresAt` values in the past/future rather
|
|
28
|
+
* than a clock hook, so the suite works unmodified against a real database
|
|
29
|
+
* comparing to wall-clock `now`.
|
|
30
|
+
*
|
|
31
|
+
* This module statically imports `vitest`. Only load it from test code — arc's
|
|
32
|
+
* production bundle never references this subpath, so the import tree stays
|
|
33
|
+
* clean under tree-shaking.
|
|
34
|
+
*/
|
|
35
|
+
const ACTOR = {
|
|
36
|
+
ref: "user:contract",
|
|
37
|
+
kind: "user"
|
|
38
|
+
};
|
|
39
|
+
function plan(recipeId) {
|
|
40
|
+
return {
|
|
41
|
+
recipeId,
|
|
42
|
+
parameters: {},
|
|
43
|
+
items: [{
|
|
44
|
+
resource: "orders",
|
|
45
|
+
estimated: 1
|
|
46
|
+
}],
|
|
47
|
+
retains: [],
|
|
48
|
+
blockers: [],
|
|
49
|
+
rebuildActions: [],
|
|
50
|
+
warnings: [],
|
|
51
|
+
estimatedTotal: 1,
|
|
52
|
+
confirmationPhrase: recipeId,
|
|
53
|
+
digest: `digest-${recipeId}`
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** A queued run. `concurrencyKey` is opt-in so key-free runs can be exercised. */
|
|
57
|
+
function makeRun(id, overrides = {}) {
|
|
58
|
+
return {
|
|
59
|
+
id,
|
|
60
|
+
recipeId: "contract.recipe",
|
|
61
|
+
recipeVersion: "1",
|
|
62
|
+
status: "queued",
|
|
63
|
+
planDigest: `digest-${id}`,
|
|
64
|
+
sealedPlan: plan("contract.recipe"),
|
|
65
|
+
parameters: {},
|
|
66
|
+
actor: ACTOR,
|
|
67
|
+
requestedBy: ACTOR.ref,
|
|
68
|
+
reason: "contract suite",
|
|
69
|
+
operationId: `op-${id}`,
|
|
70
|
+
destructive: true,
|
|
71
|
+
attempt: 0,
|
|
72
|
+
progress: {
|
|
73
|
+
processed: 0,
|
|
74
|
+
steps: 0
|
|
75
|
+
},
|
|
76
|
+
cancelRequested: false,
|
|
77
|
+
queuedAt: /* @__PURE__ */ new Date(),
|
|
78
|
+
...overrides
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const future = (ms = 6e4) => new Date(Date.now() + ms);
|
|
82
|
+
const past = (ms = 6e4) => new Date(Date.now() - ms);
|
|
83
|
+
function runCleanupRunStoreContract(name, setup) {
|
|
84
|
+
describe(`CleanupRunStore contract: ${name}`, () => {
|
|
85
|
+
let store;
|
|
86
|
+
let teardown;
|
|
87
|
+
let seq = 0;
|
|
88
|
+
const uid = (label) => `${label}-${Date.now()}-${seq++}`;
|
|
89
|
+
beforeAll(async () => {
|
|
90
|
+
const result = await setup();
|
|
91
|
+
store = result.store;
|
|
92
|
+
teardown = result.teardown;
|
|
93
|
+
});
|
|
94
|
+
afterAll(async () => {
|
|
95
|
+
if (teardown) await teardown();
|
|
96
|
+
});
|
|
97
|
+
describe("createIfPermitted — admission control", () => {
|
|
98
|
+
it("inserts a run and makes it readable", async () => {
|
|
99
|
+
const run = makeRun(uid("insert"), { concurrencyKey: void 0 });
|
|
100
|
+
expect(await store.createIfPermitted(run)).toEqual({ created: true });
|
|
101
|
+
const stored = await store.get(run.id);
|
|
102
|
+
expect(stored?.id).toBe(run.id);
|
|
103
|
+
expect(stored?.status).toBe("queued");
|
|
104
|
+
});
|
|
105
|
+
it("returns null from get() for an unknown id", async () => {
|
|
106
|
+
expect(await store.get(uid("missing"))).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
it("refuses a second non-terminal run holding the same concurrencyKey", async () => {
|
|
109
|
+
const key = uid("key");
|
|
110
|
+
const first = makeRun(uid("first"), { concurrencyKey: key });
|
|
111
|
+
const second = makeRun(uid("second"), { concurrencyKey: key });
|
|
112
|
+
expect(await store.createIfPermitted(first)).toEqual({ created: true });
|
|
113
|
+
const result = await store.createIfPermitted(second);
|
|
114
|
+
expect(result.created).toBe(false);
|
|
115
|
+
if (result.created === false) expect(result.activeRunId).toBe(first.id);
|
|
116
|
+
expect(await store.get(second.id)).toBeNull();
|
|
117
|
+
});
|
|
118
|
+
it("admits the key again once the holder reaches a terminal status", async () => {
|
|
119
|
+
const key = uid("key-free");
|
|
120
|
+
const first = makeRun(uid("holder"), { concurrencyKey: key });
|
|
121
|
+
await store.createIfPermitted(first);
|
|
122
|
+
await store.compareAndTransition(first.id, ["queued"], "completed");
|
|
123
|
+
const next = makeRun(uid("next"), { concurrencyKey: key });
|
|
124
|
+
expect(await store.createIfPermitted(next)).toEqual({ created: true });
|
|
125
|
+
});
|
|
126
|
+
it("never serializes runs that carry no concurrencyKey", async () => {
|
|
127
|
+
const a = makeRun(uid("keyless-a"), { concurrencyKey: void 0 });
|
|
128
|
+
const b = makeRun(uid("keyless-b"), { concurrencyKey: void 0 });
|
|
129
|
+
expect(await store.createIfPermitted(a)).toEqual({ created: true });
|
|
130
|
+
expect(await store.createIfPermitted(b)).toEqual({ created: true });
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe("claim — exclusive worker lease", () => {
|
|
134
|
+
it("grants the lease on a queued run and counts the attempt", async () => {
|
|
135
|
+
const run = makeRun(uid("claim"), { concurrencyKey: void 0 });
|
|
136
|
+
await store.createIfPermitted(run);
|
|
137
|
+
const token = uid("token");
|
|
138
|
+
const claimed = await store.claim(run.id, {
|
|
139
|
+
token,
|
|
140
|
+
expiresAt: future()
|
|
141
|
+
});
|
|
142
|
+
expect(claimed).not.toBeNull();
|
|
143
|
+
expect(claimed?.leaseToken).toBe(token);
|
|
144
|
+
expect(claimed?.attempt).toBe(run.attempt + 1);
|
|
145
|
+
});
|
|
146
|
+
it("refuses a run whose lease is still live (another worker owns it)", async () => {
|
|
147
|
+
const run = makeRun(uid("owned"), { concurrencyKey: void 0 });
|
|
148
|
+
await store.createIfPermitted(run);
|
|
149
|
+
await store.claim(run.id, {
|
|
150
|
+
token: uid("owner"),
|
|
151
|
+
expiresAt: future()
|
|
152
|
+
});
|
|
153
|
+
await store.compareAndTransition(run.id, ["queued"], "running");
|
|
154
|
+
expect(await store.claim(run.id, {
|
|
155
|
+
token: uid("thief"),
|
|
156
|
+
expiresAt: future()
|
|
157
|
+
})).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
it("recovers a running run whose lease EXPIRED (crash recovery)", async () => {
|
|
160
|
+
const run = makeRun(uid("crashed"), { concurrencyKey: void 0 });
|
|
161
|
+
await store.createIfPermitted(run);
|
|
162
|
+
await store.claim(run.id, {
|
|
163
|
+
token: uid("dead"),
|
|
164
|
+
expiresAt: past()
|
|
165
|
+
});
|
|
166
|
+
await store.compareAndTransition(run.id, ["queued"], "running");
|
|
167
|
+
const token = uid("recovered");
|
|
168
|
+
const reclaimed = await store.claim(run.id, {
|
|
169
|
+
token,
|
|
170
|
+
expiresAt: future()
|
|
171
|
+
});
|
|
172
|
+
expect(reclaimed).not.toBeNull();
|
|
173
|
+
expect(reclaimed?.leaseToken).toBe(token);
|
|
174
|
+
});
|
|
175
|
+
it("recovers a finalizing run whose lease expired", async () => {
|
|
176
|
+
const run = makeRun(uid("finalizing"), { concurrencyKey: void 0 });
|
|
177
|
+
await store.createIfPermitted(run);
|
|
178
|
+
await store.claim(run.id, {
|
|
179
|
+
token: uid("dead"),
|
|
180
|
+
expiresAt: past()
|
|
181
|
+
});
|
|
182
|
+
await store.compareAndTransition(run.id, ["queued"], "running");
|
|
183
|
+
await store.compareAndTransition(run.id, ["running"], "finalizing");
|
|
184
|
+
const reclaimed = await store.claim(run.id, {
|
|
185
|
+
token: uid("recovered"),
|
|
186
|
+
expiresAt: future()
|
|
187
|
+
});
|
|
188
|
+
expect(reclaimed).not.toBeNull();
|
|
189
|
+
expect(reclaimed?.status).toBe("finalizing");
|
|
190
|
+
});
|
|
191
|
+
it("refuses a terminal run", async () => {
|
|
192
|
+
const run = makeRun(uid("terminal"), { concurrencyKey: void 0 });
|
|
193
|
+
await store.createIfPermitted(run);
|
|
194
|
+
await store.compareAndTransition(run.id, ["queued"], "completed");
|
|
195
|
+
expect(await store.claim(run.id, {
|
|
196
|
+
token: uid("t"),
|
|
197
|
+
expiresAt: future()
|
|
198
|
+
})).toBeNull();
|
|
199
|
+
});
|
|
200
|
+
it("refuses an unknown run", async () => {
|
|
201
|
+
expect(await store.claim(uid("ghost"), {
|
|
202
|
+
token: "t",
|
|
203
|
+
expiresAt: future()
|
|
204
|
+
})).toBeNull();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
describe("compareAndTransition — guarded status change", () => {
|
|
208
|
+
it("applies the status and the patch when the current status matches", async () => {
|
|
209
|
+
const run = makeRun(uid("cas"), { concurrencyKey: void 0 });
|
|
210
|
+
await store.createIfPermitted(run);
|
|
211
|
+
const started = /* @__PURE__ */ new Date();
|
|
212
|
+
const updated = await store.compareAndTransition(run.id, ["queued"], "running", { startedAt: started });
|
|
213
|
+
expect(updated?.status).toBe("running");
|
|
214
|
+
expect(updated?.startedAt?.getTime()).toBe(started.getTime());
|
|
215
|
+
expect((await store.get(run.id))?.status).toBe("running");
|
|
216
|
+
});
|
|
217
|
+
it("refuses when the current status is not in `expected`", async () => {
|
|
218
|
+
const run = makeRun(uid("wrong-state"), { concurrencyKey: void 0 });
|
|
219
|
+
await store.createIfPermitted(run);
|
|
220
|
+
expect(await store.compareAndTransition(run.id, ["running"], "completed")).toBeNull();
|
|
221
|
+
expect((await store.get(run.id))?.status).toBe("queued");
|
|
222
|
+
});
|
|
223
|
+
it("refuses a stale ex-owner's write (lease-token mismatch)", async () => {
|
|
224
|
+
const run = makeRun(uid("stale"), { concurrencyKey: void 0 });
|
|
225
|
+
await store.createIfPermitted(run);
|
|
226
|
+
await store.claim(run.id, {
|
|
227
|
+
token: uid("dead"),
|
|
228
|
+
expiresAt: past()
|
|
229
|
+
});
|
|
230
|
+
await store.compareAndTransition(run.id, ["queued"], "running");
|
|
231
|
+
const live = uid("live");
|
|
232
|
+
await store.claim(run.id, {
|
|
233
|
+
token: live,
|
|
234
|
+
expiresAt: future()
|
|
235
|
+
});
|
|
236
|
+
expect(await store.compareAndTransition(run.id, ["running"], "completed", { completedAt: /* @__PURE__ */ new Date() }, "a-stale-token")).toBeNull();
|
|
237
|
+
expect((await store.get(run.id))?.status).toBe("running");
|
|
238
|
+
expect((await store.compareAndTransition(run.id, ["running"], "completed", void 0, live))?.status).toBe("completed");
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
describe("requestCancel — durable cancellation flag", () => {
|
|
242
|
+
it("sets the flag with audit metadata WITHOUT changing status", async () => {
|
|
243
|
+
const run = makeRun(uid("cancel"), { concurrencyKey: void 0 });
|
|
244
|
+
await store.createIfPermitted(run);
|
|
245
|
+
await store.compareAndTransition(run.id, ["queued"], "running");
|
|
246
|
+
const requestedAt = /* @__PURE__ */ new Date();
|
|
247
|
+
await store.requestCancel(run.id, {
|
|
248
|
+
actor: ACTOR,
|
|
249
|
+
reason: "operator changed their mind",
|
|
250
|
+
requestedAt
|
|
251
|
+
});
|
|
252
|
+
const stored = await store.get(run.id);
|
|
253
|
+
expect(stored?.cancelRequested).toBe(true);
|
|
254
|
+
expect(stored?.cancelReason).toBe("operator changed their mind");
|
|
255
|
+
expect(stored?.cancelRequestedBy?.ref).toBe(ACTOR.ref);
|
|
256
|
+
expect(stored?.status).toBe("running");
|
|
257
|
+
});
|
|
258
|
+
it("is idempotent — the FIRST request's audit metadata wins", async () => {
|
|
259
|
+
const run = makeRun(uid("cancel-twice"), { concurrencyKey: void 0 });
|
|
260
|
+
await store.createIfPermitted(run);
|
|
261
|
+
await store.requestCancel(run.id, {
|
|
262
|
+
actor: ACTOR,
|
|
263
|
+
reason: "first",
|
|
264
|
+
requestedAt: /* @__PURE__ */ new Date()
|
|
265
|
+
});
|
|
266
|
+
await store.requestCancel(run.id, {
|
|
267
|
+
actor: {
|
|
268
|
+
ref: "user:other",
|
|
269
|
+
kind: "user"
|
|
270
|
+
},
|
|
271
|
+
reason: "second",
|
|
272
|
+
requestedAt: /* @__PURE__ */ new Date()
|
|
273
|
+
});
|
|
274
|
+
const stored = await store.get(run.id);
|
|
275
|
+
expect(stored?.cancelReason).toBe("first");
|
|
276
|
+
expect(stored?.cancelRequestedBy?.ref).toBe(ACTOR.ref);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
describe("saveProgress — heartbeat + bounded summary", () => {
|
|
280
|
+
it("updates progress and extends the lease for the current owner", async () => {
|
|
281
|
+
const run = makeRun(uid("progress"), { concurrencyKey: void 0 });
|
|
282
|
+
await store.createIfPermitted(run);
|
|
283
|
+
const token = uid("owner");
|
|
284
|
+
await store.claim(run.id, {
|
|
285
|
+
token,
|
|
286
|
+
expiresAt: future(1e3)
|
|
287
|
+
});
|
|
288
|
+
const renewed = future(12e4);
|
|
289
|
+
await store.saveProgress(run.id, {
|
|
290
|
+
processed: 7,
|
|
291
|
+
steps: 2
|
|
292
|
+
}, {
|
|
293
|
+
token,
|
|
294
|
+
expiresAt: renewed
|
|
295
|
+
});
|
|
296
|
+
const stored = await store.get(run.id);
|
|
297
|
+
expect(stored?.progress.processed).toBe(7);
|
|
298
|
+
expect(stored?.progress.steps).toBe(2);
|
|
299
|
+
expect(stored?.leaseExpiresAt?.getTime()).toBe(renewed.getTime());
|
|
300
|
+
});
|
|
301
|
+
it("ignores a write from a stale ex-owner", async () => {
|
|
302
|
+
const run = makeRun(uid("progress-stale"), { concurrencyKey: void 0 });
|
|
303
|
+
await store.createIfPermitted(run);
|
|
304
|
+
const live = uid("live");
|
|
305
|
+
await store.claim(run.id, {
|
|
306
|
+
token: live,
|
|
307
|
+
expiresAt: future()
|
|
308
|
+
});
|
|
309
|
+
await store.saveProgress(run.id, {
|
|
310
|
+
processed: 5,
|
|
311
|
+
steps: 1
|
|
312
|
+
}, {
|
|
313
|
+
token: live,
|
|
314
|
+
expiresAt: future()
|
|
315
|
+
});
|
|
316
|
+
await store.saveProgress(run.id, {
|
|
317
|
+
processed: 999,
|
|
318
|
+
steps: 999
|
|
319
|
+
}, {
|
|
320
|
+
token: "a-stale-token",
|
|
321
|
+
expiresAt: future()
|
|
322
|
+
});
|
|
323
|
+
expect((await store.get(run.id))?.progress.processed).toBe(5);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
describe("reArmIfPermitted — retry under the creation admission policy", () => {
|
|
327
|
+
it("re-queues a failed run and clears cancel + lease + finalization state", async () => {
|
|
328
|
+
const run = makeRun(uid("rearm"), { concurrencyKey: void 0 });
|
|
329
|
+
await store.createIfPermitted(run);
|
|
330
|
+
const token = uid("owner");
|
|
331
|
+
await store.claim(run.id, {
|
|
332
|
+
token,
|
|
333
|
+
expiresAt: future()
|
|
334
|
+
});
|
|
335
|
+
await store.compareAndTransition(run.id, ["queued"], "running", void 0, token);
|
|
336
|
+
await store.requestCancel(run.id, {
|
|
337
|
+
actor: ACTOR,
|
|
338
|
+
reason: "stop",
|
|
339
|
+
requestedAt: /* @__PURE__ */ new Date()
|
|
340
|
+
});
|
|
341
|
+
await store.compareAndTransition(run.id, ["running"], "failed", void 0, token);
|
|
342
|
+
const requeued = await store.reArmIfPermitted(run.id, {
|
|
343
|
+
queuedAt: /* @__PURE__ */ new Date(),
|
|
344
|
+
cancelRequested: false
|
|
345
|
+
});
|
|
346
|
+
expect(requeued?.status).toBe("queued");
|
|
347
|
+
expect(requeued?.cancelRequested).toBe(false);
|
|
348
|
+
expect(requeued?.leaseToken).toBeUndefined();
|
|
349
|
+
expect(requeued?.finalization).toBeUndefined();
|
|
350
|
+
});
|
|
351
|
+
it("re-queues a cancelled run", async () => {
|
|
352
|
+
const run = makeRun(uid("rearm-cancelled"), { concurrencyKey: void 0 });
|
|
353
|
+
await store.createIfPermitted(run);
|
|
354
|
+
await store.compareAndTransition(run.id, ["queued"], "cancelled");
|
|
355
|
+
expect((await store.reArmIfPermitted(run.id, { queuedAt: /* @__PURE__ */ new Date() }))?.status).toBe("queued");
|
|
356
|
+
});
|
|
357
|
+
it("refuses a run that is not in a terminal status", async () => {
|
|
358
|
+
const run = makeRun(uid("rearm-live"), { concurrencyKey: void 0 });
|
|
359
|
+
await store.createIfPermitted(run);
|
|
360
|
+
expect(await store.reArmIfPermitted(run.id, { queuedAt: /* @__PURE__ */ new Date() })).toBeNull();
|
|
361
|
+
});
|
|
362
|
+
it("refuses when another non-terminal run holds the same concurrencyKey", async () => {
|
|
363
|
+
const key = uid("rearm-key");
|
|
364
|
+
const failed = makeRun(uid("rearm-blocked"), { concurrencyKey: key });
|
|
365
|
+
await store.createIfPermitted(failed);
|
|
366
|
+
await store.compareAndTransition(failed.id, ["queued"], "failed");
|
|
367
|
+
const active = makeRun(uid("rearm-active"), { concurrencyKey: key });
|
|
368
|
+
expect(await store.createIfPermitted(active)).toEqual({ created: true });
|
|
369
|
+
expect(await store.reArmIfPermitted(failed.id, { queuedAt: /* @__PURE__ */ new Date() })).toBeNull();
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
//#endregion
|
|
375
|
+
export { runCleanupRunStoreContract };
|
package/dist/testing/index.d.mts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { V as ResourceDefinition } from "../index-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
1
|
+
import { V as ResourceDefinition } from "../index-Q5gzEIWY.mjs";
|
|
2
|
+
import { l as AnyRecord, r as PermissionCheck } from "../types-ChvvVNP8.mjs";
|
|
3
|
+
import { n as ArcModuleInput } from "../types-D_n8L6VY.mjs";
|
|
4
|
+
import "../index-CCzVR-N3.mjs";
|
|
5
|
+
import { n as preloadResourcesAsync, r as CreateAppOptions, t as preloadResources } from "../preloadResources-DCfa2egz.mjs";
|
|
5
6
|
import { StorageContractSetup, StorageContractSetupResult, runStorageContract } from "./storageContract.mjs";
|
|
6
7
|
import { FastifyInstance, FastifyServerOptions } from "fastify";
|
|
7
8
|
import { Mock } from "vitest";
|
|
@@ -71,6 +72,34 @@ interface ArcAssertion {
|
|
|
71
72
|
}
|
|
72
73
|
declare function expectArc(response: ArcResponseLike): ArcAssertion;
|
|
73
74
|
//#endregion
|
|
75
|
+
//#region src/testing/authorizationConformance.d.ts
|
|
76
|
+
/** One surface's observed enforcement: the HTTP status + the filter arc passed the repo. */
|
|
77
|
+
interface SurfaceObservation {
|
|
78
|
+
/** HTTP status the surface returned (200 = allowed, 401/403 = denied). */
|
|
79
|
+
status: number;
|
|
80
|
+
/** The filter arc handed the repository for this surface (undefined if never reached). */
|
|
81
|
+
filter: AnyRecord | undefined;
|
|
82
|
+
}
|
|
83
|
+
/** Result of {@link runAuthorizationConformance} — one observation per surface. */
|
|
84
|
+
interface AuthorizationConformance {
|
|
85
|
+
/** `GET /confs` — the CRUD list surface. */
|
|
86
|
+
list: SurfaceObservation;
|
|
87
|
+
/** `GET /confs/aggregations/tally` — the aggregation surface. */
|
|
88
|
+
aggregation: SurfaceObservation;
|
|
89
|
+
/** Close the throwaway app. Always call (e.g. in `finally`). */
|
|
90
|
+
close(): Promise<void>;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Boot a one-resource app gated by `permission` on both list + an aggregation,
|
|
94
|
+
* hit both surfaces anonymously, and report what arc enforced on each.
|
|
95
|
+
*/
|
|
96
|
+
declare function runAuthorizationConformance(opts: {
|
|
97
|
+
/** The permission under test — applied to `list` and the aggregation alike. */
|
|
98
|
+
permission: PermissionCheck;
|
|
99
|
+
/** Rows the mock repo returns for list (aggregation returns a count). */
|
|
100
|
+
rows?: AnyRecord[];
|
|
101
|
+
}): Promise<AuthorizationConformance>;
|
|
102
|
+
//#endregion
|
|
74
103
|
//#region src/testing/authSession.d.ts
|
|
75
104
|
/**
|
|
76
105
|
* A concrete auth session — headers ready to drop into `app.inject`.
|
|
@@ -621,4 +650,4 @@ declare function createTestApp(options?: CreateTestAppOptions): Promise<TestAppC
|
|
|
621
650
|
*/
|
|
622
651
|
declare function createMinimalTestApp(options?: FastifyServerOptions): FastifyInstance;
|
|
623
652
|
//#endregion
|
|
624
|
-
export { type ArcAssertion, type ArcResponseLike, type AuthMode, type AuthResponse, type BetterAuthTestHelpers, type BetterAuthTestHelpersOptions, type BetterAuthTestUser, type BootModuleAppOptions, type BootModulesInput, type CreateOrgInput, type CreateTestAppOptions, type DbMode, type FixtureDestroyer, type FixtureFactory, type FixtureRegistration, HttpTestHarness, type HttpTestHarnessOptions, type MockRepository, type ModuleTestApp, type OrgResponse, type RoleConfig, type SetupBetterAuthTestAppInput, type SetupBetterAuthTestAppResult, type SignInInput, type SignUpInput, type StorageContractSetup, type StorageContractSetupResult, type TestAppContext, type TestAuthProvider, type TestAuthSession, type TestDatabase, type TestDatabaseFactory, type TestFixtures, type TestkitContext, bootModuleApp, createBetterAuthProvider, createBetterAuthTestHelpers, createCustomAuthProvider, createDataFactory, createHttpTestHarness, createJwtAuthProvider, createMinimalTestApp, createMockController, createMockReply, createMockRepository, createMockRequest, createMockUser, createSpy, createTestApp, createTestFixtures, createTestTimer, expectArc, mongoMemoryDatabase, preloadResources, preloadResourcesAsync, runStorageContract, safeParseBody, setupBetterAuthTestApp, waitFor };
|
|
653
|
+
export { type ArcAssertion, type ArcResponseLike, type AuthMode, type AuthResponse, type AuthorizationConformance, type BetterAuthTestHelpers, type BetterAuthTestHelpersOptions, type BetterAuthTestUser, type BootModuleAppOptions, type BootModulesInput, type CreateOrgInput, type CreateTestAppOptions, type DbMode, type FixtureDestroyer, type FixtureFactory, type FixtureRegistration, HttpTestHarness, type HttpTestHarnessOptions, type MockRepository, type ModuleTestApp, type OrgResponse, type RoleConfig, type SetupBetterAuthTestAppInput, type SetupBetterAuthTestAppResult, type SignInInput, type SignUpInput, type StorageContractSetup, type StorageContractSetupResult, type SurfaceObservation, type TestAppContext, type TestAuthProvider, type TestAuthSession, type TestDatabase, type TestDatabaseFactory, type TestFixtures, type TestkitContext, bootModuleApp, createBetterAuthProvider, createBetterAuthTestHelpers, createCustomAuthProvider, createDataFactory, createHttpTestHarness, createJwtAuthProvider, createMinimalTestApp, createMockController, createMockReply, createMockRepository, createMockRequest, createMockUser, createSpy, createTestApp, createTestFixtures, createTestTimer, expectArc, mongoMemoryDatabase, preloadResources, preloadResourcesAsync, runAuthorizationConformance, runStorageContract, safeParseBody, setupBetterAuthTestApp, waitFor };
|