Files
Ferrous-Solitaire/solitaire_engine/src/schedule_checks.rs
T
funman300 b402c01918
Test / test (pull_request) Failing after 17m5s
refactor(engine): ambiguity burn-down batch 4 — zero ambiguities, gate enforced
Clears the final 47 pairs and turns the ratchet into a hard gate
(AMBIGUITY_BASELINE = 0, assert_eq):

- HudButtons: the 14 HUD button/popover handlers run as one chain,
  before ui_focus::FocusKeys — Esc/keyboard consumption order is now
  defined (restore prompt → buttons/popovers → focus navigation →
  settings toggle) instead of scheduler-dependent.
- HUD text updaters (update_hud, update_selection_hud,
  update_won_previously) chained, in UiTextFx, after the new
  AutoComplete set (update_hud reads AutoCompleteState).
- restore_hud_on_modal → apply_hud_visibility chained before
  UpdateOnResize: HudVisibility writes, application, and the layout
  read happen in a fixed order.
- New writer sets UndoRequestWriters / InfoToastWriters (same
  self-ambiguous pattern as NewGameRequestWriters).
- Logic-before-paint: check_no_moves and the AutoComplete chain order
  before BoardVisuals; SettingsMutation after UpdateOnResize.
- MarkerVisuals set wraps the table painter chain; chrome fx declare
  disjointness from it.
- update_hud_typography after BoardVisuals; avatar/settings-toggle
  interaction handlers declared disjoint from HudButtons.

302 → 198 → 171 → 47 → 0 in four batches, one day. New systems now
fail CI unless they declare their ordering or their disjointness.

Closes #143

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:13:58 -07:00

109 lines
4.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Schedule hygiene checks (issue #143).
//!
//! Bevy runs systems with conflicting data access in nondeterministic order
//! unless an ordering edge exists. Nothing enforced this historically, and
//! the headless gameplay cluster carries a large legacy backlog of such
//! pairs — most benign (event/resource writers that never observably race),
//! but unproven. Rather than a permanently red hard-error test or no test
//! at all, this is a RATCHET: the count may only go down. New conflicting
//! pairs fail CI immediately; the backlog can be triaged incrementally
//! (add `.before`/`.after` where order matters, `.ambiguous_with` where it
//! provably doesn't, then lower `AMBIGUITY_BASELINE`).
//!
//! To see the offending pair names, add the `bevy_debug` / debug feature to
//! the bevy dev-dependency (system names are stripped otherwise) and set
//! `ambiguity_detection: LogLevel::Error` manually.
//!
//! Scope note: `CoreGamePlugin` (the full composition) performs real
//! storage/platform I/O in `build` and cannot run in a unit test; the
//! cluster below is the same one the engine's headless behaviour tests use.
#[cfg(test)]
mod tests {
use bevy::ecs::schedule::{LogLevel, ScheduleBuildSettings};
use bevy::prelude::*;
use crate::auto_complete_plugin::AutoCompletePlugin;
use crate::card_plugin::CardPlugin;
use crate::game_plugin::GamePlugin;
use crate::hud_plugin::HudPlugin;
use crate::settings_plugin::SettingsPlugin;
use crate::table_plugin::TablePlugin;
use crate::ui_focus::UiFocusPlugin;
use crate::ui_modal::UiModalPlugin;
/// The backlog (302 pairs on 2026-07-06) was burned down to ZERO the
/// same day (#143, PRs #146#149) — this is now a hard gate. If your
/// change trips this assertion you have added a pair of systems with
/// conflicting data access and no ordering edge: add `.before`/`.after`
/// where order matters, or `.ambiguous_with` the relevant domain set
/// (BoardVisuals, MarkerVisuals, UiTextFx, HudButtons, writer sets)
/// where it provably does not. Do not raise this constant.
const AMBIGUITY_BASELINE: usize = 0;
fn cluster_app() -> App {
let mut app = App::new();
app.add_plugins(MinimalPlugins)
.add_plugins(GamePlugin)
.add_plugins(TablePlugin)
.add_plugins(CardPlugin)
.add_plugins(HudPlugin)
.add_plugins(AutoCompletePlugin)
.add_plugins(UiModalPlugin)
.add_plugins(UiFocusPlugin)
.add_plugins(SettingsPlugin::headless());
app.init_resource::<ButtonInput<KeyCode>>();
app
}
#[test]
fn update_schedule_ambiguities_do_not_grow() {
let mut app = cluster_app();
app.edit_schedule(Update, |s| {
s.set_build_settings(ScheduleBuildSettings {
ambiguity_detection: LogLevel::Error,
..Default::default()
});
});
// With LogLevel::Error the first schedule build panics when any
// ambiguity exists, and the panic message begins with the pair
// count. Catch it and ratchet on that count. (Parsing a panic
// message is brittle by design — if a Bevy upgrade rewords it,
// this test fails loudly and the parse below needs one-line
// maintenance, which is preferable to losing the ratchet.)
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
app.update();
}));
let count = match result {
Ok(()) => 0,
Err(payload) => {
let msg = payload
.downcast_ref::<String>()
.cloned()
.or_else(|| payload.downcast_ref::<&str>().map(|s| (*s).to_string()))
.unwrap_or_default();
let parsed = msg
.split(" pairs of systems")
.next()
.and_then(|prefix| prefix.split_whitespace().last())
.and_then(|n| n.parse::<usize>().ok());
parsed.unwrap_or_else(|| {
panic!(
"ambiguity panic message no longer parseable (Bevy upgrade?): {msg}"
)
})
}
};
assert_eq!(
count, AMBIGUITY_BASELINE,
"system-order ambiguities changed from the enforced baseline. \
Add .before/.after or .ambiguous_with at the new registration site \
(or, if the count legitimately dropped below a nonzero baseline, \
lower AMBIGUITY_BASELINE).",
);
}
}