Skip to content

Run the specification closure once: single-pass recording replaces the probe (0.12.0) - #143

Merged
rasuvaeff merged 2 commits into
masterfrom
fix/single-pass-specification-recording
Sep 19, 2026
Merged

rasuvaeff merged 2 commits into
masterfrom
fix/single-pass-specification-recording

Conversation

@rasuvaeff

Copy link
Copy Markdown
Owner

Fixes #142

Since 0.11.0 the recording ended on the first double call and re-ran the closure through Runtime::probe() to see whether a second call hid in the arguments of the first. The closure is user code, so every side effect in it happened twice: a counter went up twice, a factory built twice, an Understudy::for() inside it created a second double that stayed in the context and failed verifyAll(strictStubs: true) as a stub never used.

The recording now keeps going past a call: the dispatcher retains the signal and answers the call with the mode's type-safe default out of a throwaway context, so every call the closure makes is seen in one pass and more than one is refused. A method with no safe default (never, an object type) still ends the closure on its signal. Code after the call runs against the default, and where it does not survive it the specification stands — that code was never run before and is not the specification.

Two calls side by side are refused like a nested pair, and the refusal reads the same for both shapes; InvalidCallSpecification::nestedCall() is moreThanOneCall() — the one BC break, declared by the 0.12.0 heading.

  • composer build: 1113 tests green
  • rector: clean
  • mutation: 3406 mutants, MSI 93% (gate 92)
  • bin/package-audit: clean

Found by the 2026-09-19 family review (H-1).

…e probe

Since 0.11.0 the recording ended on the first double call and re-ran the
closure through Runtime::probe() to see whether a second call hid in the
arguments of the first. The closure is user code, so every side effect in
it happened twice: a counter went up twice, a factory built twice, an
Understudy::for() inside it created a second double that stayed in the
context and failed verifyAll(strictStubs: true) as a stub never used.

The recording now keeps going past a call: the dispatcher retains the
signal and answers the call with the mode's type-safe default out of a
throwaway context, so every call the closure makes is seen in one pass and
more than one is refused. A method with no safe default (never, an object
type) still ends the closure on its signal. Code after the call runs
against the default, and where it does not survive it the specification
stands — that code was never run before and is not the specification.

Two calls side by side are refused like a nested pair, and the refusal
reads the same for both shapes; nestedCall() is moreThanOneCall().

Fixes #142
@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 0ab9eee1-a6cf-4788-8972-238469d57c8d


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@rasuvaeff
rasuvaeff merged commit 1f011ec into master Sep 19, 2026
20 checks passed
@rasuvaeff
rasuvaeff deleted the fix/single-pass-specification-recording branch September 19, 2026 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Specification closure is run twice: Runtime::probe() repeats its side effects and registers a second double

1 participant