From 01d1545c404b05ef5ae524a5a7036dff7e4581f1 Mon Sep 17 00:00:00 2001 From: atagen Date: Thu, 21 May 2026 21:16:54 +1000 Subject: [PATCH] Assert multiphase rejection diagnostics --- docs/window-animations-plan.md | 4 ++ src/animation/multiphase.rs | 96 +++++++++++++++++++++++++++++++--- 2 files changed, 92 insertions(+), 8 deletions(-) diff --git a/docs/window-animations-plan.md b/docs/window-animations-plan.md index 890bd55b..f8912ac0 100644 --- a/docs/window-animations-plan.md +++ b/docs/window-animations-plan.md @@ -245,6 +245,9 @@ Current pure planner status: - Multiphase planning also has an explained-plan entry point. Accepted plans report the deterministic strategy, phase reasons, participating nodes, and validation result; rejected plans report every attempted strategy and failure. +- Rejection diagnostics are treated as contractual test output for unsupported + patterns and analytically invalid candidate plans, including attempted strategy + order and exact validation failure. - Planner tests now include a deterministic split-tree generator. It builds valid weighted tiling layouts, derives leaf hierarchy metadata, mutates them through supported transitions, and runs the real planner plus exact validator. @@ -262,6 +265,7 @@ Tests: - reversing direction produces equivalent motion in reverse - accepted and rejected plans expose deterministic strategy explanations - bounded generated split-tree corpus produces identical plans on repeated runs +- unsupported and invalid candidate plans produce exact expected diagnostics - child waits for parent/container-space phases when moving upward into a toplevel peer position - mono-mode tab switches do not animate, while entering/exiting mono can animate diff --git a/src/animation/multiphase.rs b/src/animation/multiphase.rs index 4c37e676..d0ab4b70 100644 --- a/src/animation/multiphase.rs +++ b/src/animation/multiphase.rs @@ -1538,6 +1538,49 @@ mod tests { .to } + fn no_pattern_attempts(direction: PlanDirection) -> Vec { + vec![ + RejectedStrategy { + direction, + strategy: PlanStrategy::SingleAction, + reason: MultiphasePlanFailure::NoPattern, + }, + RejectedStrategy { + direction, + strategy: PlanStrategy::HierarchyOrderedScales, + reason: MultiphasePlanFailure::NoPattern, + }, + RejectedStrategy { + direction, + strategy: PlanStrategy::SwapLanes { + axis: PhaseAxis::Horizontal, + }, + reason: MultiphasePlanFailure::NoPattern, + }, + RejectedStrategy { + direction, + strategy: PlanStrategy::SwapLanes { + axis: PhaseAxis::Vertical, + }, + reason: MultiphasePlanFailure::NoPattern, + }, + RejectedStrategy { + direction, + strategy: PlanStrategy::SpaceThenOrthogonalGrowth { + axis: PhaseAxis::Horizontal, + }, + reason: MultiphasePlanFailure::NoPattern, + }, + RejectedStrategy { + direction, + strategy: PlanStrategy::SpaceThenOrthogonalGrowth { + axis: PhaseAxis::Vertical, + }, + reason: MultiphasePlanFailure::NoPattern, + }, + ] + } + #[test] fn horizontal_swap_shrinks_moves_then_grows_without_overlap() { let req = request(vec![ @@ -2136,14 +2179,9 @@ mod tests { let diagnostic = plan_no_overlap_with_diagnostics(&req).unwrap_err(); assert_eq!(diagnostic.forward, MultiphasePlanFailure::NoPattern); assert_eq!(diagnostic.reverse, Some(MultiphasePlanFailure::NoPattern)); - assert!( - diagnostic - .attempted - .iter() - .any(|attempt| attempt.direction == PlanDirection::Forward - && attempt.strategy == PlanStrategy::SingleAction - && attempt.reason == MultiphasePlanFailure::NoPattern) - ); + let mut expected = no_pattern_attempts(PlanDirection::Forward); + expected.extend(no_pattern_attempts(PlanDirection::Reverse)); + assert_eq!(diagnostic.attempted, expected); } #[test] @@ -2176,6 +2214,48 @@ mod tests { )); } + #[test] + fn diagnostics_report_candidate_validation_rejections() { + let req = request(vec![ + MultiphaseWindow { + node_id: id(1), + from: rect(0, 0, 60, 60), + to: rect(180, 0, 240, 60), + hierarchy: Default::default(), + }, + MultiphaseWindow { + node_id: id(2), + from: rect(90, 0, 150, 60), + to: rect(90, 0, 150, 60), + hierarchy: Default::default(), + }, + ]); + let rejection = + MultiphasePlanFailure::Validation(MultiphaseValidationError::PhaseOverlap { + phase: 0, + a: id(1), + b: id(2), + }); + let diagnostic = plan_no_overlap_with_diagnostics(&req).unwrap_err(); + + assert_eq!(diagnostic.forward, rejection); + assert_eq!(diagnostic.reverse, Some(rejection)); + assert_eq!( + diagnostic.attempted[0], + RejectedStrategy { + direction: PlanDirection::Forward, + strategy: PlanStrategy::SingleAction, + reason: rejection, + } + ); + assert!(diagnostic.attempted.iter().any(|attempt| *attempt + == RejectedStrategy { + direction: PlanDirection::Reverse, + strategy: PlanStrategy::SingleAction, + reason: rejection, + })); + } + #[test] fn hierarchy_metadata_classifies_depth_and_mono_transitions() { let source = MultiphaseHierarchyPosition {