@saasicat/persistence-testing 1.0.0-rc.13 → 1.0.0-rc.14

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
@@ -2,12 +2,133 @@
2
2
  import assert from "node:assert/strict";
3
3
  import { after, before, beforeEach, describe, test } from "node:test";
4
4
  var LOCK_HOLD_MS = 150;
5
+ var CONTRACT_GAPS = {
6
+ atomicPlanBinding: {
7
+ reason: "adapter does not expose atomic plan-binding writes",
8
+ present: ({ adapter }) => Boolean(adapter.tenantSubscriptionWrite)
9
+ },
10
+ atomicOnboarding: {
11
+ reason: "adapter does not expose atomic onboarding writes",
12
+ present: ({ adapter }) => Boolean(adapter.tenantSubscriptionWrite?.applyOnboardingSelection)
13
+ },
14
+ promoCodes: {
15
+ reason: "adapter provides no PromoCodeRepository",
16
+ present: ({ adapter }) => Boolean(adapter.promoCodeRepository)
17
+ },
18
+ promoCodeRedemptions: {
19
+ reason: "adapter provides no PromoCodeRedemptionRepository",
20
+ present: ({ adapter }) => Boolean(adapter.promoCodeRedemptionRepository)
21
+ },
22
+ promoSubscriptionLookup: {
23
+ reason: "adapter provides no PromoSubscriptionLookup",
24
+ present: ({ adapter }) => Boolean(adapter.promoSubscriptionLookup)
25
+ },
26
+ planRepository: {
27
+ reason: "adapter provides no PlanRepository",
28
+ present: ({ adapter }) => Boolean(adapter.planRepository)
29
+ },
30
+ planLifecycle: {
31
+ reason: "adapter provides no time-aware PlanRepository lifecycle",
32
+ present: ({ adapter }) => {
33
+ const repository = adapter.planRepository;
34
+ return Boolean(
35
+ repository?.createPlanVersionDraft && repository.publishPlanVersionDraft && repository.findVersionById && repository.findActivePlanVersion
36
+ );
37
+ }
38
+ },
39
+ planRetirement: {
40
+ reason: "adapter provides no PlanRepository that retires and finds plans by key",
41
+ present: ({ adapter }) => Boolean(adapter.planRepository?.softDelete && adapter.planRepository.findByKey)
42
+ },
43
+ planVersionReads: {
44
+ reason: "adapter provides no PlanRepository that reads versions by plan key",
45
+ present: ({ adapter }) => {
46
+ const repository = adapter.planRepository;
47
+ return Boolean(
48
+ repository?.listVersions && repository.findCurrentDraft && repository.findLatestLivePlanVersion
49
+ );
50
+ }
51
+ },
52
+ planVersionRetirement: {
53
+ reason: "adapter provides no PlanRepository that reads versions and retires plans",
54
+ present: ({ adapter }) => {
55
+ const repository = adapter.planRepository;
56
+ return Boolean(
57
+ repository?.createPlanVersionDraft && repository.publishPlanVersionDraft && repository.listVersions && repository.findCurrentDraft && repository.findLatestLivePlanVersion && repository.softDelete
58
+ );
59
+ }
60
+ },
61
+ bundleRepository: {
62
+ reason: "adapter provides no BundleRepository",
63
+ present: ({ adapter }) => Boolean(adapter.bundleRepository)
64
+ },
65
+ bundleValidity: {
66
+ reason: "adapter provides no time-aware BundleRepository",
67
+ present: ({ adapter }) => Boolean(adapter.bundleRepository?.findActiveBundleVersion)
68
+ },
69
+ bundleDraftDiscard: {
70
+ reason: "adapter provides no BundleRepository that discards drafts",
71
+ present: ({ adapter }) => Boolean(adapter.bundleRepository?.deleteDraft)
72
+ },
73
+ bundleDraftPublish: {
74
+ reason: "adapter provides no BundleRepository that publishes drafts",
75
+ present: ({ adapter }) => Boolean(adapter.bundleRepository?.publishDraft)
76
+ },
77
+ bundleRetirement: {
78
+ reason: "adapter provides no BundleRepository that retires and finds bundles by key",
79
+ present: ({ adapter }) => Boolean(adapter.bundleRepository?.softDelete && adapter.bundleRepository.findByKey)
80
+ },
81
+ bundleBookings: {
82
+ reason: "adapter provides no SubscriptionBundleRepository or bundle catalog",
83
+ present: ({ adapter, seed }) => Boolean(adapter.subscriptionBundleRepository && seed.createBundleVersion)
84
+ },
85
+ halfCancelledBookingSeed: {
86
+ reason: "adapter harness cannot write the half-cancelled shape",
87
+ present: ({ seed }) => Boolean(seed.clearBookingRequestDate)
88
+ },
89
+ countByPlanVersionId: {
90
+ reason: "adapter does not implement countByPlanVersionId (fail-closed fallback)",
91
+ present: ({ adapter }) => Boolean(adapter.subscriptionRepository.countByPlanVersionId)
92
+ },
93
+ audit: {
94
+ reason: "adapter provides no AuditPort/AuditQueryPort pair",
95
+ present: ({ adapter }) => Boolean(adapter.audit && adapter.auditQuery)
96
+ },
97
+ mfa: {
98
+ reason: "adapter provides no MfaPort",
99
+ present: ({ adapter }) => Boolean(adapter.mfa)
100
+ },
101
+ subscriptionContracts: {
102
+ reason: "adapter provides no SubscriptionContractRepository",
103
+ present: ({ adapter }) => Boolean(adapter.subscriptionContractRepository)
104
+ },
105
+ appliedSettings: {
106
+ reason: "adapter provides no AppliedSettingsPort",
107
+ present: ({ adapter }) => Boolean(adapter.appliedSettings)
108
+ }
109
+ };
5
110
  function sleep(ms) {
6
111
  return new Promise((resolve) => setTimeout(resolve, ms));
7
112
  }
8
113
  function persistenceAdapterContract(options) {
114
+ const declaredGaps = new Set(options.gaps ?? []);
115
+ let harness;
116
+ function missing(t, gap) {
117
+ const { reason, present } = CONTRACT_GAPS[gap];
118
+ if (present(harness)) {
119
+ assert.fail(
120
+ `'${gap}' counts as provided, yet this scenario found a member it needs missing or not callable. Check that the harness wires the port itself; if it does, the contract's check for '${gap}' and this scenario disagree, which is a defect in @saasicat/persistence-testing.`
121
+ );
122
+ }
123
+ if (declaredGaps.has(gap)) {
124
+ t.skip(reason);
125
+ return;
126
+ }
127
+ assert.fail(
128
+ `${reason}. Wire it into the harness, or declare \`gaps: ['${gap}']\` in persistenceAdapterContract if the adapter deliberately does not provide it.`
129
+ );
130
+ }
9
131
  describe(`persistence adapter contract: ${options.name}`, () => {
10
- let harness;
11
132
  before(async () => {
12
133
  harness = await options.create();
13
134
  });
@@ -17,6 +138,20 @@ function persistenceAdapterContract(options) {
17
138
  beforeEach(async () => {
18
139
  await harness.reset();
19
140
  });
141
+ test("the declared gaps are exactly the parts the harness does not provide", () => {
142
+ const gaps = Object.keys(CONTRACT_GAPS);
143
+ const absent = gaps.filter((gap) => !CONTRACT_GAPS[gap].present(harness));
144
+ const known = (gap) => Object.prototype.hasOwnProperty.call(CONTRACT_GAPS, gap);
145
+ const unknown = [...declaredGaps].filter((gap) => !known(gap));
146
+ const stale = [...declaredGaps].filter((gap) => known(gap) && !absent.includes(gap));
147
+ const undeclared = absent.filter((gap) => !declaredGaps.has(gap));
148
+ const problems = [
149
+ unknown.length > 0 && `declared as gaps but not parts of the contract: ${unknown.join(", ")} (ContractGap lists the names)`,
150
+ stale.length > 0 && `declared as gaps but wired into the harness: ${stale.join(", ")}`,
151
+ undeclared.length > 0 && `not wired into the harness and not declared as gaps: ${undeclared.join(", ")}`
152
+ ].filter(Boolean);
153
+ assert.deepEqual(problems, [], problems.join("; "));
154
+ });
20
155
  test("findByTenantId returns the tenant subscription with plan-version limits", async () => {
21
156
  const { seed, adapter } = harness;
22
157
  const { planVersionId } = await seed.createPlanVersion({
@@ -91,7 +226,7 @@ function persistenceAdapterContract(options) {
91
226
  test("immediate plan change binds plan and active PlanVersion consistently", async (t) => {
92
227
  const { seed, adapter } = harness;
93
228
  if (!adapter.tenantSubscriptionWrite) {
94
- t.skip("adapter does not expose atomic plan-binding writes");
229
+ missing(t, "atomicPlanBinding");
95
230
  return;
96
231
  }
97
232
  const oldVersion = await seed.createPlanVersion({
@@ -136,12 +271,12 @@ function persistenceAdapterContract(options) {
136
271
  const { seed, adapter } = harness;
137
272
  const writer = adapter.tenantSubscriptionWrite;
138
273
  if (!writer?.applyOnboardingSelection) {
139
- t.skip("adapter does not expose atomic onboarding writes");
274
+ missing(t, "atomicOnboarding");
140
275
  return;
141
276
  }
142
277
  const redemptions = adapter.promoCodeRedemptionRepository;
143
278
  if (!redemptions) {
144
- t.skip("adapter provides no PromoCodeRedemptionRepository");
279
+ missing(t, "promoCodeRedemptions");
145
280
  return;
146
281
  }
147
282
  const oldVersion = await seed.createPlanVersion({
@@ -220,7 +355,7 @@ function persistenceAdapterContract(options) {
220
355
  test("plan lifecycle keeps semantic identity and auto-succeeds validity windows", async (t) => {
221
356
  const repository = harness.adapter.planRepository;
222
357
  if (!repository?.createPlanVersionDraft || !repository.publishPlanVersionDraft || !repository.findVersionById || !repository.findActivePlanVersion) {
223
- t.skip("adapter provides no time-aware PlanRepository lifecycle");
358
+ missing(t, "planLifecycle");
224
359
  return;
225
360
  }
226
361
  const plan = await repository.create({
@@ -284,7 +419,7 @@ function persistenceAdapterContract(options) {
284
419
  test("bundle lifecycle roundtrips validity and auto-succeeds atomically", async (t) => {
285
420
  const repository = harness.adapter.bundleRepository;
286
421
  if (!repository?.findActiveBundleVersion) {
287
- t.skip("adapter provides no time-aware BundleRepository");
422
+ missing(t, "bundleValidity");
288
423
  return;
289
424
  }
290
425
  const bundle = await repository.create({
@@ -343,7 +478,7 @@ function persistenceAdapterContract(options) {
343
478
  const repository = harness.adapter.subscriptionBundleRepository;
344
479
  const { seed } = harness;
345
480
  if (!repository || !seed.createBundleVersion) {
346
- t.skip("adapter provides no SubscriptionBundleRepository or bundle catalog");
481
+ missing(t, "bundleBookings");
347
482
  return;
348
483
  }
349
484
  const { planVersionId } = await seed.createPlanVersion({
@@ -398,7 +533,7 @@ function persistenceAdapterContract(options) {
398
533
  const repository = harness.adapter.subscriptionBundleRepository;
399
534
  const { seed } = harness;
400
535
  if (!repository || !seed.createBundleVersion) {
401
- t.skip("adapter provides no SubscriptionBundleRepository or bundle catalog");
536
+ missing(t, "bundleBookings");
402
537
  return;
403
538
  }
404
539
  const { planVersionId } = await seed.createPlanVersion({
@@ -452,7 +587,7 @@ function persistenceAdapterContract(options) {
452
587
  const repository = harness.adapter.subscriptionBundleRepository;
453
588
  const { seed } = harness;
454
589
  if (!repository || !seed.createBundleVersion) {
455
- t.skip("adapter provides no SubscriptionBundleRepository or bundle catalog");
590
+ missing(t, "bundleBookings");
456
591
  return;
457
592
  }
458
593
  const { planVersionId } = await seed.createPlanVersion({
@@ -497,7 +632,7 @@ function persistenceAdapterContract(options) {
497
632
  const repository = harness.adapter.subscriptionBundleRepository;
498
633
  const { seed } = harness;
499
634
  if (!repository || !seed.createBundleVersion) {
500
- t.skip("adapter provides no SubscriptionBundleRepository or bundle catalog");
635
+ missing(t, "bundleBookings");
501
636
  return;
502
637
  }
503
638
  const { planVersionId } = await seed.createPlanVersion({
@@ -528,7 +663,7 @@ function persistenceAdapterContract(options) {
528
663
  });
529
664
  const clearRequestDate = harness.seed.clearBookingRequestDate;
530
665
  if (!clearRequestDate) {
531
- t.skip("adapter harness cannot write the half-cancelled shape");
666
+ missing(t, "halfCancelledBookingSeed");
532
667
  return;
533
668
  }
534
669
  await clearRequestDate(booking.id);
@@ -549,7 +684,7 @@ function persistenceAdapterContract(options) {
549
684
  const catalog = harness.adapter.bundleRepository;
550
685
  const discardDraft = catalog?.deleteDraft?.bind(catalog);
551
686
  if (!catalog || !discardDraft) {
552
- t.skip("adapter provides no BundleRepository");
687
+ missing(t, "bundleDraftDiscard");
553
688
  return;
554
689
  }
555
690
  const bundle = await catalog.create({
@@ -581,7 +716,7 @@ function persistenceAdapterContract(options) {
581
716
  const catalog = harness.adapter.bundleRepository;
582
717
  const publish = catalog?.publishDraft?.bind(catalog);
583
718
  if (!catalog || !publish) {
584
- t.skip("adapter provides no BundleRepository");
719
+ missing(t, "bundleDraftPublish");
585
720
  return;
586
721
  }
587
722
  const bundle = await catalog.create({
@@ -644,7 +779,7 @@ function persistenceAdapterContract(options) {
644
779
  test("a plan key names one plan for the whole installation", async (t) => {
645
780
  const repository = harness.adapter.planRepository;
646
781
  if (!repository) {
647
- t.skip("adapter provides no PlanRepository");
782
+ missing(t, "planRepository");
648
783
  return;
649
784
  }
650
785
  await repository.create({ planKey: "DOUBLE", label: "First" });
@@ -656,7 +791,7 @@ function persistenceAdapterContract(options) {
656
791
  test("a bundle key names one bundle for the whole installation", async (t) => {
657
792
  const catalog = harness.adapter.bundleRepository;
658
793
  if (!catalog) {
659
- t.skip("adapter provides no BundleRepository");
794
+ missing(t, "bundleRepository");
660
795
  return;
661
796
  }
662
797
  await catalog.create({ bundleKey: "DOUBLE", label: "First" });
@@ -670,7 +805,7 @@ function persistenceAdapterContract(options) {
670
805
  const retire = repository?.softDelete?.bind(repository);
671
806
  const byKey = repository?.findByKey?.bind(repository);
672
807
  if (!repository || !retire || !byKey) {
673
- t.skip("adapter provides no PlanRepository");
808
+ missing(t, "planRetirement");
674
809
  return;
675
810
  }
676
811
  const plan = await repository.create({ planKey: "RETIRED_PLAN", label: "Retired" });
@@ -684,12 +819,90 @@ function persistenceAdapterContract(options) {
684
819
  "a retired plan is not in the catalogue an operator browses"
685
820
  );
686
821
  });
822
+ test("a plan key no plan has finds no versions, rather than failing", async (t) => {
823
+ const repository = harness.adapter.planRepository;
824
+ if (!repository?.listVersions || !repository.findCurrentDraft || !repository.findLatestLivePlanVersion) {
825
+ missing(t, "planVersionReads");
826
+ return;
827
+ }
828
+ const entitlementVersions = harness.adapter.planVersionRepository;
829
+ assert.deepEqual(await repository.listVersions("NO_SUCH_PLAN"), []);
830
+ assert.equal(await repository.findCurrentDraft("NO_SUCH_PLAN"), null);
831
+ assert.equal(await repository.findLatestLivePlanVersion("NO_SUCH_PLAN"), null);
832
+ if (repository.findActivePlanVersion) {
833
+ assert.equal(
834
+ await repository.findActivePlanVersion("NO_SUCH_PLAN", /* @__PURE__ */ new Date()),
835
+ null
836
+ );
837
+ }
838
+ assert.equal(await entitlementVersions.findLatestLive("NO_SUCH_PLAN"), null);
839
+ if (entitlementVersions.findActive) {
840
+ assert.equal(
841
+ await entitlementVersions.findActive("NO_SUCH_PLAN", /* @__PURE__ */ new Date()),
842
+ null
843
+ );
844
+ }
845
+ });
846
+ test("retiring a plan hides none of its versions", async (t) => {
847
+ const repository = harness.adapter.planRepository;
848
+ if (!repository?.createPlanVersionDraft || !repository.publishPlanVersionDraft || !repository.listVersions || !repository.findCurrentDraft || !repository.findLatestLivePlanVersion || !repository.softDelete) {
849
+ missing(t, "planVersionRetirement");
850
+ return;
851
+ }
852
+ const listVersions = repository.listVersions.bind(repository);
853
+ const findCurrentDraft = repository.findCurrentDraft.bind(repository);
854
+ const findLatestLive = repository.findLatestLivePlanVersion.bind(repository);
855
+ const entitlementVersions = harness.adapter.planVersionRepository;
856
+ const plan = await repository.create({ planKey: "RETIRING", label: "Retiring" });
857
+ const firstDraft = await repository.createPlanVersionDraft({
858
+ planId: "RETIRING",
859
+ features: ["CORE"],
860
+ quotas: { users: 5 },
861
+ monthlyNet: "10.00",
862
+ yearlyNet: "100.00",
863
+ validFrom: "2026-01-01"
864
+ });
865
+ const live = await repository.publishPlanVersionDraft(firstDraft.id, {
866
+ publishedByUserId: null,
867
+ publishedChanges: [],
868
+ nonRegressive: true,
869
+ validFrom: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
870
+ validUntil: null
871
+ });
872
+ const openDraft = await repository.createPlanVersionDraft({
873
+ planId: "RETIRING",
874
+ baseVersionId: live.id,
875
+ features: ["CORE", "PLUS"],
876
+ quotas: { users: 10 },
877
+ monthlyNet: "15.00",
878
+ yearlyNet: "150.00",
879
+ validFrom: "2026-06-01"
880
+ });
881
+ const reads = async () => ({
882
+ versions: (await listVersions("RETIRING")).map((row) => row.id),
883
+ draft: (await findCurrentDraft("RETIRING"))?.id ?? null,
884
+ latestLive: (await findLatestLive("RETIRING"))?.id ?? null,
885
+ entitlementFeatures: (await entitlementVersions.findLatestLive("RETIRING"))?.features ?? null
886
+ });
887
+ const beforeRetiring = await reads();
888
+ assert.deepEqual(
889
+ {
890
+ versions: beforeRetiring.versions,
891
+ draft: beforeRetiring.draft,
892
+ latestLive: beforeRetiring.latestLive
893
+ },
894
+ { versions: [live.id, openDraft.id], draft: openDraft.id, latestLive: live.id },
895
+ "a live plan reads its versions, so the comparison below has a subject"
896
+ );
897
+ await repository.softDelete(plan.id);
898
+ assert.deepEqual(await reads(), beforeRetiring, "retiring the plan hid its versions");
899
+ });
687
900
  test("a retired bundle still occupies its key", async (t) => {
688
901
  const catalog = harness.adapter.bundleRepository;
689
902
  const retire = catalog?.softDelete?.bind(catalog);
690
903
  const byKey = catalog?.findByKey?.bind(catalog);
691
904
  if (!catalog || !retire || !byKey) {
692
- t.skip("adapter provides no BundleRepository");
905
+ missing(t, "bundleRetirement");
693
906
  return;
694
907
  }
695
908
  const bundle = await catalog.create({
@@ -711,7 +924,7 @@ function persistenceAdapterContract(options) {
711
924
  test("countByPlanVersionId counts current AND pending bindings in one query", async (t) => {
712
925
  const { seed, adapter } = harness;
713
926
  if (!adapter.subscriptionRepository.countByPlanVersionId) {
714
- t.skip("adapter does not implement countByPlanVersionId (fail-closed fallback)");
927
+ missing(t, "countByPlanVersionId");
715
928
  return;
716
929
  }
717
930
  const v1 = await seed.createPlanVersion({
@@ -751,9 +964,7 @@ function persistenceAdapterContract(options) {
751
964
  return;
752
965
  }
753
966
  if (!adapter.promoCodeRedemptionRepository) {
754
- t.skip(
755
- "adapter provides no PromoCodeRedemptionRepository (needed as tx write probe)"
756
- );
967
+ missing(t, "promoCodeRedemptions");
757
968
  return;
758
969
  }
759
970
  const redemptions = adapter.promoCodeRedemptionRepository;
@@ -836,7 +1047,7 @@ function persistenceAdapterContract(options) {
836
1047
  test("concurrent claimSlot grants exactly maxRedemptions slots", async (t) => {
837
1048
  const { seed, adapter } = harness;
838
1049
  if (!adapter.promoCodeRepository) {
839
- t.skip("adapter provides no PromoCodeRepository");
1050
+ missing(t, "promoCodes");
840
1051
  return;
841
1052
  }
842
1053
  const promoCodes = adapter.promoCodeRepository;
@@ -857,7 +1068,7 @@ function persistenceAdapterContract(options) {
857
1068
  test("claimSlot / markExhaustedIfFull / releaseSlot lifecycle", async (t) => {
858
1069
  const { seed, adapter } = harness;
859
1070
  if (!adapter.promoCodeRepository) {
860
- t.skip("adapter provides no PromoCodeRepository");
1071
+ missing(t, "promoCodes");
861
1072
  return;
862
1073
  }
863
1074
  const promoCodes = adapter.promoCodeRepository;
@@ -877,7 +1088,7 @@ function persistenceAdapterContract(options) {
877
1088
  test("a subscription cannot redeem twice (unique guard)", async (t) => {
878
1089
  const { seed, adapter } = harness;
879
1090
  if (!adapter.promoCodeRedemptionRepository) {
880
- t.skip("adapter provides no PromoCodeRedemptionRepository");
1091
+ missing(t, "promoCodeRedemptions");
881
1092
  return;
882
1093
  }
883
1094
  const redemptions = adapter.promoCodeRedemptionRepository;
@@ -917,7 +1128,7 @@ function persistenceAdapterContract(options) {
917
1128
  test("audit write \u2192 query roundtrip with actorTag filters", async (t) => {
918
1129
  const { adapter } = harness;
919
1130
  if (!adapter.audit || !adapter.auditQuery) {
920
- t.skip("adapter provides no AuditPort/AuditQueryPort pair");
1131
+ missing(t, "audit");
921
1132
  return;
922
1133
  }
923
1134
  await adapter.audit.write({
@@ -957,7 +1168,7 @@ function persistenceAdapterContract(options) {
957
1168
  test("MFA secret roundtrip", async (t) => {
958
1169
  const { adapter } = harness;
959
1170
  if (!adapter.mfa) {
960
- t.skip("adapter provides no MfaPort");
1171
+ missing(t, "mfa");
961
1172
  return;
962
1173
  }
963
1174
  assert.equal(await adapter.mfa.getSecret("admin-1"), null);
@@ -972,7 +1183,7 @@ function persistenceAdapterContract(options) {
972
1183
  test("finds the subscription the id names, not merely a subscription", async (t) => {
973
1184
  const { adapter, seed } = harness;
974
1185
  if (!adapter.promoSubscriptionLookup) {
975
- t.skip("adapter provides no PromoSubscriptionLookup");
1186
+ missing(t, "promoSubscriptionLookup");
976
1187
  return;
977
1188
  }
978
1189
  const { planVersionId } = await seed.createPlanVersion({
@@ -1001,7 +1212,7 @@ function persistenceAdapterContract(options) {
1001
1212
  test("returns null for an id that does not exist", async (t) => {
1002
1213
  const { adapter, seed } = harness;
1003
1214
  if (!adapter.promoSubscriptionLookup) {
1004
- t.skip("adapter provides no PromoSubscriptionLookup");
1215
+ missing(t, "promoSubscriptionLookup");
1005
1216
  return;
1006
1217
  }
1007
1218
  const { planVersionId } = await seed.createPlanVersion({
@@ -1022,7 +1233,7 @@ function persistenceAdapterContract(options) {
1022
1233
  test("carries the fields a promo rule reads: cycle and start date", async (t) => {
1023
1234
  const { adapter, seed } = harness;
1024
1235
  if (!adapter.promoSubscriptionLookup) {
1025
- t.skip("adapter provides no PromoSubscriptionLookup");
1236
+ missing(t, "promoSubscriptionLookup");
1026
1237
  return;
1027
1238
  }
1028
1239
  const { planVersionId } = await seed.createPlanVersion({
@@ -1057,7 +1268,7 @@ function persistenceAdapterContract(options) {
1057
1268
  test("reads inside a transaction, so validation and redemption agree", async (t) => {
1058
1269
  const { adapter, seed } = harness;
1059
1270
  if (!adapter.promoSubscriptionLookup) {
1060
- t.skip("adapter provides no PromoSubscriptionLookup");
1271
+ missing(t, "promoSubscriptionLookup");
1061
1272
  return;
1062
1273
  }
1063
1274
  const { planVersionId } = await seed.createPlanVersion({
@@ -1081,7 +1292,7 @@ function persistenceAdapterContract(options) {
1081
1292
  test("a contract keeps what was agreed, and ending it does not rewrite it", async (t) => {
1082
1293
  const contracts = harness.adapter.subscriptionContractRepository;
1083
1294
  if (!contracts) {
1084
- t.skip("adapter provides no SubscriptionContractRepository");
1295
+ missing(t, "subscriptionContracts");
1085
1296
  return;
1086
1297
  }
1087
1298
  const tenantId = "tenant-contract-lifecycle";
@@ -1237,7 +1448,7 @@ function persistenceAdapterContract(options) {
1237
1448
  test("a successor takes over without erasing the contract it replaces", async (t) => {
1238
1449
  const contracts = harness.adapter.subscriptionContractRepository;
1239
1450
  if (!contracts) {
1240
- t.skip("adapter provides no SubscriptionContractRepository");
1451
+ missing(t, "subscriptionContracts");
1241
1452
  return;
1242
1453
  }
1243
1454
  const tenantId = "tenant-contract-succession";
@@ -1339,7 +1550,7 @@ function persistenceAdapterContract(options) {
1339
1550
  test("a line keeps the currency and the tax it was booked with", async (t) => {
1340
1551
  const contracts = harness.adapter.subscriptionContractRepository;
1341
1552
  if (!contracts) {
1342
- t.skip("adapter provides no SubscriptionContractRepository");
1553
+ missing(t, "subscriptionContracts");
1343
1554
  return;
1344
1555
  }
1345
1556
  const tenantId = "tenant-contract-money-facts";
@@ -1437,7 +1648,7 @@ function persistenceAdapterContract(options) {
1437
1648
  test("no record before the first boot that could write one", async (t) => {
1438
1649
  const port = harness.adapter.appliedSettings;
1439
1650
  if (!port) {
1440
- t.skip("adapter provides no AppliedSettingsPort");
1651
+ missing(t, "appliedSettings");
1441
1652
  return;
1442
1653
  }
1443
1654
  assert.equal(await port.readApplied(), null);
@@ -1446,7 +1657,7 @@ function persistenceAdapterContract(options) {
1446
1657
  test("the record comes back as it was written \u2014 values, source and moment", async (t) => {
1447
1658
  const port = harness.adapter.appliedSettings;
1448
1659
  if (!port) {
1449
- t.skip("adapter provides no AppliedSettingsPort");
1660
+ missing(t, "appliedSettings");
1450
1661
  return;
1451
1662
  }
1452
1663
  assert.equal(await port.writeApplied(applied("sha256-a", FIRST_START), null), true);
@@ -1460,7 +1671,7 @@ function persistenceAdapterContract(options) {
1460
1671
  test("writing again replaces the one row rather than adding a second", async (t) => {
1461
1672
  const port = harness.adapter.appliedSettings;
1462
1673
  if (!port) {
1463
- t.skip("adapter provides no AppliedSettingsPort");
1674
+ missing(t, "appliedSettings");
1464
1675
  return;
1465
1676
  }
1466
1677
  await port.writeApplied(applied("sha256-a", FIRST_START), null);
@@ -1476,7 +1687,7 @@ function persistenceAdapterContract(options) {
1476
1687
  test("the first record is written once: a second writer that read none is refused", async (t) => {
1477
1688
  const port = harness.adapter.appliedSettings;
1478
1689
  if (!port) {
1479
- t.skip("adapter provides no AppliedSettingsPort");
1690
+ missing(t, "appliedSettings");
1480
1691
  return;
1481
1692
  }
1482
1693
  assert.equal(await port.writeApplied(applied("sha256-a", FIRST_START), null), true);
@@ -1486,7 +1697,7 @@ function persistenceAdapterContract(options) {
1486
1697
  test("a write guarded on a fingerprint the row no longer carries is refused", async (t) => {
1487
1698
  const port = harness.adapter.appliedSettings;
1488
1699
  if (!port) {
1489
- t.skip("adapter provides no AppliedSettingsPort");
1700
+ missing(t, "appliedSettings");
1490
1701
  return;
1491
1702
  }
1492
1703
  await port.writeApplied(applied("sha256-a", FIRST_START), null);
@@ -1509,7 +1720,7 @@ function persistenceAdapterContract(options) {
1509
1720
  test("a change lands with the record it supersedes, and is listed newest first", async (t) => {
1510
1721
  const port = harness.adapter.appliedSettings;
1511
1722
  if (!port) {
1512
- t.skip("adapter provides no AppliedSettingsPort");
1723
+ missing(t, "appliedSettings");
1513
1724
  return;
1514
1725
  }
1515
1726
  await port.writeApplied(applied("sha256-a", FIRST_START), null);
@@ -1547,7 +1758,7 @@ function persistenceAdapterContract(options) {
1547
1758
  test("a change whose record has moved on is refused whole: no change, and the record as it was", async (t) => {
1548
1759
  const port = harness.adapter.appliedSettings;
1549
1760
  if (!port) {
1550
- t.skip("adapter provides no AppliedSettingsPort");
1761
+ missing(t, "appliedSettings");
1551
1762
  return;
1552
1763
  }
1553
1764
  await port.writeApplied(applied("sha256-a", FIRST_START), null);
@@ -1565,7 +1776,7 @@ function persistenceAdapterContract(options) {
1565
1776
  test("starts noticing the same difference at once record it once", async (t) => {
1566
1777
  const port = harness.adapter.appliedSettings;
1567
1778
  if (!port) {
1568
- t.skip("adapter provides no AppliedSettingsPort");
1779
+ missing(t, "appliedSettings");
1569
1780
  return;
1570
1781
  }
1571
1782
  await port.writeApplied(applied("sha256-a", FIRST_START), null);
@@ -1589,7 +1800,7 @@ function persistenceAdapterContract(options) {
1589
1800
  test("several first starts write the record once", async (t) => {
1590
1801
  const port = harness.adapter.appliedSettings;
1591
1802
  if (!port) {
1592
- t.skip("adapter provides no AppliedSettingsPort");
1803
+ missing(t, "appliedSettings");
1593
1804
  return;
1594
1805
  }
1595
1806
  const written = await Promise.all(
@@ -1601,7 +1812,7 @@ function persistenceAdapterContract(options) {
1601
1812
  test("changes are listed in the order they were recorded, latest first \u2014 not by the moment they carry", async (t) => {
1602
1813
  const port = harness.adapter.appliedSettings;
1603
1814
  if (!port) {
1604
- t.skip("adapter provides no AppliedSettingsPort");
1815
+ missing(t, "appliedSettings");
1605
1816
  return;
1606
1817
  }
1607
1818
  await port.writeApplied(applied("sha256-0", FIRST_START, {}), null);
@@ -1633,7 +1844,7 @@ function persistenceAdapterContract(options) {
1633
1844
  test("acknowledging a change is recorded once, and filters it out of what is owed", async (t) => {
1634
1845
  const port = harness.adapter.appliedSettings;
1635
1846
  if (!port) {
1636
- t.skip("adapter provides no AppliedSettingsPort");
1847
+ missing(t, "appliedSettings");
1637
1848
  return;
1638
1849
  }
1639
1850
  await port.writeApplied(applied("sha256-a", FIRST_START), null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasicat/persistence-testing",
3
- "version": "1.0.0-rc.13",
3
+ "version": "1.0.0-rc.14",
4
4
  "description": "Contract test kit for SaaSiCat persistence adapters: one node:test suite that every adapter (Prisma, Drizzle, ...) must pass against a real database — locks, transaction rollback, atomic promo claims, tenant isolation, audit/MFA roundtrips.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -23,7 +23,7 @@
23
23
  "dist"
24
24
  ],
25
25
  "dependencies": {
26
- "@saasicat/core": "^1.0.0-rc.13"
26
+ "@saasicat/core": "^1.0.0-rc.14"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^25.6.0",