@rulvar/store-conformance 1.44.0 → 1.45.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.d.ts +4 -1
- package/dist/index.js +195 -69
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -47,6 +47,9 @@ declare function leasableStoreConformance(mk: StoreFactory<LeasableStore>, optio
|
|
|
47
47
|
ttlMs?: number;
|
|
48
48
|
}): ConformanceSuite;
|
|
49
49
|
//#endregion
|
|
50
|
+
//#region src/fenced-writes.d.ts
|
|
51
|
+
declare function fencedWritesConformance(mk: StoreFactory<LeasableStore>): ConformanceSuite;
|
|
52
|
+
//#endregion
|
|
50
53
|
//#region src/fixtures/golden-fold.d.ts
|
|
51
54
|
/**
|
|
52
55
|
* seq 0 agent spawn (running; abandoned by seq 6)
|
|
@@ -71,4 +74,4 @@ declare function foldStateSha256(entries: readonly JournalEntry[]): string;
|
|
|
71
74
|
/** The reference hash; computed once from the kernel fold and frozen. */
|
|
72
75
|
declare const GOLDEN_FOLD_STATE_SHA256 = "81e6ccff549fb3e6c1de4d34ba65b912162eba6f66403b5d5f23a3e1ec69243c";
|
|
73
76
|
//#endregion
|
|
74
|
-
export { type ConformanceCheck, type ConformanceSuite, GOLDEN_FOLD_JOURNAL, GOLDEN_FOLD_STATE_SHA256, type StoreFactory, type TestRegistrar, foldStateSha256, journalStoreConformance, leasableStoreConformance, makeSuite, materializeFoldState, registerConformance, stableStringify };
|
|
77
|
+
export { type ConformanceCheck, type ConformanceSuite, GOLDEN_FOLD_JOURNAL, GOLDEN_FOLD_STATE_SHA256, type StoreFactory, type TestRegistrar, fencedWritesConformance, foldStateSha256, journalStoreConformance, leasableStoreConformance, makeSuite, materializeFoldState, registerConformance, stableStringify };
|
package/dist/index.js
CHANGED
|
@@ -219,7 +219,7 @@ const GOLDEN_FOLD_STATE_SHA256 = "81e6ccff549fb3e6c1de4d34ba65b912162eba6f66403b
|
|
|
219
219
|
* (FakeAdapter call counters) live in the M2 cassette suite
|
|
220
220
|
* (@rulvar/testing).
|
|
221
221
|
*/
|
|
222
|
-
const RUN$
|
|
222
|
+
const RUN$2 = "conformance-run";
|
|
223
223
|
function baseEntry(seq, overrides) {
|
|
224
224
|
return {
|
|
225
225
|
hashVersion: 2,
|
|
@@ -238,7 +238,7 @@ function baseEntry(seq, overrides) {
|
|
|
238
238
|
function sameEntry(a, b) {
|
|
239
239
|
return stableStringify(a) === stableStringify(b);
|
|
240
240
|
}
|
|
241
|
-
function meta(runId, overrides) {
|
|
241
|
+
function meta$1(runId, overrides) {
|
|
242
242
|
return {
|
|
243
243
|
runId,
|
|
244
244
|
status: "running",
|
|
@@ -259,14 +259,14 @@ function journalStoreConformance(mk) {
|
|
|
259
259
|
blob: `${big}-${i}`,
|
|
260
260
|
sentinel: "END-OF-ENTRY"
|
|
261
261
|
} }));
|
|
262
|
-
const appends = submitted.map((item) => store.append(RUN$
|
|
263
|
-
const midLoads = [store.load(RUN$
|
|
262
|
+
const appends = submitted.map((item) => store.append(RUN$2, item));
|
|
263
|
+
const midLoads = [store.load(RUN$2), store.load(RUN$2)];
|
|
264
264
|
await Promise.all([...appends, ...midLoads]);
|
|
265
265
|
for (const seen of await Promise.all(midLoads)) for (const item of seen) {
|
|
266
266
|
const original = submitted.find((candidate) => candidate.seq === item.seq);
|
|
267
267
|
ensure(original !== void 0 && sameEntry(item, original), "a1-append-atomicity", `a load observed a torn or foreign entry at seq ${item.seq}`);
|
|
268
268
|
}
|
|
269
|
-
const loaded = await store.load(RUN$
|
|
269
|
+
const loaded = await store.load(RUN$2);
|
|
270
270
|
ensure(loaded.length === submitted.length, "a1-append-atomicity", `expected ${submitted.length} whole entries, loaded ${loaded.length}`);
|
|
271
271
|
for (const item of loaded) {
|
|
272
272
|
const original = submitted.find((candidate) => candidate.seq === item.seq);
|
|
@@ -280,12 +280,12 @@ function journalStoreConformance(mk) {
|
|
|
280
280
|
async run() {
|
|
281
281
|
const store = await mk();
|
|
282
282
|
const entries = Array.from({ length: 10 }, (_, i) => baseEntry(i));
|
|
283
|
-
for (const item of entries) await store.append(RUN$
|
|
284
|
-
const first = await store.load(RUN$
|
|
283
|
+
for (const item of entries) await store.append(RUN$2, item);
|
|
284
|
+
const first = await store.load(RUN$2);
|
|
285
285
|
ensure(first.map((item) => item.seq).join(",") === entries.map((item) => item.seq).join(","), "a2-total-order", `sequential appends loaded out of order: ${first.map((item) => item.seq).join(",")}`);
|
|
286
|
-
await Promise.all(Array.from({ length: 8 }, (_, i) => store.append(RUN$
|
|
287
|
-
const second = await store.load(RUN$
|
|
288
|
-
const third = await store.load(RUN$
|
|
286
|
+
await Promise.all(Array.from({ length: 8 }, (_, i) => store.append(RUN$2, baseEntry(10 + i))));
|
|
287
|
+
const second = await store.load(RUN$2);
|
|
288
|
+
const third = await store.load(RUN$2);
|
|
289
289
|
ensure(second.map((item) => item.seq).join(",") === third.map((item) => item.seq).join(","), "a2-total-order", "two loads of the same run disagree on entry order");
|
|
290
290
|
ensure(second.length === 18 && new Set(second.map((item) => item.seq)).size === 18, "a2-total-order", "concurrent appends were lost or duplicated");
|
|
291
291
|
ensure(second.slice(0, 10).map((item) => item.seq).join(",") === entries.map((item) => item.seq).join(","), "a2-total-order", "later appends reordered the existing prefix");
|
|
@@ -298,8 +298,8 @@ function journalStoreConformance(mk) {
|
|
|
298
298
|
const store = await mk();
|
|
299
299
|
for (let i = 0; i < 5; i += 1) {
|
|
300
300
|
const item = baseEntry(i);
|
|
301
|
-
await store.append(RUN$
|
|
302
|
-
const loaded = await store.load(RUN$
|
|
301
|
+
await store.append(RUN$2, item);
|
|
302
|
+
const loaded = await store.load(RUN$2);
|
|
303
303
|
const last = loaded[loaded.length - 1];
|
|
304
304
|
ensure(loaded.length === i + 1 && last !== void 0 && sameEntry(last, item), "a3-read-your-writes", `append of seq ${i} was acknowledged but not visible to load`);
|
|
305
305
|
}
|
|
@@ -331,8 +331,8 @@ function journalStoreConformance(mk) {
|
|
|
331
331
|
spanId: "s",
|
|
332
332
|
startedAt: "2026-01-01T00:00:00.000Z"
|
|
333
333
|
};
|
|
334
|
-
await store.append(RUN$
|
|
335
|
-
const [loaded] = await store.load(RUN$
|
|
334
|
+
await store.append(RUN$2, exotic);
|
|
335
|
+
const [loaded] = await store.load(RUN$2);
|
|
336
336
|
ensure(loaded !== void 0 && sameEntry(loaded, exotic), "a4-opaque-payload", "the exotic entry did not round-trip byte-exactly (normalized, stripped, or rewritten)");
|
|
337
337
|
ensure(loaded.hashVersion === void 0, "a4-opaque-payload", "the store injected hashVersion into a legacy entry (normalization is read-side only)");
|
|
338
338
|
}
|
|
@@ -342,12 +342,12 @@ function journalStoreConformance(mk) {
|
|
|
342
342
|
title: "append rejects a duplicate or stale seq with the typed journal_order_violation",
|
|
343
343
|
async run() {
|
|
344
344
|
const store = await mk();
|
|
345
|
-
await store.append(RUN$
|
|
346
|
-
await store.append(RUN$
|
|
345
|
+
await store.append(RUN$2, baseEntry(0));
|
|
346
|
+
await store.append(RUN$2, baseEntry(1));
|
|
347
347
|
for (const stale of [1, 0]) {
|
|
348
348
|
let thrown;
|
|
349
349
|
try {
|
|
350
|
-
await store.append(RUN$
|
|
350
|
+
await store.append(RUN$2, baseEntry(stale, { value: {
|
|
351
351
|
seq: stale,
|
|
352
352
|
second: true
|
|
353
353
|
} }));
|
|
@@ -356,9 +356,9 @@ function journalStoreConformance(mk) {
|
|
|
356
356
|
}
|
|
357
357
|
ensure(thrown?.code === "journal_order_violation", "a5-monotonic-seq", `append of stale seq ${stale} must reject with code 'journal_order_violation'`);
|
|
358
358
|
}
|
|
359
|
-
ensure((await store.load(RUN$
|
|
360
|
-
await store.append(RUN$
|
|
361
|
-
ensure((await store.load(RUN$
|
|
359
|
+
ensure((await store.load(RUN$2)).map((item) => item.seq).join(",") === "0,1", "a5-monotonic-seq", "a rejected stale append must never become visible");
|
|
360
|
+
await store.append(RUN$2, baseEntry(2));
|
|
361
|
+
ensure((await store.load(RUN$2)).length === 3, "a5-monotonic-seq", "the append of the true next seq must succeed after rejections");
|
|
362
362
|
}
|
|
363
363
|
},
|
|
364
364
|
{
|
|
@@ -366,11 +366,11 @@ function journalStoreConformance(mk) {
|
|
|
366
366
|
title: "two writers appending the same next seq: exactly one persists, the loser is typed",
|
|
367
367
|
async run() {
|
|
368
368
|
const store = await mk();
|
|
369
|
-
for (let i = 0; i < 3; i += 1) await store.append(RUN$
|
|
370
|
-
const settled = await Promise.allSettled([store.append(RUN$
|
|
369
|
+
for (let i = 0; i < 3; i += 1) await store.append(RUN$2, baseEntry(i));
|
|
370
|
+
const settled = await Promise.allSettled([store.append(RUN$2, baseEntry(3, { value: {
|
|
371
371
|
seq: 3,
|
|
372
372
|
writer: "a"
|
|
373
|
-
} })), store.append(RUN$
|
|
373
|
+
} })), store.append(RUN$2, baseEntry(3, { value: {
|
|
374
374
|
seq: 3,
|
|
375
375
|
writer: "b"
|
|
376
376
|
} }))]);
|
|
@@ -378,7 +378,7 @@ function journalStoreConformance(mk) {
|
|
|
378
378
|
const rejected = settled.filter((item) => item.status === "rejected");
|
|
379
379
|
ensure(fulfilled.length === 1 && rejected.length === 1, "a5-stale-tail-race", `exactly one of two same-seq appends may persist (got ${fulfilled.length} fulfilled)`);
|
|
380
380
|
ensure((rejected[0]?.reason)?.code === "journal_order_violation", "a5-stale-tail-race", "the losing writer must observe the typed 'journal_order_violation' conflict");
|
|
381
|
-
const loaded = await store.load(RUN$
|
|
381
|
+
const loaded = await store.load(RUN$2);
|
|
382
382
|
ensure(loaded.length === 4 && loaded.filter((item) => item.seq === 3).length === 1, "a5-stale-tail-race", "the journal must hold exactly one entry at the raced seq");
|
|
383
383
|
const seqs = loaded.map((item) => item.seq);
|
|
384
384
|
ensure(seqs.every((seq, index) => index === 0 || seq > (seqs[index - 1] ?? NaN)), "a5-stale-tail-race", "reloading after the race must show a strictly increasing total order");
|
|
@@ -390,7 +390,7 @@ function journalStoreConformance(mk) {
|
|
|
390
390
|
async run() {
|
|
391
391
|
const store = await mk();
|
|
392
392
|
const seed = new Replayer({
|
|
393
|
-
runId: RUN$
|
|
393
|
+
runId: RUN$2,
|
|
394
394
|
store
|
|
395
395
|
});
|
|
396
396
|
for (let i = 0; i < 3; i += 1) await seed.appendSinglePhase({
|
|
@@ -401,14 +401,14 @@ function journalStoreConformance(mk) {
|
|
|
401
401
|
spanId: "conf-span",
|
|
402
402
|
value: { i }
|
|
403
403
|
});
|
|
404
|
-
const tail = await store.load(RUN$
|
|
404
|
+
const tail = await store.load(RUN$2);
|
|
405
405
|
const writerA = new Replayer({
|
|
406
|
-
runId: RUN$
|
|
406
|
+
runId: RUN$2,
|
|
407
407
|
store,
|
|
408
408
|
priorEntries: tail
|
|
409
409
|
});
|
|
410
410
|
const writerB = new Replayer({
|
|
411
|
-
runId: RUN$
|
|
411
|
+
runId: RUN$2,
|
|
412
412
|
store,
|
|
413
413
|
priorEntries: tail
|
|
414
414
|
});
|
|
@@ -430,7 +430,7 @@ function journalStoreConformance(mk) {
|
|
|
430
430
|
const rejected = settled.filter((item) => item.status === "rejected");
|
|
431
431
|
ensure(settled.filter((item) => item.status === "fulfilled").length === 1 && rejected.length === 1, "a5-stale-replayer-fencing", "exactly one stale-tail replayer append may persist");
|
|
432
432
|
ensure((rejected[0]?.reason)?.code === "journal_order_violation", "a5-stale-replayer-fencing", "the losing replayer must observe the typed 'journal_order_violation' conflict");
|
|
433
|
-
const seqs = (await store.load(RUN$
|
|
433
|
+
const seqs = (await store.load(RUN$2)).map((item) => item.seq);
|
|
434
434
|
ensure(new Set(seqs).size === seqs.length && seqs.every((seq, index) => index === 0 || seq > (seqs[index - 1] ?? NaN)), "a5-stale-replayer-fencing", "reopening the store must show a unique, strictly increasing seq order");
|
|
435
435
|
}
|
|
436
436
|
},
|
|
@@ -440,28 +440,28 @@ function journalStoreConformance(mk) {
|
|
|
440
440
|
async run() {
|
|
441
441
|
const store = await mk();
|
|
442
442
|
ensure((await store.load("never-written")).length === 0, "meta-separation", "load of an unknown run must return an empty journal");
|
|
443
|
-
await store.append(RUN$
|
|
444
|
-
await store.putMeta(meta(RUN$
|
|
443
|
+
await store.append(RUN$2, baseEntry(0));
|
|
444
|
+
await store.putMeta(meta$1(RUN$2, {
|
|
445
445
|
name: "wf-a",
|
|
446
446
|
tags: ["team:core"]
|
|
447
447
|
}));
|
|
448
|
-
await store.putMeta(meta("other-run", {
|
|
448
|
+
await store.putMeta(meta$1("other-run", {
|
|
449
449
|
status: "suspended",
|
|
450
450
|
name: "wf-b"
|
|
451
451
|
}));
|
|
452
|
-
ensure((await store.listRuns()).filter((candidate) => candidate.runId === RUN$
|
|
453
|
-
await store.putMeta(meta(RUN$
|
|
452
|
+
ensure((await store.listRuns()).filter((candidate) => candidate.runId === RUN$2).length === 1, "meta-separation", "putMeta must upsert one record per run");
|
|
453
|
+
await store.putMeta(meta$1(RUN$2, {
|
|
454
454
|
status: "suspended",
|
|
455
455
|
name: "wf-a",
|
|
456
456
|
tags: ["team:core"]
|
|
457
457
|
}));
|
|
458
458
|
const suspended = await store.listRuns({ status: "suspended" });
|
|
459
|
-
ensure(suspended.some((candidate) => candidate.runId === RUN$
|
|
459
|
+
ensure(suspended.some((candidate) => candidate.runId === RUN$2) && suspended.every((candidate) => candidate.status === "suspended"), "meta-separation", "listRuns must reflect the replaced meta and honor the status filter");
|
|
460
460
|
const byName = await store.listRuns({ name: "wf-b" });
|
|
461
461
|
ensure(byName.length === 1 && byName[0]?.runId === "other-run", "meta-separation", "listRuns must honor the name filter");
|
|
462
462
|
const byTag = await store.listRuns({ tags: ["team:core"] });
|
|
463
|
-
ensure(byTag.length === 1 && byTag[0]?.runId === RUN$
|
|
464
|
-
await store.putMeta(meta(RUN$
|
|
463
|
+
ensure(byTag.length === 1 && byTag[0]?.runId === RUN$2, "meta-separation", "listRuns must honor the tags filter");
|
|
464
|
+
await store.putMeta(meta$1(RUN$2, {
|
|
465
465
|
status: "suspended",
|
|
466
466
|
name: "wf-a",
|
|
467
467
|
tags: ["team:core"],
|
|
@@ -471,20 +471,20 @@ function journalStoreConformance(mk) {
|
|
|
471
471
|
argsHash: "a".repeat(64),
|
|
472
472
|
genesis: "g".repeat(26)
|
|
473
473
|
}));
|
|
474
|
-
const roundTripped = (await store.listRuns()).find((candidate) => candidate.runId === RUN$
|
|
474
|
+
const roundTripped = (await store.listRuns()).find((candidate) => candidate.runId === RUN$2);
|
|
475
475
|
ensure(roundTripped?.budgetUsd === 12.5, "meta-separation", "putMeta/listRuns must round-trip optional RunMeta fields (budgetUsd)");
|
|
476
476
|
ensure(roundTripped?.segments === 3, "meta-separation", "putMeta/listRuns must round-trip optional RunMeta fields (segments)");
|
|
477
477
|
ensure(roundTripped?.argsProvided === true && roundTripped.argsHash === "a".repeat(64), "meta-separation", "putMeta/listRuns must round-trip optional RunMeta fields (argsProvided, argsHash)");
|
|
478
478
|
ensure(roundTripped?.genesis === "g".repeat(26), "meta-separation", "putMeta/listRuns must round-trip optional RunMeta fields (genesis)");
|
|
479
|
-
ensure((await store.listRuns({ statuses: ["suspended", "running"] })).some((candidate) => candidate.runId === RUN$
|
|
479
|
+
ensure((await store.listRuns({ statuses: ["suspended", "running"] })).some((candidate) => candidate.runId === RUN$2), "meta-separation", "listRuns({ statuses }) must include every meta whose status matches (supersets are allowed, drops are not)");
|
|
480
480
|
const lookup = store.getMeta;
|
|
481
481
|
if (typeof lookup === "function") {
|
|
482
|
-
const fetched = await lookup.call(store, RUN$
|
|
483
|
-
ensure(fetched?.runId === RUN$
|
|
482
|
+
const fetched = await lookup.call(store, RUN$2);
|
|
483
|
+
ensure(fetched?.runId === RUN$2 && fetched.genesis === "g".repeat(26), "meta-separation", "getMeta must return the stored meta for an existing run");
|
|
484
484
|
ensure(await lookup.call(store, "conformance-missing-run") === void 0, "meta-separation", "getMeta must resolve undefined for a missing run, never reject");
|
|
485
485
|
}
|
|
486
|
-
await store.delete(RUN$
|
|
487
|
-
ensure((await store.load(RUN$
|
|
486
|
+
await store.delete(RUN$2);
|
|
487
|
+
ensure((await store.load(RUN$2)).length === 0 && !(await store.listRuns()).some((candidate) => candidate.runId === RUN$2), "meta-separation", "delete must remove the journal and the meta record");
|
|
488
488
|
}
|
|
489
489
|
},
|
|
490
490
|
{
|
|
@@ -492,8 +492,8 @@ function journalStoreConformance(mk) {
|
|
|
492
492
|
title: "the kernel fold over the stored golden journal hashes to the reference",
|
|
493
493
|
async run() {
|
|
494
494
|
const store = await mk();
|
|
495
|
-
for (const item of GOLDEN_FOLD_JOURNAL) await store.append(RUN$
|
|
496
|
-
const hash = foldStateSha256(await store.load(RUN$
|
|
495
|
+
for (const item of GOLDEN_FOLD_JOURNAL) await store.append(RUN$2, item);
|
|
496
|
+
const hash = foldStateSha256(await store.load(RUN$2));
|
|
497
497
|
ensure(hash === GOLDEN_FOLD_STATE_SHA256, "golden-fold-state", `fold state hash ${hash} differs from the reference ${GOLDEN_FOLD_STATE_SHA256}: the store influenced replay semantics`);
|
|
498
498
|
}
|
|
499
499
|
},
|
|
@@ -503,7 +503,7 @@ function journalStoreConformance(mk) {
|
|
|
503
503
|
async run() {
|
|
504
504
|
const store = await mk();
|
|
505
505
|
const writer = new Replayer({
|
|
506
|
-
runId: RUN$
|
|
506
|
+
runId: RUN$2,
|
|
507
507
|
store
|
|
508
508
|
});
|
|
509
509
|
const suspended = await writer.appendSuspended({
|
|
@@ -525,17 +525,17 @@ function journalStoreConformance(mk) {
|
|
|
525
525
|
})]);
|
|
526
526
|
const outcomes = [first, second];
|
|
527
527
|
ensure(outcomes.filter((outcome) => outcome.applied).length === 1, "decide-once-oracle", "a race of two resolution attempts must settle exactly one applied");
|
|
528
|
-
const journaled = await store.load(RUN$
|
|
528
|
+
const journaled = await store.load(RUN$2);
|
|
529
529
|
ensure(journaled.filter((item) => item.kind === "resolution").length === 2, "decide-once-oracle", "both attempts must be journaled (the loser is a journaled noop)");
|
|
530
530
|
const state = new Replayer({
|
|
531
|
-
runId: RUN$
|
|
531
|
+
runId: RUN$2,
|
|
532
532
|
store,
|
|
533
533
|
priorEntries: journaled,
|
|
534
534
|
strict: true
|
|
535
535
|
}).suspensionState(suspended.seq);
|
|
536
536
|
const winnerSeq = outcomes.find((outcome) => outcome.applied)?.seq;
|
|
537
537
|
ensure(state.state === "resolved" && state.by === winnerSeq, "decide-once-oracle", "the replayed fold disagrees with the live winner");
|
|
538
|
-
const reload = await store.load(RUN$
|
|
538
|
+
const reload = await store.load(RUN$2);
|
|
539
539
|
ensure(foldStateSha256(journaled) === foldStateSha256(reload), "decide-once-oracle", "two loads of the raced journal fold to different states");
|
|
540
540
|
}
|
|
541
541
|
},
|
|
@@ -545,7 +545,7 @@ function journalStoreConformance(mk) {
|
|
|
545
545
|
async run() {
|
|
546
546
|
const store = await mk();
|
|
547
547
|
const writer = new Replayer({
|
|
548
|
-
runId: RUN$
|
|
548
|
+
runId: RUN$2,
|
|
549
549
|
store
|
|
550
550
|
});
|
|
551
551
|
const branchIdentity = {
|
|
@@ -581,9 +581,9 @@ function journalStoreConformance(mk) {
|
|
|
581
581
|
reason: "conformance abandon"
|
|
582
582
|
})).applied, "abandon-derived-skip", "the covering abandon must apply");
|
|
583
583
|
const replayed = new Replayer({
|
|
584
|
-
runId: RUN$
|
|
584
|
+
runId: RUN$2,
|
|
585
585
|
store,
|
|
586
|
-
priorEntries: await store.load(RUN$
|
|
586
|
+
priorEntries: await store.load(RUN$2),
|
|
587
587
|
strict: true
|
|
588
588
|
});
|
|
589
589
|
replayed.setDisposition(dispositionHook(replayed.fold.abandonFold, buildDeriverRegistry()));
|
|
@@ -613,7 +613,7 @@ function journalStoreConformance(mk) {
|
|
|
613
613
|
* ships in M5 (@rulvar/store-sqlite); the multi-process soak re-runs
|
|
614
614
|
* these sections under real concurrency in M8.
|
|
615
615
|
*/
|
|
616
|
-
const RUN = "lease-run";
|
|
616
|
+
const RUN$1 = "lease-run";
|
|
617
617
|
function leaseEntry(seq) {
|
|
618
618
|
return {
|
|
619
619
|
hashVersion: 2,
|
|
@@ -645,11 +645,11 @@ function leasableStoreConformance(mk, options) {
|
|
|
645
645
|
title: "acquire on a held lease rejects with the typed LeaseHeldError",
|
|
646
646
|
async run() {
|
|
647
647
|
const store = await mk();
|
|
648
|
-
const held = await store.acquire(RUN, "owner-a");
|
|
649
|
-
ensure(held.runId === RUN && held.owner === "owner-a" && typeof held.epoch === "number", "lease-exclusive-acquire", "acquire must return a Lease with runId, owner, and a numeric fencing epoch");
|
|
650
|
-
ensure(await mustReject(() => store.acquire(RUN, "owner-b")) instanceof LeaseHeldError, "lease-exclusive-acquire", "a second acquire while held must reject with LeaseHeldError (typed, code lease_held)");
|
|
648
|
+
const held = await store.acquire(RUN$1, "owner-a");
|
|
649
|
+
ensure(held.runId === RUN$1 && held.owner === "owner-a" && typeof held.epoch === "number", "lease-exclusive-acquire", "acquire must return a Lease with runId, owner, and a numeric fencing epoch");
|
|
650
|
+
ensure(await mustReject(() => store.acquire(RUN$1, "owner-b")) instanceof LeaseHeldError, "lease-exclusive-acquire", "a second acquire while held must reject with LeaseHeldError (typed, code lease_held)");
|
|
651
651
|
await store.release(held);
|
|
652
|
-
ensure((await store.acquire(RUN, "owner-b")).owner === "owner-b", "lease-exclusive-acquire", "after release, acquire must succeed for a new owner");
|
|
652
|
+
ensure((await store.acquire(RUN$1, "owner-b")).owner === "owner-b", "lease-exclusive-acquire", "after release, acquire must succeed for a new owner");
|
|
653
653
|
}
|
|
654
654
|
},
|
|
655
655
|
{
|
|
@@ -657,14 +657,14 @@ function leasableStoreConformance(mk, options) {
|
|
|
657
657
|
title: "the epoch is monotonic; a stale-epoch append is rejected and invisible",
|
|
658
658
|
async run() {
|
|
659
659
|
const store = await mk();
|
|
660
|
-
const first = await store.acquire(RUN, "owner-a");
|
|
661
|
-
await store.append(RUN, leaseEntry(0), first);
|
|
660
|
+
const first = await store.acquire(RUN$1, "owner-a");
|
|
661
|
+
await store.append(RUN$1, leaseEntry(0), first);
|
|
662
662
|
await store.release(first);
|
|
663
|
-
const second = await store.acquire(RUN, "owner-b");
|
|
663
|
+
const second = await store.acquire(RUN$1, "owner-b");
|
|
664
664
|
ensure(second.epoch > first.epoch, "fencing-stale-epoch", `the fencing epoch must be monotonic per run (got ${second.epoch} after ${first.epoch})`);
|
|
665
|
-
await store.append(RUN, leaseEntry(1), second);
|
|
666
|
-
ensure(await mustReject(() => store.append(RUN, leaseEntry(2), first)) !== void 0, "fencing-stale-epoch", "an append carrying a stale epoch must be rejected");
|
|
667
|
-
const loaded = await store.load(RUN);
|
|
665
|
+
await store.append(RUN$1, leaseEntry(1), second);
|
|
666
|
+
ensure(await mustReject(() => store.append(RUN$1, leaseEntry(2), first)) !== void 0, "fencing-stale-epoch", "an append carrying a stale epoch must be rejected");
|
|
667
|
+
const loaded = await store.load(RUN$1);
|
|
668
668
|
ensure(loaded.length === 2 && !loaded.some((item) => item.seq === 2), "fencing-stale-epoch", "a rejected stale-epoch append must never appear in load");
|
|
669
669
|
}
|
|
670
670
|
},
|
|
@@ -673,12 +673,12 @@ function leasableStoreConformance(mk, options) {
|
|
|
673
673
|
title: "a released lease can neither renew nor append",
|
|
674
674
|
async run() {
|
|
675
675
|
const store = await mk();
|
|
676
|
-
const lease = await store.acquire(RUN, "owner-a");
|
|
676
|
+
const lease = await store.acquire(RUN$1, "owner-a");
|
|
677
677
|
await store.renew(lease);
|
|
678
678
|
await store.release(lease);
|
|
679
679
|
ensure(await mustReject(() => store.renew(lease)) !== void 0, "lease-release-fences", "renew of a released lease must reject");
|
|
680
|
-
ensure(await mustReject(() => store.append(RUN, leaseEntry(0), lease)) !== void 0, "lease-release-fences", "append under a released lease must reject");
|
|
681
|
-
ensure((await store.load(RUN)).length === 0, "lease-release-fences", "nothing may be visible after only-rejected appends");
|
|
680
|
+
ensure(await mustReject(() => store.append(RUN$1, leaseEntry(0), lease)) !== void 0, "lease-release-fences", "append under a released lease must reject");
|
|
681
|
+
ensure((await store.load(RUN$1)).length === 0, "lease-release-fences", "nothing may be visible after only-rejected appends");
|
|
682
682
|
}
|
|
683
683
|
}
|
|
684
684
|
];
|
|
@@ -688,13 +688,13 @@ function leasableStoreConformance(mk, options) {
|
|
|
688
688
|
title: "renew at ttl/3 keeps the lease held past ttl; an unrenewed lease expires",
|
|
689
689
|
async run() {
|
|
690
690
|
const store = await mk();
|
|
691
|
-
const held = await store.acquire(RUN, "owner-a");
|
|
691
|
+
const held = await store.acquire(RUN$1, "owner-a");
|
|
692
692
|
const cadence = Math.floor(ttlMs / 3);
|
|
693
693
|
const deadline = Date.now() + Math.ceil(ttlMs * 1.5);
|
|
694
694
|
while (Date.now() < deadline) {
|
|
695
695
|
await sleep(cadence);
|
|
696
696
|
await store.renew(held);
|
|
697
|
-
ensure(await mustReject(() => store.acquire(RUN, "owner-b")) instanceof LeaseHeldError, "lease-ttl-and-renew-cadence", "a lease renewed at ttl/3 must stay held past the original ttl");
|
|
697
|
+
ensure(await mustReject(() => store.acquire(RUN$1, "owner-b")) instanceof LeaseHeldError, "lease-ttl-and-renew-cadence", "a lease renewed at ttl/3 must stay held past the original ttl");
|
|
698
698
|
}
|
|
699
699
|
await store.release(held);
|
|
700
700
|
const abandoned = await store.acquire("expiring-run", "owner-a");
|
|
@@ -705,4 +705,130 @@ function leasableStoreConformance(mk, options) {
|
|
|
705
705
|
return makeSuite("leasableStoreConformance", checks);
|
|
706
706
|
}
|
|
707
707
|
//#endregion
|
|
708
|
-
|
|
708
|
+
//#region src/fenced-writes.ts
|
|
709
|
+
/**
|
|
710
|
+
* fencedWritesConformance (the fenced run state RFC, phase 2): the
|
|
711
|
+
* executable definition of the `fencedWrites` capability. Run it ONLY
|
|
712
|
+
* against stores that declare the marker; the checks are the promise
|
|
713
|
+
* the marker makes:
|
|
714
|
+
* - a mutation carrying a stale or released lease (putMeta, delete,
|
|
715
|
+
* append) rejects with the typed LeaseHeldError and mutates NOTHING;
|
|
716
|
+
* - a mutation guarded by a live lease for a DIFFERENT run rejects the
|
|
717
|
+
* same way (a lease fences exactly the run it names);
|
|
718
|
+
* - a mutation carrying no lease keeps single-writer semantics.
|
|
719
|
+
*
|
|
720
|
+
* Staleness is produced with release+reacquire (the epoch is monotonic
|
|
721
|
+
* per the leasable contract), so the suite needs no wall-clock sleeps.
|
|
722
|
+
*/
|
|
723
|
+
const RUN = "fenced-run";
|
|
724
|
+
const OTHER = "fenced-other-run";
|
|
725
|
+
function fencedEntry(seq) {
|
|
726
|
+
return {
|
|
727
|
+
hashVersion: 2,
|
|
728
|
+
seq,
|
|
729
|
+
scope: "",
|
|
730
|
+
key: `fenced-key-${seq}`,
|
|
731
|
+
ordinal: 0,
|
|
732
|
+
kind: "step",
|
|
733
|
+
status: "ok",
|
|
734
|
+
value: { seq },
|
|
735
|
+
spanId: "fenced-span",
|
|
736
|
+
startedAt: (/* @__PURE__ */ new Date(17e11 + seq * 1e3)).toISOString()
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
function meta(runId, status, segments) {
|
|
740
|
+
return {
|
|
741
|
+
runId,
|
|
742
|
+
status,
|
|
743
|
+
segments,
|
|
744
|
+
updatedAt: `at-${status}-${String(segments)}`
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
async function readMeta(store, runId) {
|
|
748
|
+
const lookup = store;
|
|
749
|
+
if (typeof lookup.getMeta === "function") return lookup.getMeta(runId);
|
|
750
|
+
return (await store.listRuns()).find((m) => m.runId === runId);
|
|
751
|
+
}
|
|
752
|
+
async function mustRejectFenced(checkId, what, operation) {
|
|
753
|
+
try {
|
|
754
|
+
await operation();
|
|
755
|
+
} catch (thrown) {
|
|
756
|
+
ensure(thrown instanceof LeaseHeldError, checkId, `${what} must reject with the typed LeaseHeldError, got ${String(thrown)}`);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
ensure(false, checkId, `${what} must reject`);
|
|
760
|
+
}
|
|
761
|
+
function fencedWritesConformance(mk) {
|
|
762
|
+
return makeSuite("fencedWritesConformance", [
|
|
763
|
+
{
|
|
764
|
+
id: "fenced-marker-declared",
|
|
765
|
+
title: "the store declares the fencedWrites promise",
|
|
766
|
+
async run() {
|
|
767
|
+
ensure((await mk()).fencedWrites === true, "fenced-marker-declared", "fencedWritesConformance targets stores declaring `fencedWrites: true`; the marker is a promise, not an inference");
|
|
768
|
+
}
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
id: "fenced-meta-stale-rejected",
|
|
772
|
+
title: "a stale putMeta rejects typed and the successor meta is intact",
|
|
773
|
+
async run() {
|
|
774
|
+
const store = await mk();
|
|
775
|
+
const first = await store.acquire(RUN, "owner-a");
|
|
776
|
+
await store.putMeta(meta(RUN, "running", 2), first);
|
|
777
|
+
await store.release(first);
|
|
778
|
+
const second = await store.acquire(RUN, "owner-b");
|
|
779
|
+
await store.putMeta(meta(RUN, "running", 3), second);
|
|
780
|
+
await mustRejectFenced("fenced-meta-stale-rejected", "a stale putMeta", () => store.putMeta(meta(RUN, "cancelled", 2), first));
|
|
781
|
+
const current = await readMeta(store, RUN);
|
|
782
|
+
ensure(current !== void 0 && current.status === "running" && current.segments === 3, "fenced-meta-stale-rejected", `the successor meta (status running, segments 3) must survive a stale putMeta unchanged, got ${JSON.stringify(current)}`);
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
id: "fenced-delete-stale-rejected",
|
|
787
|
+
title: "a stale delete rejects typed and journal plus meta are intact",
|
|
788
|
+
async run() {
|
|
789
|
+
const store = await mk();
|
|
790
|
+
const first = await store.acquire(RUN, "owner-a");
|
|
791
|
+
await store.append(RUN, fencedEntry(0), first);
|
|
792
|
+
await store.putMeta(meta(RUN, "running", 1), first);
|
|
793
|
+
await store.release(first);
|
|
794
|
+
const second = await store.acquire(RUN, "owner-b");
|
|
795
|
+
await mustRejectFenced("fenced-delete-stale-rejected", "a stale delete", () => store.delete(RUN, first));
|
|
796
|
+
ensure((await store.load(RUN)).length === 1, "fenced-delete-stale-rejected", "the journal must survive a stale delete unchanged");
|
|
797
|
+
ensure(await readMeta(store, RUN) !== void 0, "fenced-delete-stale-rejected", "the meta row must survive a stale delete unchanged");
|
|
798
|
+
await store.delete(RUN, second);
|
|
799
|
+
ensure((await store.load(RUN)).length === 0 && await readMeta(store, RUN) === void 0, "fenced-delete-stale-rejected", "a delete under the current lease must remove the journal and the meta row");
|
|
800
|
+
}
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
id: "fenced-run-match",
|
|
804
|
+
title: "a live lease for a different run guards nothing",
|
|
805
|
+
async run() {
|
|
806
|
+
const store = await mk();
|
|
807
|
+
const foreign = await store.acquire(OTHER, "owner-a");
|
|
808
|
+
await store.putMeta(meta(RUN, "running", 1));
|
|
809
|
+
await store.append(RUN, fencedEntry(0));
|
|
810
|
+
await mustRejectFenced("fenced-run-match", "a cross-run putMeta", () => store.putMeta(meta(RUN, "cancelled", 1), foreign));
|
|
811
|
+
await mustRejectFenced("fenced-run-match", "a cross-run append", () => store.append(RUN, fencedEntry(1), foreign));
|
|
812
|
+
await mustRejectFenced("fenced-run-match", "a cross-run delete", () => store.delete(RUN, foreign));
|
|
813
|
+
const current = await readMeta(store, RUN);
|
|
814
|
+
ensure(current !== void 0 && current.status === "running", "fenced-run-match", "the run guarded by nobody must survive every cross-run mutation attempt");
|
|
815
|
+
ensure((await store.load(RUN)).length === 1, "fenced-run-match", "a cross-run append must never become visible");
|
|
816
|
+
}
|
|
817
|
+
},
|
|
818
|
+
{
|
|
819
|
+
id: "fenced-unleased-passthrough",
|
|
820
|
+
title: "mutations carrying no lease keep single-writer semantics",
|
|
821
|
+
async run() {
|
|
822
|
+
const store = await mk();
|
|
823
|
+
await store.putMeta(meta(RUN, "running", 1));
|
|
824
|
+
await store.append(RUN, fencedEntry(0));
|
|
825
|
+
const current = await readMeta(store, RUN);
|
|
826
|
+
ensure(current !== void 0 && current.status === "running", "fenced-unleased-passthrough", "an unleased putMeta must keep working");
|
|
827
|
+
await store.delete(RUN);
|
|
828
|
+
ensure((await store.load(RUN)).length === 0 && await readMeta(store, RUN) === void 0, "fenced-unleased-passthrough", "an unleased delete must keep working");
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
]);
|
|
832
|
+
}
|
|
833
|
+
//#endregion
|
|
834
|
+
export { GOLDEN_FOLD_JOURNAL, GOLDEN_FOLD_STATE_SHA256, fencedWritesConformance, foldStateSha256, journalStoreConformance, leasableStoreConformance, makeSuite, materializeFoldState, registerConformance, stableStringify };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rulvar/store-conformance",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.45.0",
|
|
4
4
|
"description": "Rulvar executable store conformance kit (DEF-4).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@rulvar/core": "1.
|
|
25
|
+
"@rulvar/core": "1.45.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.20.0",
|