Skip to content

Commit 7272d42

Browse files
tests: Add mutation cursor Quint Connect replays
Exercise the remaining deterministic scenarios and guarded no-op regressions through the real Rust protocol, then compare generated Quint traces against the model across 500 samples. This expands MBT coverage without changing production protocol behavior. Plan: mutation-cursor-quint-connect, T05 Co-authored-by: SCE <sce@crocoder.dev>
1 parent 535a22c commit 7272d42

7 files changed

Lines changed: 806 additions & 106 deletions

File tree

.github/workflows/quint.yml

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
exit 0
4141
fi
4242
43-
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '(\.qnt$|^\.github/workflows/quint\.yml$|^\.github/workflows/quint-deep-verify\.yml$|^flake\.nix$|^flake\.lock$)'; then
43+
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '(\.qnt$|^cli/src/services/mutation_trace/mbt/|^cli/src/services/mutation_trace/protocol\.rs$|^cli/src/services/mutation_trace/types\.rs$|^cli/Cargo\.toml$|^cli/Cargo\.lock$|^\.github/workflows/quint\.yml$|^\.github/workflows/quint-deep-verify\.yml$|^flake\.nix$|^flake\.lock$)'; then
4444
echo "quint=true" >> "$GITHUB_OUTPUT"
4545
else
4646
echo "quint=false" >> "$GITHUB_OUTPUT"
@@ -55,7 +55,9 @@ jobs:
5555
needs: detect
5656
if: needs.detect.outputs.quint == 'true'
5757
runs-on: ubuntu-latest
58-
timeout-minutes: 15
58+
# The Quint Connect Nix check compiles the CLI crate, so this needs more
59+
# headroom than the pure-Quint steps alone required.
60+
timeout-minutes: 30
5961
steps:
6062
- name: Harden the runner (Audit all outbound calls)
6163
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
@@ -78,7 +80,11 @@ jobs:
7880
run: nix run .#quint -- typecheck spec/mutation_cursor.qnt
7981

8082
- name: Run Quint tests
81-
run: nix run .#quint -- test spec/mutation_cursor.qnt
83+
# `quint test` without `--match` silently selects zero tests (exit 0,
84+
# no output) on this spec instead of running every named `run`
85+
# scenario — matching every top-level `test...`-named `run` is the
86+
# explicit selection that actually exercises them.
87+
run: nix run .#quint -- test spec/mutation_cursor.qnt --match '^test.*'
8288

8389
- name: Randomized Quint safety check
8490
run: >
@@ -89,6 +95,13 @@ jobs:
8995
--max-samples=5000
9096
--max-steps=20
9197
98+
- name: Quint Connect model-based tests (Nix-pinned Rust + Quint)
99+
# The entire MBT invocation goes through this Nix check rather than
100+
# `cargo test` on the runner's own Cargo, so both the Rust toolchain
101+
# and the Quint binary come from the repository's pinned flake
102+
# inputs, never a second, unpinned Rust installation.
103+
run: nix build .#checks.x86_64-linux.mutation-trace-quint-connect --print-build-logs
104+
92105
gate:
93106
name: Quint gate
94107
if: always()

cli/src/services/mutation_trace/mbt/tests.rs

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Deterministic Quint Connect replays through the real `protocol.rs`.
22
3-
use quint_connect::quint_test;
3+
use quint_connect::{quint_run, quint_test};
44

55
use super::driver::MutationCursorDriver;
66

@@ -20,3 +20,119 @@ use super::driver::MutationCursorDriver;
2020
fn mutation_cursor_transports_non_default_arguments() -> impl Driver {
2121
MutationCursorDriver::default()
2222
}
23+
24+
/// Replays `testStartObservesBeforeActivation`: the freshness-boundary
25+
/// semantics for a `Start` observation.
26+
#[quint_test(
27+
spec = "../spec/mutation_cursor.qnt",
28+
test = "testStartObservesBeforeActivation"
29+
)]
30+
fn mutation_cursor_start_observes_before_activation() -> impl Driver {
31+
MutationCursorDriver::default()
32+
}
33+
34+
/// Replays `testCloseObservesBeforeDeactivation`.
35+
#[quint_test(
36+
spec = "../spec/mutation_cursor.qnt",
37+
test = "testCloseObservesBeforeDeactivation"
38+
)]
39+
fn mutation_cursor_close_observes_before_deactivation() -> impl Driver {
40+
MutationCursorDriver::default()
41+
}
42+
43+
/// Replays `testContendedIntervalsRemainAiContended`.
44+
#[quint_test(
45+
spec = "../spec/mutation_cursor.qnt",
46+
test = "testContendedIntervalsRemainAiContended"
47+
)]
48+
fn mutation_cursor_contended_intervals_remain_ai_contended() -> impl Driver {
49+
MutationCursorDriver::default()
50+
}
51+
52+
/// Replays `testNoChangeHookReplayCannotStealFutureChange`.
53+
#[quint_test(
54+
spec = "../spec/mutation_cursor.qnt",
55+
test = "testNoChangeHookReplayCannotStealFutureChange"
56+
)]
57+
fn mutation_cursor_no_change_hook_replay_cannot_steal_future_change() -> impl Driver {
58+
MutationCursorDriver::default()
59+
}
60+
61+
/// Replays `testConcurrentObservationsHaveOneWinner`.
62+
#[quint_test(
63+
spec = "../spec/mutation_cursor.qnt",
64+
test = "testConcurrentObservationsHaveOneWinner"
65+
)]
66+
fn mutation_cursor_concurrent_observations_have_one_winner() -> impl Driver {
67+
MutationCursorDriver::default()
68+
}
69+
70+
/// Replays `testTaintInvalidatesPreparedObservation`.
71+
#[quint_test(
72+
spec = "../spec/mutation_cursor.qnt",
73+
test = "testTaintInvalidatesPreparedObservation"
74+
)]
75+
fn mutation_cursor_taint_invalidates_prepared_observation() -> impl Driver {
76+
MutationCursorDriver::default()
77+
}
78+
79+
/// Replays `testRecoveryEstablishesBaseline`.
80+
#[quint_test(
81+
spec = "../spec/mutation_cursor.qnt",
82+
test = "testRecoveryEstablishesBaseline"
83+
)]
84+
fn mutation_cursor_recovery_establishes_baseline() -> impl Driver {
85+
MutationCursorDriver::default()
86+
}
87+
88+
/// Replays `testClosedScopeCannotReactivate`.
89+
#[quint_test(
90+
spec = "../spec/mutation_cursor.qnt",
91+
test = "testClosedScopeCannotReactivate"
92+
)]
93+
fn mutation_cursor_closed_scope_cannot_reactivate() -> impl Driver {
94+
MutationCursorDriver::default()
95+
}
96+
97+
/// Guarded-no-op regression: replays
98+
/// `testMbtGuardedPrepareInvokesRealPrepare`
99+
/// (`init.then(prepare(Attempt0, Start(...))).then(prepare(Attempt0,
100+
/// Advance(...)))`), where the second `prepare` guards because `Attempt0` is
101+
/// no longer `Available`. A passing replay proves the driver still calls
102+
/// `protocol::prepare` on the guarded step — dispatch is on the `MbtAction`
103+
/// variant Quint recorded, never skipped because Quint's own state happened
104+
/// not to change — and independently reaches the same no-op outcome.
105+
#[quint_test(
106+
spec = "../spec/mutation_cursor.qnt",
107+
test = "testMbtGuardedPrepareInvokesRealPrepare"
108+
)]
109+
fn mutation_cursor_guarded_prepare_invokes_real_prepare() -> impl Driver {
110+
MutationCursorDriver::default()
111+
}
112+
113+
/// Guarded-no-op regression: replays
114+
/// `testMbtGuardedRecoverInvokesRealRecover` (`init.then(recover(WT0))`),
115+
/// where `recover` guards because `WT0` is neither tainted, externally
116+
/// tainted, nor needing rebaseline. A passing replay proves the driver still
117+
/// calls `protocol::recover` on the guarded step and independently reaches
118+
/// the same no-op outcome.
119+
#[quint_test(
120+
spec = "../spec/mutation_cursor.qnt",
121+
test = "testMbtGuardedRecoverInvokesRealRecover"
122+
)]
123+
fn mutation_cursor_guarded_recover_invokes_real_recover() -> impl Driver {
124+
MutationCursorDriver::default()
125+
}
126+
127+
/// Generated-trace refinement: replays Quint-generated randomized
128+
/// traces through the real `protocol.rs`, comparing `ModelState` against
129+
/// Quint's semantic state after every step. Reproducible with a fixed
130+
/// `QUINT_SEED`.
131+
#[quint_run(
132+
spec = "../spec/mutation_cursor.qnt",
133+
max_samples = 500,
134+
max_steps = 30
135+
)]
136+
fn mutation_cursor_generated_traces_refine_rust_protocol() -> impl Driver {
137+
MutationCursorDriver::default()
138+
}

0 commit comments

Comments
 (0)