//! Invariant guard tests (behavior + source-inspection). //! //! These tests protect the AGENTS.md "GUI First-Frame Visibility" and "Dock / //! Float Regression Protection" invariants. They come in two flavors: //! //! * **Behavior tests** exercise the running state machine (no GPU) to prove //! an invariant holds at runtime. //! * **Source-inspection tests** embed the relevant source files via //! `include_str!` and assert that the protected lines/patterns are present. //! They catch the case where an agent edits the invariant file directly. //! //! These are deliberately run on every CI build (no `#[ignore]`). If any fails, //! a protected mechanism was broken and the change is not ready to commit. use hcie_engine_api::Engine; use hcie_gui_egui::app::dock::HciePane; use hcie_gui_egui::app::dock_controller::DockController; // ── Source files embedded for source-inspection assertions ──────────────── const CANVAS_MOD: &str = include_str!("../src/canvas/mod.rs"); const APP_MOD: &str = include_str!("../src/app/mod.rs"); const DOCK_CONTROLLER: &str = include_str!("../src/app/dock_controller.rs"); const DOCK_ENGINE: &str = include_str!("../src/app/dock_engine.rs"); // ── Behavior: DrawingFinished does NOT clear dirty flags ─────────────────── // // AGENTS.md: "DrawingFinished does NOT clear dirty flags ... Calling // clear_dirty_flags() inside DrawingFinished races with the next render pass // and makes newly drawn vector shapes disappear." // // We assert the behavior at the engine level: drawing marks composite dirty, // and simply handling the DrawingFinished event path (which is a no-op for // dirty flags) leaves the dirty flag set so the next render uploads. #[test] fn drawing_finished_event_does_not_clear_dirty() { let mut engine = Engine::new(10, 10); // Drawing a stroke marks the composite dirty. engine.draw_filled_rect_rgba(0, 0, 4, 4, [255, 0, 0, 255]); assert!( engine.is_composite_dirty(), "after drawing, composite must be dirty so render_composition uploads" ); // The DrawingFinished event handler in mod.rs:1955-1962 is a documented // no-op w.r.t. dirty flags; we simulate that contract here by NOT calling // clear_dirty_flags and asserting the flag survives. // (If an agent adds clear_dirty_flags() to the event handler, the source // inspection test `protected_drawing_finished_noop` below catches it.) assert!( engine.is_composite_dirty(), "DrawingFinished must not clear dirty flags before render_composition runs" ); } // ── Source-inspection: canvas multi-repaint on new texture ──────────────── // // AGENTS.md: "Extra repaint after new texture ... Requests multiple repaints // after render_composition creates a brand-new texture." // // The pattern is: `if created_new_texture { request_repaint(); // request_repaint_after(...) }`. We assert both calls appear within the // `created_new_texture` block in canvas/mod.rs. #[test] fn protected_canvas_multi_repaint_on_new_texture() { // Locate the `created_new_texture` branch and assert both repaint calls // appear shortly after it. This tolerates formatting changes but catches // removal of one of the two repaint calls. let created_idx = CANVAS_MOD .find("if created_new_texture") .or_else(|| CANVAS_MOD.find("created_new_texture {")) .expect("canvas/mod.rs must retain the `created_new_texture` branch"); let window = &CANVAS_MOD[created_idx..created_idx.saturating_add(800).min(CANVAS_MOD.len())]; assert!( window.contains("request_repaint()"), "canvas/mod.rs: created_new_texture branch must call request_repaint() (first-frame visibility invariant)" ); assert!( window.contains("request_repaint_after"), "canvas/mod.rs: created_new_texture branch must call request_repaint_after(...) (second/third frame display)" ); } // ── Source-inspection: DrawingFinished no dirty clear ────────────────────── // // AGENTS.md: the `DrawingFinished` arm must NOT call `clear_dirty`. We assert // that within the DrawingFinished match arm there is no `clear_dirty` call. #[test] fn protected_drawing_finished_noop() { let arm_idx = APP_MOD .find("AppEvent::DrawingFinished =>") .expect("app/mod.rs must retain the DrawingFinished match arm"); // Take the slice until the next match arm or a reasonable window. let end = APP_MOD[arm_idx..].find("AppEvent::Undo =>").unwrap_or(400); let arm = &APP_MOD[arm_idx..arm_idx + end]; assert!( !arm.contains("clear_dirty"), "app/mod.rs: DrawingFinished arm must NOT call clear_dirty_flags (AGENTS.md protected invariant)" ); } // ── Source-inspection: DockController private fields ─────────────────────── // // AGENTS.md: "All DockController fields are private; mutation only through // methods." We assert the struct fields are declared without `pub`. #[test] fn protected_dock_controller_fields_private() { let struct_idx = DOCK_CONTROLLER .find("pub struct DockController {") .expect("dock_controller.rs must define pub struct DockController"); let end = DOCK_CONTROLLER[struct_idx..].find('}').unwrap_or(800); let body = &DOCK_CONTROLLER[struct_idx..struct_idx + end]; assert!( !body.contains("pub tree:") && !body.contains("pub floating:") && !body.contains("pub pinned:") && !body.contains("pub active_tab:"), "dock_controller.rs: DockController fields must be private (no `pub`) so external mutation is forced through methods" ); } // ── Source-inspection: drain order fixed ─────────────────────────────────── // // AGENTS.md: "Drains to_close / to_focus / to_collapse / to_float queues in a // fixed order." We assert the DRAIN_ORDER const lists Close before Float before // Focus before Collapse. #[test] fn protected_drain_order_fixed() { let order_idx = DOCK_CONTROLLER .find("const DRAIN_ORDER") .expect("dock_controller.rs must define DRAIN_ORDER"); let window = &DOCK_CONTROLLER[order_idx..order_idx + 200]; let close = window.find("QueueKind::Close").unwrap(); let float = window.find("QueueKind::Float").unwrap(); let focus = window.find("QueueKind::Focus").unwrap(); let collapse = window.find("QueueKind::Collapse").unwrap(); assert!(close < float, "DRAIN_ORDER must be Close before Float"); assert!(float < focus, "DRAIN_ORDER must be Float before Focus"); assert!( focus < collapse, "DRAIN_ORDER must be Focus before Collapse" ); } // ── Behavior: initial active tab is Document(0) ──────────────────────────── // // AGENTS.md: "Initial document active tab ... Sets Document(0) as the active // dock tab on startup so the canvas widget's ui() is called on the first frame." #[test] fn protected_initial_active_tab_document_zero() { let ctrl = DockController::new(); assert_eq!(*ctrl.active_tab(), HciePane::Document(0)); } // ── Source-inspection: document/tools cannot float guard present ────────── // // AGENTS.md: "Rejects float_pane calls for Document(_) and Tools panes." We // assert the `float_pane` method contains the guard check. #[test] fn protected_document_and_tools_cannot_float_guard() { let method_idx = DOCK_CONTROLLER .find("pub fn float_pane") .expect("dock_controller.rs must define float_pane"); let end = DOCK_CONTROLLER[method_idx..] .find("pub fn dock_pane") .unwrap_or(1200); let body = &DOCK_CONTROLLER[method_idx..method_idx + end]; assert!( body.contains("HciePane::Tools"), "float_pane must explicitly reject HciePane::Tools" ); assert!( body.contains("is_document"), "float_pane must reject documents via is_document()" ); } // ── Source-inspection: EmptyCenter insertion logic present ───────────────── // // AGENTS.md: "Inserts an EmptyCenter tab into the editor leaf when no document // tabs remain." We assert drain_queues contains the EmptyCenter insert branch. #[test] fn protected_emptycenter_insertion_present() { let drain_idx = DOCK_CONTROLLER .find("pub fn drain_queues") .expect("dock_controller.rs must define drain_queues"); let end = DOCK_CONTROLLER[drain_idx..] .find("pub fn persist") .unwrap_or(3000); let body = &DOCK_CONTROLLER[drain_idx..drain_idx + end]; assert!( body.contains("EmptyCenter"), "drain_queues must insert EmptyCenter when no documents remain" ); assert!( body.contains("PaneRole::Editor"), "EmptyCenter insertion must target the Editor role leaf" ); } // ── Source-inspection: persistence single-schema round-trip present ──────── // // AGENTS.md: "Single serde schema for tree + floating + pinned + active tab. // Restore of an unrecognized (legacy) JSON falls back to the default layout." #[test] fn protected_persistence_single_schema_and_fallback() { assert!( DOCK_CONTROLLER.contains("struct PersistedSnapshot"), "dock_controller.rs must define a single PersistedSnapshot schema" ); assert!( DOCK_CONTROLLER.contains("unrecognized JSON, falling back to default layout"), "restore() must fall back to default on unrecognized JSON (legacy migration guard)" ); assert!( DOCK_ENGINE.contains("PANEL_TREE_FORMAT_VERSION"), "dock_engine.rs must define PANEL_TREE_FORMAT_VERSION for version-gated restore" ); }