Files
hcie-rust-v3.05/hcie-egui-app/crates/hcie-gui-egui/tests/invariant_guards.rs
T
2026-07-09 02:59:53 +03:00

225 lines
9.7 KiB
Rust

//! 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"
);
}