@saasicat/persistence-testing 1.0.0-rc.12 → 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/README.md CHANGED
@@ -27,10 +27,20 @@ Verified scenarios:
27
27
  concurrently — with the change and the record it supersedes landing together;
28
28
  changes listed in the order they were recorded, acknowledged once
29
29
 
30
- Scenario groups gate on declared capabilities and provided slices; a
31
- gated-off group reports as **skipped with reason** coverage gaps stay
32
- visible, never silent. Roadmap scenarios (subscription contracts, reference
33
- migrations N→N+1) are registered as visible skips until the slices ship.
30
+ Scenario groups gate on declared capabilities and provided slices. A group the
31
+ capabilities rule out, such as the lock scenarios with `pessimisticLocking:
32
+ false`, reports as **skipped with reason**. A group whose port or seed writer
33
+ the harness does not provide **fails**, unless the adapter names it in `gaps` —
34
+ then it reports as skipped. A gap named there that the harness does provide
35
+ fails the suite, so the list stays true. A skipped scenario is easy to read
36
+ past in a green run; a harness that forgot to wire a port would otherwise pass
37
+ without checking it.
38
+
39
+ The list describes the harness as it is built, not the adapter package. Where a
40
+ port adds a member only under an option — `@saasicat/adapter-prisma`'s
41
+ `validityWindows` and `atomicOnboardingSelection`, off by default for a 0.6
42
+ schema — compute `gaps` from the same option rather than writing a constant, so
43
+ the declaration moves when the schema does.
34
44
 
35
45
  ## What this is not
36
46
 
@@ -49,26 +59,39 @@ find one.
49
59
  import { persistenceAdapterContract } from '@saasicat/persistence-testing';
50
60
 
51
61
  persistenceAdapterContract({
52
- name: 'adapter-drizzle @ postgres',
62
+ name: 'my-adapter @ postgres',
53
63
  create: async () => ({
54
64
  adapter: {
55
65
  capabilities: { transactions: true, pessimisticLocking: true /* … */ },
56
66
  transactionRunner,
57
67
  subscriptionRepository,
58
68
  planVersionRepository,
59
- promoCodeRepository, // optional slices activate more scenarios
69
+ planRepository,
70
+ bundleRepository,
71
+ subscriptionBundleRepository,
72
+ tenantSubscriptionWrite,
73
+ promoCodeRepository,
60
74
  promoCodeRedemptionRepository,
75
+ promoSubscriptionLookup,
61
76
  mfa,
62
77
  audit,
63
78
  auditQuery,
64
- tenantSubscriptionWrite, // optional: enables atomic plan-binding scenarios
65
- planRepository, // optional: enables plan lifecycle scenarios
66
- bundleRepository, // optional: enables bundle validity scenarios
79
+ // Leave a part out and name it in `gaps` below; left out and not
80
+ // named, its scenarios fail.
81
+ },
82
+ seed: {
83
+ createPlanVersion,
84
+ createSubscription,
85
+ createBundleVersion,
86
+ clearBookingRequestDate,
87
+ createPromoCode,
67
88
  },
68
- seed: { createPlanVersion, createSubscription, createPromoCode },
69
89
  reset: () => truncatePlatformTables(),
70
90
  close: () => pool.end(),
71
91
  }),
92
+ // The parts this adapter deliberately does not provide. A part named here
93
+ // that the harness does provide fails the suite as well.
94
+ gaps: ['subscriptionContracts', 'appliedSettings'],
72
95
  });
73
96
  ```
74
97
 
package/dist/.build-stamp CHANGED
@@ -1 +1 @@
1
- 348525d327142f83e519330a84e7e48987e378b9a395beeda5f0e23be02d9fde
1
+ fe1ab043363d0764aaf580f2167335fcb0c2659c445bcc9540708782cb751a27
package/dist/index.cjs CHANGED
@@ -38,12 +38,133 @@ module.exports = __toCommonJS(index_exports);
38
38
  var import_strict = __toESM(require("node:assert/strict"), 1);
39
39
  var import_node_test = require("node:test");
40
40
  var LOCK_HOLD_MS = 150;
41
+ var CONTRACT_GAPS = {
42
+ atomicPlanBinding: {
43
+ reason: "adapter does not expose atomic plan-binding writes",
44
+ present: ({ adapter }) => Boolean(adapter.tenantSubscriptionWrite)
45
+ },
46
+ atomicOnboarding: {
47
+ reason: "adapter does not expose atomic onboarding writes",
48
+ present: ({ adapter }) => Boolean(adapter.tenantSubscriptionWrite?.applyOnboardingSelection)
49
+ },
50
+ promoCodes: {
51
+ reason: "adapter provides no PromoCodeRepository",
52
+ present: ({ adapter }) => Boolean(adapter.promoCodeRepository)
53
+ },
54
+ promoCodeRedemptions: {
55
+ reason: "adapter provides no PromoCodeRedemptionRepository",
56
+ present: ({ adapter }) => Boolean(adapter.promoCodeRedemptionRepository)
57
+ },
58
+ promoSubscriptionLookup: {
59
+ reason: "adapter provides no PromoSubscriptionLookup",
60
+ present: ({ adapter }) => Boolean(adapter.promoSubscriptionLookup)
61
+ },
62
+ planRepository: {
63
+ reason: "adapter provides no PlanRepository",
64
+ present: ({ adapter }) => Boolean(adapter.planRepository)
65
+ },
66
+ planLifecycle: {
67
+ reason: "adapter provides no time-aware PlanRepository lifecycle",
68
+ present: ({ adapter }) => {
69
+ const repository = adapter.planRepository;
70
+ return Boolean(
71
+ repository?.createPlanVersionDraft && repository.publishPlanVersionDraft && repository.findVersionById && repository.findActivePlanVersion
72
+ );
73
+ }
74
+ },
75
+ planRetirement: {
76
+ reason: "adapter provides no PlanRepository that retires and finds plans by key",
77
+ present: ({ adapter }) => Boolean(adapter.planRepository?.softDelete && adapter.planRepository.findByKey)
78
+ },
79
+ planVersionReads: {
80
+ reason: "adapter provides no PlanRepository that reads versions by plan key",
81
+ present: ({ adapter }) => {
82
+ const repository = adapter.planRepository;
83
+ return Boolean(
84
+ repository?.listVersions && repository.findCurrentDraft && repository.findLatestLivePlanVersion
85
+ );
86
+ }
87
+ },
88
+ planVersionRetirement: {
89
+ reason: "adapter provides no PlanRepository that reads versions and retires plans",
90
+ present: ({ adapter }) => {
91
+ const repository = adapter.planRepository;
92
+ return Boolean(
93
+ repository?.createPlanVersionDraft && repository.publishPlanVersionDraft && repository.listVersions && repository.findCurrentDraft && repository.findLatestLivePlanVersion && repository.softDelete
94
+ );
95
+ }
96
+ },
97
+ bundleRepository: {
98
+ reason: "adapter provides no BundleRepository",
99
+ present: ({ adapter }) => Boolean(adapter.bundleRepository)
100
+ },
101
+ bundleValidity: {
102
+ reason: "adapter provides no time-aware BundleRepository",
103
+ present: ({ adapter }) => Boolean(adapter.bundleRepository?.findActiveBundleVersion)
104
+ },
105
+ bundleDraftDiscard: {
106
+ reason: "adapter provides no BundleRepository that discards drafts",
107
+ present: ({ adapter }) => Boolean(adapter.bundleRepository?.deleteDraft)
108
+ },
109
+ bundleDraftPublish: {
110
+ reason: "adapter provides no BundleRepository that publishes drafts",
111
+ present: ({ adapter }) => Boolean(adapter.bundleRepository?.publishDraft)
112
+ },
113
+ bundleRetirement: {
114
+ reason: "adapter provides no BundleRepository that retires and finds bundles by key",
115
+ present: ({ adapter }) => Boolean(adapter.bundleRepository?.softDelete && adapter.bundleRepository.findByKey)
116
+ },
117
+ bundleBookings: {
118
+ reason: "adapter provides no SubscriptionBundleRepository or bundle catalog",
119
+ present: ({ adapter, seed }) => Boolean(adapter.subscriptionBundleRepository && seed.createBundleVersion)
120
+ },
121
+ halfCancelledBookingSeed: {
122
+ reason: "adapter harness cannot write the half-cancelled shape",
123
+ present: ({ seed }) => Boolean(seed.clearBookingRequestDate)
124
+ },
125
+ countByPlanVersionId: {
126
+ reason: "adapter does not implement countByPlanVersionId (fail-closed fallback)",
127
+ present: ({ adapter }) => Boolean(adapter.subscriptionRepository.countByPlanVersionId)
128
+ },
129
+ audit: {
130
+ reason: "adapter provides no AuditPort/AuditQueryPort pair",
131
+ present: ({ adapter }) => Boolean(adapter.audit && adapter.auditQuery)
132
+ },
133
+ mfa: {
134
+ reason: "adapter provides no MfaPort",
135
+ present: ({ adapter }) => Boolean(adapter.mfa)
136
+ },
137
+ subscriptionContracts: {
138
+ reason: "adapter provides no SubscriptionContractRepository",
139
+ present: ({ adapter }) => Boolean(adapter.subscriptionContractRepository)
140
+ },
141
+ appliedSettings: {
142
+ reason: "adapter provides no AppliedSettingsPort",
143
+ present: ({ adapter }) => Boolean(adapter.appliedSettings)
144
+ }
145
+ };
41
146
  function sleep(ms) {
42
147
  return new Promise((resolve) => setTimeout(resolve, ms));
43
148
  }
44
149
  function persistenceAdapterContract(options) {
150
+ const declaredGaps = new Set(options.gaps ?? []);
151
+ let harness;
152
+ function missing(t, gap) {
153
+ const { reason, present } = CONTRACT_GAPS[gap];
154
+ if (present(harness)) {
155
+ import_strict.default.fail(
156
+ `'${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.`
157
+ );
158
+ }
159
+ if (declaredGaps.has(gap)) {
160
+ t.skip(reason);
161
+ return;
162
+ }
163
+ import_strict.default.fail(
164
+ `${reason}. Wire it into the harness, or declare \`gaps: ['${gap}']\` in persistenceAdapterContract if the adapter deliberately does not provide it.`
165
+ );
166
+ }
45
167
  (0, import_node_test.describe)(`persistence adapter contract: ${options.name}`, () => {
46
- let harness;
47
168
  (0, import_node_test.before)(async () => {
48
169
  harness = await options.create();
49
170
  });
@@ -53,6 +174,20 @@ function persistenceAdapterContract(options) {
53
174
  (0, import_node_test.beforeEach)(async () => {
54
175
  await harness.reset();
55
176
  });
177
+ (0, import_node_test.test)("the declared gaps are exactly the parts the harness does not provide", () => {
178
+ const gaps = Object.keys(CONTRACT_GAPS);
179
+ const absent = gaps.filter((gap) => !CONTRACT_GAPS[gap].present(harness));
180
+ const known = (gap) => Object.prototype.hasOwnProperty.call(CONTRACT_GAPS, gap);
181
+ const unknown = [...declaredGaps].filter((gap) => !known(gap));
182
+ const stale = [...declaredGaps].filter((gap) => known(gap) && !absent.includes(gap));
183
+ const undeclared = absent.filter((gap) => !declaredGaps.has(gap));
184
+ const problems = [
185
+ unknown.length > 0 && `declared as gaps but not parts of the contract: ${unknown.join(", ")} (ContractGap lists the names)`,
186
+ stale.length > 0 && `declared as gaps but wired into the harness: ${stale.join(", ")}`,
187
+ undeclared.length > 0 && `not wired into the harness and not declared as gaps: ${undeclared.join(", ")}`
188
+ ].filter(Boolean);
189
+ import_strict.default.deepEqual(problems, [], problems.join("; "));
190
+ });
56
191
  (0, import_node_test.test)("findByTenantId returns the tenant subscription with plan-version limits", async () => {
57
192
  const { seed, adapter } = harness;
58
193
  const { planVersionId } = await seed.createPlanVersion({
@@ -127,7 +262,7 @@ function persistenceAdapterContract(options) {
127
262
  (0, import_node_test.test)("immediate plan change binds plan and active PlanVersion consistently", async (t) => {
128
263
  const { seed, adapter } = harness;
129
264
  if (!adapter.tenantSubscriptionWrite) {
130
- t.skip("adapter does not expose atomic plan-binding writes");
265
+ missing(t, "atomicPlanBinding");
131
266
  return;
132
267
  }
133
268
  const oldVersion = await seed.createPlanVersion({
@@ -172,12 +307,12 @@ function persistenceAdapterContract(options) {
172
307
  const { seed, adapter } = harness;
173
308
  const writer = adapter.tenantSubscriptionWrite;
174
309
  if (!writer?.applyOnboardingSelection) {
175
- t.skip("adapter does not expose atomic onboarding writes");
310
+ missing(t, "atomicOnboarding");
176
311
  return;
177
312
  }
178
313
  const redemptions = adapter.promoCodeRedemptionRepository;
179
314
  if (!redemptions) {
180
- t.skip("adapter provides no PromoCodeRedemptionRepository");
315
+ missing(t, "promoCodeRedemptions");
181
316
  return;
182
317
  }
183
318
  const oldVersion = await seed.createPlanVersion({
@@ -256,7 +391,7 @@ function persistenceAdapterContract(options) {
256
391
  (0, import_node_test.test)("plan lifecycle keeps semantic identity and auto-succeeds validity windows", async (t) => {
257
392
  const repository = harness.adapter.planRepository;
258
393
  if (!repository?.createPlanVersionDraft || !repository.publishPlanVersionDraft || !repository.findVersionById || !repository.findActivePlanVersion) {
259
- t.skip("adapter provides no time-aware PlanRepository lifecycle");
394
+ missing(t, "planLifecycle");
260
395
  return;
261
396
  }
262
397
  const plan = await repository.create({
@@ -320,7 +455,7 @@ function persistenceAdapterContract(options) {
320
455
  (0, import_node_test.test)("bundle lifecycle roundtrips validity and auto-succeeds atomically", async (t) => {
321
456
  const repository = harness.adapter.bundleRepository;
322
457
  if (!repository?.findActiveBundleVersion) {
323
- t.skip("adapter provides no time-aware BundleRepository");
458
+ missing(t, "bundleValidity");
324
459
  return;
325
460
  }
326
461
  const bundle = await repository.create({
@@ -379,7 +514,7 @@ function persistenceAdapterContract(options) {
379
514
  const repository = harness.adapter.subscriptionBundleRepository;
380
515
  const { seed } = harness;
381
516
  if (!repository || !seed.createBundleVersion) {
382
- t.skip("adapter provides no SubscriptionBundleRepository or bundle catalog");
517
+ missing(t, "bundleBookings");
383
518
  return;
384
519
  }
385
520
  const { planVersionId } = await seed.createPlanVersion({
@@ -434,7 +569,7 @@ function persistenceAdapterContract(options) {
434
569
  const repository = harness.adapter.subscriptionBundleRepository;
435
570
  const { seed } = harness;
436
571
  if (!repository || !seed.createBundleVersion) {
437
- t.skip("adapter provides no SubscriptionBundleRepository or bundle catalog");
572
+ missing(t, "bundleBookings");
438
573
  return;
439
574
  }
440
575
  const { planVersionId } = await seed.createPlanVersion({
@@ -488,7 +623,7 @@ function persistenceAdapterContract(options) {
488
623
  const repository = harness.adapter.subscriptionBundleRepository;
489
624
  const { seed } = harness;
490
625
  if (!repository || !seed.createBundleVersion) {
491
- t.skip("adapter provides no SubscriptionBundleRepository or bundle catalog");
626
+ missing(t, "bundleBookings");
492
627
  return;
493
628
  }
494
629
  const { planVersionId } = await seed.createPlanVersion({
@@ -533,7 +668,7 @@ function persistenceAdapterContract(options) {
533
668
  const repository = harness.adapter.subscriptionBundleRepository;
534
669
  const { seed } = harness;
535
670
  if (!repository || !seed.createBundleVersion) {
536
- t.skip("adapter provides no SubscriptionBundleRepository or bundle catalog");
671
+ missing(t, "bundleBookings");
537
672
  return;
538
673
  }
539
674
  const { planVersionId } = await seed.createPlanVersion({
@@ -564,7 +699,7 @@ function persistenceAdapterContract(options) {
564
699
  });
565
700
  const clearRequestDate = harness.seed.clearBookingRequestDate;
566
701
  if (!clearRequestDate) {
567
- t.skip("adapter harness cannot write the half-cancelled shape");
702
+ missing(t, "halfCancelledBookingSeed");
568
703
  return;
569
704
  }
570
705
  await clearRequestDate(booking.id);
@@ -585,7 +720,7 @@ function persistenceAdapterContract(options) {
585
720
  const catalog = harness.adapter.bundleRepository;
586
721
  const discardDraft = catalog?.deleteDraft?.bind(catalog);
587
722
  if (!catalog || !discardDraft) {
588
- t.skip("adapter provides no BundleRepository");
723
+ missing(t, "bundleDraftDiscard");
589
724
  return;
590
725
  }
591
726
  const bundle = await catalog.create({
@@ -617,7 +752,7 @@ function persistenceAdapterContract(options) {
617
752
  const catalog = harness.adapter.bundleRepository;
618
753
  const publish = catalog?.publishDraft?.bind(catalog);
619
754
  if (!catalog || !publish) {
620
- t.skip("adapter provides no BundleRepository");
755
+ missing(t, "bundleDraftPublish");
621
756
  return;
622
757
  }
623
758
  const bundle = await catalog.create({
@@ -680,7 +815,7 @@ function persistenceAdapterContract(options) {
680
815
  (0, import_node_test.test)("a plan key names one plan for the whole installation", async (t) => {
681
816
  const repository = harness.adapter.planRepository;
682
817
  if (!repository) {
683
- t.skip("adapter provides no PlanRepository");
818
+ missing(t, "planRepository");
684
819
  return;
685
820
  }
686
821
  await repository.create({ planKey: "DOUBLE", label: "First" });
@@ -692,7 +827,7 @@ function persistenceAdapterContract(options) {
692
827
  (0, import_node_test.test)("a bundle key names one bundle for the whole installation", async (t) => {
693
828
  const catalog = harness.adapter.bundleRepository;
694
829
  if (!catalog) {
695
- t.skip("adapter provides no BundleRepository");
830
+ missing(t, "bundleRepository");
696
831
  return;
697
832
  }
698
833
  await catalog.create({ bundleKey: "DOUBLE", label: "First" });
@@ -706,7 +841,7 @@ function persistenceAdapterContract(options) {
706
841
  const retire = repository?.softDelete?.bind(repository);
707
842
  const byKey = repository?.findByKey?.bind(repository);
708
843
  if (!repository || !retire || !byKey) {
709
- t.skip("adapter provides no PlanRepository");
844
+ missing(t, "planRetirement");
710
845
  return;
711
846
  }
712
847
  const plan = await repository.create({ planKey: "RETIRED_PLAN", label: "Retired" });
@@ -720,12 +855,90 @@ function persistenceAdapterContract(options) {
720
855
  "a retired plan is not in the catalogue an operator browses"
721
856
  );
722
857
  });
858
+ (0, import_node_test.test)("a plan key no plan has finds no versions, rather than failing", async (t) => {
859
+ const repository = harness.adapter.planRepository;
860
+ if (!repository?.listVersions || !repository.findCurrentDraft || !repository.findLatestLivePlanVersion) {
861
+ missing(t, "planVersionReads");
862
+ return;
863
+ }
864
+ const entitlementVersions = harness.adapter.planVersionRepository;
865
+ import_strict.default.deepEqual(await repository.listVersions("NO_SUCH_PLAN"), []);
866
+ import_strict.default.equal(await repository.findCurrentDraft("NO_SUCH_PLAN"), null);
867
+ import_strict.default.equal(await repository.findLatestLivePlanVersion("NO_SUCH_PLAN"), null);
868
+ if (repository.findActivePlanVersion) {
869
+ import_strict.default.equal(
870
+ await repository.findActivePlanVersion("NO_SUCH_PLAN", /* @__PURE__ */ new Date()),
871
+ null
872
+ );
873
+ }
874
+ import_strict.default.equal(await entitlementVersions.findLatestLive("NO_SUCH_PLAN"), null);
875
+ if (entitlementVersions.findActive) {
876
+ import_strict.default.equal(
877
+ await entitlementVersions.findActive("NO_SUCH_PLAN", /* @__PURE__ */ new Date()),
878
+ null
879
+ );
880
+ }
881
+ });
882
+ (0, import_node_test.test)("retiring a plan hides none of its versions", async (t) => {
883
+ const repository = harness.adapter.planRepository;
884
+ if (!repository?.createPlanVersionDraft || !repository.publishPlanVersionDraft || !repository.listVersions || !repository.findCurrentDraft || !repository.findLatestLivePlanVersion || !repository.softDelete) {
885
+ missing(t, "planVersionRetirement");
886
+ return;
887
+ }
888
+ const listVersions = repository.listVersions.bind(repository);
889
+ const findCurrentDraft = repository.findCurrentDraft.bind(repository);
890
+ const findLatestLive = repository.findLatestLivePlanVersion.bind(repository);
891
+ const entitlementVersions = harness.adapter.planVersionRepository;
892
+ const plan = await repository.create({ planKey: "RETIRING", label: "Retiring" });
893
+ const firstDraft = await repository.createPlanVersionDraft({
894
+ planId: "RETIRING",
895
+ features: ["CORE"],
896
+ quotas: { users: 5 },
897
+ monthlyNet: "10.00",
898
+ yearlyNet: "100.00",
899
+ validFrom: "2026-01-01"
900
+ });
901
+ const live = await repository.publishPlanVersionDraft(firstDraft.id, {
902
+ publishedByUserId: null,
903
+ publishedChanges: [],
904
+ nonRegressive: true,
905
+ validFrom: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
906
+ validUntil: null
907
+ });
908
+ const openDraft = await repository.createPlanVersionDraft({
909
+ planId: "RETIRING",
910
+ baseVersionId: live.id,
911
+ features: ["CORE", "PLUS"],
912
+ quotas: { users: 10 },
913
+ monthlyNet: "15.00",
914
+ yearlyNet: "150.00",
915
+ validFrom: "2026-06-01"
916
+ });
917
+ const reads = async () => ({
918
+ versions: (await listVersions("RETIRING")).map((row) => row.id),
919
+ draft: (await findCurrentDraft("RETIRING"))?.id ?? null,
920
+ latestLive: (await findLatestLive("RETIRING"))?.id ?? null,
921
+ entitlementFeatures: (await entitlementVersions.findLatestLive("RETIRING"))?.features ?? null
922
+ });
923
+ const beforeRetiring = await reads();
924
+ import_strict.default.deepEqual(
925
+ {
926
+ versions: beforeRetiring.versions,
927
+ draft: beforeRetiring.draft,
928
+ latestLive: beforeRetiring.latestLive
929
+ },
930
+ { versions: [live.id, openDraft.id], draft: openDraft.id, latestLive: live.id },
931
+ "a live plan reads its versions, so the comparison below has a subject"
932
+ );
933
+ await repository.softDelete(plan.id);
934
+ import_strict.default.deepEqual(await reads(), beforeRetiring, "retiring the plan hid its versions");
935
+ });
723
936
  (0, import_node_test.test)("a retired bundle still occupies its key", async (t) => {
724
937
  const catalog = harness.adapter.bundleRepository;
725
938
  const retire = catalog?.softDelete?.bind(catalog);
726
939
  const byKey = catalog?.findByKey?.bind(catalog);
727
940
  if (!catalog || !retire || !byKey) {
728
- t.skip("adapter provides no BundleRepository");
941
+ missing(t, "bundleRetirement");
729
942
  return;
730
943
  }
731
944
  const bundle = await catalog.create({
@@ -747,7 +960,7 @@ function persistenceAdapterContract(options) {
747
960
  (0, import_node_test.test)("countByPlanVersionId counts current AND pending bindings in one query", async (t) => {
748
961
  const { seed, adapter } = harness;
749
962
  if (!adapter.subscriptionRepository.countByPlanVersionId) {
750
- t.skip("adapter does not implement countByPlanVersionId (fail-closed fallback)");
963
+ missing(t, "countByPlanVersionId");
751
964
  return;
752
965
  }
753
966
  const v1 = await seed.createPlanVersion({
@@ -787,9 +1000,7 @@ function persistenceAdapterContract(options) {
787
1000
  return;
788
1001
  }
789
1002
  if (!adapter.promoCodeRedemptionRepository) {
790
- t.skip(
791
- "adapter provides no PromoCodeRedemptionRepository (needed as tx write probe)"
792
- );
1003
+ missing(t, "promoCodeRedemptions");
793
1004
  return;
794
1005
  }
795
1006
  const redemptions = adapter.promoCodeRedemptionRepository;
@@ -872,7 +1083,7 @@ function persistenceAdapterContract(options) {
872
1083
  (0, import_node_test.test)("concurrent claimSlot grants exactly maxRedemptions slots", async (t) => {
873
1084
  const { seed, adapter } = harness;
874
1085
  if (!adapter.promoCodeRepository) {
875
- t.skip("adapter provides no PromoCodeRepository");
1086
+ missing(t, "promoCodes");
876
1087
  return;
877
1088
  }
878
1089
  const promoCodes = adapter.promoCodeRepository;
@@ -893,7 +1104,7 @@ function persistenceAdapterContract(options) {
893
1104
  (0, import_node_test.test)("claimSlot / markExhaustedIfFull / releaseSlot lifecycle", async (t) => {
894
1105
  const { seed, adapter } = harness;
895
1106
  if (!adapter.promoCodeRepository) {
896
- t.skip("adapter provides no PromoCodeRepository");
1107
+ missing(t, "promoCodes");
897
1108
  return;
898
1109
  }
899
1110
  const promoCodes = adapter.promoCodeRepository;
@@ -913,7 +1124,7 @@ function persistenceAdapterContract(options) {
913
1124
  (0, import_node_test.test)("a subscription cannot redeem twice (unique guard)", async (t) => {
914
1125
  const { seed, adapter } = harness;
915
1126
  if (!adapter.promoCodeRedemptionRepository) {
916
- t.skip("adapter provides no PromoCodeRedemptionRepository");
1127
+ missing(t, "promoCodeRedemptions");
917
1128
  return;
918
1129
  }
919
1130
  const redemptions = adapter.promoCodeRedemptionRepository;
@@ -953,7 +1164,7 @@ function persistenceAdapterContract(options) {
953
1164
  (0, import_node_test.test)("audit write \u2192 query roundtrip with actorTag filters", async (t) => {
954
1165
  const { adapter } = harness;
955
1166
  if (!adapter.audit || !adapter.auditQuery) {
956
- t.skip("adapter provides no AuditPort/AuditQueryPort pair");
1167
+ missing(t, "audit");
957
1168
  return;
958
1169
  }
959
1170
  await adapter.audit.write({
@@ -993,7 +1204,7 @@ function persistenceAdapterContract(options) {
993
1204
  (0, import_node_test.test)("MFA secret roundtrip", async (t) => {
994
1205
  const { adapter } = harness;
995
1206
  if (!adapter.mfa) {
996
- t.skip("adapter provides no MfaPort");
1207
+ missing(t, "mfa");
997
1208
  return;
998
1209
  }
999
1210
  import_strict.default.equal(await adapter.mfa.getSecret("admin-1"), null);
@@ -1008,7 +1219,7 @@ function persistenceAdapterContract(options) {
1008
1219
  (0, import_node_test.test)("finds the subscription the id names, not merely a subscription", async (t) => {
1009
1220
  const { adapter, seed } = harness;
1010
1221
  if (!adapter.promoSubscriptionLookup) {
1011
- t.skip("adapter provides no PromoSubscriptionLookup");
1222
+ missing(t, "promoSubscriptionLookup");
1012
1223
  return;
1013
1224
  }
1014
1225
  const { planVersionId } = await seed.createPlanVersion({
@@ -1037,7 +1248,7 @@ function persistenceAdapterContract(options) {
1037
1248
  (0, import_node_test.test)("returns null for an id that does not exist", async (t) => {
1038
1249
  const { adapter, seed } = harness;
1039
1250
  if (!adapter.promoSubscriptionLookup) {
1040
- t.skip("adapter provides no PromoSubscriptionLookup");
1251
+ missing(t, "promoSubscriptionLookup");
1041
1252
  return;
1042
1253
  }
1043
1254
  const { planVersionId } = await seed.createPlanVersion({
@@ -1058,7 +1269,7 @@ function persistenceAdapterContract(options) {
1058
1269
  (0, import_node_test.test)("carries the fields a promo rule reads: cycle and start date", async (t) => {
1059
1270
  const { adapter, seed } = harness;
1060
1271
  if (!adapter.promoSubscriptionLookup) {
1061
- t.skip("adapter provides no PromoSubscriptionLookup");
1272
+ missing(t, "promoSubscriptionLookup");
1062
1273
  return;
1063
1274
  }
1064
1275
  const { planVersionId } = await seed.createPlanVersion({
@@ -1093,7 +1304,7 @@ function persistenceAdapterContract(options) {
1093
1304
  (0, import_node_test.test)("reads inside a transaction, so validation and redemption agree", async (t) => {
1094
1305
  const { adapter, seed } = harness;
1095
1306
  if (!adapter.promoSubscriptionLookup) {
1096
- t.skip("adapter provides no PromoSubscriptionLookup");
1307
+ missing(t, "promoSubscriptionLookup");
1097
1308
  return;
1098
1309
  }
1099
1310
  const { planVersionId } = await seed.createPlanVersion({
@@ -1117,7 +1328,7 @@ function persistenceAdapterContract(options) {
1117
1328
  (0, import_node_test.test)("a contract keeps what was agreed, and ending it does not rewrite it", async (t) => {
1118
1329
  const contracts = harness.adapter.subscriptionContractRepository;
1119
1330
  if (!contracts) {
1120
- t.skip("adapter provides no SubscriptionContractRepository");
1331
+ missing(t, "subscriptionContracts");
1121
1332
  return;
1122
1333
  }
1123
1334
  const tenantId = "tenant-contract-lifecycle";
@@ -1273,7 +1484,7 @@ function persistenceAdapterContract(options) {
1273
1484
  (0, import_node_test.test)("a successor takes over without erasing the contract it replaces", async (t) => {
1274
1485
  const contracts = harness.adapter.subscriptionContractRepository;
1275
1486
  if (!contracts) {
1276
- t.skip("adapter provides no SubscriptionContractRepository");
1487
+ missing(t, "subscriptionContracts");
1277
1488
  return;
1278
1489
  }
1279
1490
  const tenantId = "tenant-contract-succession";
@@ -1375,7 +1586,7 @@ function persistenceAdapterContract(options) {
1375
1586
  (0, import_node_test.test)("a line keeps the currency and the tax it was booked with", async (t) => {
1376
1587
  const contracts = harness.adapter.subscriptionContractRepository;
1377
1588
  if (!contracts) {
1378
- t.skip("adapter provides no SubscriptionContractRepository");
1589
+ missing(t, "subscriptionContracts");
1379
1590
  return;
1380
1591
  }
1381
1592
  const tenantId = "tenant-contract-money-facts";
@@ -1473,7 +1684,7 @@ function persistenceAdapterContract(options) {
1473
1684
  (0, import_node_test.test)("no record before the first boot that could write one", async (t) => {
1474
1685
  const port = harness.adapter.appliedSettings;
1475
1686
  if (!port) {
1476
- t.skip("adapter provides no AppliedSettingsPort");
1687
+ missing(t, "appliedSettings");
1477
1688
  return;
1478
1689
  }
1479
1690
  import_strict.default.equal(await port.readApplied(), null);
@@ -1482,7 +1693,7 @@ function persistenceAdapterContract(options) {
1482
1693
  (0, import_node_test.test)("the record comes back as it was written \u2014 values, source and moment", async (t) => {
1483
1694
  const port = harness.adapter.appliedSettings;
1484
1695
  if (!port) {
1485
- t.skip("adapter provides no AppliedSettingsPort");
1696
+ missing(t, "appliedSettings");
1486
1697
  return;
1487
1698
  }
1488
1699
  import_strict.default.equal(await port.writeApplied(applied("sha256-a", FIRST_START), null), true);
@@ -1496,7 +1707,7 @@ function persistenceAdapterContract(options) {
1496
1707
  (0, import_node_test.test)("writing again replaces the one row rather than adding a second", async (t) => {
1497
1708
  const port = harness.adapter.appliedSettings;
1498
1709
  if (!port) {
1499
- t.skip("adapter provides no AppliedSettingsPort");
1710
+ missing(t, "appliedSettings");
1500
1711
  return;
1501
1712
  }
1502
1713
  await port.writeApplied(applied("sha256-a", FIRST_START), null);
@@ -1512,7 +1723,7 @@ function persistenceAdapterContract(options) {
1512
1723
  (0, import_node_test.test)("the first record is written once: a second writer that read none is refused", async (t) => {
1513
1724
  const port = harness.adapter.appliedSettings;
1514
1725
  if (!port) {
1515
- t.skip("adapter provides no AppliedSettingsPort");
1726
+ missing(t, "appliedSettings");
1516
1727
  return;
1517
1728
  }
1518
1729
  import_strict.default.equal(await port.writeApplied(applied("sha256-a", FIRST_START), null), true);
@@ -1522,7 +1733,7 @@ function persistenceAdapterContract(options) {
1522
1733
  (0, import_node_test.test)("a write guarded on a fingerprint the row no longer carries is refused", async (t) => {
1523
1734
  const port = harness.adapter.appliedSettings;
1524
1735
  if (!port) {
1525
- t.skip("adapter provides no AppliedSettingsPort");
1736
+ missing(t, "appliedSettings");
1526
1737
  return;
1527
1738
  }
1528
1739
  await port.writeApplied(applied("sha256-a", FIRST_START), null);
@@ -1545,7 +1756,7 @@ function persistenceAdapterContract(options) {
1545
1756
  (0, import_node_test.test)("a change lands with the record it supersedes, and is listed newest first", async (t) => {
1546
1757
  const port = harness.adapter.appliedSettings;
1547
1758
  if (!port) {
1548
- t.skip("adapter provides no AppliedSettingsPort");
1759
+ missing(t, "appliedSettings");
1549
1760
  return;
1550
1761
  }
1551
1762
  await port.writeApplied(applied("sha256-a", FIRST_START), null);
@@ -1583,7 +1794,7 @@ function persistenceAdapterContract(options) {
1583
1794
  (0, import_node_test.test)("a change whose record has moved on is refused whole: no change, and the record as it was", async (t) => {
1584
1795
  const port = harness.adapter.appliedSettings;
1585
1796
  if (!port) {
1586
- t.skip("adapter provides no AppliedSettingsPort");
1797
+ missing(t, "appliedSettings");
1587
1798
  return;
1588
1799
  }
1589
1800
  await port.writeApplied(applied("sha256-a", FIRST_START), null);
@@ -1601,7 +1812,7 @@ function persistenceAdapterContract(options) {
1601
1812
  (0, import_node_test.test)("starts noticing the same difference at once record it once", 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-a", FIRST_START), null);
@@ -1625,7 +1836,7 @@ function persistenceAdapterContract(options) {
1625
1836
  (0, import_node_test.test)("several first starts write the record once", async (t) => {
1626
1837
  const port = harness.adapter.appliedSettings;
1627
1838
  if (!port) {
1628
- t.skip("adapter provides no AppliedSettingsPort");
1839
+ missing(t, "appliedSettings");
1629
1840
  return;
1630
1841
  }
1631
1842
  const written = await Promise.all(
@@ -1637,7 +1848,7 @@ function persistenceAdapterContract(options) {
1637
1848
  (0, import_node_test.test)("changes are listed in the order they were recorded, latest first \u2014 not by the moment they carry", async (t) => {
1638
1849
  const port = harness.adapter.appliedSettings;
1639
1850
  if (!port) {
1640
- t.skip("adapter provides no AppliedSettingsPort");
1851
+ missing(t, "appliedSettings");
1641
1852
  return;
1642
1853
  }
1643
1854
  await port.writeApplied(applied("sha256-0", FIRST_START, {}), null);
@@ -1669,7 +1880,7 @@ function persistenceAdapterContract(options) {
1669
1880
  (0, import_node_test.test)("acknowledging a change is recorded once, and filters it out of what is owed", async (t) => {
1670
1881
  const port = harness.adapter.appliedSettings;
1671
1882
  if (!port) {
1672
- t.skip("adapter provides no AppliedSettingsPort");
1883
+ missing(t, "appliedSettings");
1673
1884
  return;
1674
1885
  }
1675
1886
  await port.writeApplied(applied("sha256-a", FIRST_START), null);