Skip to content

test(act): property-based suite covering five framework invariants#647

Merged
Rotorsoft merged 1 commit into
masterfrom
test/property-based-suite
May 4, 2026
Merged

test(act): property-based suite covering five framework invariants#647
Rotorsoft merged 1 commit into
masterfrom
test/property-based-suite

Conversation

@Rotorsoft
Copy link
Copy Markdown
Owner

Summary

PR-B1 from the architectural plan. Adds fast-check + @fast-check/vitest and 5 property suites that exercise the framework's load-bearing contracts under combinatorially-generated input.

The 5 suites cover what hand-written tests can't reach: arbitrary commit interleavings, lease lifecycle traces, sequences of action()s with cache invalidation, exhaustive correlate→drain delivery, and close idempotency.

Suites added

File Properties asserted
commit-version.property.spec.ts (1) per-stream version is strictly monotonic from 0 regardless of cross-stream interleaving; (2) expectedVersion enforcement throws and commits no events on mismatch
claim-lifecycle.property.spec.ts (1) every successful claim() reaches ack() or block() (no leaks); (2) ack() advances per-stream watermarks monotonically; (3) blocked streams cannot be re-claimed
cache-coherence.property.spec.ts (1) cached load() equals fresh-load (cache cleared) state-for-state and version-for-version; (2) ConcurrencyError invalidates the cached entry; (3) snapshot.version equals the head event's version
correlate-drain.property.spec.ts (1) every committed reactive event is delivered to exactly one handler invocation after enough drains (no losses, no duplicates); (2) drain is idempotent once settled
close.property.spec.ts (1) close({restart:false}) makes subsequent action() throw StreamClosedError; (2) close() is idempotent on already-tombstoned streams; (3) close({restart:true}) preserves observable state

13 properties total, ~750 generated cases per CI build (100 per commit-version and claim-lifecycle properties; 50 per the heavier action()-driven ones).

Why all 5 in one PR

Originally I proposed staging this as 3-now / 2-later. But:

  • The shared scaffolding (Counter state, target() helper, beforeEach/afterEach ports setup) is the same across suites
  • Splitting into two PRs would just relocate the same generators
  • Each suite is small (~100 lines) and the patterns are consistent

You explicitly asked for all 5 in one PR; this delivers that.

Verification

  • All 13 properties pass (970 total tests in libs/act, was 957 before)
  • 100% coverage maintained (statements, branches, functions, lines)
  • 5 consecutive soak runs of the property suite — no flakes
  • Each property runs ≥ 50 cases (most run 100); fast-check seeds are random per run
  • Counterexamples are reported reproducibly via seed (e.g., Property failed after 1 tests { seed: -1219815470, path: "0:1:0:2:2:3:3", endOnFailure: true }) — caught one bug in my initial cache-coherence assertion logic during development
  • CI green on push

What's intentionally NOT in this PR

  • Cross-adapter property runs — properties run against InMemoryStore. Same suites against act-pg/act-sqlite belongs in PR-C1 (concurrency stress harness).
  • No code changes — purely additive. Properties found no actual bugs in the framework; one subtle bug was in my own initial test logic.
  • No CI changes — properties run as part of the normal test suite. If they get slow enough to warrant a separate lane, that's a follow-up.

🤖 Generated with Claude Code

…riants

Introduces fast-check + @fast-check/vitest as workspace dev deps and
adds 5 property suites under libs/act/test/property/ targeting the
framework's load-bearing contracts. 100 cases per property by default
(50 for the heavier action()-driven suites), no flakiness across 5
consecutive soak runs locally.

Suites:

- commit-version.property — per-stream `version` is strictly monotonic
  starting at 0 regardless of cross-stream interleaving; expectedVersion
  enforcement throws and commits no events on mismatch.

- claim-lifecycle.property — every successful claim() reaches ack() or
  block() (no leaks); ack monotonically advances per-stream watermarks;
  blocked streams cannot be re-claimed.

- cache-coherence.property — after any random sequence of action()s,
  cached load() equals fresh-load (cache cleared) state-and-version
  for every stream; ConcurrencyError invalidates the cached entry;
  snapshot.version equals the head event's version.

- correlate-drain.property — every committed reactive event is
  delivered to exactly one handler invocation after enough drains
  (no losses, no duplicates); drain is idempotent once settled.

- close.property — close() with restart=false makes subsequent
  action() throw StreamClosedError; close() is idempotent on already-
  tombstoned streams; close() with restart=true preserves observable
  state.

These complement the existing 957 imperative tests by covering
combinatorial input space the hand-written tests can't reach.

The 2 deferred suites from the original PR-B1 plan (correlate-drain
and close) are included here; B1 lands as a single comprehensive PR
rather than the originally-proposed staged rollout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Rotorsoft Rotorsoft merged commit c0be04e into master May 4, 2026
6 checks passed
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 4, 2026

🎉 This PR is included in version @rotorsoft/act-v0.33.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant