225 lines
9.7 KiB
Rust
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"
|
||
|
|
);
|
||
|
|
}
|