|
| 1 | +//! Seam falsifier: `SurrealMailboxView` → `VersionScheduler::on_version` → `KanbanMove`. |
| 2 | +//! |
| 3 | +//! ADDITIVE. Asserts the IN-direction wiring the module doc-comments *claim* |
| 4 | +//! ("a version tick, fed to `VersionScheduler::on_version`, lowers to the next |
| 5 | +//! legal Rubicon move over a `MailboxSoaView`") actually holds end-to-end — |
| 6 | +//! driven by the REAL `SurrealMailboxView` read-glove, not a hand-rolled fake, |
| 7 | +//! across the FULL Rubicon lifecycle (the in-crate unit test covers one tick). |
| 8 | +//! |
| 9 | +//! Each test states its kill-condition: the observation that would prove the |
| 10 | +//! seam WRONG. None of these pin "current behaviour" — they pin the contract |
| 11 | +//! the doc-comments assert. |
| 12 | +
|
| 13 | +use lance_graph_contract::kanban::{ExecTarget, KanbanColumn}; |
| 14 | +use lance_graph_contract::scheduler::{DatasetVersion, NextPhaseScheduler, VersionScheduler}; |
| 15 | +use lance_graph_contract::soa_view::MailboxSoaView; |
| 16 | +use surreal_container::view::SurrealMailboxView; |
| 17 | + |
| 18 | +/// One empty-column view at `phase` (the scheduler reads only `phase()` + |
| 19 | +/// `mailbox_id()` + `current_cycle()`; row columns are irrelevant to lowering). |
| 20 | +fn view_at(phase: KanbanColumn) -> SurrealMailboxView<'static> { |
| 21 | + SurrealMailboxView::from_columns(7, 5, 13, phase, &[], &[], &[], &[]) |
| 22 | +} |
| 23 | + |
| 24 | +/// KILL-CONDITION: if any forward tick lowers to a column the Rubicon DAG does |
| 25 | +/// not sanction (`can_transition_to` == false), the scheduler is mis-wired. |
| 26 | +/// Walks the whole canonical arc through the real view. |
| 27 | +#[test] |
| 28 | +fn full_rubicon_arc_lowers_to_legal_successors() { |
| 29 | + let scheduler = NextPhaseScheduler; |
| 30 | + // The canonical forward arc the doc-comment names. |
| 31 | + let arc = [ |
| 32 | + (KanbanColumn::Planning, KanbanColumn::CognitiveWork), |
| 33 | + (KanbanColumn::CognitiveWork, KanbanColumn::Evaluation), |
| 34 | + (KanbanColumn::Evaluation, KanbanColumn::Commit), |
| 35 | + (KanbanColumn::Plan, KanbanColumn::Planning), // re-deliberate |
| 36 | + ]; |
| 37 | + for (i, (from, want_to)) in arc.iter().enumerate() { |
| 38 | + let mv = scheduler |
| 39 | + .on_version(&view_at(*from), DatasetVersion(i as u64 + 1), ExecTarget::Native) |
| 40 | + .unwrap_or_else(|| panic!("{from:?} must schedule a forward move")); |
| 41 | + assert_eq!(mv.from, *from, "move.from must echo the observed phase"); |
| 42 | + assert_eq!(mv.to, *want_to, "{from:?} must lower to {want_to:?}"); |
| 43 | + // The DAG itself must sanction the emitted edge — the falsifier. |
| 44 | + assert!( |
| 45 | + from.can_transition_to(mv.to), |
| 46 | + "scheduler emitted an illegal Rubicon edge {from:?}->{:?}", |
| 47 | + mv.to |
| 48 | + ); |
| 49 | + } |
| 50 | +} |
| 51 | + |
| 52 | +/// KILL-CONDITION: an absorbing column (`Commit`/`Prune`) is cycle-end — it must |
| 53 | +/// schedule NOTHING. If `on_version` returns `Some` here, the lifecycle would |
| 54 | +/// never terminate (a mailbox past Commit would keep being advanced). |
| 55 | +#[test] |
| 56 | +fn absorbing_columns_schedule_no_move() { |
| 57 | + let scheduler = NextPhaseScheduler; |
| 58 | + for phase in [KanbanColumn::Commit, KanbanColumn::Prune] { |
| 59 | + assert!(phase.is_absorbing(), "{phase:?} must be absorbing (precondition)"); |
| 60 | + assert!( |
| 61 | + scheduler |
| 62 | + .on_version(&view_at(phase), DatasetVersion(99), ExecTarget::Native) |
| 63 | + .is_none(), |
| 64 | + "{phase:?} is absorbing — it must schedule no advance" |
| 65 | + ); |
| 66 | + } |
| 67 | +} |
| 68 | + |
| 69 | +/// KILL-CONDITION: the Libet `-550_000 µs` readiness-potential anchor is stamped |
| 70 | +/// ONLY on the Planning→CognitiveWork Σ-commit crossing. If any other tick |
| 71 | +/// carries a non-zero offset (or that crossing carries zero), the Libet anchor |
| 72 | +/// is mis-placed. |
| 73 | +#[test] |
| 74 | +fn libet_anchor_only_on_sigma_commit_crossing() { |
| 75 | + let scheduler = NextPhaseScheduler; |
| 76 | + |
| 77 | + let crossing = scheduler |
| 78 | + .on_version(&view_at(KanbanColumn::Planning), DatasetVersion(1), ExecTarget::Native) |
| 79 | + .expect("Planning advances"); |
| 80 | + assert_eq!(crossing.to, KanbanColumn::CognitiveWork); |
| 81 | + assert_eq!( |
| 82 | + crossing.libet_offset_us, -550_000, |
| 83 | + "the Σ-commit crossing must carry the -550ms Libet anchor" |
| 84 | + ); |
| 85 | + |
| 86 | + for from in [KanbanColumn::CognitiveWork, KanbanColumn::Evaluation, KanbanColumn::Plan] { |
| 87 | + let mv = scheduler |
| 88 | + .on_version(&view_at(from), DatasetVersion(2), ExecTarget::Native) |
| 89 | + .expect("non-absorbing column advances"); |
| 90 | + assert_eq!( |
| 91 | + mv.libet_offset_us, 0, |
| 92 | + "{from:?} is not the Σ-commit crossing — Libet offset must be 0" |
| 93 | + ); |
| 94 | + } |
| 95 | +} |
| 96 | + |
| 97 | +/// KILL-CONDITION: the scheduler is a pure function of (view, version, exec). |
| 98 | +/// Same inputs MUST yield the same move — the determinism the whole |
| 99 | +/// version-tick → lifecycle mechanism stands on. A difference proves hidden |
| 100 | +/// state leaked in. |
| 101 | +#[test] |
| 102 | +fn lowering_is_deterministic() { |
| 103 | + let scheduler = NextPhaseScheduler; |
| 104 | + let a = scheduler |
| 105 | + .on_version(&view_at(KanbanColumn::CognitiveWork), DatasetVersion(7), ExecTarget::Jit) |
| 106 | + .expect("advances"); |
| 107 | + let b = scheduler |
| 108 | + .on_version(&view_at(KanbanColumn::CognitiveWork), DatasetVersion(7), ExecTarget::Jit) |
| 109 | + .expect("advances"); |
| 110 | + assert_eq!(a, b, "same (view, version, exec) must lower to the same move"); |
| 111 | +} |
| 112 | + |
| 113 | +/// KILL-CONDITION: the `exec` backend selector must ride through the lowering |
| 114 | +/// onto the emitted move unchanged (it names where the precipitated plan runs). |
| 115 | +/// If it is dropped or rewritten, the planner's execution-target choice is lost. |
| 116 | +#[test] |
| 117 | +fn exec_target_rides_onto_the_move() { |
| 118 | + let scheduler = NextPhaseScheduler; |
| 119 | + for exec in [ExecTarget::Native, ExecTarget::Jit, ExecTarget::SurrealQl, ExecTarget::Elixir] { |
| 120 | + let mv = scheduler |
| 121 | + .on_version(&view_at(KanbanColumn::Planning), DatasetVersion(3), exec) |
| 122 | + .expect("advances"); |
| 123 | + assert_eq!(mv.exec, exec, "exec target must survive the lowering"); |
| 124 | + } |
| 125 | +} |
0 commit comments