Skip to content

Commit 8e89094

Browse files
mutation-trace: Add Quint Connect model-based test driver
Connect the mutation cursor Quint model to the pure Rust protocol so non-default trace arguments and resulting comparable state are exercised end to end. Add ITF wire mappings, a test-only driver, and a non-default-value replay scenario without changing production behavior. Plan: mutation-cursor-quint-connect T04 Co-authored-by: SCE <sce@crocoder.dev>
1 parent e56c2f5 commit 8e89094

7 files changed

Lines changed: 871 additions & 11 deletions

File tree

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
//! MBT driver: connects Quint Connect's generated/replayed traces to the
2+
//! real `protocol.rs` transition functions.
3+
//!
4+
//! [`MutationCursorDriver::step`] dispatches on the `MbtAction` variant Quint
5+
//! recorded — never on a before/after state diff — and every arm
6+
//! unconditionally calls the corresponding `protocol::*` function with the
7+
//! transported arguments, including on an `MbtAction` variant Quint produced
8+
//! from a guarded/no-op path (T02's `mbtStutterAs` instrumentation): the
9+
//! driver has no way to distinguish that case from a real transition, and
10+
//! must not try to, since replaying the guarded call and comparing the
11+
//! resulting no-op state against Quint is the point of the regressions in
12+
//! T05.
13+
14+
use std::collections::{BTreeMap, BTreeSet};
15+
16+
use quint_connect::{switch, Config, Driver, Result, State, Step};
17+
18+
use super::super::protocol;
19+
use super::super::types::{
20+
boundary_worktree, ActorKind, AttemptId, AttemptState, AttemptStatus, Boundary, FailureKind,
21+
ProtocolState, ScopeId, ScopeState, ScopeStatus, TreeId, WorktreeId, WorktreeState,
22+
};
23+
use super::model::{
24+
ModelState, WireAttemptId, WireBoundary, WireScopeId, WireTreeId, WireWorktreeId,
25+
};
26+
27+
fn worktree(id: &str) -> WorktreeId {
28+
WorktreeId(id.to_string())
29+
}
30+
31+
fn scope(id: &str) -> ScopeId {
32+
ScopeId(id.to_string())
33+
}
34+
35+
fn tree(id: &str) -> TreeId {
36+
TreeId(id.to_string())
37+
}
38+
39+
fn attempt_id(id: &str) -> AttemptId {
40+
AttemptId(id.to_string())
41+
}
42+
43+
/// Replays a trace generated from/for `spec/mutation_cursor.qnt` through the
44+
/// real `protocol.rs` functions.
45+
///
46+
/// Holds exactly the state the plan authorizes: the pure protocol state
47+
/// (refines `worktrees`/`scopes`/`externalTaint`/`processedEvents`/
48+
/// `attempts`/`mutationEvents`) plus `worktree_trees`, the driver-only
49+
/// analogue of Quint's `worktreeTrees` var — the observed-tree input
50+
/// `prepare`/`recover` take explicitly, since the pure kernel performs no Git
51+
/// I/O. `MbtMutate` is the only action that touches `worktree_trees`; every
52+
/// other action calls a `protocol::*` function, never reimplementing its
53+
/// logic.
54+
pub(super) struct MutationCursorDriver {
55+
protocol: ProtocolState,
56+
worktree_trees: BTreeMap<WorktreeId, TreeId>,
57+
}
58+
59+
impl MutationCursorDriver {
60+
/// Exactly `spec/mutation_cursor.qnt`'s `init`: both worktrees at
61+
/// `Tree0`/revision `0`/healthy/no-rebaseline, all four scopes
62+
/// `NeverSeen` with `scopeActor`'s fixed partition (`Scope0`/`Scope1`
63+
/// Claude Code and `Scope2` Codex on `WT0`, `Scope3` `OpenCode` on `WT1`),
64+
/// and all six attempts `Available` with the same placeholder
65+
/// `Flush(WT0)`/revision `0`/`Tree0`/`Tree0` baseline Quint's `init`
66+
/// assigns every `AttemptId`.
67+
fn init() -> Self {
68+
let wt0 = worktree("wt0");
69+
let wt1 = worktree("wt1");
70+
71+
let mut worktrees = BTreeMap::new();
72+
let mut worktree_trees = BTreeMap::new();
73+
for id in [&wt0, &wt1] {
74+
worktrees.insert(
75+
id.clone(),
76+
WorktreeState {
77+
cursor_tree: tree("tree0"),
78+
revision: 0,
79+
tainted: false,
80+
failure_kind: FailureKind::Healthy,
81+
needs_rebaseline: false,
82+
},
83+
);
84+
worktree_trees.insert(id.clone(), tree("tree0"));
85+
}
86+
87+
let scope_partition: [(&str, &WorktreeId, ActorKind); 4] = [
88+
("scope0", &wt0, ActorKind::ClaudeCode),
89+
("scope1", &wt0, ActorKind::ClaudeCode),
90+
("scope2", &wt0, ActorKind::Codex),
91+
("scope3", &wt1, ActorKind::OpenCode),
92+
];
93+
let mut scopes = BTreeMap::new();
94+
for (id, owning_worktree, actor_kind) in scope_partition {
95+
scopes.insert(
96+
scope(id),
97+
ScopeState {
98+
status: ScopeStatus::NeverSeen,
99+
actor_kind,
100+
worktree_id: owning_worktree.clone(),
101+
},
102+
);
103+
}
104+
105+
let mut attempts = BTreeMap::new();
106+
for id in [
107+
"attempt0", "attempt1", "attempt2", "attempt3", "attempt4", "attempt5",
108+
] {
109+
attempts.insert(
110+
attempt_id(id),
111+
AttemptState {
112+
status: AttemptStatus::Available,
113+
boundary: Boundary::Flush {
114+
worktree: wt0.clone(),
115+
},
116+
expected_revision: 0,
117+
before_tree: tree("tree0"),
118+
after_tree: tree("tree0"),
119+
},
120+
);
121+
}
122+
123+
Self {
124+
protocol: ProtocolState {
125+
worktrees,
126+
scopes,
127+
external_taint: BTreeSet::new(),
128+
processed_events: BTreeSet::new(),
129+
attempts,
130+
mutation_events: BTreeSet::new(),
131+
},
132+
worktree_trees,
133+
}
134+
}
135+
136+
fn mbt_init(&mut self) {
137+
*self = Self::init();
138+
}
139+
140+
fn mbt_mutate(&mut self, worktree: WorktreeId, tree: TreeId) {
141+
self.worktree_trees.insert(worktree, tree);
142+
}
143+
144+
fn observed_tree(&self, worktree: &WorktreeId) -> TreeId {
145+
self.worktree_trees
146+
.get(worktree)
147+
.cloned()
148+
.expect("every worktree tracked since init has an observed tree")
149+
}
150+
151+
fn mbt_prepare(&mut self, attempt: AttemptId, boundary: Boundary) {
152+
let worktree = boundary_worktree(&boundary, &self.protocol.scopes)
153+
.expect("every boundary's scope is registered by init, matching Quint's static scopeWorktree partition");
154+
let observed_tree = self.observed_tree(&worktree);
155+
self.protocol = protocol::prepare(&self.protocol, attempt, boundary, observed_tree);
156+
}
157+
158+
fn mbt_commit(&mut self, attempt: &AttemptId) {
159+
self.protocol = protocol::commit(&self.protocol, attempt).state;
160+
}
161+
162+
fn mbt_taint(&mut self, worktree: &WorktreeId) {
163+
self.protocol = protocol::taint(&self.protocol, worktree);
164+
}
165+
166+
fn mbt_database_failure(&mut self, worktree: &WorktreeId) {
167+
self.protocol = protocol::database_failure(&self.protocol, worktree);
168+
}
169+
170+
fn mbt_abandon(&mut self, scope: &ScopeId) {
171+
self.protocol = protocol::abandon(&self.protocol, scope);
172+
}
173+
174+
fn mbt_recover(&mut self, worktree: &WorktreeId) {
175+
let observed_tree = self.observed_tree(worktree);
176+
self.protocol = protocol::recover(&self.protocol, worktree, observed_tree);
177+
}
178+
179+
/// Refines the explicit top-level `stutter` action: no state change.
180+
#[allow(clippy::unused_self)]
181+
fn mbt_stutter(&self) {}
182+
}
183+
184+
impl Default for MutationCursorDriver {
185+
fn default() -> Self {
186+
Self::init()
187+
}
188+
}
189+
190+
impl Driver for MutationCursorDriver {
191+
type State = ModelState;
192+
193+
fn config() -> Config {
194+
Config {
195+
state: &[],
196+
nondet: &["mbtAction"],
197+
}
198+
}
199+
200+
fn step(&mut self, step: &Step) -> Result {
201+
switch!(step {
202+
MbtInit => self.mbt_init(),
203+
MbtMutate(worktree: WireWorktreeId, tree: WireTreeId) =>
204+
self.mbt_mutate(worktree.into(), tree.into()),
205+
MbtPrepare(attempt: WireAttemptId, boundary: WireBoundary) =>
206+
self.mbt_prepare(attempt.into(), boundary.into()),
207+
MbtCommit(attempt: WireAttemptId) => self.mbt_commit(&attempt.into()),
208+
MbtTaint(worktree: WireWorktreeId) => self.mbt_taint(&worktree.into()),
209+
MbtDatabaseFailure(worktree: WireWorktreeId) =>
210+
self.mbt_database_failure(&worktree.into()),
211+
MbtAbandon(scope: WireScopeId) => self.mbt_abandon(&scope.into()),
212+
MbtRecover(worktree: WireWorktreeId) => self.mbt_recover(&worktree.into()),
213+
MbtStutter => self.mbt_stutter(),
214+
})
215+
}
216+
}
217+
218+
impl State<MutationCursorDriver> for ModelState {
219+
fn from_driver(driver: &MutationCursorDriver) -> Result<Self> {
220+
Ok(ModelState {
221+
worktrees: driver.protocol.worktrees.clone(),
222+
scopes: driver.protocol.scopes.clone(),
223+
worktree_trees: driver.worktree_trees.clone(),
224+
external_taint: driver.protocol.external_taint.clone(),
225+
processed_events: driver.protocol.processed_events.clone(),
226+
attempts: driver.protocol.attempts.clone(),
227+
mutation_events: driver.protocol.mutation_events.clone(),
228+
})
229+
}
230+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//! Model-based testing harness connecting the verified
2+
//! `spec/mutation_cursor.qnt` model to the pure Rust refinement in
3+
//! `super::protocol`/`super::types` via Quint Connect.
4+
//!
5+
//! Test-only (`#[cfg(test)]`, gated from `mutation_trace/mod.rs`): no
6+
//! production code depends on this module, and it introduces no Git,
7+
//! database, filesystem, environment, network, async, or lock I/O of its
8+
//! own — every state transition is delegated to `super::protocol`'s pure
9+
//! functions.
10+
11+
mod driver;
12+
mod model;
13+
mod tests;

0 commit comments

Comments
 (0)