#![cfg(test)]
use std::time::Duration;
use chrono::{DateTime, Utc};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use optionstratlib::chains::OptionData;
use optionstratlib::chains::chain::OptionChain;
use optionstratlib::prelude::{Decimal, Positive, VolatilitySmile};
use optionstratlib::visualization::{GraphData, Series2D, Surface3D};
use ratatui::Terminal;
use ratatui::backend::TestBackend;
use ratatui::buffer::Buffer;
use ratatui::style::{Color, Modifier};
use tokio::sync::mpsc;
use crate::app::{
App, EventBridge, LiveScreen, LiveState, Mode, ScreenLoad, SourceBinding, is_screen_reachable,
};
use crate::chain::{
AliasCatalog, ChainFetch, ChainSource, ChainStore, DepthLadder, DepthLevel, DepthStatus,
ExpirySource, InstrumentKey, MarketUpdate, ProviderId, StreamHealth, depth_continues,
};
use crate::config::ThemeChoice;
use crate::event::{AppEvent, Command};
use crate::providers::deribit::{
fixture_btc_chain_fetch_named, fixture_btc_depth_ladder, fixture_btc_stream_updates,
};
use crate::providers::{ChainCapability, GreeksCapability, ProviderCapabilities};
use crate::ui::chain::draw as draw_chain;
use crate::ui::golden::{GOLDEN_HEIGHT, GOLDEN_WIDTH, assert_golden, buffer_to_text};
use crate::ui::graph::{EmptyReason, GraphProjection, project};
use crate::ui::render;
use crate::ui::theme::Theme;
use crate::ui::view::ViewState;
const AS_OF: i64 = 1_700_000_000;
#[track_caller]
fn pid(id: &str) -> ProviderId {
match ProviderId::new(id) {
Ok(p) => p,
Err(e) => panic!("expected a valid provider id `{id}`, got: {e}"),
}
}
#[track_caller]
fn utc(secs: i64) -> DateTime<Utc> {
match DateTime::<Utc>::from_timestamp(secs, 0) {
Some(t) => t,
None => panic!("invalid test timestamp: {secs}"),
}
}
#[track_caller]
fn pos(value: f64) -> Positive {
match Positive::new(value) {
Ok(p) => p,
Err(e) => panic!("invalid test positive `{value}`: {e}"),
}
}
fn caps() -> ProviderCapabilities {
ProviderCapabilities::builder()
.chain(ChainCapability::Assemble)
.depth(true)
.greeks(GreeksCapability::Provided)
.build()
}
fn theme() -> Theme {
Theme::resolve(ThemeChoice::Auto, false)
}
#[track_caller]
fn terminal(width: u16, height: u16) -> Terminal<TestBackend> {
match Terminal::new(TestBackend::new(width, height)) {
Ok(t) => t,
Err(e) => panic!("TestBackend construction failed: {e}"),
}
}
fn live_ready(store: ChainStore, provider: ProviderId) -> LiveState {
let mut live = LiveState::new(
SourceBinding::new(provider, caps(), StreamHealth::Live),
store,
);
live.screen = LiveScreen::Chain;
live.load = ScreenLoad::Ready;
live
}
#[track_caller]
fn render_chain_body(live: &LiveState) -> String {
let mut term = terminal(GOLDEN_WIDTH, GOLDEN_HEIGHT);
match term.draw(|frame| draw_chain(live, frame, frame.area(), theme(), 0, utc(AS_OF))) {
Ok(_) => {}
Err(e) => panic!("chain golden draw failed: {e}"),
}
buffer_to_text(term.backend().buffer())
}
#[track_caller]
fn live_of(app: &App) -> &LiveState {
match &app.mode {
Mode::Live(live) => live,
Mode::Replay(_) => panic!("expected a live app"),
}
}
#[test]
fn test_live_path_deribit_fixture_poll_stream_merge_renders_btc_atm_golden() {
let mut store = ChainStore::seed(
fixture_btc_chain_fetch_named("BTC"),
ChainSource::Merged,
Duration::from_secs(2),
utc(AS_OF),
);
store.apply_poll(fixture_btc_chain_fetch_named("BTC"), utc(AS_OF));
let live = live_ready(store, pid("deribit"));
let (tx, _rx) = mpsc::channel::<Command>(16);
let mut app = App::new(Mode::Live(live), ThemeChoice::Auto, tx);
app.mark_drawn();
let updates = fixture_btc_stream_updates(utc(AS_OF));
assert!(
!updates.is_empty(),
"the recorded fixture yields streaming overlay updates"
);
let quotes: Vec<MarketUpdate> = updates
.into_iter()
.filter(|u| matches!(u, MarketUpdate::Quote(_)))
.collect();
assert!(!quotes.is_empty(), "the overlay carries quotes to merge");
let (mut bridge, senders) = EventBridge::new(64);
let mut sink = senders.market_update_sink();
for update in quotes {
assert_eq!(
sink.publish_coalesced(update),
crate::providers::SendState::Open,
"the overlay update is accepted by the bounded coalesced channel",
);
}
bridge.pump(&mut app, |_command| {});
assert!(
app.dirty,
"the real-seam overlay fold changed store rows and marked the frame dirty"
);
app.mark_drawn();
bridge.pump(&mut app, |_command| {});
assert!(
!app.dirty,
"no residual staged updates after the coalesced flush"
);
assert_golden(
"chain",
"deribit_btc_atm.txt",
&render_chain_body(live_of(&app)),
);
}
#[test]
fn test_render_is_provider_id_agnostic_faux_renders_identical_golden() {
assert!(
!pid("faux").is_reserved(),
"`faux` is an external, non-reserved provider id (parity with a built-in)"
);
let fetch = fixture_btc_chain_fetch_named("BTC");
let store = ChainStore::seed(
fetch,
ChainSource::Merged,
Duration::from_secs(2),
utc(AS_OF),
);
let live = live_ready(store, pid("faux"));
assert_golden("chain", "deribit_btc_atm.txt", &render_chain_body(&live));
}
#[test]
fn test_draw_path_runs_without_async_runtime_and_mutates_nothing() {
let fetch = fixture_btc_chain_fetch_named("BTC");
let store = ChainStore::seed(
fetch,
ChainSource::Merged,
Duration::from_secs(2),
utc(AS_OF),
);
let live = live_ready(store, pid("deribit"));
let (tx, _rx) = mpsc::channel::<Command>(16);
let mut app = App::new(Mode::Live(live), ThemeChoice::Auto, tx);
app.mark_drawn();
let before = (app.dirty, app.help_open, app.should_quit, app.tick_count);
let mut view = crate::ui::view::ViewState::new();
view.sync(&app);
let mut term = terminal(GOLDEN_WIDTH, GOLDEN_HEIGHT);
match term.draw(|frame| crate::ui::render(&app, &view, frame)) {
Ok(_) => {}
Err(e) => panic!("full-frame render failed: {e}"),
}
let after = (app.dirty, app.help_open, app.should_quit, app.tick_count);
assert_eq!(
before, after,
"the draw path must not mutate app state (purity enforced by the &App signature)"
);
}
fn key(c: char) -> KeyEvent {
KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE)
}
fn seed_store(chain: OptionChain) -> ChainStore {
ChainStore::seed(
ChainFetch::new(
chain,
ExpirySource::new("BTC", utc(AS_OF), pid("deribit")),
AliasCatalog::new(),
),
ChainSource::Merged,
Duration::from_secs(2),
utc(AS_OF),
)
}
#[track_caller]
fn render_live_frame(state: LiveState) -> String {
let (tx, _rx) = mpsc::channel::<Command>(16);
let mut app = App::new(Mode::Live(state), ThemeChoice::Auto, tx);
app.mark_drawn();
let mut view = ViewState::new();
view.sync(&app);
let mut term = terminal(GOLDEN_WIDTH, GOLDEN_HEIGHT);
match term.draw(|frame| render(&app, &view, frame)) {
Ok(_) => {}
Err(e) => panic!("live frame render failed: {e}"),
}
buffer_to_text(term.backend().buffer())
}
fn smile_chain() -> OptionChain {
const SKEW: [(f64, f64); 7] = [
(56_000.0, 0.72),
(58_000.0, 0.66),
(60_000.0, 0.60),
(62_000.0, 0.55),
(64_000.0, 0.58),
(66_000.0, 0.63),
(68_000.0, 0.69),
];
let mut chain = OptionChain::new("BTC", pos(62_000.0), "2025-06-27".to_owned(), None, None);
for (strike, iv) in SKEW {
let mut od = OptionData {
strike_price: pos(strike),
call_bid: Some(pos(3_000.0)),
call_ask: Some(pos(3_100.0)),
put_bid: Some(pos(2_000.0)),
put_ask: Some(pos(2_100.0)),
implied_volatility: pos(iv),
..Default::default()
};
od.set_mid_prices();
let _ = chain.options.insert(od);
}
chain
}
fn surface_state(chain: OptionChain, view_toggles: u8) -> LiveState {
let mut live = LiveState::new(
SourceBinding::new(pid("deribit"), caps(), StreamHealth::Live),
seed_store(chain),
);
live.screen = LiveScreen::Surface;
live.load = ScreenLoad::Ready;
for _ in 0..view_toggles {
let _ = crate::ui::surface::handle_key(&mut live, key('x'));
}
live
}
fn empty_iv_chain() -> OptionChain {
let mut chain = OptionChain::new("BTC", pos(60_000.0), "2025-06-27".to_owned(), None, None);
let _ = chain.options.insert(OptionData {
strike_price: pos(60_000.0),
implied_volatility: Positive::ZERO,
..Default::default()
});
chain
}
fn depth_chain() -> OptionChain {
let mut chain = OptionChain::new("BTC", pos(60_000.0), "2025-06-27".to_owned(), None, None);
let mut od = OptionData {
strike_price: pos(60_000.0),
call_bid: Some(pos(0.05)),
call_ask: Some(pos(0.06)),
put_bid: Some(pos(0.04)),
put_ask: Some(pos(0.05)),
implied_volatility: pos(0.5),
..Default::default()
};
od.set_mid_prices();
let _ = chain.options.insert(od);
chain
}
fn no_depth_caps() -> ProviderCapabilities {
ProviderCapabilities::builder()
.chain(ChainCapability::Assemble)
.depth(false)
.greeks(GreeksCapability::Provided)
.build()
}
fn depth_ladder_state() -> LiveState {
let mut live = LiveState::new(
SourceBinding::new(pid("deribit"), caps(), StreamHealth::Live),
seed_store(depth_chain()),
);
live.screen = LiveScreen::Depth;
live.load = ScreenLoad::Ready;
live.selection.focused_row = Some(0);
if let Some(depth_key) = live.selected_depth_key() {
let _ = live
.depth_store
.apply(fixture_btc_depth_ladder(depth_key, utc(AS_OF)));
}
live
}
fn depth_unavailable_state() -> LiveState {
let mut live = LiveState::new(
SourceBinding::new(pid("deribit"), no_depth_caps(), StreamHealth::Live),
seed_store(depth_chain()),
);
live.screen = LiveScreen::Depth;
live.load = ScreenLoad::Ready;
live
}
#[test]
fn test_surface_smile_golden() {
assert_golden(
"surface",
"deribit_btc_smile.txt",
&render_live_frame(surface_state(smile_chain(), 0)),
);
}
#[test]
fn test_surface_curve_golden() {
assert_golden(
"surface",
"deribit_btc_curve.txt",
&render_live_frame(surface_state(smile_chain(), 1)),
);
}
#[test]
fn test_surface_heatmap_golden() {
let text = render_live_frame(surface_state(smile_chain(), 2));
assert!(
text.contains("top") && text.contains("bottom"),
"the heat-map header marks the vol-axis direction: {text:?}",
);
assert_golden("surface", "deribit_btc_surface.txt", &text);
}
#[test]
fn test_surface_empty_golden() {
let text = render_live_frame(surface_state(empty_iv_chain(), 0));
assert!(
text.contains("insufficient IV"),
"the empty state names the insufficient-IV cause: {text:?}",
);
assert_golden("surface", "surface_empty.txt", &text);
}
#[test]
fn test_depth_ladder_golden() {
let text = render_live_frame(depth_ladder_state());
assert!(
text.contains("bid") && text.contains("ask"),
"the ladder shows both sides with text labels (NO_COLOR-safe): {text:?}",
);
assert!(
text.contains("BTC-27JUN25-60000-C"),
"the venue instrument names the ladder title: {text:?}",
);
assert!(
text.contains("spread"),
"the spread footer renders: {text:?}"
);
assert!(
text.contains("0.049") && text.contains("0.048"),
"sub-unit venue prices render at their own decimal scale, not truncated: {text:?}",
);
assert!(
!text.contains(" 0.04 "),
"the old 2-decimal truncation is gone: {text:?}",
);
assert_golden("depth", "deribit_btc_ladder.txt", &text);
}
#[test]
fn test_depth_unavailable_golden() {
let text = render_live_frame(depth_unavailable_state());
assert!(
text.contains("not available"),
"the unavailable body names itself: {text:?}",
);
assert_golden("depth", "depth_unavailable.txt", &text);
}
fn depth_focused_state() -> LiveState {
let mut live = LiveState::new(
SourceBinding::new(pid("deribit"), caps(), StreamHealth::Live),
seed_store(depth_chain()),
);
live.screen = LiveScreen::Depth;
live.load = ScreenLoad::Ready;
live.selection.focused_row = Some(0);
live
}
fn depth_app(state: LiveState) -> App {
let (tx, _rx) = mpsc::channel::<Command>(16);
App::new(Mode::Live(state), ThemeChoice::Auto, tx)
}
fn depth_ladder_cid(key: &InstrumentKey, change_id: Option<u64>) -> DepthLadder {
let mut ladder = fixture_btc_depth_ladder(key.clone(), utc(AS_OF));
ladder.change_id = change_id;
ladder
}
fn depth_ladder_levels(
key: &InstrumentKey,
change_id: Option<u64>,
bids: &[(f64, f64)],
asks: &[(f64, f64)],
) -> DepthLadder {
let mut ladder = fixture_btc_depth_ladder(key.clone(), utc(AS_OF));
ladder.change_id = change_id;
ladder.bids = bids
.iter()
.map(|&(p, s)| DepthLevel {
price: pos(p),
size: pos(s),
})
.collect();
ladder.asks = asks
.iter()
.map(|&(p, s)| DepthLevel {
price: pos(p),
size: pos(s),
})
.collect();
ladder
}
fn fold_depth(app: &mut App, key: &InstrumentKey, change_id: Option<u64>) {
app.on_event(AppEvent::Market(MarketUpdate::Depth(depth_ladder_cid(
key, change_id,
))));
}
#[track_caller]
fn depth_key(state: &LiveState) -> InstrumentKey {
match state.selected_depth_key() {
Some(key) => key,
None => panic!("the focused depth state resolves a selected contract"),
}
}
#[track_caller]
fn render_depth_buffer(live: &LiveState) -> Buffer {
let mut term = terminal(GOLDEN_WIDTH, GOLDEN_HEIGHT);
match term.draw(|frame| crate::ui::depth::draw(live, frame, frame.area(), theme(), 0)) {
Ok(_) => {}
Err(e) => panic!("depth draw failed: {e}"),
}
term.backend().buffer().clone()
}
fn depth_text(live: &LiveState) -> String {
buffer_to_text(&render_depth_buffer(live))
}
fn body_has_dimmed_digit(buffer: &Buffer, height: u16) -> bool {
let width = buffer.area().width;
for y in 2..(height / 2) {
for x in 0..width {
if let Some(cell) = buffer.cell((x, y))
&& cell.symbol().chars().any(|c| c.is_ascii_digit())
&& cell.modifier.contains(Modifier::DIM)
{
return true;
}
}
}
false
}
#[track_caller]
fn render_app_text(app: &App) -> String {
let mut view = ViewState::new();
view.sync(app);
let mut term = terminal(GOLDEN_WIDTH, GOLDEN_HEIGHT);
match term.draw(|frame| render(app, &view, frame)) {
Ok(_) => {}
Err(e) => panic!("full-frame render failed: {e}"),
}
buffer_to_text(term.backend().buffer())
}
#[test]
fn test_depth_regression_flips_resync_badge_and_dims_ladder() {
let mut app = depth_app(depth_focused_state());
let key = depth_key(live_of(&app));
fold_depth(&mut app, &key, Some(10));
fold_depth(&mut app, &key, Some(3));
let live = live_of(&app);
let book = match live.depth_store.book(&key) {
Some(b) => b,
None => panic!("the folded book is retrievable by the selected key"),
};
assert_eq!(
book.status(),
DepthStatus::ResyncNeeded,
"a change_id regression flips the visible book to ResyncNeeded",
);
let buffer = render_depth_buffer(live);
let text = buffer_to_text(&buffer);
assert!(
text.contains("resyncing"),
"the resync badge renders through the real draw: {text:?}",
);
assert!(
body_has_dimmed_digit(&buffer, GOLDEN_HEIGHT),
"the ladder body dims under resync — never a bright, trusted-looking book",
);
assert!(
text.contains("bid") && text.contains("ask"),
"the dimmed ladder still renders both sides: {text:?}",
);
}
#[test]
fn test_depth_lost_sequence_flips_resync() {
let mut app = depth_app(depth_focused_state());
let key = depth_key(live_of(&app));
fold_depth(&mut app, &key, Some(10));
fold_depth(&mut app, &key, None);
let live = live_of(&app);
match live.depth_store.book(&key) {
Some(book) => assert_eq!(
book.status(),
DepthStatus::ResyncNeeded,
"losing the change_id sequence flags a resync",
),
None => panic!("book present"),
}
assert!(
depth_text(live).contains("resyncing"),
"the lost-sequence resync badges the ladder",
);
}
#[test]
fn test_depth_forward_skip_stays_fresh_and_bright() {
assert!(
depth_continues(Some(10), Some(50)),
"a forward skip continues (benign coalesced snapshot) — the reconciled model",
);
assert!(
!depth_continues(Some(10), Some(9)),
"only a regression is a discontinuity",
);
assert!(
!depth_continues(Some(10), None),
"only a lost sequence is a discontinuity",
);
let mut app = depth_app(depth_focused_state());
let key = depth_key(live_of(&app));
fold_depth(&mut app, &key, Some(10));
fold_depth(&mut app, &key, Some(50));
let live = live_of(&app);
match live.depth_store.book(&key) {
Some(book) => assert_eq!(
book.status(),
DepthStatus::Fresh,
"a forward change_id skip stays Fresh — no false resync",
),
None => panic!("book present"),
}
let buffer = render_depth_buffer(live);
let text = buffer_to_text(&buffer);
assert!(
!text.contains("resyncing"),
"a benign skip shows no resync badge: {text:?}",
);
assert!(
!body_has_dimmed_digit(&buffer, GOLDEN_HEIGHT),
"a benign skip leaves the ladder body bright (only the footer is dim)",
);
}
#[test]
fn test_depth_gap_never_renders_a_torn_book() {
let mut app = depth_app(depth_focused_state());
let key = depth_key(live_of(&app));
app.on_event(AppEvent::Market(MarketUpdate::Depth(depth_ladder_levels(
&key,
Some(10),
&[(60_000.0, 1.0), (59_990.0, 2.0)],
&[(60_010.0, 1.0)],
))));
app.on_event(AppEvent::Market(MarketUpdate::Depth(depth_ladder_levels(
&key,
Some(3),
&[(50_000.0, 3.0)],
&[(50_010.0, 4.0)],
))));
let live = live_of(&app);
let book = match live.depth_store.book(&key) {
Some(b) => b,
None => panic!("book present"),
};
let ladder = book.ladder();
assert_eq!(ladder.bids.len(), 1, "the book is B's complete snapshot");
assert_eq!(
ladder.bids.first().map(|l| l.price),
Some(pos(50_000.0)),
"the best bid is B's, not A's 60 000 (no stitch)",
);
assert_eq!(ladder.asks.first().map(|l| l.price), Some(pos(50_010.0)));
assert!(
!ladder.bids.iter().any(|l| l.price == pos(60_000.0)),
"A's levels are gone — the frame is not torn across the gap",
);
assert!(
book.needs_resync(),
"the discontinuity is honestly flagged for the badge/dim, not silently trusted",
);
let text = depth_text(live);
assert!(text.contains("50000.00"), "B's best bid renders: {text:?}");
assert!(text.contains("resyncing"), "badged resyncing");
}
#[test]
fn test_depth_resync_state_keeps_rendering_frames() {
let mut app = depth_app(depth_focused_state());
let key = depth_key(live_of(&app));
fold_depth(&mut app, &key, Some(1_000));
for round in 0..5u64 {
let cid = 1_000 - (round + 1) * 10; fold_depth(&mut app, &key, Some(cid));
app.on_event(AppEvent::Tick);
let text = render_app_text(&app);
assert!(
text.contains("resyncing"),
"round {round}: the resync badge is never dropped for a stale-bright frame: {text:?}",
);
assert!(
text.contains("bid") && text.contains("ask"),
"round {round}: the ladder still renders (never blank): {text:?}",
);
}
}
#[test]
fn test_depth_burst_through_the_real_bridge_coalesces_latest_value_wins() {
let mut app = depth_app(depth_focused_state());
let key = depth_key(live_of(&app));
let (mut bridge, senders) = EventBridge::new(64);
let mut sink = senders.market_update_sink();
const BURST: u64 = 512; for cid in 1..=BURST {
let _ = sink.publish_coalesced(MarketUpdate::Depth(depth_ladder_cid(&key, Some(cid))));
}
let _ = sink.flush();
bridge.pump(&mut app, |_command| {});
let _ = sink.flush();
bridge.pump(&mut app, |_command| {});
let live = live_of(&app);
assert_eq!(
live.depth_store.len(),
1,
"one bounded book after the real-path burst"
);
match live.depth_store.book(&key) {
Some(book) => assert_eq!(
book.ladder().change_id,
Some(BURST),
"coalescing is latest-value-wins: the newest change_id survives the saturation",
),
None => panic!("the burst book exists"),
}
let text = render_app_text(&app);
assert!(
!text.is_empty(),
"a frame renders after the real-path burst"
);
}
#[test]
fn test_depth_burst_folds_without_freezing_a_frame_and_stays_bounded() {
let mut app = depth_app(depth_focused_state());
let key = depth_key(live_of(&app));
const BURST: u64 = 128;
for cid in 1..=BURST {
fold_depth(&mut app, &key, Some(cid)); let text = render_app_text(&app);
assert!(
!text.is_empty(),
"cid {cid}: a frame renders during the burst — the draw never stalls",
);
}
let live = live_of(&app);
assert_eq!(
live.depth_store.len(),
1,
"the bounded store holds ONE book for the single contract — memory does not grow",
);
match live.depth_store.book(&key) {
Some(book) => {
assert_eq!(
book.status(),
DepthStatus::Fresh,
"an advancing burst stays Fresh (no false resync)",
);
assert_eq!(
book.ladder().change_id,
Some(BURST),
"latest-value-wins: the newest frame is shown",
);
}
None => panic!("book present"),
}
assert!(
!body_has_dimmed_digit(&render_depth_buffer(live), GOLDEN_HEIGHT),
"a continuous burst renders bright (dim is reserved for the honest resync)",
);
}
#[test]
fn test_depth_screen_is_capability_gated_and_unavailable_renders_honestly() {
assert!(
!is_screen_reachable(LiveScreen::Depth, &no_depth_caps()),
"a depth-less venue cannot reach the Depth screen",
);
assert!(
is_screen_reachable(LiveScreen::Depth, &caps()),
"a depth-capable venue reaches the Depth screen",
);
let mut depthless = LiveState::new(
SourceBinding::new(pid("deribit"), no_depth_caps(), StreamHealth::Live),
seed_store(depth_chain()),
);
assert_eq!(depthless.screen, LiveScreen::Chain);
assert!(
!depthless.set_screen(LiveScreen::Depth),
"set_screen refuses the unreachable Depth screen (capability-gated)",
);
assert_eq!(
depthless.screen,
LiveScreen::Chain,
"the screen never switched to the gated Depth",
);
let text = render_live_frame(depth_unavailable_state());
assert!(
text.contains("not available"),
"the unavailable body names itself: {text:?}",
);
assert!(
!text.contains("resyncing") && !text.contains("spread"),
"no fabricated ladder machinery leaks into the unavailable state: {text:?}",
);
}
fn parity_smile_chain() -> OptionChain {
const SKEW: [(f64, f64); 5] = [
(56_000.0, 0.72),
(58_000.0, 0.66),
(60_000.0, 0.60),
(62_000.0, 0.55),
(64_000.0, 0.58),
];
let mut chain = OptionChain::new("BTC", pos(60_000.0), "2025-06-27".to_owned(), None, None);
for (strike, iv) in SKEW {
let _ = chain.options.insert(OptionData {
strike_price: pos(strike),
implied_volatility: pos(iv),
..Default::default()
});
}
chain
}
#[track_caller]
fn series_xy(graph: &GraphData) -> (Vec<Decimal>, Vec<Decimal>) {
match graph {
GraphData::Series(series) => (series.x.clone(), series.y.clone()),
other => panic!("expected a smile Series, got {other:?}"),
}
}
#[test]
fn test_surface_smile_renders_optionstratlib_smile_output() {
let store = seed_store(parity_smile_chain());
let live = LiveState::new(
SourceBinding::new(pid("deribit"), caps(), StreamHealth::Live),
store,
);
let built = live.surface.active_graph_data();
let (built_x, built_y) = series_xy(built);
assert!(
!built_x.is_empty(),
"the parity chain builds a non-empty smile (not two compared empties)",
);
let oracle: GraphData = live.store.chain().smile().into();
let (oracle_x, oracle_y) = series_xy(&oracle);
assert_eq!(
built_x, oracle_x,
"the rendered smile's strike axis is exactly smile()'s x",
);
assert_eq!(
built_y, oracle_y,
"the rendered smile's IV axis is exactly smile()'s y (pass-through, not recomputed)",
);
assert_eq!(
built_x.len(),
5,
"one smile point per reliable strike (the five seeded)",
);
}
#[test]
fn test_surface_insufficient_iv_renders_empty_state_off_the_draw_path() {
let state = surface_state(empty_iv_chain(), 0);
assert_eq!(
project(state.surface.active_graph_data()).empty_reason(),
Some(EmptyReason::NoData),
"the IV-sparse expiry routes to the insufficient-IV empty projection",
);
let text = render_live_frame(state);
assert!(
text.contains("insufficient IV"),
"the empty state names the insufficient-IV cause: {text:?}",
);
}
fn degenerate_series() -> GraphData {
GraphData::Series(Series2D {
x: vec![Decimal::ZERO],
y: Vec::new(),
..Series2D::default()
})
}
fn degenerate_surface() -> GraphData {
GraphData::GraphSurface(Surface3D {
x: vec![Decimal::ZERO],
y: Vec::new(),
z: Vec::new(),
..Surface3D::default()
})
}
#[track_caller]
fn render_surface_with(state: &LiveState, projection: &GraphProjection) -> String {
let mut term = terminal(GOLDEN_WIDTH, GOLDEN_HEIGHT);
match term.draw(|frame| {
crate::ui::surface::draw(state, projection, frame, frame.area(), theme(), 0);
}) {
Ok(_) => {}
Err(e) => panic!("surface draw failed: {e}"),
}
buffer_to_text(term.backend().buffer())
}
#[test]
fn test_surface_curve_err_renders_degenerate_state_not_a_corrupt_chart() {
let projection = project(°enerate_series());
assert_eq!(
projection.empty_reason(),
Some(EmptyReason::Degenerate),
"the curve-Err sentinel projects degenerate geometry",
);
let state = surface_state(smile_chain(), 1); let text = render_surface_with(&state, &projection);
assert!(
text.contains("degenerate geometry"),
"the Err path renders the distinguishable degenerate-geometry state: {text:?}",
);
assert!(
!text.contains("insufficient IV"),
"the Err path is NOT folded into the insufficient-IV state (P3-01)",
);
}
#[test]
fn test_surface_surface_err_renders_degenerate_state_not_a_corrupt_chart() {
let projection = project(°enerate_surface());
assert_eq!(
projection.empty_reason(),
Some(EmptyReason::Degenerate),
"the surface-Err sentinel projects degenerate geometry",
);
let state = surface_state(smile_chain(), 2); let text = render_surface_with(&state, &projection);
assert!(
text.contains("degenerate geometry"),
"the surface Err path renders the degenerate-geometry state: {text:?}",
);
}
#[track_caller]
fn render_chain_buffer_themed(live: &LiveState, no_color: bool) -> Buffer {
let theme = Theme::resolve(ThemeChoice::Auto, no_color);
let mut term = terminal(GOLDEN_WIDTH, GOLDEN_HEIGHT);
match term.draw(|frame| draw_chain(live, frame, frame.area(), theme, 0, utc(AS_OF))) {
Ok(_) => {}
Err(e) => panic!("chain themed draw failed: {e}"),
}
term.backend().buffer().clone()
}
fn any_cell_colored(buffer: &Buffer) -> bool {
buffer
.content()
.iter()
.any(|cell| cell.fg != Color::Reset || cell.bg != Color::Reset)
}
#[test]
fn test_chain_no_color_golden_pair_is_marker_legible_and_color_is_stripped() {
let store = ChainStore::seed(
fixture_btc_chain_fetch_named("BTC"),
ChainSource::Merged,
Duration::from_secs(2),
utc(AS_OF),
);
let live = live_ready(store, pid("deribit"));
let color = render_chain_buffer_themed(&live, false);
let no_color = render_chain_buffer_themed(&live, true);
let color_text = buffer_to_text(&color);
let no_color_text = buffer_to_text(&no_color);
assert_golden("chain", "deribit_btc_atm.txt", &color_text);
assert_golden("chain", "deribit_btc_atm_no_color.txt", &no_color_text);
assert_eq!(
color_text, no_color_text,
"NO_COLOR carries no glyph the color mode lacks: the visible marker set is identical",
);
assert!(
any_cell_colored(&color),
"the color render sets a semantic foreground somewhere",
);
assert!(
!any_cell_colored(&no_color),
"NO_COLOR strips every foreground/background color (falls back to markers + intensity)",
);
}
#[test]
fn test_too_small_terminal_golden_widen_hint() {
let store = ChainStore::seed(
fixture_btc_chain_fetch_named("BTC"),
ChainSource::Merged,
Duration::from_secs(2),
utc(AS_OF),
);
let live = live_ready(store, pid("deribit"));
let (tx, _rx) = mpsc::channel::<Command>(16);
let mut app = App::new(Mode::Live(live), ThemeChoice::Auto, tx);
app.mark_drawn();
let mut view = ViewState::new();
view.sync(&app);
let mut term = terminal(30, 6);
match term.draw(|frame| render(&app, &view, frame)) {
Ok(_) => {}
Err(e) => panic!("too-small render failed: {e}"),
}
let text = buffer_to_text(term.backend().buffer());
assert!(
text.contains("widen the terminal"),
"the too-small state names its recovery: {text:?}",
);
assert_golden("common", "too_small.txt", &text);
}
fn surface_stale_state(toggles: u8) -> LiveState {
let mut live = surface_state(smile_chain(), toggles);
live.source.health = StreamHealth::Stale { since: utc(AS_OF) };
live
}
const RAMP_GLYPHS: [char; 7] = ['·', ':', '+', '*', '#', '%', '@'];
fn any_ramp_cell_colored(buffer: &Buffer) -> bool {
buffer.content().iter().any(|cell| {
cell.symbol()
.chars()
.next()
.is_some_and(|c| RAMP_GLYPHS.contains(&c))
&& cell.fg != Color::Reset
})
}
#[track_caller]
fn render_live_buffer(state: LiveState) -> Buffer {
let (tx, _rx) = mpsc::channel::<Command>(16);
let mut app = App::new(Mode::Live(state), ThemeChoice::Auto, tx);
app.mark_drawn();
let mut view = ViewState::new();
view.sync(&app);
let mut term = terminal(GOLDEN_WIDTH, GOLDEN_HEIGHT);
match term.draw(|frame| render(&app, &view, frame)) {
Ok(_) => {}
Err(e) => panic!("live buffer render failed: {e}"),
}
term.backend().buffer().clone()
}
#[test]
fn test_surface_stale_golden() {
let text = render_live_frame(surface_stale_state(0));
assert!(
text.contains("stale"),
"the surface title badges the dropped stream: {text:?}",
);
assert!(
text.contains("strike"),
"the last-known smile still renders (never blanked): {text:?}",
);
assert_golden("surface", "deribit_btc_smile_stale.txt", &text);
}
#[test]
fn test_surface_heat_view_stale_golden_dims_the_grid() {
let stale_text = render_live_frame(surface_stale_state(2));
assert!(
stale_text.contains("stale"),
"the heat-view title badges the dropped stream: {stale_text:?}",
);
assert_golden("surface", "deribit_btc_surface_stale.txt", &stale_text);
let live_grid = render_live_buffer(surface_state(smile_chain(), 2));
let stale_grid = render_live_buffer(surface_stale_state(2));
assert!(
any_ramp_cell_colored(&live_grid),
"the live heat grid tints high-intensity cells (bright)",
);
assert!(
!any_ramp_cell_colored(&stale_grid),
"the stale heat grid dims every cell — no colored ramp cell survives",
);
}
#[test]
fn test_live_error_states_offer_the_r_reconnect_key_not_reload() {
let store = || {
ChainStore::seed(
fixture_btc_chain_fetch_named("BTC"),
ChainSource::Merged,
Duration::from_secs(2),
utc(AS_OF),
)
};
for screen in [LiveScreen::Chain, LiveScreen::Depth, LiveScreen::Surface] {
let mut live = LiveState::new(
SourceBinding::new(pid("deribit"), caps(), StreamHealth::Live),
store(),
);
live.screen = screen;
live.load = ScreenLoad::Error {
message: "provider unreachable".to_owned(),
};
let text = render_live_frame(live);
assert!(
text.contains("press r to reconnect"),
"the {screen:?} Live error offers the `r` reconnect key: {text:?}",
);
}
}