use std::sync::Arc;
use chrono::{TimeZone, Utc};
use parking_lot::RwLock;
use ratatui::Terminal;
use ratatui::backend::TestBackend;
use zero_engine_client::{EngineState, LiveCockpit, Positions, Risk, Source, Stat};
use zero_operator_state::{Label, Snapshot as OperatorSnapshot, StateVector};
use zero_tui::app::log::{EntryKind, LogEntry};
use zero_tui::app::render::render_at;
use zero_tui::{AppState, Mode};
fn frozen() -> chrono::DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 4, 21, 18, 30, 0).unwrap()
}
fn base_state() -> AppState {
let engine = EngineState::shared();
let mut s = AppState::new(engine);
s.log = zero_tui::app::log::ConversationLog::with_capacity(2048);
s.log
.push(LogEntry::new(EntryKind::System, "zero — deterministic test harness").at(frozen()));
s
}
fn grid_to_text(term: &Terminal<TestBackend>) -> String {
let buf = term.backend().buffer();
let mut out = String::new();
for y in 0..buf.area.height {
for x in 0..buf.area.width {
out.push_str(buf[(x, y)].symbol());
}
out.push('\n');
}
out
}
#[test]
fn conversation_mode_empty_engine() {
let backend = TestBackend::new(80, 24);
let mut term = Terminal::new(backend).unwrap();
let state = base_state();
term.draw(|f| render_at(f, &state, frozen())).unwrap();
let snap = grid_to_text(&term);
insta::assert_snapshot!("conversation_empty_engine", snap);
}
#[test]
fn conversation_mode_with_prompt_text() {
let backend = TestBackend::new(80, 24);
let mut term = Terminal::new(backend).unwrap();
let mut state = base_state();
for c in "/help".chars() {
state.prompt.insert(c);
}
term.draw(|f| render_at(f, &state, frozen())).unwrap();
let snap = grid_to_text(&term);
insta::assert_snapshot!("conversation_with_prompt", snap);
}
#[test]
fn positions_mode_with_one_position() {
let engine: Arc<RwLock<EngineState>> = EngineState::shared();
{
let mut e = engine.write();
let mut pos = Positions::default();
pos.items.push(zero_engine_client::Position {
symbol: "BTC".into(),
side: "long".into(),
size: 0.42,
entry: 64_120.5,
mark: Some(64_480.0),
unrealized_pnl: Some(151.13),
unrealized_r: Some(0.82),
..Default::default()
});
e.apply_positions(pos, frozen(), Source::Ws);
}
let mut state = AppState::new(engine);
state.log = zero_tui::app::log::ConversationLog::with_capacity(2048);
state.mode = Mode::Positions;
let backend = TestBackend::new(80, 24);
let mut term = Terminal::new(backend).unwrap();
term.draw(|f| render_at(f, &state, frozen())).unwrap();
let snap = grid_to_text(&term);
insta::assert_snapshot!("positions_one_position", snap);
}
#[test]
fn heat_mode_with_risk_ok() {
let engine: Arc<RwLock<EngineState>> = EngineState::shared();
{
let mut e = engine.write();
let risk = Risk {
account_value: Some(10_034.12),
drawdown_pct: Some(0.8),
daily_loss_usd: Some(20.0),
peak_equity: Some(10_000.0),
open_count: Some(1),
..Default::default()
};
e.apply_risk(risk, frozen(), Source::Ws);
}
let mut state = AppState::new(engine);
state.log = zero_tui::app::log::ConversationLog::with_capacity(2048);
state.mode = Mode::Heat;
let backend = TestBackend::new(80, 24);
let mut term = Terminal::new(backend).unwrap();
term.draw(|f| render_at(f, &state, frozen())).unwrap();
let snap = grid_to_text(&term);
insta::assert_snapshot!("heat_with_risk_ok", snap);
}
#[test]
fn cockpit_mode_renders_live_readiness_board() {
let engine: Arc<RwLock<EngineState>> = EngineState::shared();
{
let mut e = engine.write();
let cockpit: LiveCockpit = serde_json::from_value(serde_json::json!({
"schema_version": "zero.live_cockpit.v1",
"mode": "paper",
"live_mode": "refused",
"ready": false,
"controls_ready": true,
"risk_increasing_allowed": false,
"next_action": "fix preflight check live_executor: mock has no live executor",
"operator_context": {
"operator_id": "mock-operator-id",
"handle": "mock-operator",
"role": "maintainer",
"scope": "local-private"
},
"preflight": {
"schema_version": "zero.live_preflight.v1",
"ready": false,
"live_mode": "refused",
"controls_ready": true,
"summary": {"total": 9, "passed": 8, "failed": 1},
"failed_checks": [
{"name": "live_executor", "status": "fail", "note": "mock has no live executor"}
]
},
"immune": {
"schema_version": "zero.immune.v1",
"risk_increasing_allowed": false,
"summary": {"total": 3, "open": 2, "closed": 1, "warning": 0, "risk_blocking": 2},
"open_breakers": [
{
"name": "dead_man",
"status": "open",
"blocks_risk": true,
"severity": "critical",
"reason": "live executor not configured"
}
]
},
"reconciliation": {
"schema_version": "zero.reconciliation.v1",
"status": "ok",
"risk_increasing_allowed": true,
"reason": "local runtime and Hyperliquid account state are reconciled",
"drifts": 0
},
"certification": {
"schema_version": "zero.live_certification.v1",
"mode": "dry_run",
"passed": true,
"live_start_certified": true,
"summary": {"total": 10, "passed": 10, "failed": 0},
"failed_drills": []
},
"heartbeat": {
"configured": false,
"expired": true,
"last_heartbeat_at": null,
"timeout_s": null
},
"live_records": {
"total": 0,
"accepted": 0,
"refused": 0,
"exchange_error": 0,
"recent": []
},
"operator_actions": {"recent": []}
}))
.expect("cockpit fixture");
e.apply_live_cockpit(cockpit, frozen());
}
let mut state = AppState::new(engine);
state.log = zero_tui::app::log::ConversationLog::with_capacity(2048);
state.mode = Mode::Cockpit;
let backend = TestBackend::new(100, 24);
let mut term = Terminal::new(backend).unwrap();
term.draw(|f| render_at(f, &state, frozen())).unwrap();
let snap = grid_to_text(&term);
for needle in [
"live cockpit",
"live_mode=refused",
"risk_allowed=false",
"operator: handle=mock-operator",
"preflight: passed=8/9 failed=1",
"receipts: total=0 accepted=0 refused=0 exchange_error=0",
" [LIVE]",
] {
assert!(snap.contains(needle), "missing {needle}: {snap}");
}
}
#[test]
fn conversation_mode_with_tilt_label() {
let engine: Arc<RwLock<EngineState>> = EngineState::shared();
{
let mut e = engine.write();
let snap = OperatorSnapshot::new(Label::Tilt, StateVector::default(), frozen(), 1);
e.operator_state = Some(Stat::new(snap, Source::Http).with_as_of(frozen()));
}
let mut state = AppState::new(engine);
state.log = zero_tui::app::log::ConversationLog::with_capacity(2048);
state
.log
.push(LogEntry::new(EntryKind::System, "deterministic test").at(frozen()));
let backend = TestBackend::new(80, 24);
let mut term = Terminal::new(backend).unwrap();
term.draw(|f| render_at(f, &state, frozen())).unwrap();
let snap = grid_to_text(&term);
insta::assert_snapshot!("conversation_with_tilt_label", snap);
}
#[test]
fn state_overlay_over_conversation() {
let engine: Arc<RwLock<EngineState>> = EngineState::shared();
{
let mut e = engine.write();
let snap = OperatorSnapshot::new(Label::Elevated, StateVector::default(), frozen(), 1);
e.operator_state = Some(Stat::new(snap, Source::Http).with_as_of(frozen()));
}
let mut state = AppState::new(engine);
state.log = zero_tui::app::log::ConversationLog::with_capacity(2048);
state
.log
.push(LogEntry::new(EntryKind::System, "deterministic test").at(frozen()));
state.overlay = Some(zero_tui::ActiveOverlay::State);
let backend = TestBackend::new(80, 24);
let mut term = Terminal::new(backend).unwrap();
term.draw(|f| render_at(f, &state, frozen())).unwrap();
let snap = grid_to_text(&term);
insta::assert_snapshot!("state_overlay_elevated", snap);
}
#[test]
fn decisions_mode_is_placeholder() {
let mut state = base_state();
state.mode = Mode::Decisions;
let backend = TestBackend::new(80, 24);
let mut term = Terminal::new(backend).unwrap();
term.draw(|f| render_at(f, &state, frozen())).unwrap();
let snap = grid_to_text(&term);
insta::assert_snapshot!("decisions_placeholder", snap);
}
fn rich_state(mode: Mode) -> AppState {
let engine: Arc<RwLock<EngineState>> = EngineState::shared();
{
let mut e = engine.write();
let mut pos = Positions::default();
pos.items.push(zero_engine_client::Position {
symbol: "BTC".into(),
side: "long".into(),
size: 0.42,
entry: 64_120.5,
mark: Some(64_480.0),
unrealized_pnl: Some(151.13),
unrealized_r: Some(0.82),
..Default::default()
});
e.apply_positions(pos, frozen(), Source::Ws);
e.apply_risk(
Risk {
account_value: Some(10_034.12),
drawdown_pct: Some(2.5),
daily_loss_usd: Some(20.0),
peak_equity: Some(10_000.0),
open_count: Some(1),
..Default::default()
},
frozen(),
Source::Ws,
);
let snap = OperatorSnapshot::new(Label::Elevated, StateVector::default(), frozen(), 1);
e.operator_state = Some(Stat::new(snap, Source::Http).with_as_of(frozen()));
e.on_ws_connected();
}
let mut state = AppState::new(engine);
state.log = zero_tui::app::log::ConversationLog::with_capacity(2048);
state
.log
.push(LogEntry::new(EntryKind::System, "deterministic test").at(frozen()));
state.mode = mode;
state
}
#[test]
fn responsive_60_cols_drops_diagnostics_keeps_risk() {
let state = rich_state(Mode::Conversation);
let backend = TestBackend::new(60, 18);
let mut term = Terminal::new(backend).unwrap();
term.draw(|f| render_at(f, &state, frozen())).unwrap();
let snap = grid_to_text(&term);
assert!(snap.contains("ops:ELEVATED"), "{snap}");
assert!(snap.contains("dd:2.5%"), "{snap}");
insta::assert_snapshot!("responsive_60_cols", snap);
}
#[test]
fn responsive_120_cols_renders_full_status_bar() {
let state = rich_state(Mode::Conversation);
let backend = TestBackend::new(120, 24);
let mut term = Terminal::new(backend).unwrap();
term.draw(|f| render_at(f, &state, frozen())).unwrap();
let snap = grid_to_text(&term);
for needle in [
" [CONV]",
"engine:OK",
"feed:0s",
"rate:?",
"hl:?",
"dd:2.5%",
"ops:ELEVATED",
] {
assert!(snap.contains(needle), "missing {needle}: {snap}");
}
insta::assert_snapshot!("responsive_120_cols", snap);
}
#[test]
fn responsive_200_cols_renders_full_status_bar_with_padding() {
let state = rich_state(Mode::Conversation);
let backend = TestBackend::new(200, 24);
let mut term = Terminal::new(backend).unwrap();
term.draw(|f| render_at(f, &state, frozen())).unwrap();
let snap = grid_to_text(&term);
for needle in [" [CONV]", "engine:OK", "dd:2.5%", "ops:ELEVATED"] {
assert!(snap.contains(needle), "missing {needle}: {snap}");
}
insta::assert_snapshot!("responsive_200_cols", snap);
}
#[test]
fn slash_picker_appears_above_prompt_when_filter_matches() {
let mut state = base_state();
for c in "/st".chars() {
state.prompt.insert(c);
}
state.refresh_picker();
assert!(state.picker.is_some(), "picker must open for /st");
let backend = TestBackend::new(80, 24);
let mut term = Terminal::new(backend).unwrap();
term.draw(|f| render_at(f, &state, frozen())).unwrap();
let snap = grid_to_text(&term);
assert!(snap.contains("› /status"), "picker missing: {snap}");
assert!(snap.contains("> /st"), "prompt row missing: {snap}");
insta::assert_snapshot!("slash_picker_st_filter", snap);
}
#[test]
fn multiline_prompt_renders_continuation_cue() {
let mut state = base_state();
for c in "line one".chars() {
state.prompt.insert(c);
}
state.prompt.insert_newline();
for c in "line two".chars() {
state.prompt.insert(c);
}
let backend = TestBackend::new(80, 24);
let mut term = Terminal::new(backend).unwrap();
term.draw(|f| render_at(f, &state, frozen())).unwrap();
let snap = grid_to_text(&term);
assert!(snap.contains("> line one"), "first row missing: {snap}");
assert!(snap.contains(". line two"), "continuation missing: {snap}");
insta::assert_snapshot!("multiline_prompt_two_rows", snap);
}
#[test]
fn scrolled_conversation_shows_up_arrow_cue() {
let mut state = base_state();
state.log = zero_tui::app::log::ConversationLog::with_capacity(2048);
for i in 0..40 {
state
.log
.push(LogEntry::new(EntryKind::System, format!("entry {i:02}")).at(frozen()));
}
state.scroll_log_up(15);
let backend = TestBackend::new(80, 24);
let mut term = Terminal::new(backend).unwrap();
term.draw(|f| render_at(f, &state, frozen())).unwrap();
let snap = grid_to_text(&term);
assert!(snap.contains('↑'), "scrolled-up indicator missing: {snap}");
insta::assert_snapshot!("conversation_scrolled_up", snap);
}
fn widget_grid(width: u16, height: u16, draw: impl FnOnce(&mut ratatui::Frame<'_>)) -> String {
let backend = TestBackend::new(width, height);
let mut term = Terminal::new(backend).unwrap();
term.draw(draw).unwrap();
grid_to_text(&term)
}
#[test]
fn widget_position_row_long_with_all_fields() {
use zero_tui::widgets::position_row::PositionRow;
let p = zero_engine_client::Position {
symbol: "BTC".into(),
side: "long".into(),
size: 0.42,
entry: 64_120.50,
mark: Some(64_480.0),
unrealized_pnl: Some(151.13),
unrealized_r: Some(0.82),
stop: Some(63_500.0),
target: Some(66_000.0),
..Default::default()
};
let snap = widget_grid(120, 1, |f| {
f.render_widget(
PositionRow {
position: &p,
theme: zero_tui::theme::Theme::default(),
},
f.area(),
);
});
insta::assert_snapshot!("widget_position_row_long", snap);
}
#[test]
fn widget_verdict_block_pass_with_gates() {
use zero_engine_client::models::EvaluationLayer;
use zero_tui::widgets::verdict::VerdictBlock;
let layer = |name: &str, passed: bool| EvaluationLayer {
layer: name.into(),
passed,
value: serde_json::Value::Null,
detail: String::new(),
};
let e = zero_engine_client::Evaluation {
coin: Some("BTC".into()),
direction: Some("LONG".into()),
conviction: Some(0.72),
regime: Some("trending".into()),
consensus: Some(8),
layers: vec![
layer("layer_0", true),
layer("layer_1", true),
layer("layer_2", true),
],
..Default::default()
};
let snap = widget_grid(60, 6, |f| {
f.render_widget(
VerdictBlock {
evaluation: &e,
theme: zero_tui::theme::Theme::default(),
},
f.area(),
);
});
insta::assert_snapshot!("widget_verdict_pass", snap);
}
#[test]
fn widget_verdict_block_empty_is_honest() {
use zero_tui::widgets::verdict::VerdictBlock;
let e = zero_engine_client::Evaluation::default();
let snap = widget_grid(60, 3, |f| {
f.render_widget(
VerdictBlock {
evaluation: &e,
theme: zero_tui::theme::Theme::default(),
},
f.area(),
);
});
insta::assert_snapshot!("widget_verdict_empty", snap);
}
#[test]
fn widget_calibration_above_threshold() {
use zero_tui::widgets::calibration::{CalibrationBar, CalibrationSample};
let sample = CalibrationSample {
predicted: 0.72,
observed: 0.68,
n_samples: 134,
};
let snap = widget_grid(60, 1, |f| {
f.render_widget(
CalibrationBar {
sample: Some(sample),
theme: zero_tui::theme::Theme::default(),
},
f.area(),
);
});
insta::assert_snapshot!("widget_calibration_ok", snap);
}
#[test]
fn verdict_overlay_over_conversation() {
use zero_engine_client::models::EvaluationLayer;
let layer = |name: &str, passed: bool| EvaluationLayer {
layer: name.into(),
passed,
value: serde_json::Value::Null,
detail: String::new(),
};
let eval = zero_engine_client::Evaluation {
coin: Some("BTC".into()),
direction: Some("LONG".into()),
conviction: Some(0.72),
regime: Some("trending".into()),
consensus: Some(8),
layers: vec![
layer("layer_0", true),
layer("layer_1", true),
layer("layer_2", true),
],
..Default::default()
};
let mut state = base_state();
state.overlay = Some(zero_tui::ActiveOverlay::Verdict(Box::new(eval)));
let backend = TestBackend::new(80, 24);
let mut term = Terminal::new(backend).unwrap();
term.draw(|f| render_at(f, &state, frozen())).unwrap();
let snap = grid_to_text(&term);
insta::assert_snapshot!("verdict_overlay_pass_btc", snap);
}
#[test]
fn widget_calibration_below_threshold_is_honest() {
use zero_tui::widgets::calibration::{CalibrationBar, CalibrationSample};
let sample = CalibrationSample {
predicted: 0.70,
observed: 0.65,
n_samples: 12,
};
let snap = widget_grid(72, 1, |f| {
f.render_widget(
CalibrationBar {
sample: Some(sample),
theme: zero_tui::theme::Theme::default(),
},
f.area(),
);
});
insta::assert_snapshot!("widget_calibration_insufficient", snap);
}
#[test]
fn live_stream_pane_with_mixed_events() {
use chrono::TimeZone;
use zero_engine_client::{EngineEvent, models::Risk};
let mut state = base_state();
state.live_stream_visible = true;
let ts = |h, m, s| Utc.with_ymd_and_hms(2026, 4, 21, h, m, s).unwrap();
state.record_engine_event(EngineEvent::Heartbeat(ts(18, 29, 55)));
let risk = Risk {
drawdown_pct: Some(1.25),
daily_loss_usd: Some(5.0),
peak_equity: Some(1000.0),
..Default::default()
};
state.record_engine_event_at(EngineEvent::Risk(Box::new(risk)), ts(18, 29, 57));
state.record_events_lagged_at(3, ts(18, 29, 58));
state.record_engine_event(EngineEvent::Heartbeat(ts(18, 30, 0)));
let backend = TestBackend::new(80, 24);
let mut term = Terminal::new(backend).unwrap();
term.draw(|f| render_at(f, &state, frozen())).unwrap();
let snap = grid_to_text(&term);
insta::assert_snapshot!("live_stream_pane_mixed", snap);
}
fn l3_overlay_state() -> AppState {
use zero_operator_state::friction::FrictionLevel;
let engine: Arc<RwLock<EngineState>> = EngineState::shared();
{
let mut e = engine.write();
let snap = OperatorSnapshot {
label: Label::Tilt,
friction: FrictionLevel::L3,
vector: StateVector::default(),
as_of: frozen(),
version: 7,
};
e.operator_state = Some(Stat::new(snap, Source::Ws).with_as_of(frozen()));
let risk = Risk {
drawdown_pct: Some(4.6),
last_drawdown_alert_pct: Some(5.0),
account_value: Some(9_700.0),
peak_equity: Some(10_000.0),
..Default::default()
};
e.apply_risk(risk, frozen(), Source::Ws);
}
let mut s = AppState::new(engine);
s.log = zero_tui::app::log::ConversationLog::with_capacity(2048);
s.log
.push(LogEntry::new(EntryKind::System, "deterministic test").at(frozen()));
assert!(
matches!(s.overlay, Some(zero_tui::ActiveOverlay::Risk { .. })),
"L3 snapshot must auto-open the risk overlay"
);
s
}
fn l4_overlay_state() -> AppState {
use zero_operator_state::friction::FrictionLevel;
let engine: Arc<RwLock<EngineState>> = EngineState::shared();
{
let mut e = engine.write();
let snap = OperatorSnapshot {
label: Label::Tilt,
friction: FrictionLevel::L4,
vector: StateVector::default(),
as_of: frozen(),
version: 9,
};
e.operator_state = Some(Stat::new(snap, Source::Ws).with_as_of(frozen()));
let risk = Risk {
drawdown_pct: Some(5.4),
last_drawdown_alert_pct: Some(5.0),
account_value: Some(9_460.0),
peak_equity: Some(10_000.0),
halted: true,
halt_reason: Some("drawdown limit hit".into()),
..Default::default()
};
e.apply_risk(risk, frozen(), Source::Ws);
}
let mut s = AppState::new(engine);
s.log = zero_tui::app::log::ConversationLog::with_capacity(2048);
s.log
.push(LogEntry::new(EntryKind::System, "deterministic test").at(frozen()));
assert!(
matches!(s.overlay, Some(zero_tui::ActiveOverlay::Risk { .. })),
"L4 snapshot must auto-open the risk overlay"
);
s
}
#[test]
fn risk_overlay_l3_proximity() {
let state = l3_overlay_state();
let backend = TestBackend::new(80, 24);
let mut term = Terminal::new(backend).unwrap();
term.draw(|f| render_at(f, &state, frozen())).unwrap();
let snap = grid_to_text(&term);
insta::assert_snapshot!("risk_overlay_l3_proximity", snap);
}
#[test]
fn risk_overlay_l4_halted_banner() {
let state = l4_overlay_state();
let backend = TestBackend::new(80, 24);
let mut term = Terminal::new(backend).unwrap();
term.draw(|f| render_at(f, &state, frozen())).unwrap();
let snap = grid_to_text(&term);
insta::assert_snapshot!("risk_overlay_l4_halted", snap);
}