@rotorsoft/act-tck
v1.30.0
Published
Test Compatibility Kit for @rotorsoft/act Store, Cache, and Logger ports
Maintainers
Readme
@rotorsoft/act-tck
Test Compatibility Kit for the Store, Cache, and Logger ports of @rotorsoft/act. Lane conformance suite (claim filter, subscribe UPSERT, ack/block round-trip) added in v0.4.0 (ACT-1103).
Why this package
A port without an executable contract is undefined behavior. Act has three pluggable ports (event store, snapshot cache, logger), each with multiple in-tree adapters and an open door for third-party implementations. Before this package, every adapter's test file independently re-stated what the contract was — tribal knowledge, not a spec.
act-tck turns the contract into a runnable spec. Drop it into your adapter's test file, point it at your implementation, and vitest will execute the same conformance suite the in-tree adapters pass. New port methods land here first; adapters add capability flags and opt in.
Installation
pnpm add -D @rotorsoft/act-tckThe kit is a dev dependency — it ships test code, not runtime code.
Quick start
// libs/act-mysql/test/store-tck.spec.ts
import { runStoreTck } from "@rotorsoft/act-tck";
import { MysqlStore } from "../src/index.js";
runStoreTck({
name: "MysqlStore",
factory: () => new MysqlStore({ host: "localhost", database: "act_tck" }),
capabilities: { notify: false },
});That's the whole integration. run*Tck calls vitest's describe/it internally; your test runner drives execution. A fixed Counter-style fixture domain keeps tests deterministic and self-contained.
API
runStoreTck(options)— everyStoremethod, capability-gated where optional.runStoreDifferentialTck(options)— drives a family of randomized, seeded workloads against multipleStoreinstances and asserts identical normalized output (event order,with_snapsfloor,query_stats/query_streams) for every workload. Catches cross-adapter drift a single-adapter suite can't.runStorePropertyTck(options)— property-based store invariants (commit version monotonicity, claim/lease no-leak, watermark monotonicity, block exclusion) over fast-check-generated sequences.runCacheTck(options)— everyCachemethod, cross-stream isolation, dispose idempotency.runCacheDifferentialTck(options)— drives randomizedset/invalidate/clearworkloads against multipleCacheinstances and asserts identical observableget()after every op.runLoggerTck(options)— structural smoke test of theLoggercontract.runLoggerDifferentialTck(options)— drives the identical call surface against multipleLoggerinstances and asserts robustness + structural parity (what throws, what conforms).runStabilityTck(options)— snapshot-based public-API stability gate. Catches accidental rename / removal / signature drift on a package's public surface before it merges.StoreCapabilities/CacheCapabilities/LoggerCapabilities— flag types for opting into optional surface (e.g.,Store.notify).- Fixture helpers re-exported from
@rotorsoft/act-tck/fixturesfor adapter-specific tests that want the same Counter domain.
What's covered
runStoreTck
Every method on the Store interface in libs/act/src/types/ports.ts:
commit— single + multi-event commits, optimistic concurrency, preserved state on rejectionquery— stream, names, correlation, before/after, created_after/before, limit, with_snaps, stream_exact, backward traversalsubscribe— idempotent re-subscribeclaim/ack— lease lifecycle, dual frontiers, leased streams not double-claimed, ack rejected from wrong holder, exact-source has-work matching, timed-out-lease retry accountingblock— blocked streams hidden from claim, same-drainer-only enforcementreset— restart watermarks (including blocked), no-op for missingprioritize— bulk priority updates by filtertruncate— snapshot vs tombstone seeding, empty inputs, missing streamsquery_streams— filters, exact-match, pagination, blockedquery_stats— array + filter forms, opt-in count/tail/names, exclude + before, snapshot count vianamesnotify(capability-gated) — cross-instance delivery, self-filtering (an instance never receives its own commits), one notification per commit transaction with the full event batch; requires thefactoryto produce instances sharing one backing store
runStoreDifferentialTck
Where runStoreTck proves each adapter honors the contract in isolation, the differential harness proves they honor it identically. It replays a family of randomized, seeded workloads — commits, inline snapshots, truncates, subscriptions, in a seed-varying order — against two or more Store instances (in-memory as the reference, durable adapters as comparands), then asserts their normalized outputs match exactly for every workload. Each workload runs from a distinct seed (seed, seed + 1, …, seed + runs - 1), so divergence is hunted across the input space rather than one fixed script; a failing workload names its seed for replay:
- global forward
queryorder - per-stream
with_snapssnapshot floor - backward traversal order
query_statshead / tail / count / names (plus filter-form key order)query_streamsrows (source, watermark, blocked, priority, lane)
Normalization drops only what legitimately differs between stores (absolute event ids, created timestamps, correlation/causation uuids). A one-adapter with_snaps regression surfaces as a diff against the reference. Wire it with the in-memory store first:
import { runStoreDifferentialTck } from "@rotorsoft/act-tck";
import { InMemoryStore } from "@rotorsoft/act";
import { MysqlStore } from "../src/index.js";
runStoreDifferentialTck({
name: "InMemory vs Mysql",
stores: [
{ name: "InMemoryStore", factory: () => new InMemoryStore() },
{ name: "MysqlStore", factory: () => new MysqlStore({ /* … */ }) },
],
});runCacheTck
Every method on the Cache interface: get on unset stream returns undefined; set then get round-trip; set overwrites; invalidate removes one stream, leaves others; invalidate/clear no-op on absent state; clear empties every stream; cross-stream isolation; dispose idempotency.
runCacheDifferentialTck
The Cache analog of the store differential. It drives a family of randomized, seeded workloads (set / invalidate / clear over a small key set kept within capacity, so eviction — an adapter policy, not a contract guarantee — never enters the comparison) against two or more Cache instances and asserts their observable get() snapshot is identical after every operation. A cache that mishandles overwrite ordering, leaks an invalidated key, or clears partially diverges on the exact op that broke it.
import { runCacheDifferentialTck } from "@rotorsoft/act-tck";
import { InMemoryCache } from "@rotorsoft/act";
import { RedisCache } from "../src/index.js";
runCacheDifferentialTck({
name: "InMemory vs Redis",
caches: [
{ name: "InMemoryCache", factory: () => new InMemoryCache({ maxSize: 1000 }) },
{ name: "RedisCache", factory: () => new RedisCache({ /* … */ }) },
],
});runLoggerTck
Structural smoke test of the Logger interface: level is a non-empty string; every level method callable with both overload signatures; null and cyclic payloads don't throw; child(bindings) returns a Logger satisfying the same contract; dispose is idempotent and awaitable.
runLoggerDifferentialTck
A logger has no portable output to byte-compare — its format is adapter-specific by design. The meaningful differential is robustness and structural parity: driven through the identical call surface (every level, both overloads, null + cyclic payloads, child spawning), two implementations must agree on what throws and what conforms. A logger that throws on a cyclic payload the reference tolerates, or returns a non-conforming child, diverges from the reference outcome vector.
import { runLoggerDifferentialTck } from "@rotorsoft/act-tck";
import { ConsoleLogger } from "@rotorsoft/act";
import { PinoLogger } from "../src/index.js";
runLoggerDifferentialTck({
name: "Console vs Pino",
loggers: [
{ name: "ConsoleLogger", factory: () => new ConsoleLogger({ level: "trace" }) },
{ name: "PinoLogger", factory: () => new PinoLogger({ level: "trace" }) },
],
});runStabilityTck
Walks the source of every declared entry point, follows relative re-exports recursively, and snapshots the concatenated text via Vitest. Any rename, removed export, or signature change to the public surface shows up as a snapshot diff in the PR — reviewers either accept the change (re-run with -u) or push back. Stops at non-relative imports (other packages, node:*); each package owns its own snapshot.
// libs/act-mysql/test/stability.spec.ts
import path from "node:path";
import { fileURLToPath } from "node:url";
import { runStabilityTck } from "@rotorsoft/act-tck";
const src = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "src");
runStabilityTck({
name: "@rotorsoft/act-mysql",
entryPoints: { "": path.join(src, "index.ts") },
});The TCK reads source (.ts) rather than built .d.ts — no prior pnpm build required, and the snapshot text matches what reviewers actually read.
Common patterns
Capability flags for optional methods
Optional methods are gated so adapters can opt out of features they don't implement:
runStoreTck({
name: "MysqlStore",
factory: () => new MysqlStore({ /* … */ }),
capabilities: { notify: true }, // adapter implements Store.notify
});Adding adapter-specific tests alongside the TCK
The TCK validates the contract; adapter-specific edge cases (defensive rowCount ?? 0 branches, dialect-specific SQL paths) belong in the adapter's own test file. See libs/act-pg/test/store.error.spec.ts and libs/act-sqlite/test/store.error.spec.ts for the fault-injection patterns the in-tree adapters use to round out the 100% coverage gate.
When the port interface changes
New / changed methods on Store, Cache, or Logger are added to libs/act-tck/src/ in lockstep. Optional methods land behind a Capabilities flag so existing adapters keep passing until they opt in.
Compatibility
- Node: >=22.18.0
- Peer:
@rotorsoft/act(workspace version),vitest>=3.0.9,zod^4.4.3 - Runtime deps: none — pure test code
Stability
Public API governed by the Act Stability Charter. The TCK's published surface — runStoreTck, runCacheTck, runLoggerTck, the Capabilities types, and the fixture helpers — is now covered by SemVer alongside the Store/Cache/Logger contracts it validates. Charter is in effect as of 1.0.0; the milestone tracker is milestone 1.0.
Related packages
- @rotorsoft/act — the framework defining the ports this kit validates.
- @rotorsoft/act-pg / @rotorsoft/act-sqlite — reference
Storeadapters; both passrunStoreTck. - @rotorsoft/act-pino — reference
Loggeradapter; passesrunLoggerTck.
The in-tree InMemoryStore / InMemoryCache / ConsoleLogger (bundled with @rotorsoft/act) are the first customers — they prove the TCK works before any external adapter ships.
Documentation
- Writing a custom Store adapter — full walkthrough, with
runStoreTckas the acceptance harness. - Writing a custom Cache adapter.
- Writing a custom Logger adapter.
License
MIT
