//! Headless runtime-journey gates (TRAFFIC-14). //! //! The Makepad GUI harness cannot boot in CI (see tests/ui.rs ignore //! reasons), so these journeys drive the PRODUCTION path one layer down: //! scenario selection → phase gating → tick → terminal result → restart / //! next / previous → persistence-shape result — through `TrafficWorld`, //! the same simulation/rules/scoring the GUI drives. `tests/ui_basic.rs` //! remains the headless smoke test and is labelled as such; this file is //! the lifecycle/assessment-honesty coverage, not GUI coverage. use nigig_traffic::traffic::scenario::all; use nigig_traffic::traffic::{allowed_transition, Phase, TrafficWorld, Transition}; #[test] fn journey_select_drive_complete_restart() { let mut w = TrafficWorld::new(); w.load_scenario(0); assert_eq!(w.run.phase, Phase::Intro); // Intro ignores throttle: no head start. let full = makepad_game_blocks::DriveInput { throttle: 1.0, ..makepad_game_blocks::DriveInput::default() }; let start = w.sim.world.entity(0u64).map(|e| (e.pos.x, e.pos.z)); for _ in 0..60 { w.tick(&full); } assert_eq!(w.run.phase, Phase::Driving); let now = w.sim.world.entity(0u64).map(|e| (e.pos.x, e.pos.z)); assert_eq!(start, now, "intro must not displace the car"); // Restart returns to Intro with cleared score. w.load_scenario(0); assert_eq!(w.run.phase, Phase::Intro); assert_eq!(w.run.score.mistakes, 0); } #[test] fn journey_next_prev_cycle_all_scenarios() { let mut w = TrafficWorld::new(); w.load_scenario(0); let n = all().len(); let first = w.scenario().expect("scenario").id; for _ in 0..n { w.next(); } assert_eq!(w.scenario().expect("scenario").id, first); w.next(); let second = w.scenario().expect("scenario").id; w.prev(); assert_eq!(w.scenario().expect("scenario").id, first); assert_ne!(first, second); } #[test] fn journey_pause_resume_freezes_and_continues() { let mut w = TrafficWorld::new(); w.load_scenario(0); for _ in 0..61 { w.tick(&makepad_game_blocks::DriveInput::default()); } assert_eq!(w.run.phase, Phase::Driving); assert!(w.pause()); let tick = w.run.tick; for _ in 0..120 { w.tick(&makepad_game_blocks::DriveInput { throttle: 1.0, ..makepad_game_blocks::DriveInput::default() }); } assert_eq!(w.run.tick, tick); assert!(w.resume()); w.tick(&makepad_game_blocks::DriveInput::default()); assert_eq!(w.run.tick, tick + 1); } #[test] fn journey_terminal_result_is_immutable() { let mut w = TrafficWorld::new(); w.load_scenario(0); for _ in 0..61 { w.tick(&makepad_game_blocks::DriveInput::default()); } // Force terminal through the public transition table only. assert!(allowed_transition(w.run.phase, Transition::Complete)); // Direct terminal simulation: complete via score path then hammer ticks. w.run.score.completed = true; w.run.phase = Phase::Complete; let snap = ( w.run.phase, w.run.score.total_points, w.run.score.mistakes, w.run.tick, ); for _ in 0..1000 { w.tick(&makepad_game_blocks::DriveInput { throttle: 1.0, steer: 1.0, ..makepad_game_blocks::DriveInput::default() }); } assert_eq!( ( w.run.phase, w.run.score.total_points, w.run.score.mistakes, w.run.tick ), snap ); } #[test] fn journey_result_binds_id_and_content_version() { // Results bind scenario ID + content version, never list index: moving // through the catalog keeps identity stable. let w = TrafficWorld::new(); let ids: Vec<&str> = all().iter().map(|s| s.id).collect(); for (i, s) in all().iter().enumerate() { assert_eq!(ids[i], s.id); assert_eq!(s.content_version(), 0); } assert!(w.scenario().is_none()); }