@rotorsoft/act-tck 1.26.8 → 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.
package/dist/index.js CHANGED
@@ -394,20 +394,28 @@ var mulberry322 = (seed) => {
394
394
  };
395
395
  };
396
396
  var normalize_event = (e) => ({
397
+ stream: e.stream,
398
+ version: e.version,
399
+ name: e.name,
400
+ data: e.data,
401
+ pii: e.pii != null
402
+ });
403
+ var normalize_identity = (e) => ({
397
404
  stream: e.stream,
398
405
  version: e.version,
399
406
  name: e.name,
400
407
  data: e.data
401
408
  });
402
- var build_plan = (seed, stream_count) => {
409
+ var build_plan = (seed, stream_count, pii_isolation) => {
403
410
  const rng = mulberry322(seed);
404
411
  const tag = uid();
405
- const event_prefix = `diff-${tag}-evt-`;
406
- const sub_prefix = `diff-${tag}-sub-`;
412
+ const event_prefix = `Diff-${tag}-Evt-`;
413
+ const sub_prefix = `Diff-${tag}-Sub-`;
407
414
  const event_streams = Array.from(
408
415
  { length: stream_count },
409
- (_, i) => `${event_prefix}${i}`
416
+ (_, i) => `${event_prefix}S${i}`
410
417
  );
418
+ const correlation = `corr-${tag}`;
411
419
  let type_cursor = 0;
412
420
  const next_msg = () => {
413
421
  const kind = type_cursor++ % 3;
@@ -416,65 +424,246 @@ var build_plan = (seed, stream_count) => {
416
424
  };
417
425
  const batch = () => Array.from({ length: 1 + Math.floor(rng() * 3) }, next_msg);
418
426
  const pick_stream = () => event_streams[Math.floor(rng() * event_streams.length)];
419
- const ops = [];
420
- for (const stream of event_streams)
421
- ops.push({ t: "commit", stream, msgs: batch() });
422
- const middle = 8 + Math.floor(rng() * 16);
423
- for (let i = 0; i < middle; i++) {
424
- const stream = pick_stream();
425
- const kind = Math.floor(rng() * 3);
426
- if (kind === 0) ops.push({ t: "commit", stream, msgs: batch() });
427
- else if (kind === 1)
428
- ops.push({ t: "snapshot", stream, count: Math.floor(rng() * 1e3) });
429
- else ops.push({ t: "truncate", stream, count: Math.floor(rng() * 1e3) });
430
- }
431
- for (const stream of event_streams)
432
- ops.push({ t: "commit", stream, msgs: batch() });
433
427
  const lanes = ["default", "slow", "fast"];
434
- const subs = event_streams.map((source, i) => ({
435
- stream: `${sub_prefix}${i}`,
428
+ const lease_prefix = `Diff-${tag}-Src-`;
429
+ const lease_sources = event_streams.map((_, i) => `${lease_prefix}S${i}`);
430
+ const subs = lease_sources.map((source, i) => ({
431
+ stream: `${sub_prefix}S${i}`,
436
432
  source,
437
433
  lane: lanes[i % lanes.length],
438
434
  priority: i % 4
439
435
  }));
440
- return { event_prefix, event_streams, sub_prefix, ops, subs };
436
+ const pick_sub = () => Math.floor(rng() * subs.length);
437
+ const ops = [];
438
+ const pii_stream = event_streams[0];
439
+ event_streams.forEach((stream, i) => {
440
+ ops.push({
441
+ t: "commit",
442
+ stream,
443
+ msgs: batch(),
444
+ pii: pii_isolation && i === 0 ? { email: `u-${tag}@example.com`, name: "Ursula" } : void 0
445
+ });
446
+ });
447
+ for (const src of lease_sources)
448
+ ops.push({ t: "commit", stream: src, msgs: [inc(1)] });
449
+ const middle = 12 + Math.floor(rng() * 20);
450
+ for (let i = 0; i < middle; i++) {
451
+ const kind = Math.floor(rng() * 10);
452
+ if (kind === 0) {
453
+ ops.push({ t: "commit", stream: pick_stream(), msgs: batch() });
454
+ } else if (kind === 1) {
455
+ ops.push({
456
+ t: "snapshot",
457
+ stream: pick_stream(),
458
+ count: Math.floor(rng() * 1e3)
459
+ });
460
+ } else if (kind === 2) {
461
+ ops.push({
462
+ t: "truncate",
463
+ stream: pick_stream(),
464
+ count: Math.floor(rng() * 1e3)
465
+ });
466
+ } else if (kind === 3) {
467
+ ops.push({
468
+ t: "truncate_windowed",
469
+ stream: pick_stream(),
470
+ max_id: rng() < 0.5
471
+ });
472
+ } else if (kind === 4) {
473
+ ops.push({ t: "claim", millis: 1e5 });
474
+ } else if (kind === 5) {
475
+ ops.push({ t: "ack", sub: pick_sub() });
476
+ } else if (kind === 6) {
477
+ ops.push({
478
+ t: "defer",
479
+ sub: pick_sub(),
480
+ due: 41024448e5 + Math.floor(rng() * 1e3)
481
+ });
482
+ } else if (kind === 7) {
483
+ ops.push({
484
+ t: "block",
485
+ sub: pick_sub(),
486
+ error: `boom-${Math.floor(rng() * 100)}`
487
+ });
488
+ } else if (kind === 8) {
489
+ ops.push({ t: "unblock", sub: pick_sub() });
490
+ } else {
491
+ if (rng() < 0.5) ops.push({ t: "reset_sub", sub: pick_sub() });
492
+ else
493
+ ops.push({
494
+ t: "prioritize",
495
+ sub: pick_sub(),
496
+ priority: Math.floor(rng() * 10)
497
+ });
498
+ }
499
+ }
500
+ const filter_name = "Incremented";
501
+ for (const stream of event_streams)
502
+ ops.push({ t: "commit", stream, msgs: [inc(1)] });
503
+ const query_cases = [
504
+ {
505
+ label: "names filter (single event name)",
506
+ query: { stream: `^${event_prefix}`, names: [filter_name] }
507
+ },
508
+ {
509
+ label: "correlation filter",
510
+ query: { stream: `^${event_prefix}`, correlation }
511
+ },
512
+ {
513
+ label: "limit bound",
514
+ query: { stream: `^${event_prefix}`, limit: 3 }
515
+ },
516
+ {
517
+ label: "backward + limit",
518
+ query: { stream: `^${event_prefix}`, backward: true, limit: 4 }
519
+ },
520
+ // Absolute `id` bounds (`after`/`before` with positive ids) are
521
+ // deliberately absent: event ids are the canonical "legitimately
522
+ // differs between adapters" field (see `normalize_event`), and full
523
+ // truncates renumber them differently per store, so a positive id
524
+ // bound selects a different absolute window on each adapter. The
525
+ // gated falsy-zero cases below compare only the *guard* behavior
526
+ // (filter applied → subset, vs filter dropped → all), which stays a
527
+ // well-defined divergence regardless of the concrete id values.
528
+ // #1199 edge inputs — opposite semantics across adapters today.
529
+ {
530
+ label: "empty names filter",
531
+ query: { stream: `^${event_prefix}`, names: [] },
532
+ gated: "queryEdgeInputs"
533
+ },
534
+ {
535
+ label: "before: 0 (falsy-zero guard)",
536
+ query: { stream: `^${event_prefix}`, before: 0 },
537
+ gated: "queryEdgeInputs"
538
+ },
539
+ {
540
+ label: "backward + after: 0 (falsy-zero guard)",
541
+ query: { stream: `^${event_prefix}`, backward: true, after: 0 },
542
+ gated: "queryEdgeInputs"
543
+ }
544
+ ];
545
+ return {
546
+ event_prefix,
547
+ event_streams,
548
+ sub_prefix,
549
+ ops,
550
+ subs,
551
+ correlation,
552
+ pii_stream,
553
+ query_cases,
554
+ filter_name
555
+ };
441
556
  };
442
- var apply_plan = async (store, plan) => {
557
+ var apply_plan = async (store, plan, pii_isolation) => {
558
+ await store.subscribe(
559
+ plan.subs.map((s) => ({
560
+ stream: s.stream,
561
+ source: s.source,
562
+ lane: s.lane,
563
+ priority: s.priority
564
+ }))
565
+ );
566
+ const held = /* @__PURE__ */ new Map();
567
+ const owned = new Set(plan.subs.map((s) => s.stream));
443
568
  for (const op of plan.ops) {
444
569
  if (op.t === "commit") {
570
+ const msgs = op.pii && pii_isolation ? op.msgs.map((m, i) => i === 0 ? { ...m, pii: op.pii } : m) : op.msgs;
445
571
  await store.commit(
446
572
  op.stream,
447
- op.msgs,
448
- make_meta({ stream: op.stream })
573
+ msgs,
574
+ make_meta({ stream: op.stream, correlation: plan.correlation })
449
575
  );
450
576
  } else if (op.t === "snapshot") {
451
577
  await store.commit(
452
578
  op.stream,
453
579
  [{ name: SNAP_EVENT, data: { count: op.count } }],
454
- make_meta({ stream: op.stream })
580
+ make_meta({ stream: op.stream, correlation: plan.correlation })
455
581
  );
456
- } else {
582
+ } else if (op.t === "truncate") {
457
583
  await store.truncate([
458
584
  { stream: op.stream, snapshot: { count: op.count } }
459
585
  ]);
586
+ } else if (op.t === "truncate_windowed") {
587
+ await store.commit(
588
+ op.stream,
589
+ [{ name: SNAP_EVENT, data: { count: 1 } }],
590
+ make_meta({ stream: op.stream, correlation: plan.correlation })
591
+ );
592
+ await store.truncate([
593
+ {
594
+ stream: op.stream,
595
+ before: new Date(Date.now() + 6e4),
596
+ // A wide cap that still fits a 32-bit event-id column, so it
597
+ // never excludes the boundary snapshot on any adapter.
598
+ ...op.max_id ? { max_id: 2147483647 } : {}
599
+ }
600
+ ]);
601
+ } else if (op.t === "claim") {
602
+ const by = uid();
603
+ const leased = await store.claim(1e5, 1e5, by, op.millis);
604
+ for (const l of leased) if (owned.has(l.stream)) held.set(l.stream, by);
605
+ } else if (op.t === "ack") {
606
+ const stream = plan.subs[op.sub].stream;
607
+ const by = held.get(stream);
608
+ if (by) {
609
+ await store.ack([{ stream, at: 0, by, retry: 0, lagging: true }]);
610
+ held.delete(stream);
611
+ }
612
+ } else if (op.t === "defer") {
613
+ const stream = plan.subs[op.sub].stream;
614
+ const by = held.get(stream);
615
+ if (by) {
616
+ const lease = {
617
+ stream,
618
+ at: 0,
619
+ by,
620
+ retry: 0,
621
+ lagging: true,
622
+ due: op.due
623
+ };
624
+ await store.ack([lease]);
625
+ held.delete(stream);
626
+ }
627
+ } else if (op.t === "block") {
628
+ const stream = plan.subs[op.sub].stream;
629
+ const by = held.get(stream);
630
+ if (by) {
631
+ const lease = {
632
+ stream,
633
+ at: 0,
634
+ by,
635
+ retry: 0,
636
+ lagging: true,
637
+ error: op.error
638
+ };
639
+ await store.block([lease]);
640
+ held.delete(stream);
641
+ }
642
+ } else if (op.t === "unblock") {
643
+ const stream = plan.subs[op.sub].stream;
644
+ await store.unblock([stream]);
645
+ held.delete(stream);
646
+ } else if (op.t === "reset_sub") {
647
+ const stream = plan.subs[op.sub].stream;
648
+ await store.reset([stream]);
649
+ held.delete(stream);
650
+ } else {
651
+ await store.prioritize(
652
+ { stream: plan.subs[op.sub].stream, stream_exact: true },
653
+ op.priority
654
+ );
460
655
  }
461
656
  }
462
- await store.subscribe(
463
- plan.subs.map((s) => ({
464
- stream: s.stream,
465
- source: s.source,
466
- lane: s.lane,
467
- priority: s.priority
468
- }))
469
- );
470
657
  };
471
658
  var runStoreDifferentialTck = (options) => {
472
659
  describe6(`TCK / Store differential / ${options.name}`, () => {
473
660
  const base_seed = options.seed ?? 2759;
474
661
  const stream_count = options.streams ?? 4;
662
+ const pii_isolation = options.piiIsolation ?? true;
663
+ const skip = { ...options.skip };
475
664
  const plans = Array.from(
476
665
  { length: options.runs ?? 8 },
477
- (_, r) => build_plan(base_seed + r, stream_count)
666
+ (_, r) => build_plan(base_seed + r, stream_count, pii_isolation)
478
667
  );
479
668
  const live = [];
480
669
  beforeAll2(async () => {
@@ -482,7 +671,7 @@ var runStoreDifferentialTck = (options) => {
482
671
  const store = await spec.factory();
483
672
  await store.drop();
484
673
  await store.seed();
485
- for (const plan of plans) await apply_plan(store, plan);
674
+ for (const plan of plans) await apply_plan(store, plan, pii_isolation);
486
675
  live.push({ name: spec.name, store });
487
676
  }
488
677
  });
@@ -546,6 +735,21 @@ var runStoreDifferentialTck = (options) => {
546
735
  return by_stream;
547
736
  });
548
737
  });
738
+ plan.query_cases.forEach((qc) => {
739
+ const gated = qc.gated && skip[qc.gated];
740
+ it6.skipIf(gated)(
741
+ `yields identical events under ${qc.label}`,
742
+ async () => {
743
+ await assert_identical(`query / ${qc.label}`, async (store) => {
744
+ const out = [];
745
+ await store.query((e) => {
746
+ out.push(normalize_event(e));
747
+ }, qc.query);
748
+ return out;
749
+ });
750
+ }
751
+ );
752
+ });
549
753
  it6("yields identical query_stats output (head/tail/count/names)", async () => {
550
754
  await assert_identical("query_stats", async (store) => {
551
755
  const stats = await store.query_stats(
@@ -556,8 +760,8 @@ var runStoreDifferentialTck = (options) => {
556
760
  const content = {};
557
761
  for (const [stream, s] of stats) {
558
762
  content[stream] = {
559
- head: normalize_event(s.head),
560
- tail: normalize_event(
763
+ head: normalize_identity(s.head),
764
+ tail: normalize_identity(
561
765
  s.tail
562
766
  ),
563
767
  count: s.count,
@@ -567,7 +771,7 @@ var runStoreDifferentialTck = (options) => {
567
771
  return { keys, content };
568
772
  });
569
773
  });
570
- it6("yields identical query_streams output", async () => {
774
+ it6("yields identical query_streams output (incl. lease-lifecycle state)", async () => {
571
775
  await assert_identical("query_streams", async (store) => {
572
776
  const rows = [];
573
777
  const { count } = await store.query_streams(
@@ -578,7 +782,10 @@ var runStoreDifferentialTck = (options) => {
578
782
  at: p.at,
579
783
  blocked: p.blocked,
580
784
  priority: p.priority,
581
- lane: p.lane
785
+ lane: p.lane,
786
+ retry: p.retry,
787
+ error: p.error,
788
+ leased: p.leased_by != null
582
789
  });
583
790
  },
584
791
  { stream: `^${plan.sub_prefix}`, limit: 1e3 }
@@ -587,6 +794,60 @@ var runStoreDifferentialTck = (options) => {
587
794
  return { count, rows };
588
795
  });
589
796
  });
797
+ it6.skipIf(skip.caseInsensitivePatterns)(
798
+ "matches case-sensitively under a lower-cased pattern filter",
799
+ async () => {
800
+ await assert_identical(
801
+ "query_streams lower-cased pattern",
802
+ async (store) => {
803
+ const collect_streams = async (pattern) => {
804
+ const streams = [];
805
+ await store.query_streams(
806
+ (p) => {
807
+ streams.push(p.stream);
808
+ },
809
+ { stream: pattern, limit: 1e3 }
810
+ );
811
+ streams.sort((a, b) => a.localeCompare(b));
812
+ return streams;
813
+ };
814
+ const matched = await collect_streams(`^${plan.sub_prefix}S`);
815
+ const lowered = await collect_streams(
816
+ `^${plan.sub_prefix.toLowerCase()}s`
817
+ );
818
+ return { matched, lowered };
819
+ }
820
+ );
821
+ }
822
+ );
823
+ it6("yields identical events under an exact mixed-case stream lookup", async () => {
824
+ await assert_identical("query exact mixed-case", async (store) => {
825
+ const out = [];
826
+ await store.query(
827
+ (e) => {
828
+ out.push(normalize_event(e));
829
+ },
830
+ { stream: plan.event_streams[0], stream_exact: true }
831
+ );
832
+ return out;
833
+ });
834
+ });
835
+ it6.skipIf(!pii_isolation)(
836
+ "erases pii identically via forget_pii",
837
+ async () => {
838
+ await assert_identical("forget_pii erase", async (store) => {
839
+ const erased = await store.forget_pii(plan.pii_stream);
840
+ const after = [];
841
+ await store.query(
842
+ (e) => {
843
+ after.push(normalize_event(e));
844
+ },
845
+ { stream: plan.pii_stream, stream_exact: true }
846
+ );
847
+ return { erased, after };
848
+ });
849
+ }
850
+ );
590
851
  });
591
852
  });
592
853
  });
@@ -1007,6 +1268,54 @@ var runStoreTck = (options) => {
1007
1268
  committed.slice(0, -1).map((c) => c.id)
1008
1269
  );
1009
1270
  });
1271
+ it7("names:[] matches no events", async () => {
1272
+ const s = `q-names-empty-${uid()}`;
1273
+ await store.commit(
1274
+ s,
1275
+ [inc(1), dec(1)],
1276
+ make_meta({ stream: s })
1277
+ );
1278
+ const none = await collect(store, {
1279
+ stream: s,
1280
+ stream_exact: true,
1281
+ names: []
1282
+ });
1283
+ expect8(none).toHaveLength(0);
1284
+ const all = await collect(store, { stream: s, stream_exact: true });
1285
+ expect8(all).toHaveLength(2);
1286
+ });
1287
+ it7("after:0 and before:0 are honored as id bounds", async () => {
1288
+ const s = `q-zero-bound-${uid()}`;
1289
+ await store.commit(
1290
+ s,
1291
+ [inc(1), inc(2), inc(3)],
1292
+ make_meta({ stream: s })
1293
+ );
1294
+ expect8(
1295
+ await collect(store, { stream: s, stream_exact: true, before: 0 })
1296
+ ).toHaveLength(0);
1297
+ expect8(
1298
+ await collect(store, {
1299
+ stream: s,
1300
+ stream_exact: true,
1301
+ before: 0,
1302
+ backward: true
1303
+ })
1304
+ ).toHaveLength(0);
1305
+ const forward = await collect(store, {
1306
+ stream: s,
1307
+ stream_exact: true,
1308
+ after: 0
1309
+ });
1310
+ expect8(forward.every((e) => e.id > 0)).toBe(true);
1311
+ const backward = await collect(store, {
1312
+ stream: s,
1313
+ stream_exact: true,
1314
+ after: 0,
1315
+ backward: true
1316
+ });
1317
+ expect8(backward.every((e) => e.id > 0)).toBe(true);
1318
+ });
1010
1319
  it7("created_after/created_before filter by timestamp", async () => {
1011
1320
  const s = `q-ts-${uid()}`;
1012
1321
  const committed = await store.commit(
@@ -1245,6 +1554,34 @@ var runStoreTck = (options) => {
1245
1554
  if (error !== void 0) expect8(error).toBeInstanceOf(ValidationError);
1246
1555
  else expect8(count).toBe(1);
1247
1556
  });
1557
+ it7("stream filters match case-sensitively (query)", async () => {
1558
+ const tag = uid();
1559
+ const lower = `order-${tag}`;
1560
+ const upper = `Order-${tag}`;
1561
+ await seed_streams([lower, upper]);
1562
+ expect8(await streams_matching(`^order-${tag}`)).toEqual([lower]);
1563
+ expect8(await streams_matching(`^Order-${tag}$`)).toEqual([upper]);
1564
+ expect8(await streams_matching(`order-${tag}`)).toEqual([lower]);
1565
+ });
1566
+ it7("stream filters match case-sensitively (position filters)", async () => {
1567
+ const tag = uid();
1568
+ const lower = `csp-order-${tag}`;
1569
+ const upper = `csp-Order-${tag}`;
1570
+ await store.subscribe([{ stream: lower }, { stream: upper }]);
1571
+ const got = [];
1572
+ await store.query_streams((p) => got.push(p.stream), {
1573
+ stream: `^csp-order-${tag}`
1574
+ });
1575
+ expect8(got).toEqual([lower]);
1576
+ });
1577
+ it7("bulk stream ops match case-sensitively", async () => {
1578
+ const tag = uid();
1579
+ const lower = `bcs-order-${tag}`;
1580
+ const upper = `bcs-Order-${tag}`;
1581
+ await store.subscribe([{ stream: lower }, { stream: upper }]);
1582
+ const count = await store.reset({ stream: `^bcs-order-${tag}` });
1583
+ expect8(count).toBe(1);
1584
+ });
1248
1585
  });
1249
1586
  describe8("subscribe + claim + ack", () => {
1250
1587
  it7("subscribes new streams and is idempotent on repeat", async () => {
@@ -1827,6 +2164,28 @@ var runStoreTck = (options) => {
1827
2164
  const leased = await store.claim(100, 100, `w-${uid()}`, 1e5);
1828
2165
  expect8(leased.find((l) => l.stream === s)).toBeDefined();
1829
2166
  });
2167
+ it7("exposes a future deferred_at via query_streams (#1221)", async () => {
2168
+ const deferred = `defer-qs-${uid()}`;
2169
+ const plain = `defer-qs-plain-${uid()}`;
2170
+ await store.subscribe([{ stream: deferred }, { stream: plain }]);
2171
+ for (const s of [deferred, plain])
2172
+ await store.commit(
2173
+ s,
2174
+ [inc(1)],
2175
+ make_meta({ stream: s })
2176
+ );
2177
+ const due = Date.now() + 36e5;
2178
+ await store.defer([deferred], due);
2179
+ const seen = /* @__PURE__ */ new Map();
2180
+ await store.query_streams((p) => {
2181
+ if (p.stream === deferred || p.stream === plain)
2182
+ seen.set(p.stream, p.deferred_at);
2183
+ });
2184
+ const got = seen.get(deferred);
2185
+ expect8(got).toBeDefined();
2186
+ expect8(Math.abs(got - due)).toBeLessThan(2e3);
2187
+ expect8(seen.get(plain)).toBeUndefined();
2188
+ });
1830
2189
  it7("defers streams matching a filter and counts matches", async () => {
1831
2190
  const tag = uid();
1832
2191
  const a = `deferfilter-${tag}-a`;