#![cfg(test)]
use std::path::{Path, PathBuf};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::Terminal;
use ratatui::backend::TestBackend;
use tokio::sync::mpsc;
use crate::app::tests_support::loaded_bundle;
use crate::app::{
App, BundleLoad, KeyRoute, Mode, ReplayScreen, ReplayState, is_replay_screen_reachable,
};
use crate::config::ThemeChoice;
use crate::error::BundleError;
use crate::event::{AppEvent, BundleLoadResult, Command, ReplayControl, SeekTo};
use crate::replay::{BundleReader, LoadedBundle};
use crate::ui::golden::{GOLDEN_HEIGHT, GOLDEN_WIDTH, assert_golden, buffer_to_text};
use crate::ui::render;
use crate::ui::view::ViewState;
const VALID_END_STEP: u32 = 3;
fn fixture_dir(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("bundle")
.join(name)
}
#[track_caller]
fn load_fixture_bundle(name: &str) -> LoadedBundle {
let reader = match BundleReader::open(fixture_dir(name)) {
Ok(reader) => reader,
Err(e) => panic!("the `{name}` fixture must open through the real reader: {e}"),
};
match reader.load() {
Ok(bundle) => bundle,
Err(e) => panic!("the `{name}` fixture must load through the real reader: {e}"),
}
}
#[track_caller]
fn replay_app_from_fixture(name: &str) -> (App, mpsc::Receiver<Command>) {
let loaded = load_fixture_bundle(name);
let (tx, rx) = mpsc::channel::<Command>(16);
let mut app = App::new(
Mode::Replay(ReplayState::new(fixture_dir(name))),
ThemeChoice::Auto,
tx,
);
app.on_event(AppEvent::BundleLoaded(BundleLoadResult::Loaded(Box::new(
loaded,
))));
app.mark_drawn();
(app, rx)
}
#[track_caller]
fn render_replay_frame(app: &App) -> String {
let mut view = ViewState::new();
view.sync(app);
let mut term = match Terminal::new(TestBackend::new(GOLDEN_WIDTH, GOLDEN_HEIGHT)) {
Ok(term) => term,
Err(e) => panic!("TestBackend construction failed: {e}"),
};
match term.draw(|frame| render(app, &view, frame)) {
Ok(_) => {}
Err(e) => panic!("replay frame render failed: {e}"),
}
buffer_to_text(term.backend().buffer())
}
fn press(c: char) -> KeyEvent {
KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE)
}
fn tab() -> KeyEvent {
KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE)
}
fn dispatch_key(app: &mut App, key: KeyEvent) {
match app.dispatch_key_global(key) {
KeyRoute::Consumed => {}
KeyRoute::ToScreen => {
let follow = match &mut app.mode {
Mode::Replay(replay) => crate::ui::replay::handle_key(replay, key),
Mode::Live(_) => panic!("expected replay mode"),
};
if let Some(follow) = follow {
app.on_event(follow);
}
}
}
}
#[track_caller]
fn replay_screen(app: &App) -> ReplayScreen {
match &app.mode {
Mode::Replay(replay) => replay.screen,
Mode::Live(_) => panic!("expected replay mode"),
}
}
#[track_caller]
fn replay_bundle(app: &App) -> &BundleLoad {
match &app.mode {
Mode::Replay(replay) => &replay.bundle,
Mode::Live(_) => panic!("expected replay mode"),
}
}
#[track_caller]
fn is_playing(app: &App) -> bool {
match &app.mode {
Mode::Replay(replay) => replay.is_playing(),
Mode::Live(_) => panic!("expected replay mode"),
}
}
#[test]
fn test_replay_path_valid_fixture_end_to_end_renders_populated_screen() {
let decoded = load_fixture_bundle("valid");
let n = usize::try_from(VALID_END_STEP).unwrap_or(usize::MAX) + 1;
assert_eq!(
decoded.fills.len(),
n,
"the reader decoded one fill per step"
);
assert_eq!(decoded.equity.len(), n, "one equity row per step");
assert_eq!(decoded.positions.len(), n, "one position row per step");
assert_eq!(decoded.greeks.len(), n, "one greeks row per step");
let (mut app, _rx) = replay_app_from_fixture("valid");
assert!(
matches!(replay_bundle(&app), BundleLoad::Ready(_)),
"the valid bundle folds Loading -> Ready through on_event",
);
app.on_event(AppEvent::ReplaySeek(SeekTo::Step(u32::MAX)));
let text = render_replay_frame(&app);
assert!(
text.contains("replay"),
"the status line names replay mode: {text:?}"
);
assert!(
text.contains("valid"),
"the status line names the run/dir: {text:?}"
);
for panel in ["equity", "drawdown", "P&L attribution", "fills"] {
assert!(
text.contains(panel),
"the `{panel}` panel renders: {text:?}"
);
}
assert!(
!text.contains("no equity rows"),
"the equity chart is populated at the head, not the empty state: {text:?}",
);
assert!(
text.contains("theta"),
"a by-Greek attribution term is labelled: {text:?}"
);
assert!(
text.contains("$0.40"),
"theta 40c renders as $0.40: {text:?}"
);
assert!(
text.contains("$1.20"),
"delta 120c magnitude renders as $1.20: {text:?}"
);
assert!(
text.contains('−'),
"a negative term carries the − sign glyph: {text:?}"
);
assert!(text.contains("LONG"), "a fill side renders: {text:?}");
assert!(
text.contains("BTC"),
"the fill underlying renders: {text:?}"
);
assert!(
text.contains("$125.00"),
"the fill price 12500c renders as $125.00: {text:?}"
);
assert!(
text.contains("2 Payoff") && !text.contains("(Payoff)"),
"the keybar shows the Payoff slot as a live, undimmed number key: {text:?}",
);
}
#[test]
fn test_replay_scrub_updates_all_panels_to_one_coherent_head() {
let (mut app, _rx) = replay_app_from_fixture("valid");
for step in 0..=VALID_END_STEP {
app.on_event(AppEvent::ReplaySeek(SeekTo::Step(step)));
let text = render_replay_frame(&app);
assert!(
text.contains(&format!("step {step}/{VALID_END_STEP}")),
"the title shows head step {step}: {text:?}",
);
assert!(
text.contains(&format!("P&L attribution @ step {step}")),
"the attribution panel is @ head step {step}: {text:?}",
);
match &app.mode {
Mode::Replay(replay) => match replay.loaded() {
Some(loaded) => {
assert_eq!(
loaded.cursor.position(),
step,
"the cursor sits at head {step}"
);
let visible = loaded.cursor.visible_fills(&loaded.bundle);
let expected = usize::try_from(step).unwrap_or(usize::MAX) + 1;
assert_eq!(
visible.len(),
expected,
"exactly {expected} fills are visible up to head {step}",
);
match loaded.cursor.head_greeks(&loaded.bundle) {
Some(g) => assert_eq!(g.step, step, "head greeks is the step-{step} row"),
None => panic!("the head greeks row must exist at step {step}"),
}
match loaded.cursor.head_equity(&loaded.bundle) {
Some(e) => assert_eq!(e.step, step, "head equity is the step-{step} row"),
None => panic!("the head equity row must exist at step {step}"),
}
}
None => panic!("the bundle must be Ready while scrubbing"),
},
Mode::Live(_) => panic!("expected replay mode"),
}
}
}
#[test]
fn test_replay_bad_schema_renders_actionable_error_not_partial() {
let dir = fixture_dir("bad_schema");
let err = match BundleReader::open(fixture_dir("bad_schema")) {
Ok(_) => panic!("the bad_schema fixture must be rejected at open()"),
Err(BundleError::UnsupportedSchema(tag)) => {
assert_eq!(tag, "ironcondor.bundle.v2", "the offending tag is echoed");
BundleError::UnsupportedSchema(tag)
}
Err(other) => {
panic!("a major-incompatible schema must be UnsupportedSchema, got {other:?}")
}
};
let (tx, _rx) = mpsc::channel::<Command>(16);
let mut app = App::new(Mode::Replay(ReplayState::new(dir)), ThemeChoice::Auto, tx);
app.on_event(AppEvent::BundleLoaded(BundleLoadResult::Failed(
err.to_string(),
)));
assert!(
matches!(replay_bundle(&app), BundleLoad::Error { .. }),
"the rejected bundle folds to a retryable Error",
);
let text = render_replay_frame(&app);
assert!(
text.contains("unsupported schema"),
"the schema error is shown: {text:?}"
);
assert!(
text.contains("ironcondor.bundle.v2"),
"the offending schema tag is shown: {text:?}",
);
assert!(
text.contains("press R to retry"),
"the retry affordance is shown: {text:?}"
);
for panel in ["P&L attribution", "drawdown", "fills"] {
assert!(
!text.contains(panel),
"a rejected bundle must not partial-render the `{panel}` panel: {text:?}",
);
}
}
#[test]
fn test_replay_reaches_both_documented_screens_payoff_live() {
assert!(
is_replay_screen_reachable(ReplayScreen::Replay),
"the equity/attribution/drill-down screen is reachable",
);
assert!(
is_replay_screen_reachable(ReplayScreen::Payoff),
"the payoff-at-head panel is reachable from v0.5 (#49)",
);
let (mut app, _rx) = replay_app_from_fixture("valid");
match app.dispatch_key_global(press('2')) {
KeyRoute::Consumed => {}
KeyRoute::ToScreen => panic!("a screen-switch number key is a consumed global"),
}
assert_eq!(
replay_screen(&app),
ReplayScreen::Payoff,
"the Payoff slot switches to the now-reachable payoff panel",
);
let _ = app.dispatch_key_global(press('1'));
assert_eq!(replay_screen(&app), ReplayScreen::Replay);
let _ = app.dispatch_key_global(tab());
assert_eq!(
replay_screen(&app),
ReplayScreen::Payoff,
"Tab cycles onto the reachable Payoff screen",
);
let _ = app.dispatch_key_global(tab());
assert_eq!(
replay_screen(&app),
ReplayScreen::Replay,
"Tab cycles back to the Replay screen",
);
let (fresh, _rx2) = replay_app_from_fixture("valid");
let text = render_replay_frame(&fresh);
assert!(
text.contains("1 Replay"),
"the Replay slot is a live number key: {text:?}"
);
assert!(
text.contains("2 Payoff"),
"the Payoff slot is a live number key, undimmed: {text:?}",
);
assert!(
!text.contains("(Payoff)"),
"the Payoff slot is no longer a dimmed/parenthesized dead key: {text:?}",
);
assert!(
text.contains("P&L attribution"),
"the reachable Replay body renders: {text:?}",
);
}
#[test]
fn test_replay_payoff_at_head_renders_end_to_end() {
let (mut app, _rx) = replay_app_from_fixture("valid");
let _ = app.dispatch_key_global(press('2'));
assert_eq!(replay_screen(&app), ReplayScreen::Payoff);
app.on_event(AppEvent::ReplaySeek(SeekTo::Step(u32::MAX)));
let text = render_replay_frame(&app);
assert!(
text.contains("Payoff"),
"the payoff-at-head panel renders its title: {text:?}",
);
assert!(
text.contains("flat at this step")
|| text.contains("not a bit-exact reprice")
|| text.contains("payoff unavailable"),
"the panel renders an honest state (curve caveat or flat/unavailable): {text:?}",
);
}
#[test]
fn test_replay_load_error_retry_round_trips_to_ready() {
let dir = fixture_dir("bad_schema");
let err = match BundleReader::open(fixture_dir("bad_schema")) {
Ok(_) => panic!("the bad_schema fixture must reject at open()"),
Err(e) => e,
};
let (tx, mut rx) = mpsc::channel::<Command>(16);
let mut app = App::new(
Mode::Replay(ReplayState::new(dir.clone())),
ThemeChoice::Auto,
tx,
);
app.on_event(AppEvent::BundleLoaded(BundleLoadResult::Failed(
err.to_string(),
)));
assert!(
matches!(replay_bundle(&app), BundleLoad::Error { .. }),
"the rejected bundle is a retryable Error",
);
match app.dispatch_key_global(press('R')) {
KeyRoute::Consumed => {}
KeyRoute::ToScreen => panic!("R is a consumed global"),
}
assert!(
matches!(replay_bundle(&app), BundleLoad::Loading),
"R returns the screen to the Loading state",
);
match rx.try_recv() {
Ok(Command::ReloadBundle(reload_dir)) => {
assert_eq!(reload_dir, dir, "the reload targets the same bundle dir");
}
other => panic!("R must emit ReloadBundle for the same dir, got {other:?}"),
}
let loaded = load_fixture_bundle("valid");
app.on_event(AppEvent::BundleLoaded(BundleLoadResult::Loaded(Box::new(
loaded,
))));
assert!(
matches!(replay_bundle(&app), BundleLoad::Ready(_)),
"a fresh valid load completes the retry to Ready",
);
let text = render_replay_frame(&app);
assert!(
text.contains("P&L attribution"),
"the reloaded bundle renders the populated screen: {text:?}",
);
}
#[test]
fn test_replay_playback_ticks_advance_head_and_auto_pause_at_end() {
let (mut app, _rx) = replay_app_from_fixture("valid");
app.on_event(AppEvent::ReplayControl(ReplayControl::PlayPause));
assert!(is_playing(&app), "Space starts playback");
for _ in 0..16 {
app.on_event(AppEvent::Tick);
}
assert!(!is_playing(&app), "playback auto-pauses at the tape end");
match &app.mode {
Mode::Replay(replay) => match replay.loaded() {
Some(loaded) => assert_eq!(
loaded.cursor.position(),
VALID_END_STEP,
"the play-head lands on the last step and never wraps",
),
None => panic!("the bundle must be Ready during playback"),
},
Mode::Live(_) => panic!("expected replay mode"),
}
let text = render_replay_frame(&app);
assert!(
text.contains(&format!("step {VALID_END_STEP}/{VALID_END_STEP}")),
"the play-head reached the tape end: {text:?}",
);
}
#[test]
fn test_replay_equity_curve_golden() {
let (mut app, _rx) = replay_app_from_fixture("valid");
app.on_event(AppEvent::ReplaySeek(SeekTo::Step(u32::MAX)));
assert_golden("replay", "equity_curve.txt", &render_replay_frame(&app));
}
#[test]
fn test_replay_greek_attribution_golden() {
let (mut app, _rx) = replay_app_from_fixture("valid");
app.on_event(AppEvent::ReplaySeek(SeekTo::Step(2)));
assert_golden(
"replay",
"greek_attribution.txt",
&render_replay_frame(&app),
);
}
#[test]
fn test_replay_trade_drilldown_golden() {
let (mut app, _rx) = replay_app_from_fixture("valid");
app.on_event(AppEvent::ReplaySeek(SeekTo::Step(u32::MAX)));
dispatch_key(&mut app, press('.'));
match &app.mode {
Mode::Replay(replay) => match replay.loaded() {
Some(loaded) => assert!(
loaded.selection.is_some(),
"`.` drilled into the most recent visible fill",
),
None => panic!("the bundle must be Ready"),
},
Mode::Live(_) => panic!("expected replay mode"),
}
assert_golden("replay", "trade_drilldown.txt", &render_replay_frame(&app));
}
#[test]
fn test_replay_loading_golden() {
let (tx, _rx) = mpsc::channel::<Command>(16);
let app = App::new(
Mode::Replay(ReplayState::new(fixture_dir("valid"))),
ThemeChoice::Auto,
tx,
);
assert!(
matches!(replay_bundle(&app), BundleLoad::Loading),
"a fresh replay state is Loading until the bundle arrives",
);
assert_golden("replay", "loading.txt", &render_replay_frame(&app));
}
#[test]
fn test_replay_error_golden() {
let dir = fixture_dir("bad_schema");
let err = match BundleReader::open(fixture_dir("bad_schema")) {
Ok(_) => panic!("the bad_schema fixture must reject at open()"),
Err(e) => e,
};
let (tx, _rx) = mpsc::channel::<Command>(16);
let mut app = App::new(Mode::Replay(ReplayState::new(dir)), ThemeChoice::Auto, tx);
app.on_event(AppEvent::BundleLoaded(BundleLoadResult::Failed(
err.to_string(),
)));
assert_golden("replay", "error.txt", &render_replay_frame(&app));
}
#[test]
fn test_replay_empty_golden() {
let (tx, _rx) = mpsc::channel::<Command>(16);
let mut app = App::new(
Mode::Replay(ReplayState::new(PathBuf::from("empty-run"))),
ThemeChoice::Auto,
tx,
);
app.on_event(AppEvent::BundleLoaded(BundleLoadResult::Loaded(Box::new(
loaded_bundle(0),
))));
assert_golden("replay", "empty.txt", &render_replay_frame(&app));
}
#[test]
fn test_replay_payoff_head_golden() {
let (mut app, _rx) = replay_app_from_fixture("valid");
match app.dispatch_key_global(press('2')) {
KeyRoute::Consumed => {}
KeyRoute::ToScreen => panic!("a screen-switch number key is a consumed global"),
}
assert_eq!(replay_screen(&app), ReplayScreen::Payoff);
app.on_event(AppEvent::ReplaySeek(SeekTo::Step(u32::MAX)));
let text = render_replay_frame(&app);
assert!(
text.contains("expiration payoff") && text.contains("not a bit-exact reprice"),
"the panel names the expiration curve + the honest no-reprice caveat: {text:?}",
);
assert!(
!text.contains("flat at this step"),
"an open position at the head is not the flat state: {text:?}",
);
assert!(
text.contains("+$6.00"),
"the head mark P&L renders from integer cents as +$6.00: {text:?}",
);
assert_golden("replay", "payoff_head.txt", &text);
}
#[test]
fn test_replay_payoff_head_flat_state_renders() {
let (mut app, _rx) = replay_app_from_fixture("valid");
let _ = app.dispatch_key_global(press('2'));
assert_eq!(replay_screen(&app), ReplayScreen::Payoff);
app.on_event(AppEvent::ReplaySeek(SeekTo::Step(0)));
let text = render_replay_frame(&app);
assert!(
text.contains("flat at this step"),
"step 0 (closed at expiry) renders the flat empty state: {text:?}",
);
assert!(
text.contains("scrub to an open step"),
"the flat state names its recovery affordance: {text:?}",
);
}
#[test]
fn test_replay_payoff_error_state_shows_r_retry_not_provider_key() {
let (tx, _rx) = mpsc::channel::<Command>(16);
let mut app = App::new(
Mode::Replay(ReplayState::new(fixture_dir("bad_schema"))),
ThemeChoice::Auto,
tx,
);
let _ = app.dispatch_key_global(press('2'));
assert_eq!(replay_screen(&app), ReplayScreen::Payoff);
app.on_event(AppEvent::BundleLoaded(BundleLoadResult::Failed(
"manifest.json is malformed".to_owned(),
)));
let text = render_replay_frame(&app);
assert!(
text.contains("manifest.json is malformed"),
"the payoff panel renders the bundle error message: {text:?}",
);
assert!(
text.contains("press R to retry"),
"the payoff panel offers the Replay `R` retry key on a bundle error: {text:?}",
);
assert!(
!text.contains("loading bundle"),
"a failed bundle no longer shows the loading note on the payoff panel: {text:?}",
);
assert!(
!text.to_lowercase().contains("reconnect"),
"a bundle error never surfaces the Live provider-reconnect key: {text:?}",
);
assert_golden("replay", "payoff_error.txt", &text);
}
#[track_caller]
fn replay_app_via_real_load(dir: PathBuf) -> App {
use crate::app::spawn_bundle_load;
use crate::replay::ResourceCeilings;
use tokio_util::sync::CancellationToken;
let runtime = match tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()
{
Ok(rt) => rt,
Err(e) => panic!("tokio runtime for the load worker failed: {e}"),
};
let (tx_events, mut rx_events) = mpsc::channel::<AppEvent>(16);
let cancel = CancellationToken::new();
let event = runtime.block_on(async {
let handle = spawn_bundle_load(
dir.clone(),
ResourceCeilings::default(),
tx_events,
cancel.child_token(),
);
let event = rx_events.recv().await;
let _ = handle.await;
event
});
let Some(event) = event else {
panic!("the real load worker posted no BundleLoaded event");
};
let (tx, _rx) = mpsc::channel::<Command>(16);
let mut app = App::new(Mode::Replay(ReplayState::new(dir)), ThemeChoice::Auto, tx);
app.on_event(event);
app
}
#[test]
fn test_real_spawned_load_reaches_ready_through_the_event_channel() {
let app = replay_app_via_real_load(fixture_dir("valid"));
match &app.mode {
Mode::Replay(replay) => match &replay.bundle {
BundleLoad::Ready(loaded) => {
assert_eq!(loaded.cursor.end_step(), VALID_END_STEP);
}
other => panic!("the real load must reach Ready, got {other:?}"),
},
Mode::Live(_) => panic!("a replay app"),
}
let frame = render_replay_frame(&app);
assert!(
frame.contains("Replay"),
"the Ready frame renders the replay screen"
);
}
#[test]
fn test_real_spawned_load_surfaces_a_typed_error_through_the_event_channel() {
let app = replay_app_via_real_load(fixture_dir("bad_schema"));
match &app.mode {
Mode::Replay(replay) => match &replay.bundle {
BundleLoad::Error { .. } => {}
other => panic!("a bad-schema bundle must surface Error, got {other:?}"),
},
Mode::Live(_) => panic!("a replay app"),
}
let frame = render_replay_frame(&app);
assert!(
frame.contains('R'),
"the Error frame offers the R retry affordance"
);
}