@rotorsoft/act-tck 1.26.9 → 1.26.10

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.
@@ -14,6 +14,32 @@ export type DifferentialStore = {
14
14
  */
15
15
  readonly factory: () => Store | Promise<Store>;
16
16
  };
17
+ /**
18
+ * Known cross-adapter divergences the caller wants the differential to
19
+ * skip until the backing fix lands. Each flag suppresses exactly the
20
+ * assertions that a currently-filed bug makes red, so the differential
21
+ * stays green on master and the gate un-gates itself (delete the flag)
22
+ * when the fix merges. Every flag names its issue so the reason is
23
+ * auditable at the call site.
24
+ */
25
+ export type DifferentialSkips = {
26
+ /**
27
+ * #1197 — SQLite `LIKE` is ASCII-case-insensitive, so a **mixed-case
28
+ * regex pattern** filter overmatches vs PG (`~`) / InMemory (`RegExp`),
29
+ * which are case-sensitive. Skips only the pattern-driven mixed-case
30
+ * assertions; exact-match (`stream_exact`) mixed-case still runs, since
31
+ * that path is case-exact on every adapter. Un-gate when #1197 lands.
32
+ */
33
+ readonly caseInsensitivePatterns?: boolean;
34
+ /**
35
+ * #1199 — `names: []` and falsy-zero `before`/`after: 0` guards differ
36
+ * across adapters (`names: []` → PG returns all, InMemory/SQLite return
37
+ * none; `before: 0` / `after: 0` are dropped by some adapters' truthy
38
+ * guards). Skips only those specific edge-input query assertions.
39
+ * Un-gate when #1199 lands.
40
+ */
41
+ readonly queryEdgeInputs?: boolean;
42
+ };
17
43
  /**
18
44
  * Options for {@link runStoreDifferentialTck}.
19
45
  */
@@ -46,15 +72,36 @@ export type StoreDifferentialTckOptions = {
46
72
  * explores; fewer keep durable-adapter suites fast. Default `8`.
47
73
  */
48
74
  readonly runs?: number;
75
+ /**
76
+ * Whether the comparand adapters support the optional {@link Store.forget_pii}
77
+ * surface (and the `pii` field on commit). When `true`, the workload
78
+ * commits PII-bearing events and the differential asserts that
79
+ * `forget_pii` wipes them identically across adapters. All in-tree
80
+ * adapters implement it, so it defaults to `true`; a third-party store
81
+ * that opts out sets it `false`. Default `true`.
82
+ */
83
+ readonly piiIsolation?: boolean;
84
+ /**
85
+ * Known-divergence gates — see {@link DifferentialSkips}. Each flag
86
+ * suppresses the assertions a currently-filed bug makes red so the
87
+ * differential stays green until the fix merges. Default: nothing
88
+ * skipped.
89
+ */
90
+ readonly skip?: DifferentialSkips;
49
91
  };
50
92
  /**
51
- * Cross-adapter differential contract (#1030, fuzz workloads #1057).
93
+ * Cross-adapter differential contract (#1030, fuzz workloads #1057,
94
+ * lease/truncate/pii/query fuzz #1200).
52
95
  *
53
96
  * Adapters can drift in ways per-adapter cases don't catch — event
54
97
  * ordering, the `with_snaps` snapshot floor, the exact shape of
55
- * `query_stats` / `query_streams` output. This harness drives a **family
56
- * of randomized, seeded workloads** (commits, inline snapshots, truncates,
57
- * subscriptions) against every store in `options.stores`, then asserts
98
+ * `query_stats` / `query_streams` output, the lease lifecycle's effect on
99
+ * a stream's `retry`/`blocked`/`error`, windowed-truncate boundaries,
100
+ * `forget_pii` erasure, and query-option edge cases. This harness drives a
101
+ * **family of randomized, seeded workloads** — commits (some PII-bearing),
102
+ * inline + windowed truncates, subscriptions, and the full lease lifecycle
103
+ * (`claim` / `ack` / `defer` / `block` / `unblock` / `reset` /
104
+ * `prioritize`) — against every store in `options.stores`, then asserts
58
105
  * their **normalized** outputs are byte-for-byte identical for every
59
106
  * workload.
60
107
  *
@@ -65,10 +112,12 @@ export type StoreDifferentialTckOptions = {
65
112
  *
66
113
  * Normalization drops only the fields that legitimately differ between
67
114
  * stores (absolute event ids, `created` timestamps, correlation/causation
68
- * uuids) and keeps everything that defines correctness (stream, version,
69
- * name, data, and emission order). A one-adapter `with_snaps` regression —
70
- * the canonical failure mode surfaces as a diff against the in-memory
71
- * reference, with the offending seed in the describe block.
115
+ * uuids, lease holder UUIDs, and wall-clock lease expiries) and keeps
116
+ * everything that defines correctness (stream, version, name, data,
117
+ * emission order, and the durable subscription state a lease op leaves
118
+ * behind: watermark, retry, blocked, error, priority, lane). Stream names
119
+ * carry mixed case so a case-insensitive pattern filter (#1197) surfaces as
120
+ * a diff.
72
121
  *
73
122
  * Wire it with the in-memory store as the reference and one or more
74
123
  * durable adapters as comparands:
@@ -1 +1 @@
1
- {"version":3,"file":"store-differential-tck.d.ts","sourceRoot":"","sources":["../../src/store-differential-tck.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAsB,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAYtE;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;CAChD,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,2BAA2B,GAAG;IACxC,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;IAClD;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAmLF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,eAAO,MAAM,uBAAuB,GAClC,SAAS,2BAA2B,KACnC,IAwKF,CAAC"}
1
+ {"version":3,"file":"store-differential-tck.d.ts","sourceRoot":"","sources":["../../src/store-differential-tck.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAKV,KAAK,EACN,MAAM,sBAAsB,CAAC;AAY9B;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;CAChD,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;;;OAMG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;CACpC,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,2BAA2B,GAAG;IACxC,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;IAClD;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;OAOG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,iBAAiB,CAAC;CACnC,CAAC;AAgfF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,eAAO,MAAM,uBAAuB,GAClC,SAAS,2BAA2B,KACnC,IA0RF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"store-tck.d.ts","sourceRoot":"","sources":["../../src/store-tck.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAQV,KAAK,EAEN,MAAM,sBAAsB,CAAC;AAc9B;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;;OAOG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC;;;;;;;;;OASG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IACpC;;;;;;;;OAQG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAClC;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,oBAAoB,CAAC,EAAE,OAAO,CAAC;IACxC;;;;;;;;OAQG;IACH,QAAQ,CAAC,gCAAgC,CAAC,EAAE,OAAO,CAAC;CACrD,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC/C;;OAEG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,iBAAiB,CAAC;CAC3C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,eAAO,MAAM,WAAW,GAAI,SAAS,eAAe,KAAG,IAykHtD,CAAC"}
1
+ {"version":3,"file":"store-tck.d.ts","sourceRoot":"","sources":["../../src/store-tck.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAQV,KAAK,EAEN,MAAM,sBAAsB,CAAC;AAc9B;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;;OAOG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC;;;;;;;;;OASG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IACpC;;;;;;;;OAQG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAClC;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,oBAAoB,CAAC,EAAE,OAAO,CAAC;IACxC;;;;;;;;OAQG;IACH,QAAQ,CAAC,gCAAgC,CAAC,EAAE,OAAO,CAAC;CACrD,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC/C;;OAEG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,iBAAiB,CAAC;CAC3C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,eAAO,MAAM,WAAW,GAAI,SAAS,eAAe,KAAG,IAurHtD,CAAC"}
package/dist/index.cjs CHANGED
@@ -448,20 +448,28 @@ var mulberry322 = (seed) => {
448
448
  };
449
449
  };
450
450
  var normalize_event = (e) => ({
451
+ stream: e.stream,
452
+ version: e.version,
453
+ name: e.name,
454
+ data: e.data,
455
+ pii: e.pii != null
456
+ });
457
+ var normalize_identity = (e) => ({
451
458
  stream: e.stream,
452
459
  version: e.version,
453
460
  name: e.name,
454
461
  data: e.data
455
462
  });
456
- var build_plan = (seed, stream_count) => {
463
+ var build_plan = (seed, stream_count, pii_isolation) => {
457
464
  const rng = mulberry322(seed);
458
465
  const tag = uid();
459
- const event_prefix = `diff-${tag}-evt-`;
460
- const sub_prefix = `diff-${tag}-sub-`;
466
+ const event_prefix = `Diff-${tag}-Evt-`;
467
+ const sub_prefix = `Diff-${tag}-Sub-`;
461
468
  const event_streams = Array.from(
462
469
  { length: stream_count },
463
- (_, i) => `${event_prefix}${i}`
470
+ (_, i) => `${event_prefix}S${i}`
464
471
  );
472
+ const correlation = `corr-${tag}`;
465
473
  let type_cursor = 0;
466
474
  const next_msg = () => {
467
475
  const kind = type_cursor++ % 3;
@@ -470,65 +478,246 @@ var build_plan = (seed, stream_count) => {
470
478
  };
471
479
  const batch = () => Array.from({ length: 1 + Math.floor(rng() * 3) }, next_msg);
472
480
  const pick_stream = () => event_streams[Math.floor(rng() * event_streams.length)];
473
- const ops = [];
474
- for (const stream of event_streams)
475
- ops.push({ t: "commit", stream, msgs: batch() });
476
- const middle = 8 + Math.floor(rng() * 16);
477
- for (let i = 0; i < middle; i++) {
478
- const stream = pick_stream();
479
- const kind = Math.floor(rng() * 3);
480
- if (kind === 0) ops.push({ t: "commit", stream, msgs: batch() });
481
- else if (kind === 1)
482
- ops.push({ t: "snapshot", stream, count: Math.floor(rng() * 1e3) });
483
- else ops.push({ t: "truncate", stream, count: Math.floor(rng() * 1e3) });
484
- }
485
- for (const stream of event_streams)
486
- ops.push({ t: "commit", stream, msgs: batch() });
487
481
  const lanes = ["default", "slow", "fast"];
488
- const subs = event_streams.map((source, i) => ({
489
- stream: `${sub_prefix}${i}`,
482
+ const lease_prefix = `Diff-${tag}-Src-`;
483
+ const lease_sources = event_streams.map((_, i) => `${lease_prefix}S${i}`);
484
+ const subs = lease_sources.map((source, i) => ({
485
+ stream: `${sub_prefix}S${i}`,
490
486
  source,
491
487
  lane: lanes[i % lanes.length],
492
488
  priority: i % 4
493
489
  }));
494
- return { event_prefix, event_streams, sub_prefix, ops, subs };
490
+ const pick_sub = () => Math.floor(rng() * subs.length);
491
+ const ops = [];
492
+ const pii_stream = event_streams[0];
493
+ event_streams.forEach((stream, i) => {
494
+ ops.push({
495
+ t: "commit",
496
+ stream,
497
+ msgs: batch(),
498
+ pii: pii_isolation && i === 0 ? { email: `u-${tag}@example.com`, name: "Ursula" } : void 0
499
+ });
500
+ });
501
+ for (const src of lease_sources)
502
+ ops.push({ t: "commit", stream: src, msgs: [inc(1)] });
503
+ const middle = 12 + Math.floor(rng() * 20);
504
+ for (let i = 0; i < middle; i++) {
505
+ const kind = Math.floor(rng() * 10);
506
+ if (kind === 0) {
507
+ ops.push({ t: "commit", stream: pick_stream(), msgs: batch() });
508
+ } else if (kind === 1) {
509
+ ops.push({
510
+ t: "snapshot",
511
+ stream: pick_stream(),
512
+ count: Math.floor(rng() * 1e3)
513
+ });
514
+ } else if (kind === 2) {
515
+ ops.push({
516
+ t: "truncate",
517
+ stream: pick_stream(),
518
+ count: Math.floor(rng() * 1e3)
519
+ });
520
+ } else if (kind === 3) {
521
+ ops.push({
522
+ t: "truncate_windowed",
523
+ stream: pick_stream(),
524
+ max_id: rng() < 0.5
525
+ });
526
+ } else if (kind === 4) {
527
+ ops.push({ t: "claim", millis: 1e5 });
528
+ } else if (kind === 5) {
529
+ ops.push({ t: "ack", sub: pick_sub() });
530
+ } else if (kind === 6) {
531
+ ops.push({
532
+ t: "defer",
533
+ sub: pick_sub(),
534
+ due: 41024448e5 + Math.floor(rng() * 1e3)
535
+ });
536
+ } else if (kind === 7) {
537
+ ops.push({
538
+ t: "block",
539
+ sub: pick_sub(),
540
+ error: `boom-${Math.floor(rng() * 100)}`
541
+ });
542
+ } else if (kind === 8) {
543
+ ops.push({ t: "unblock", sub: pick_sub() });
544
+ } else {
545
+ if (rng() < 0.5) ops.push({ t: "reset_sub", sub: pick_sub() });
546
+ else
547
+ ops.push({
548
+ t: "prioritize",
549
+ sub: pick_sub(),
550
+ priority: Math.floor(rng() * 10)
551
+ });
552
+ }
553
+ }
554
+ const filter_name = "Incremented";
555
+ for (const stream of event_streams)
556
+ ops.push({ t: "commit", stream, msgs: [inc(1)] });
557
+ const query_cases = [
558
+ {
559
+ label: "names filter (single event name)",
560
+ query: { stream: `^${event_prefix}`, names: [filter_name] }
561
+ },
562
+ {
563
+ label: "correlation filter",
564
+ query: { stream: `^${event_prefix}`, correlation }
565
+ },
566
+ {
567
+ label: "limit bound",
568
+ query: { stream: `^${event_prefix}`, limit: 3 }
569
+ },
570
+ {
571
+ label: "backward + limit",
572
+ query: { stream: `^${event_prefix}`, backward: true, limit: 4 }
573
+ },
574
+ // Absolute `id` bounds (`after`/`before` with positive ids) are
575
+ // deliberately absent: event ids are the canonical "legitimately
576
+ // differs between adapters" field (see `normalize_event`), and full
577
+ // truncates renumber them differently per store, so a positive id
578
+ // bound selects a different absolute window on each adapter. The
579
+ // gated falsy-zero cases below compare only the *guard* behavior
580
+ // (filter applied → subset, vs filter dropped → all), which stays a
581
+ // well-defined divergence regardless of the concrete id values.
582
+ // #1199 edge inputs — opposite semantics across adapters today.
583
+ {
584
+ label: "empty names filter",
585
+ query: { stream: `^${event_prefix}`, names: [] },
586
+ gated: "queryEdgeInputs"
587
+ },
588
+ {
589
+ label: "before: 0 (falsy-zero guard)",
590
+ query: { stream: `^${event_prefix}`, before: 0 },
591
+ gated: "queryEdgeInputs"
592
+ },
593
+ {
594
+ label: "backward + after: 0 (falsy-zero guard)",
595
+ query: { stream: `^${event_prefix}`, backward: true, after: 0 },
596
+ gated: "queryEdgeInputs"
597
+ }
598
+ ];
599
+ return {
600
+ event_prefix,
601
+ event_streams,
602
+ sub_prefix,
603
+ ops,
604
+ subs,
605
+ correlation,
606
+ pii_stream,
607
+ query_cases,
608
+ filter_name
609
+ };
495
610
  };
496
- var apply_plan = async (store, plan) => {
611
+ var apply_plan = async (store, plan, pii_isolation) => {
612
+ await store.subscribe(
613
+ plan.subs.map((s) => ({
614
+ stream: s.stream,
615
+ source: s.source,
616
+ lane: s.lane,
617
+ priority: s.priority
618
+ }))
619
+ );
620
+ const held = /* @__PURE__ */ new Map();
621
+ const owned = new Set(plan.subs.map((s) => s.stream));
497
622
  for (const op of plan.ops) {
498
623
  if (op.t === "commit") {
624
+ const msgs = op.pii && pii_isolation ? op.msgs.map((m, i) => i === 0 ? { ...m, pii: op.pii } : m) : op.msgs;
499
625
  await store.commit(
500
626
  op.stream,
501
- op.msgs,
502
- make_meta({ stream: op.stream })
627
+ msgs,
628
+ make_meta({ stream: op.stream, correlation: plan.correlation })
503
629
  );
504
630
  } else if (op.t === "snapshot") {
505
631
  await store.commit(
506
632
  op.stream,
507
633
  [{ name: import_act.SNAP_EVENT, data: { count: op.count } }],
508
- make_meta({ stream: op.stream })
634
+ make_meta({ stream: op.stream, correlation: plan.correlation })
509
635
  );
510
- } else {
636
+ } else if (op.t === "truncate") {
511
637
  await store.truncate([
512
638
  { stream: op.stream, snapshot: { count: op.count } }
513
639
  ]);
640
+ } else if (op.t === "truncate_windowed") {
641
+ await store.commit(
642
+ op.stream,
643
+ [{ name: import_act.SNAP_EVENT, data: { count: 1 } }],
644
+ make_meta({ stream: op.stream, correlation: plan.correlation })
645
+ );
646
+ await store.truncate([
647
+ {
648
+ stream: op.stream,
649
+ before: new Date(Date.now() + 6e4),
650
+ // A wide cap that still fits a 32-bit event-id column, so it
651
+ // never excludes the boundary snapshot on any adapter.
652
+ ...op.max_id ? { max_id: 2147483647 } : {}
653
+ }
654
+ ]);
655
+ } else if (op.t === "claim") {
656
+ const by = uid();
657
+ const leased = await store.claim(1e5, 1e5, by, op.millis);
658
+ for (const l of leased) if (owned.has(l.stream)) held.set(l.stream, by);
659
+ } else if (op.t === "ack") {
660
+ const stream = plan.subs[op.sub].stream;
661
+ const by = held.get(stream);
662
+ if (by) {
663
+ await store.ack([{ stream, at: 0, by, retry: 0, lagging: true }]);
664
+ held.delete(stream);
665
+ }
666
+ } else if (op.t === "defer") {
667
+ const stream = plan.subs[op.sub].stream;
668
+ const by = held.get(stream);
669
+ if (by) {
670
+ const lease = {
671
+ stream,
672
+ at: 0,
673
+ by,
674
+ retry: 0,
675
+ lagging: true,
676
+ due: op.due
677
+ };
678
+ await store.ack([lease]);
679
+ held.delete(stream);
680
+ }
681
+ } else if (op.t === "block") {
682
+ const stream = plan.subs[op.sub].stream;
683
+ const by = held.get(stream);
684
+ if (by) {
685
+ const lease = {
686
+ stream,
687
+ at: 0,
688
+ by,
689
+ retry: 0,
690
+ lagging: true,
691
+ error: op.error
692
+ };
693
+ await store.block([lease]);
694
+ held.delete(stream);
695
+ }
696
+ } else if (op.t === "unblock") {
697
+ const stream = plan.subs[op.sub].stream;
698
+ await store.unblock([stream]);
699
+ held.delete(stream);
700
+ } else if (op.t === "reset_sub") {
701
+ const stream = plan.subs[op.sub].stream;
702
+ await store.reset([stream]);
703
+ held.delete(stream);
704
+ } else {
705
+ await store.prioritize(
706
+ { stream: plan.subs[op.sub].stream, stream_exact: true },
707
+ op.priority
708
+ );
514
709
  }
515
710
  }
516
- await store.subscribe(
517
- plan.subs.map((s) => ({
518
- stream: s.stream,
519
- source: s.source,
520
- lane: s.lane,
521
- priority: s.priority
522
- }))
523
- );
524
711
  };
525
712
  var runStoreDifferentialTck = (options) => {
526
713
  (0, import_vitest6.describe)(`TCK / Store differential / ${options.name}`, () => {
527
714
  const base_seed = options.seed ?? 2759;
528
715
  const stream_count = options.streams ?? 4;
716
+ const pii_isolation = options.piiIsolation ?? true;
717
+ const skip = { ...options.skip };
529
718
  const plans = Array.from(
530
719
  { length: options.runs ?? 8 },
531
- (_, r) => build_plan(base_seed + r, stream_count)
720
+ (_, r) => build_plan(base_seed + r, stream_count, pii_isolation)
532
721
  );
533
722
  const live = [];
534
723
  (0, import_vitest6.beforeAll)(async () => {
@@ -536,7 +725,7 @@ var runStoreDifferentialTck = (options) => {
536
725
  const store = await spec.factory();
537
726
  await store.drop();
538
727
  await store.seed();
539
- for (const plan of plans) await apply_plan(store, plan);
728
+ for (const plan of plans) await apply_plan(store, plan, pii_isolation);
540
729
  live.push({ name: spec.name, store });
541
730
  }
542
731
  });
@@ -600,6 +789,21 @@ var runStoreDifferentialTck = (options) => {
600
789
  return by_stream;
601
790
  });
602
791
  });
792
+ plan.query_cases.forEach((qc) => {
793
+ const gated = qc.gated && skip[qc.gated];
794
+ import_vitest6.it.skipIf(gated)(
795
+ `yields identical events under ${qc.label}`,
796
+ async () => {
797
+ await assert_identical(`query / ${qc.label}`, async (store) => {
798
+ const out = [];
799
+ await store.query((e) => {
800
+ out.push(normalize_event(e));
801
+ }, qc.query);
802
+ return out;
803
+ });
804
+ }
805
+ );
806
+ });
603
807
  (0, import_vitest6.it)("yields identical query_stats output (head/tail/count/names)", async () => {
604
808
  await assert_identical("query_stats", async (store) => {
605
809
  const stats = await store.query_stats(
@@ -610,8 +814,8 @@ var runStoreDifferentialTck = (options) => {
610
814
  const content = {};
611
815
  for (const [stream, s] of stats) {
612
816
  content[stream] = {
613
- head: normalize_event(s.head),
614
- tail: normalize_event(
817
+ head: normalize_identity(s.head),
818
+ tail: normalize_identity(
615
819
  s.tail
616
820
  ),
617
821
  count: s.count,
@@ -621,7 +825,7 @@ var runStoreDifferentialTck = (options) => {
621
825
  return { keys, content };
622
826
  });
623
827
  });
624
- (0, import_vitest6.it)("yields identical query_streams output", async () => {
828
+ (0, import_vitest6.it)("yields identical query_streams output (incl. lease-lifecycle state)", async () => {
625
829
  await assert_identical("query_streams", async (store) => {
626
830
  const rows = [];
627
831
  const { count } = await store.query_streams(
@@ -632,7 +836,10 @@ var runStoreDifferentialTck = (options) => {
632
836
  at: p.at,
633
837
  blocked: p.blocked,
634
838
  priority: p.priority,
635
- lane: p.lane
839
+ lane: p.lane,
840
+ retry: p.retry,
841
+ error: p.error,
842
+ leased: p.leased_by != null
636
843
  });
637
844
  },
638
845
  { stream: `^${plan.sub_prefix}`, limit: 1e3 }
@@ -641,6 +848,60 @@ var runStoreDifferentialTck = (options) => {
641
848
  return { count, rows };
642
849
  });
643
850
  });
851
+ import_vitest6.it.skipIf(skip.caseInsensitivePatterns)(
852
+ "matches case-sensitively under a lower-cased pattern filter",
853
+ async () => {
854
+ await assert_identical(
855
+ "query_streams lower-cased pattern",
856
+ async (store) => {
857
+ const collect_streams = async (pattern) => {
858
+ const streams = [];
859
+ await store.query_streams(
860
+ (p) => {
861
+ streams.push(p.stream);
862
+ },
863
+ { stream: pattern, limit: 1e3 }
864
+ );
865
+ streams.sort((a, b) => a.localeCompare(b));
866
+ return streams;
867
+ };
868
+ const matched = await collect_streams(`^${plan.sub_prefix}S`);
869
+ const lowered = await collect_streams(
870
+ `^${plan.sub_prefix.toLowerCase()}s`
871
+ );
872
+ return { matched, lowered };
873
+ }
874
+ );
875
+ }
876
+ );
877
+ (0, import_vitest6.it)("yields identical events under an exact mixed-case stream lookup", async () => {
878
+ await assert_identical("query exact mixed-case", async (store) => {
879
+ const out = [];
880
+ await store.query(
881
+ (e) => {
882
+ out.push(normalize_event(e));
883
+ },
884
+ { stream: plan.event_streams[0], stream_exact: true }
885
+ );
886
+ return out;
887
+ });
888
+ });
889
+ import_vitest6.it.skipIf(!pii_isolation)(
890
+ "erases pii identically via forget_pii",
891
+ async () => {
892
+ await assert_identical("forget_pii erase", async (store) => {
893
+ const erased = await store.forget_pii(plan.pii_stream);
894
+ const after = [];
895
+ await store.query(
896
+ (e) => {
897
+ after.push(normalize_event(e));
898
+ },
899
+ { stream: plan.pii_stream, stream_exact: true }
900
+ );
901
+ return { erased, after };
902
+ });
903
+ }
904
+ );
644
905
  });
645
906
  });
646
907
  });
@@ -1054,6 +1315,54 @@ var runStoreTck = (options) => {
1054
1315
  committed.slice(0, -1).map((c) => c.id)
1055
1316
  );
1056
1317
  });
1318
+ (0, import_vitest9.it)("names:[] matches no events", async () => {
1319
+ const s = `q-names-empty-${uid()}`;
1320
+ await store.commit(
1321
+ s,
1322
+ [inc(1), dec(1)],
1323
+ make_meta({ stream: s })
1324
+ );
1325
+ const none = await collect(store, {
1326
+ stream: s,
1327
+ stream_exact: true,
1328
+ names: []
1329
+ });
1330
+ (0, import_vitest9.expect)(none).toHaveLength(0);
1331
+ const all = await collect(store, { stream: s, stream_exact: true });
1332
+ (0, import_vitest9.expect)(all).toHaveLength(2);
1333
+ });
1334
+ (0, import_vitest9.it)("after:0 and before:0 are honored as id bounds", async () => {
1335
+ const s = `q-zero-bound-${uid()}`;
1336
+ await store.commit(
1337
+ s,
1338
+ [inc(1), inc(2), inc(3)],
1339
+ make_meta({ stream: s })
1340
+ );
1341
+ (0, import_vitest9.expect)(
1342
+ await collect(store, { stream: s, stream_exact: true, before: 0 })
1343
+ ).toHaveLength(0);
1344
+ (0, import_vitest9.expect)(
1345
+ await collect(store, {
1346
+ stream: s,
1347
+ stream_exact: true,
1348
+ before: 0,
1349
+ backward: true
1350
+ })
1351
+ ).toHaveLength(0);
1352
+ const forward = await collect(store, {
1353
+ stream: s,
1354
+ stream_exact: true,
1355
+ after: 0
1356
+ });
1357
+ (0, import_vitest9.expect)(forward.every((e) => e.id > 0)).toBe(true);
1358
+ const backward = await collect(store, {
1359
+ stream: s,
1360
+ stream_exact: true,
1361
+ after: 0,
1362
+ backward: true
1363
+ });
1364
+ (0, import_vitest9.expect)(backward.every((e) => e.id > 0)).toBe(true);
1365
+ });
1057
1366
  (0, import_vitest9.it)("created_after/created_before filter by timestamp", async () => {
1058
1367
  const s = `q-ts-${uid()}`;
1059
1368
  const committed = await store.commit(
@@ -1292,6 +1601,34 @@ var runStoreTck = (options) => {
1292
1601
  if (error !== void 0) (0, import_vitest9.expect)(error).toBeInstanceOf(import_act2.ValidationError);
1293
1602
  else (0, import_vitest9.expect)(count).toBe(1);
1294
1603
  });
1604
+ (0, import_vitest9.it)("stream filters match case-sensitively (query)", async () => {
1605
+ const tag = uid();
1606
+ const lower = `order-${tag}`;
1607
+ const upper = `Order-${tag}`;
1608
+ await seed_streams([lower, upper]);
1609
+ (0, import_vitest9.expect)(await streams_matching(`^order-${tag}`)).toEqual([lower]);
1610
+ (0, import_vitest9.expect)(await streams_matching(`^Order-${tag}$`)).toEqual([upper]);
1611
+ (0, import_vitest9.expect)(await streams_matching(`order-${tag}`)).toEqual([lower]);
1612
+ });
1613
+ (0, import_vitest9.it)("stream filters match case-sensitively (position filters)", async () => {
1614
+ const tag = uid();
1615
+ const lower = `csp-order-${tag}`;
1616
+ const upper = `csp-Order-${tag}`;
1617
+ await store.subscribe([{ stream: lower }, { stream: upper }]);
1618
+ const got = [];
1619
+ await store.query_streams((p) => got.push(p.stream), {
1620
+ stream: `^csp-order-${tag}`
1621
+ });
1622
+ (0, import_vitest9.expect)(got).toEqual([lower]);
1623
+ });
1624
+ (0, import_vitest9.it)("bulk stream ops match case-sensitively", async () => {
1625
+ const tag = uid();
1626
+ const lower = `bcs-order-${tag}`;
1627
+ const upper = `bcs-Order-${tag}`;
1628
+ await store.subscribe([{ stream: lower }, { stream: upper }]);
1629
+ const count = await store.reset({ stream: `^bcs-order-${tag}` });
1630
+ (0, import_vitest9.expect)(count).toBe(1);
1631
+ });
1295
1632
  });
1296
1633
  (0, import_vitest9.describe)("subscribe + claim + ack", () => {
1297
1634
  (0, import_vitest9.it)("subscribes new streams and is idempotent on repeat", async () => {