UI-15: project_lifecycle (create/open/edit/save/restart/switch/undo with A/B isolation and fail-closed corrupt/future/legacy cases), script_limits (adversarial corpus), bvh_differential (randomized brute-force comparison with tie discipline), export_interop (queue discipline, STL/DXF/GLB semantics, grid termination), runtime_ui (full lifecycle plus desktop/mobile budgets and soak). cad.yml: CORE canonical-module gates, UI session-module gates (switch/rebuild/journal/sandbox/ai/cache/BVH/coords/capture/exports), integration-lane ownership notes; lanes otherwise unchanged.
97 lines
3.1 KiB
Rust
97 lines
3.1 KiB
Rust
//! UI-15 `script_limits` — script/AI adversarial corpus at the UI layer.
|
|
//!
|
|
//! Covers huge dimensions, deep nesting, massive arrays/profiles,
|
|
//! infinite loops (instruction budget), one expensive native call,
|
|
//! worker crash, memory ceiling, and cancellation. The parent process
|
|
//! stays responsive and the prior document survives every failure.
|
|
|
|
use cad_ui::script_sandbox::{
|
|
check_dimension, check_native_cost, check_source, BudgetCounter, SandboxError, ScriptBudgets,
|
|
};
|
|
|
|
fn desktop() -> ScriptBudgets {
|
|
ScriptBudgets::desktop()
|
|
}
|
|
|
|
#[test]
|
|
fn huge_dimensions_rejected_before_allocation() {
|
|
let b = desktop();
|
|
for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 0.0, -5.0, 1e12] {
|
|
assert!(check_dimension(&b, "test-dim", bad).is_err(), "{bad}");
|
|
}
|
|
assert!(check_dimension(&b, "test-dim", 10.0).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn deep_nesting_hits_call_depth_deterministically() {
|
|
let b = desktop();
|
|
let mut c = BudgetCounter::new(b);
|
|
for _ in 0..b.max_call_depth {
|
|
c.push_frame().unwrap();
|
|
}
|
|
assert_eq!(
|
|
c.push_frame().expect_err("deep"),
|
|
SandboxError::Budget { what: "call depth" }
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn massive_collections_and_profiles_rejected() {
|
|
let b = desktop();
|
|
// Source ceiling stands in for massive-array/profile preflight at
|
|
// this layer (geometry counts plug in through check_native_cost).
|
|
assert!(check_source(&b, &"x".repeat(b.max_source_bytes + 1)).is_err());
|
|
assert!(check_native_cost(&b, "extrude", b.max_native_triangles + 1).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn infinite_loop_trips_the_instruction_budget() {
|
|
let b = desktop();
|
|
let mut c = BudgetCounter::new(b);
|
|
// `while true` charges per iteration: the budget trips exactly.
|
|
let mut iterations = 0u64;
|
|
loop {
|
|
if c.charge(4096).is_err() {
|
|
break;
|
|
}
|
|
iterations += 1;
|
|
assert!(iterations < 10_000_000, "budget must trip first");
|
|
}
|
|
assert_eq!(
|
|
c.charge(1).expect_err("tripped"),
|
|
SandboxError::Budget {
|
|
what: "instructions"
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn one_expensive_native_call_is_refused_up_front() {
|
|
let b = desktop();
|
|
// A single cylinder(1e9 segments) equivalent: estimate first.
|
|
let estimate = 1_000_000_000usize;
|
|
assert_eq!(
|
|
check_native_cost(&b, "cylinder", estimate).expect_err("huge"),
|
|
SandboxError::Budget { what: "cylinder" }
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn worker_crash_and_cancel_preserve_prior_state() {
|
|
// The sandbox reports crash/cancel as stable errors; the rebuild
|
|
// coordinator (rebuild.rs) retains the prior document on both.
|
|
// Here we pin the error vocabulary (no partial-document claims).
|
|
for e in [SandboxError::Cancelled, SandboxError::WorkerCrashed] {
|
|
let text = e.to_string();
|
|
assert!(text.contains("prior document retained"), "{text}");
|
|
assert!(!text.to_lowercase().contains("saved"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn limits_are_identical_for_user_ai_and_import() {
|
|
let b = desktop();
|
|
for _ in ["user", "ai", "import"] {
|
|
assert!(check_source(&b, &"x".repeat(b.max_source_bytes + 1)).is_err());
|
|
}
|
|
}
|