test(ai-engine): cover queued cancellation and clarify widget qualification (#106)

This commit is contained in:
skylar taylor-barrick
2026-10-25 15:48:32 -06:00
committed by GitHub
parent 0e55340771
commit 6f7078666a
16 changed files with 239 additions and 35 deletions
+6 -4
View File
@@ -73,10 +73,12 @@ Read [docs/STATUS.md](docs/STATUS.md) before relying on a provider or tool. In s
- **Tested against scripted servers on localhost:** every hosted adapter, the media copier, the generic
ComfyUI adapter, the declarative adapter and the client flow.
- **Run for real on one machine:** the SAM 3 point and session clients through a headless ComfyUI; SAM 2.1 on rten with the published ONNX files; and an imported selection graph on ComfyUI 0.22.3. All on Windows, with one sample picture.
- **Migrated shared widgets, headless:** `craft-ai-egui` passed 108 tests, strict all-target clippy
and `cargo fmt --check` on Linux, five committed kittest snapshots, and the locked consumer-smoke
check with one draw test and one `craft-ai-core` identity. The rendererless smoke does not use
winit.
- **Migrated shared widgets, headless:** the targeted `craft-ai-egui`/`craft-ai-engine` tests and the
locked consumer smoke passed 126 tests with 0 failures and 4 ignored doctests under managed job
`egui037-ai-final-ci-20261025-1331`, with formatting that left the worktree unchanged, strict
all-target clippy for those two packages, five committed kittest snapshots, one draw test and one
`craft-ai-core` identity on Linux. The earlier 108-test mutation checkpoint is historical. The
rendererless smoke does not use winit.
- **Not verified:** any real hosted service, TLS behind an intercepting proxy, macOS and Linux
keychains, macOS builds of the test suite, and any app running a tool through the shared binding.
Native widget behavior on Linux X11/Wayland and Windows (focus, keyboard, clipboard, IME and
+2
View File
@@ -48,6 +48,8 @@ pub fn saved_id(event: PanelEvent) -> Option<ConnectionId> {
mod tests {
use super::*;
/// Draws the public widgets from synthetic data and local core types, starting no provider or
/// store, and discards the headless renderer's texture delta.
#[test]
fn public_widgets_draw_without_a_provider_or_store() {
let mut form = ConnectionFormState::new();
+8
View File
@@ -171,6 +171,8 @@ struct Demo {
}
impl Demo {
/// Returns sample data unless `real`, where the real log is merged; a missing or unreadable file
/// becomes an empty log.
fn load(real: bool) -> HistoryLog {
if !real {
return sample_log(now_ms());
@@ -189,6 +191,8 @@ impl Demo {
}
}
/// Refresh reloads the log, Clear empties only this window's copy, and Export writes
/// `history-export.csv` in the current directory.
fn apply(&mut self, action: HistoryAction) {
match action {
HistoryAction::Refresh => {
@@ -210,12 +214,15 @@ impl Demo {
}
impl eframe::App for Demo {
/// Drains queued history actions outside the paint callback, applying each once.
fn logic(&mut self, _ctx: &egui::Context, _frame: &mut eframe::Frame) {
while let Some(action) = self.pending.pop_front() {
self.apply(action);
}
}
/// Draws the heading, history view and last note, queueing the view's action once and requesting
/// another repaint.
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ui, |ui| {
ui.heading(if self.real {
@@ -236,6 +243,7 @@ impl eframe::App for Demo {
}
}
/// Starts the eframe history demo; `--real` selects the real log instead of the sample.
fn main() -> eframe::Result {
let real = std::env::args().any(|a| a == "--real");
let options = eframe::NativeOptions {
+11
View File
@@ -64,6 +64,8 @@ struct Sample {
secrets: Arc<MemoryStore>,
}
/// Builds a temporary sample store: local ComfyUI models only when `sam3_installed`, two remote
/// providers, one disabled local provider, and a suite default that fails for lack of a key.
fn build(sam3_installed: bool) -> Sample {
let seg = [InputRole::Source, InputRole::Prompt];
let mut connections = Vec::new();
@@ -172,6 +174,7 @@ impl Demo {
)
}
/// Recomputes the picker for the current tool and keeps the previous view if the client errors.
fn refresh(&mut self) {
let options = craft_ai_features::setup_options(&craft_ai_features::builtin());
if let Ok(view) = self.client("demo").picker(&self.role(), &options) {
@@ -187,6 +190,8 @@ impl Demo {
}
}
/// Stores the suite-wide choice through `ToolDesk` and refreshes the picker. `every_app` selects
/// the photocraft client and adds scope wording; otherwise the client is launcher.
fn choose(&mut self, connection: ConnectionId, model: ModelId, every_app: bool) {
let desk = ToolDesk::new(
self.client(if every_app { "photocraft" } else { "launcher" }),
@@ -201,6 +206,8 @@ impl Demo {
self.refresh();
}
/// Applies one queued demo action: rebuild, refresh, a picker or popup choice, a dismissal, or a
/// notice about what setup would open. Nothing is downloaded.
fn apply(&mut self, action: DemoAction) {
match action {
DemoAction::Rebuild => {
@@ -266,12 +273,15 @@ impl Demo {
}
impl eframe::App for Demo {
/// Drains queued demo actions outside the paint callback, applying each once.
fn logic(&mut self, _ctx: &egui::Context, _frame: &mut eframe::Frame) {
while let Some(action) = self.pending.pop_front() {
self.apply(action);
}
}
/// Draws the tool toggles, the picker or popup, and the note; queues each press once and asks for
/// a repaint.
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ui, |ui| {
ui.label(
@@ -318,6 +328,7 @@ impl eframe::App for Demo {
}
}
/// Starts the eframe picker demo with the sample data installed and performs the initial refresh.
fn main() -> eframe::Result {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([560.0, 640.0]),
+4
View File
@@ -126,6 +126,8 @@ impl HistoryViewState {
}
}
/// A filter combo: drops a selected value that has left `options` (so a stale filter cannot hide
/// everything), shows `all` in that case, and gives the control `name` for assistive tech.
fn pick(
ui: &mut Ui,
id: &str,
@@ -272,6 +274,8 @@ pub fn history_view(
action
}
/// Draws grouped totals in a height-capped scroll area that shrinks to its content. The scroll id includes `tab`, so Jobs and
/// Days keep separate scroll positions.
fn grouped<'a>(ui: &mut Ui, tab: Tab, rows: impl Iterator<Item = (String, &'a Totals)>) {
ScrollArea::vertical()
.id_salt(("history-list", tab))
+8
View File
@@ -74,6 +74,8 @@ struct TestIdentity {
}
impl TestIdentity {
/// Captures every draft field whose change makes a running test's answer stale: endpoint,
/// provider, key, whether a key is used, and the key header.
fn of(draft: &ConnectionDraft) -> Self {
Self {
endpoint: draft.endpoint.clone(),
@@ -295,6 +297,9 @@ impl ConnectionPanel {
self.events.pop_front()
}
/// Records an answer only while the draft still accepts models for the tested address; a rejected
/// answer leaves the test line alone rather than showing `Passed`. Vertex-express `Unsupported`
/// becomes `Unverified`, other errors `Failed`, and elapsed time saturates at `u64::MAX`.
fn apply_answer(
&mut self,
answer: Result<craft_ai_client::TestReport, craft_ai_core::AiError>,
@@ -335,6 +340,8 @@ mod tests {
use craft_ai_native::shared::SharedStore;
use std::sync::Arc;
/// A panel over a temporary shared store and in-memory secrets. Keep the returned `TempDir`
/// alive for as long as the panel is used.
fn panel() -> (tempfile::TempDir, ConnectionPanel) {
let dir = tempfile::tempdir().unwrap();
let shared = SharedStore::new(dir.path().join("connections.json"));
@@ -342,6 +349,7 @@ mod tests {
(dir, ConnectionPanel::new(manager))
}
/// A successful test report for one model named after `model`, with zero elapsed time and no probe.
fn answer(model: &str) -> Result<TestReport, craft_ai_core::AiError> {
Ok(TestReport {
models: vec![ModelInfo::new(
+2
View File
@@ -119,6 +119,8 @@ fn setup_label(group: &PickerGroup) -> Option<&'static str> {
}
}
/// Draws one provider's title, badges, note and setup button, then its model rows. Models are
/// disabled unless the group is `Ready`, while the provider stays visible and setup stays usable.
fn group_ui(ui: &mut Ui, group: &PickerGroup) -> Option<PickerAction> {
let mut action = None;
let ready = group.state == GroupState::Ready;
+10
View File
@@ -269,6 +269,8 @@ fn refresh_is_reported_and_unreadable_lines_are_mentioned() {
assert_eq!(h.state().action, Some(HistoryAction::Refresh));
}
/// With 3,000 jobs only rows in view exist: the newest is shown, the oldest is not, and the
/// summary still reports all 3,000.
#[test]
fn a_long_log_draws_only_the_rows_in_view() {
let jobs: Vec<Job> = (0..3000u64)
@@ -296,6 +298,8 @@ struct SizedState {
spacing: f32,
}
/// Builds a 40-job harness at 320×700 with 26pt body text and records the text height and item
/// spacing.
fn large_font_harness() -> Harness<'static, SizedState> {
let jobs: Vec<Job> = (0..40u64)
.map(|i| {
@@ -356,6 +360,8 @@ fn visible_rows(h: &Harness<'_, SizedState>) -> Vec<(usize, egui::Rect)> {
rows
}
/// Scrolls from the last visible row `times` times, stepping enough frames after each scroll for
/// the list to settle.
fn scroll_rows(h: &mut Harness<'_, SizedState>, times: usize) {
for _ in 0..times {
let node = h
@@ -369,6 +375,8 @@ fn scroll_rows(h: &mut Harness<'_, SizedState>, times: usize) {
}
}
/// Row pitch is one text height plus item spacing, order stays newest-first, and scrolling reaches
/// the oldest job.
#[test]
fn rows_use_one_text_height_without_double_spacing_at_a_larger_font() {
let mut h = large_font_harness();
@@ -414,6 +422,8 @@ fn rows_use_one_text_height_without_double_spacing_at_a_larger_font() {
assert!(end.first().unwrap().0 < 39);
}
/// App, model and date filters are named ComboBoxes with no unnamed widget; clicking Days then
/// Jobs leaves the selected tab at Jobs.
#[test]
fn each_filter_is_named_by_what_it_filters_not_by_its_current_value() {
let mut h = harness(HistoryViewState::default(), two_apps(), T0 + 5000);
+21
View File
@@ -34,10 +34,13 @@ struct Rig {
_dir: tempfile::TempDir,
}
/// A panel harness with a temporary shared store, in-memory secrets and no forced second pass.
fn rig() -> Rig {
rig_with(false)
}
/// Builds the shared harness; `force_multipass` makes the first pass request a discard, so a
/// press is seen twice.
fn rig_with(force_multipass: bool) -> Rig {
let dir = tempfile::tempdir().unwrap();
let shared = SharedStore::new(dir.path().join("connections.json"));
@@ -137,6 +140,8 @@ fn a_remote_connection_is_saved_with_its_key_in_the_secret_store_only() {
assert_eq!(stored.expose(), KEY);
}
/// An invalid form keeps the password field visible, emits no event or stored connection, and does not
/// echo the rejected key in its message.
#[test]
fn a_form_problem_stays_on_screen_and_saves_nothing() {
let mut r = rig();
@@ -265,6 +270,8 @@ fn slow_models_server() -> Server {
})
}
/// An answer that arrives after the address changed mid-test is neither reported as the new
/// address's connection nor saved with its models.
#[test]
fn models_from_a_test_that_was_still_running_are_not_saved_for_the_new_address() {
// The sibling test above edits the address *after* the answer is in. This is the other
@@ -307,6 +314,7 @@ fn models_from_a_test_that_was_still_running_are_not_saved_for_the_new_address()
);
}
/// Selects Ollama in the provider combo and replaces the address field.
fn choose_ollama_and_address(h: &mut Harness<'_, State>, address: &str) {
h.get_by_role(Role::ComboBox).click();
h.run();
@@ -315,6 +323,7 @@ fn choose_ollama_and_address(h: &mut Harness<'_, State>, address: &str) {
replace_text(h, 1, address);
}
/// Starts a scripted model-list server returning the named models after an optional delay.
fn named_models_server(models: &[&str], delay_ms: u64) -> Server {
let entries: Vec<String> = models
.iter()
@@ -329,6 +338,8 @@ fn named_models_server(models: &[&str], delay_ms: u64) -> Server {
})
}
/// Name and address show their visible labels, the key is a labelled password input, and turning
/// off "Uses a key" removes that input.
#[test]
fn each_field_has_its_visible_name_and_role_and_the_key_goes_away_when_not_used() {
let mut r = rig();
@@ -366,6 +377,8 @@ fn each_field_has_its_visible_name_and_role_and_the_key_goes_away_when_not_used(
}
}
/// After a Test press the next pass disables the button while the line reports `Running`; the
/// gated answer re-enables it and reports the models.
#[test]
fn test_is_disabled_immediately_and_reports_the_running_state() {
let (release, gate) = std::sync::mpsc::channel::<()>();
@@ -392,6 +405,8 @@ fn test_is_disabled_immediately_and_reports_the_running_state() {
assert!(!r.h.get_by_label("Test").accesskit_node().is_disabled());
}
/// With a forced second pass, one Test press sends one request and one Save press emits one Saved
/// event and one stored connection.
#[test]
fn one_click_tests_and_saves_once_when_egui_runs_a_second_pass() {
let server = models_server();
@@ -426,6 +441,8 @@ fn one_click_tests_and_saves_once_when_egui_runs_a_second_pass() {
assert_eq!(r.shared.load().unwrap().connections.len(), 1);
}
/// Calling `poll` outside a UI pass finishes the test, adds no UI pass, and a passing test emits
/// no panel event.
#[test]
fn a_hidden_poll_finishes_the_test_without_another_ui_pass() {
let server = models_server();
@@ -463,6 +480,8 @@ fn a_hidden_poll_finishes_the_test_without_another_ui_pass() {
assert_eq!(server.count(), 1);
}
/// A slow answer for the replaced endpoint is dropped; only the new endpoint's models are
/// reported and saved.
#[test]
fn a_late_answer_for_the_old_endpoint_cannot_replace_the_new_tests_models() {
let old = named_models_server(&["old-1", "old-2"], 600);
@@ -505,6 +524,8 @@ fn a_late_answer_for_the_old_endpoint_cannot_replace_the_new_tests_models() {
assert_eq!(saved.connections[0].models[0].id.as_str(), "new-1");
}
/// A save finishing in the same frame as Cancel delivers Saved first and Closed second, and the
/// connection is stored.
#[test]
fn a_save_that_finishes_as_cancel_is_pressed_is_reported_before_closed() {
let mut r = rig();
+9
View File
@@ -40,6 +40,8 @@ fn group(title: &str, state: GroupState, models: Vec<PickerModel>) -> PickerGrou
}
}
/// Builds a synthetic `PickerView` for a role with a title, an optional current choice and the
/// given groups.
fn view(groups: Vec<PickerGroup>, current: Option<CurrentChoice>) -> PickerView {
PickerView {
tool: RoleId::new("select-subject").unwrap(),
@@ -50,6 +52,7 @@ fn view(groups: Vec<PickerGroup>, current: Option<CurrentChoice>) -> PickerView
}
}
/// A fixed-size picker harness whose state keeps the last reported action.
fn harness_sized(size: egui::Vec2, view: PickerView) -> Harness<'static, State> {
let mut h = Harness::builder().with_size(size).build_ui_state(
|ui, st: &mut State| {
@@ -63,6 +66,7 @@ fn harness_sized(size: egui::Vec2, view: PickerView) -> Harness<'static, State>
h
}
/// A default-size picker harness whose state keeps the last reported action.
fn harness(view: PickerView) -> Harness<'static, State> {
let mut h = Harness::new_ui_state(
|ui, st: &mut State| {
@@ -319,6 +323,7 @@ fn presets_sit_above_the_full_list_with_their_reason_and_choose_like_any_row() {
);
}
/// Without presets neither the Suggested section nor the extra All Models heading is drawn.
#[test]
fn with_no_presets_there_is_no_suggested_section_and_no_extra_heading() {
let h = harness(view(
@@ -333,6 +338,8 @@ fn with_no_presets_there_is_no_suggested_section_and_no_extra_heading() {
assert!(h.query_by_label(ALL_MODELS).is_none());
}
/// A selected model is a toggled-on button, other models are toggled off, and setup is an
/// ordinary button role.
#[test]
fn a_selected_model_is_reported_as_selected_and_setup_is_an_ordinary_button() {
let mut group = group(
@@ -361,6 +368,8 @@ fn a_selected_model_is_reported_as_selected_and_setup_is_an_ordinary_button() {
assert_eq!(h.get_by_label(SET_UP).accesskit_node().role(), Role::Button);
}
/// At 180px a long provider header wraps while the setup button stays inside the window and
/// still reports its action.
#[test]
fn a_long_provider_header_wraps_its_setup_button_into_a_narrow_window() {
let mut group = group(
+18
View File
@@ -38,6 +38,7 @@ fn snapshot_builder<State>(size: egui::Vec2) -> HarnessBuilder<State> {
})
}
/// Snapshot settings: output under `tests/snapshots`, threshold 0.6 and zero failed pixels.
fn snapshot_options() -> SnapshotOptions {
SnapshotOptions::new()
.output_path("tests/snapshots")
@@ -45,6 +46,7 @@ fn snapshot_options() -> SnapshotOptions {
.max_failed_pixels(0)
}
/// The logical screen rectangle from the origin at `size`.
fn screen(size: egui::Vec2) -> egui::Rect {
egui::Rect::from_min_size(egui::Pos2::ZERO, size)
}
@@ -57,6 +59,7 @@ struct Job {
outcome: Outcome,
}
/// A synthetic completed Photocraft job for the history fixtures.
fn job(run: &'static str, at: u64, model: &'static str) -> Job {
Job {
run,
@@ -67,6 +70,7 @@ fn job(run: &'static str, at: u64, model: &'static str) -> Job {
}
}
/// Builds a matching start and end record for each job and merges them.
fn history_log(jobs: &[Job]) -> HistoryLog {
let mut records = Vec::new();
for job in jobs {
@@ -111,6 +115,7 @@ struct HistoryState {
action: Option<HistoryAction>,
}
/// A 40-job history harness at 480×700 with a 26pt body font on a fixed dark background.
fn large_font_history() -> Harness<'static, HistoryState> {
let jobs: Vec<Job> = (0..40u64)
.map(|i| {
@@ -143,6 +148,7 @@ fn large_font_history() -> Harness<'static, HistoryState> {
)
}
/// Parses `model-N` indices from accessible labels and returns visible rows sorted by top edge.
fn visible_rows(h: &Harness<'_, HistoryState>) -> Vec<(usize, egui::Rect)> {
let mut rows: Vec<(usize, egui::Rect)> = h
.query_all_by_label_contains("photocraft")
@@ -159,6 +165,8 @@ fn visible_rows(h: &Harness<'_, HistoryState>) -> Vec<(usize, egui::Rect)> {
rows
}
/// Checks one text height plus item spacing per row and newest-first order, then takes the
/// large-font snapshot.
#[test]
fn history_at_a_larger_font_uses_one_text_height_per_row() {
let mut h = large_font_history();
@@ -179,6 +187,7 @@ fn history_at_a_larger_font_uses_one_text_height_per_row() {
h.snapshot_options("history_large_font_dark_480x700", &snapshot_options());
}
/// A three-job history at 240×420 with the All date range selected and a dark background.
fn narrow_history() -> Harness<'static, HistoryState> {
let jobs = [
job("a", T0, "flux"),
@@ -209,6 +218,8 @@ fn narrow_history() -> Harness<'static, HistoryState> {
)
}
/// Filters and actions stay inside 240×420 with no unnamed widget; capture the snapshot, then
/// confirm that Refresh reports an action.
#[test]
fn history_filters_and_actions_stay_inside_a_narrow_viewport() {
let mut h = narrow_history();
@@ -249,6 +260,7 @@ struct ProgressState {
cancelled: bool,
}
/// A 320×480 progress harness at 40% in either left-to-right or right-to-left layout.
fn progress_harness(right_to_left: bool) -> Harness<'static, ProgressState> {
let size = egui::vec2(320.0, 480.0);
snapshot_builder(size).build_ui_state(
@@ -275,6 +287,7 @@ fn progress_harness(right_to_left: bool) -> Harness<'static, ProgressState> {
)
}
/// Bar and Cancel stay in the viewport, do not overlap, and follow the layout's LTR/RTL order.
fn cancel_bounds_are_usable(h: &Harness<'_, ProgressState>, right_to_left: bool) {
let viewport = screen(egui::vec2(320.0, 480.0));
let bar = h.get_by_label_contains("Working, 40%");
@@ -310,6 +323,7 @@ fn cancel_bounds_are_usable(h: &Harness<'_, ProgressState>, right_to_left: bool)
}
}
/// Takes the LTR snapshot, then confirms Cancel is clickable.
#[test]
fn progress_cancel_is_visible_and_clickable_left_to_right() {
let mut h = progress_harness(false);
@@ -321,6 +335,7 @@ fn progress_cancel_is_visible_and_clickable_left_to_right() {
assert!(h.state().cancelled);
}
/// Takes the RTL snapshot, then confirms Cancel is clickable.
#[test]
fn progress_cancel_is_visible_and_clickable_right_to_left() {
let mut h = progress_harness(true);
@@ -339,6 +354,7 @@ struct FormState {
event: Option<FormEvent>,
}
/// A connection form at 240×560 over a fixed dark background.
fn connection_harness() -> Harness<'static, FormState> {
snapshot_builder(egui::vec2(240.0, 560.0)).build_ui_state(
|ui, state: &mut FormState| {
@@ -355,6 +371,8 @@ fn connection_harness() -> Harness<'static, FormState> {
)
}
/// Fields and key keep their labels and roles; the fields, key, checkbox and buttons stay inside
/// 240×560. Capture the snapshot, then confirm that Save reports an action.
#[test]
fn connection_fields_keep_their_names_roles_and_bounds_when_narrow() {
let mut h = connection_harness();
+20
View File
@@ -15,6 +15,7 @@ use egui::accesskit::Role;
use egui_kittest::Harness;
use egui_kittest::kittest::{NodeT, Queryable};
/// Synthetic Photocraft cloud-or-local disclosure with one image input and one parameter.
fn disclosure(locality: Locality) -> Disclosure {
Disclosure {
app: AppId::new("photocraft").unwrap(),
@@ -113,6 +114,7 @@ struct StatusState {
last: Option<StatusAction>,
}
/// A status harness at the default size whose state keeps the last reported action.
fn status_harness(s: StatusState) -> Harness<'static, StatusState> {
Harness::new_ui_state(
|ui, st: &mut StatusState| {
@@ -131,6 +133,7 @@ fn status_harness(s: StatusState) -> Harness<'static, StatusState> {
)
}
/// A status harness at a fixed size whose state keeps the last reported action.
fn status_harness_sized(size: egui::Vec2, s: StatusState) -> Harness<'static, StatusState> {
Harness::builder().with_size(size).build_ui_state(
|ui, st: &mut StatusState| {
@@ -149,6 +152,8 @@ fn status_harness_sized(size: egui::Vec2, s: StatusState) -> Harness<'static, St
)
}
/// An unavailable status explains its reason, offers the launcher, and hides Reset when there is
/// no override.
#[test]
fn unavailable_feature_explains_itself_and_offers_the_launcher() {
let mut h = status_harness(StatusState {
@@ -297,6 +302,7 @@ struct ProgressState {
cancelled: bool,
}
/// A progress harness at the default size for `job`, with optional cost, recording cancellation.
fn progress_harness(job: JobState, cost: Option<CostEstimate>) -> Harness<'static, ProgressState> {
Harness::new_ui_state(
|ui, st: &mut ProgressState| {
@@ -312,6 +318,7 @@ fn progress_harness(job: JobState, cost: Option<CostEstimate>) -> Harness<'stati
)
}
/// A progress harness at a fixed size for `job`, with optional cost, recording cancellation.
fn progress_harness_sized(
size: egui::Vec2,
job: JobState,
@@ -353,6 +360,7 @@ fn progress_harness_rtl_sized(
)
}
/// A running job shows its percent and formatted cost, and Cancel reports.
#[test]
fn a_running_job_shows_percent_cost_and_can_be_cancelled() {
let cost = CostEstimate {
@@ -379,6 +387,7 @@ fn a_job_with_no_percentage_still_shows_it_is_working() {
h.get_by_label(CANCEL);
}
/// Succeeded, Cancelled and Failed show the right text and offer no Cancel.
#[test]
fn finished_jobs_say_how_they_ended_and_cannot_be_cancelled() {
for (state, text) in [
@@ -398,6 +407,8 @@ fn finished_jobs_say_how_they_ended_and_cannot_be_cancelled() {
}
}
/// The term radios expose their checked state; choosing One hour and pressing Allow returns that
/// term.
#[test]
fn timed_consent_radios_expose_their_selected_state_and_duration() {
struct State {
@@ -447,6 +458,7 @@ fn timed_consent_radios_expose_their_selected_state_and_duration() {
);
}
/// The status model combo is labelled for its feature, and no widget is unnamed.
#[test]
fn the_status_model_picker_has_a_field_name_not_just_its_value() {
let mut h = status_harness(StatusState {
@@ -468,6 +480,7 @@ fn the_status_model_picker_has_a_field_name_not_just_its_value() {
assert!(h.unnamed_widgets().is_empty());
}
/// At 180×360 Launcher and Add connection stay inside, and Add reports.
#[test]
fn status_commands_stay_inside_a_narrow_header() {
let mut h = status_harness_sized(
@@ -497,6 +510,7 @@ fn status_commands_stay_inside_a_narrow_header() {
assert_eq!(h.state().last, Some(StatusAction::AddConnection));
}
/// At 320px the bar and Cancel are contained, do not overlap, and Cancel reports.
#[test]
fn cancel_stays_visible_and_does_not_overlap_progress_at_320() {
let mut h = progress_harness_sized(
@@ -521,6 +535,7 @@ fn cancel_stays_visible_and_does_not_overlap_progress_at_320() {
assert!(h.state().cancelled);
}
/// At 120px Cancel wraps below the bar, stays inside and still reports.
#[test]
fn narrow_cancel_wraps_below_the_bar_and_stays_clickable() {
let mut h = progress_harness_sized(
@@ -544,6 +559,7 @@ fn narrow_cancel_wraps_below_the_bar_and_stays_clickable() {
assert!(h.state().cancelled);
}
/// Indeterminate progress at 120px keeps Cancel inside the view and clickable.
#[test]
fn narrow_spinner_job_also_keeps_cancel_reachable() {
let mut h = progress_harness_sized(
@@ -562,6 +578,7 @@ fn narrow_spinner_job_also_keeps_cancel_reachable() {
assert!(h.state().cancelled);
}
/// At 320px right-to-left, Cancel precedes the bar, does not overlap it and reports.
#[test]
fn rtl_narrow_progress_keeps_cancel_visible_and_apart_from_the_bar() {
let mut h = progress_harness_rtl_sized(
@@ -592,6 +609,7 @@ fn rtl_narrow_progress_keeps_cancel_visible_and_apart_from_the_bar() {
assert!(h.state().cancelled);
}
/// At 320px right-to-left, Cancel precedes the label, does not overlap it and reports.
#[test]
fn rtl_narrow_spinner_job_keeps_cancel_visible_and_clickable() {
let mut h = progress_harness_rtl_sized(
@@ -624,6 +642,8 @@ fn rtl_narrow_spinner_job_keeps_cancel_visible_and_clickable() {
assert!(h.state().cancelled);
}
/// Draws the fixture bullet, ellipsis, dash and middle dot; the harness fails if the test font
/// lacks a glyph.
#[test]
fn the_fixed_fixture_glyphs_are_in_the_test_fonts() {
let mut h = Harness::new_ui_state(
+77 -1
View File
@@ -241,6 +241,8 @@ impl Provider for EngineProvider {
Ok(self.job(job)?.snapshot())
}
/// Cancels a known job through `Job::cancel`; an unknown id fails via `self.job(job)?`.
/// Cancellation is cooperative, non-blocking and never loads or runs a model.
fn cancel(&self, job: &JobId) -> Result<(), AiError> {
self.job(job)?.cancel();
Ok(())
@@ -249,13 +251,15 @@ impl Provider for EngineProvider {
#[cfg(test)]
mod tests {
use std::time::Duration;
use std::time::{Duration, Instant};
use craft_ai_core::JobState;
use super::*;
use crate::testing::FakeRunner;
/// Writes placeholder weights into `dir`, builds an `EngineProvider` around `fake`, and returns
/// its first entry; keep `dir` alive so the weights remain on disk.
fn entry(fake: FakeRunner, dir: &tempfile::TempDir) -> Arc<Entry> {
let weights = dir.path().join("m.safetensors");
std::fs::write(&weights, b"w").unwrap();
@@ -310,4 +314,76 @@ mod tests {
assert_eq!(loads.load(Ordering::SeqCst), 1);
assert_eq!(runs.load(Ordering::SeqCst), 1);
}
/// A job queued for one model behind another job's turn there, then cancelled before it gets
/// its turn, must never load or run that model. The entry's real lock is held while the job
/// waits; dropping it lets the worker proceed. Waiting for the provider's pending count to
/// reach zero proves the worker returned from `Entry::run`, so the zero counters are not just
/// a race. A distinct model then runs an uncancelled job as the positive control.
#[test]
fn a_queued_job_for_another_model_cancelled_before_its_turn_never_loads_it() {
let dir = tempfile::tempdir().unwrap();
let first = FakeRunner::new("candle", "sam3", "image.segment");
let second = FakeRunner::new("candle", "other", "image.segment");
let (first_loads, first_runs) = (first.loads.clone(), first.runs.clone());
let (second_loads, second_runs) = (second.loads.clone(), second.runs.clone());
let first_request = first.good_request().unwrap();
let second_request = second.good_request().unwrap();
let first_model = ModelId::new("sam3").unwrap();
let second_model = ModelId::new("other").unwrap();
let weights = |name: &str| {
let path = dir.path().join(name);
std::fs::write(&path, b"w").unwrap();
path
};
let provider = EngineProvider::new(
"candle",
[
(
Arc::new(first) as Arc<dyn ModelRunner>,
weights("a.safetensors"),
),
(
Arc::new(second) as Arc<dyn ModelRunner>,
weights("b.safetensors"),
),
],
);
// A running job for the second entry would hold this lock. The queued worker cannot pass
// `Job::begin` until the guard is dropped; unwinding drops it too.
let held = provider.entries[1].loaded.lock().unwrap();
let queued = provider.submit(&second_model, &second_request).unwrap();
assert_eq!(provider.poll(&queued).unwrap().state, JobState::Queued);
provider.cancel(&queued).unwrap();
drop(held);
// `pending` falls only after the worker closure returns from `Entry::run`.
let end = Instant::now() + Duration::from_secs(10);
while provider.pending.load(Ordering::Acquire) != 0 {
assert!(Instant::now() < end, "the cancelled worker never finished");
std::thread::sleep(Duration::from_millis(1));
}
assert_eq!(second_loads.load(Ordering::SeqCst), 0);
assert_eq!(second_runs.load(Ordering::SeqCst), 0);
assert_eq!(provider.poll(&queued).unwrap().state, JobState::Cancelled);
// Positive control on the other, distinct model: the provider still loads and runs an
// uncancelled job, and the cancelled model's counters stay zero.
let control = provider.submit(&first_model, &first_request).unwrap();
let end = Instant::now() + Duration::from_secs(10);
loop {
let snapshot = provider.poll(&control).unwrap();
if snapshot.state.is_terminal() {
assert_eq!(snapshot.state, JobState::Succeeded);
break;
}
assert!(Instant::now() < end, "the control job never finished");
std::thread::sleep(Duration::from_millis(1));
}
assert_eq!(first_loads.load(Ordering::SeqCst), 1);
assert_eq!(first_runs.load(Ordering::SeqCst), 1);
assert_eq!(second_loads.load(Ordering::SeqCst), 0);
assert_eq!(second_runs.load(Ordering::SeqCst), 0);
}
}
+4
View File
@@ -237,6 +237,8 @@ fn until(what: &str, check: impl Fn() -> bool) {
}
}
/// A second job reports `Queued` behind the running first. Cancelling both leaves the second
/// `Cancelled` without running, and the runner was loaded and run only once.
#[test]
fn a_job_waiting_behind_another_says_queued_and_cancelled_it_never_runs() {
let dir = tempfile::tempdir().unwrap();
@@ -263,6 +265,8 @@ fn a_job_waiting_behind_another_says_queued_and_cancelled_it_never_runs() {
assert_eq!(loads.load(Ordering::SeqCst), 1);
}
/// Submitting a burst ends in `RateLimited` with the plain-language wait message; cancelling the
/// accepted jobs frees capacity again.
#[test]
fn a_burst_of_requests_is_refused_in_plain_words_and_room_returns_when_they_end() {
let dir = tempfile::tempdir().unwrap();
+1 -1
View File
@@ -18,7 +18,7 @@ No real hosted service, download host or repository was contacted by these tests
| Generic ComfyUI workflows | `craft-ai-comfyui` | Maps a prompt field; `image.inpaint` needs mapped `LoadImage` and `LoadImageMask` fields. A workflow is offered as a model only after `/object_info` confirms its nodes and checkpoint names; submission repeats the check. Output stays inline (32 MiB) unless the app gives a directory (1 GiB, local `file:` handle, app owns cleanup). Targeted cancel is off by default because an older server may treat it as a global interrupt. Imported image.segment workflows map a description and source to a checked, source-sized grayscale mask plus the shared result. Empty masks mean no match; source uploads are remembered for selection jobs. This is not calibrated absence detection: the example Florence graph can find a box for an absent subject. No input file handles or general parameter mapping yet. |
| Built-in engines | `craft-ai-engine`, `craft-ai-engine-candle`, `craft-ai-engine-rten`, `craft-ai-engine-burn` | Contract, registry and provider are tested with a fake runner and the shared conformance suite. SAM 3 on Candle (point and text prompts, processor, or Metal on macOS) ran on a real photograph and passes the same suite on Meta's checkpoint; the live tests skip without `CRAFT_SAM3_WEIGHTS`. It reads the Hugging Face checkpoint layout with tokenizer files, not the ComfyUI file. Pictures go in as PNG or JPEG (a JPEG's EXIF turn is applied). No Nvidia (CUDA) build, and the launcher does not download the checkpoint yet. SAM 2.1 on rten (processor only) reads the publisher's two `.onnx` files, pinned by size and SHA-256, with click and box prompts; a description is refused in plain words. Pictures are bounded by side and pixel count. With the real files in release on one Windows machine: a mask at the picture's size, and a hover through the ordinary job path with a median of about 35 ms after a first look of about 0.7 s (one picture, not a benchmark). SAM 2.1 is the first recipe for Select subject, so the plain built-in offer is SAM 2.1 on rten. No macOS or Linux run, and no app has run it through the shared binding yet. SAM 2.1 on Burn (`craft-ai-engine-burn`, cargo feature `burn`, optional, after rten in the recipe) reads weights that `craft-ai-burn-setup` prepares once on the computer; on one Windows machine with a graphics card its mask matched rten's (best-mask IoU 1.0, one click and one box, one picture, not a benchmark). Given the downloaded `encoder.onnx`, the runner prepares the saved copy itself on first load (in `burn-prepared/` beside the model folder; no progress and no Cancel during that load). No app has run it through the shared binding yet, and there has been no live run on macOS or Linux. See `docs/BURN-ENGINE.md`. |
| First-run setup flow | `craft-ai-client`, `craft-ai-egui`, `craft-ai-features` | One start button on `SetupSession` (checks complete files on disk, otherwise downloads; `blocker` says why it does nothing), a licence tick only where a file's licence needs one, `FolderDesk::ensure_default` for the suggested folder, `ToolDesk::adopt` for checked files only, a try-it sample that clicks for built-in selection models and fails a selection that chose nothing, status and picker that agree when a model's file has gone, and engine advice that does not call a measured-quick built-in engine (rten, one machine) slow. Scripted tests and a fake provider only: no real download, no model run through it, no app has mounted it, no macOS or Linux run. |
| Shared egui widgets | `craft-ai-egui` | Source and tests migrated to egui 0.37 with the reviewed storytold egui family at `6d518fc799681571963842f95b55a6b68431b15a` and winit at `a517b3846ee8457d84ee4975110bfda6797c44dc`. The final headless candidate passed 108 tests, strict all-target clippy and `cargo fmt --check` on Linux. Five deterministic kittest `wgpu`/snapshot goldens are committed; a pure-background RGB(27,27,27) to RGB(30,30,30) change failed the expected snapshot with 75,486 changed pixels, and the two compiled connection-panel lifecycle mutants were caught before the source and golden hashes were restored and the 108 tests passed again. A locked consumer-smoke check and its one draw test passed with one `craft-ai-core` identity and the pinned egui family; that rendererless root does not use the winit patch. No app imports the widgets. Native X11/Wayland/Windows behavior, IME/clipboard/focus, physical GPU qualification and performance measurements remain open. |
| Shared egui widgets | `craft-ai-egui` | Source and tests migrated to egui 0.37 with the reviewed storytold egui family at `6d518fc799681571963842f95b55a6b68431b15a` and winit at `a517b3846ee8457d84ee4975110bfda6797c44dc`. The current headless candidate passed 126 tests with 0 failures and 4 ignored doctests under managed job `egui037-ai-final-ci-20261025-1331`, with formatting that left the worktree unchanged and strict all-target clippy for `craft-ai-egui` and `craft-ai-engine` on Linux. Five deterministic kittest `wgpu`/snapshot goldens are committed and passed in that run. The historical 108-test checkpoint at `egui037-ai-regression-mutation-20261024-2205` also recorded a pure-background RGB(27,27,27) to RGB(30,30,30) change failing the expected snapshot with 75,486 changed pixels, and the two compiled connection-panel lifecycle mutants caught before the source and golden hashes were restored. A locked consumer-smoke test build and its one draw test passed with one `craft-ai-core` identity and the pinned egui family; that rendererless root does not use the winit patch. No app imports the widgets. Native X11/Wayland/Windows behavior, IME/clipboard/focus, physical GPU qualification and performance measurements remain open. Full remote workspace/platform CI also remains open. |
| Declarative adapter | `craft-ai-declarative` | Request/response and queue flows from a data file; prompt-only inputs. No launcher screen to add a spec yet. |
| Custom connections, model sources, node packs | `craft-ai-core`, `craft-ai-comfy-env` | See `docs/custom-connections-and-sources.md`. Own-host tokens only, no key sent across a redirect, pickle formats need consent, unverified checksums are flagged. |
| Model folders | `craft-ai-core`, `craft-ai-comfy-env`, `craft-ai-client` | A person's own folders of model files, kept as a map (which sub-folders hold which kind) in the shared store, never moved, renamed or deleted. Scanned read only with limits (depth, files, entries, header size), one default for downloads, folders linked from ComfyUI are read only, and the map can be written as a ComfyUI extra-paths file. Not wired yet: downloads and the installed-files scan still use ComfyUI's own folders, and no launcher screen is merged. |
+38 -29
View File
@@ -1,38 +1,42 @@
# egui 0.37 migration plan for craft-ai-integrations
Current publication candidate (2026-10-25): implementation and qualification changes are committed at `087f5a0` on the migration branch; the uncommitted-source descriptions below are historical. The current panel source, snapshot-test source and all five golden image hashes match the retained passing mutation-qualification packet. The changed widget package passed 108 tests, strict all-target lint and formatting; the separate consumer smoke passed a locked check and one draw test. Full remote workspace/platform CI, native host/IME/clipboard and paired performance acceptance remain open. Publication requires explicit approval; human review and human merge are required.
Current publication candidate (2026-10-25): `origin/main` is `0e553407716e958c6281d5b6df0a9554fc5632b7` (tree identical to the merged #105 head `7b3f3e616c37f4eadc04525f075ee1fce08448eb`) and this follow-up adds the 13-file documentation and cancellation-test change set, plus corrected qualification records, on `codex/egui-037-ai-followup-20261025`. Managed job `egui037-ai-final-ci-20261025-1331`, run on the identical `7b3f3e6` tree plus that 13-file change set, passed the targeted `craft-ai-egui` and `craft-ai-engine` tests and the locked consumer smoke: **126 passed, 0 failed, 4 ignored** (the ignored tests are doctests), with formatting and strict all-target clippy for those two packages green; the run left the worktree unchanged. The earlier 108-test mutation-qualification packet at `egui037-ai-regression-mutation-20261024-2205` is retained as historical evidence. Full remote workspace/platform CI, native host/IME/clipboard and paired performance acceptance remain open. Publication requires explicit approval; human review and human merge are required.
## Historical implementation and qualification record
Status: implementation and headless qualification are complete for the current follow-up;
Status: implementation and headless qualification have advanced through the current follow-up;
**not acceptance-complete**. The source audit at
`1a02afd6bed425bcd516d8de2f3dc3b29614b23d` is retained below as historical evidence. The current
checkout is `7c80bbc52cb5a84cebeb67529a18d670a15c511b` plus local AI-5 follow-up changes (the
deterministic snapshot suite, the separate consumer-smoke root, the panel mutation group and these
docs). The follow-up passed 108 tests, strict all-target clippy and fmt on Linux; its five candidate
snapshots, lifecycle mutants and locked consumer smoke are checked, and an independent test-packet
review passed. Native GUI/IME, physical-GPU and performance qualification remain open. No push, pull
request, comment, release, merge or other publication is authorised; each future publication and the
final merge still need fresh explicit human approval after the exact content and validation are shown.
`1a02afd6bed425bcd516d8de2f3dc3b29614b23d` is retained below as historical evidence. The
108-test checkpoint was the `7c80bbc52cb5a84cebeb67529a18d670a15c511b` checkout plus local AI-5
follow-up changes (the deterministic snapshot suite, the separate consumer-smoke root, the panel
mutation group and these docs); it passed 108 tests under managed job
`egui037-ai-regression-mutation-20261024-2205`, with strict all-target clippy and fmt on Linux, five
candidate snapshots, lifecycle mutants and the locked consumer smoke checked, and an independent
test-packet review passed. The follow-up supersedes that test count with the
126-passed/0-failed/4-ignored managed job described above; native GUI/IME, physical-GPU, performance
and remote/platform CI remain open. No push, pull request, comment, release, merge or other
publication is authorised; each future publication and the final merge still need fresh explicit
human approval after the exact content and validation are shown.
The repository is `storytold/craft-ai-integrations`. Its only egui consumer is `crates/ai-egui` (package `craft-ai-egui`).
This plan covers that crate, its examples, tests, workspace dependency graph, and the contracts it must keep for any app that
may adopt it.
### Current implemented state (2026-10-24)
### Current implemented state (2026-10-25)
| Item | Actual state |
| --- | --- |
| Source | `7c80bbc52cb5a84cebeb67529a18d670a15c511b`, plus the inherited pin-only root `Cargo.toml`/`Cargo.lock` updates and the local follow-up files listed in the change review. |
| Source | `origin/main` `0e553407716e958c6281d5b6df0a9554fc5632b7` (tree identical to `7b3f3e616c37f4eadc04525f075ee1fce08448eb`), plus the 13-file documentation and cancellation-test change set and this documentation correction; no new commit yet. |
| Production egui family | Reviewed storytold `storytold/egui` at `6d518fc799681571963842f95b55a6b68431b15a`; one family source in the root lock. |
| Production winit | Reviewed storytold `storytold/winit` at `a517b3846ee8457d84ee4975110bfda6797c44dc`. |
| Root lock | `Cargo.lock` SHA-256 `7505336b3831762c90b73610723161c163852edfee43daea350630e93dd4d799`; retained unchanged by this documentation, snapshot and smoke-root work. |
| Consumer recipe | `consumer-smoke/` is a separate workspace that repeats the complete `[patch.crates-io]` recipe, path-depends on this repository's `ai-egui` and `ai-core`, and asserts the public widget values are that `craft_ai_core` identity. Its locked check and one draw test passed with one `craft-ai-core` identity and the pinned egui family; the rendererless root leaves the winit patches unused. |
| Consumer recipe | `consumer-smoke/` is a separate workspace that repeats the complete `[patch.crates-io]` recipe, path-depends on this repository's `ai-egui` and `ai-core`, and asserts the public widget values are that `craft_ai_core` identity. Its locked test build and one draw test passed with one `craft-ai-core` identity and the pinned egui family; the rendererless root leaves the winit patches unused. |
| Font policy | The library keeps `egui` at `default-features = false`. Tests/examples opt into bundled `default_fonts`; the snapshot suite installs `FontDefinitions::default()` explicitly and keeps kittest's missing-glyph panic. A host supplies fonts or gets them through eframe; production does not force them. |
| Test-only features | `egui_kittest` uses `snapshot` + `wgpu` only as a dev dependency. `eframe` stays a dev dependency with `default_fonts`, `glow`, `x11`, `wayland`. No new public feature flag. |
| Duplicate core policy | This workspace and the smoke root use one path `craft-ai-core`. Do not combine it with `craft-libs/crates/ai-core`; replacing the local copy remains separate `CORE-SYNC` work. |
| Final headless qualification | Under managed job `egui037-ai-regression-mutation-20261024-2205`, **108 tests passed** with strict `clippy --all-targets` and `cargo fmt --all --check` green on Linux, then again after restoration. Five committed kittest snapshots are hash-stable. The `eg` group caught both compiled lifecycle mutants; changing the background from RGB(27,27,27) to RGB(30,30,30) failed the expected snapshot with 75,486 changed pixels. The locked consumer smoke passed one draw test with one `craft-ai-core` identity and the pinned egui family. An independent test-packet review passed. This is not evidence for native GUI, IME/clipboard, physical-GPU or performance acceptance. |
| Still open | Native Linux X11/Wayland and Windows widget workflows (focus, keyboard, clipboard/IME, screenshots); macOS IME/Metal/termination remains an explicit blocker; physical-GPU and performance qualification; any app adoption; human review of this documentation and fresh explicit approval for publication. The non-widget mutation groups were not rerun in the final headless job. |
| Historical headless checkpoint (2026-10-24) | Under managed job `egui037-ai-regression-mutation-20261024-2205`, **108 tests passed** with strict `clippy --all-targets` and `cargo fmt --all --check` green on Linux, then again after restoration. Five committed kittest snapshots are hash-stable. The `eg` group caught both compiled lifecycle mutants; changing the background from RGB(27,27,27) to RGB(30,30,30) failed the expected snapshot with 75,486 changed pixels. The locked consumer smoke passed one draw test with one `craft-ai-core` identity and the pinned egui family. An independent test-packet review passed. This remains historical: it is not evidence for native GUI, IME/clipboard, physical-GPU or performance acceptance. |
| Current headless qualification (2026-10-25) | Under managed job `egui037-ai-final-ci-20261025-1331`, on the `7b3f3e6` tree plus the 13-file documentation and cancellation-test change set: **126 passed, 0 failed, 4 ignored** (doctests) across the targeted `craft-ai-egui`/`craft-ai-engine` suites and the locked consumer smoke. The formatting command left the worktree unchanged, strict all-target clippy for those two packages was green, and the five kittest snapshots passed. This supersedes the 108-test count but does not rerun the historical mutation controls; it is not evidence for native GUI, IME/clipboard, physical-GPU or performance acceptance. |
| Still open | Full remote workspace/platform CI; native Linux X11/Wayland and Windows widget workflows (focus, keyboard, clipboard/IME, screenshots); macOS IME/Metal/termination remains an explicit blocker; physical-GPU and performance qualification; any app adoption; human review of this documentation and fresh explicit approval for publication. The non-widget mutation groups were not rerun in the current headless job. |
## 1. Baseline and provenance
@@ -718,9 +722,10 @@ These are external reports, not reproduced here.
### 8.3 Known limitations of this plan
- The original source audit ran no compile or test; API impact there was inferred from 0.37.0 source/changelog. The final
headless follow-up qualification covers 108 tests, five candidate snapshots, the `eg` lifecycle mutants and the locked
consumer smoke; it does not cover native workflows, physical GPUs, performance or the other mutation groups.
- The original source audit ran no compile or test; API impact there was inferred from 0.37.0 source/changelog. The
historical 108-test checkpoint covers five candidate snapshots, the `eg` lifecycle mutants and the locked consumer
smoke; the current 126-passed/0-failed/4-ignored managed job covers the targeted widget and engine tests plus that
smoke. Neither covers native workflows, physical GPUs, performance, remote/platform CI or the other mutation groups.
- The consumer table reflects the exact planning checkouts; re-inventory before final integration.
- The fork/winit candidates are not accepted storytold revisions; E1–E21, W15P, E4/P2b and E21b/P7b are not revalidated here.
Upstream #8731, #8736, #8512, #8763, #8764 and #8748 are external reports.
@@ -754,19 +759,23 @@ The checkout now resolves the reviewed storytold egui family at
native event bridge, and `consumer-smoke/` repeats that recipe because patches do not propagate to
a consumer root. The root lockfile SHA-256 is recorded above.
This dependency state satisfies the pin half of AI-2. The follow-up source and headless tests are
now qualified as recorded above: 108 tests, five candidate snapshots, the `eg` lifecycle mutant group
and the locked consumer smoke. Native/platform and performance evidence remain absent, the other
mutation groups were not rerun, and the repository has no published app adoption. No
acceptance-complete claim is made; publication still needs explicit human approval.
This dependency state satisfies the pin half of AI-2. The historical checkpoint and the current
managed job are recorded above: the earlier 108-test mutation run and the current
126-passed/0-failed/4-ignored targeted suite plus locked consumer smoke. Native/platform, performance
and remote-CI evidence remain absent, the other mutation groups were not rerun, and the repository
has no published app adoption. No acceptance-complete claim is made; publication still needs
explicit human approval.
## Human-review readiness summary — 2026-10-24
## Human-review readiness summary — 2026-10-25
The documentation now records only the qualification actually run for this follow-up: 108 passing
tests, strict all-target clippy and fmt, five hash-stable candidate snapshots, both compiled
connection-panel lifecycle mutants caught, the negative pixel control caught at 75,486 changed
pixels, restored source and goldens, a passing locked consumer smoke with one core identity, and a
passing independent test-packet review.
The documentation now separates the historical 108-test mutation checkpoint from the current
managed qualification. The historical checkpoint recorded strict all-target clippy and fmt, five
hash-stable candidate snapshots, both compiled connection-panel lifecycle mutants caught, the
negative pixel control caught at 75,486 changed pixels, restored source and goldens, a passing
locked consumer smoke with one core identity, and a passing independent test-packet review. The
current job `egui037-ai-final-ci-20261025-1331` records 126 passed, 0 failed and 4 ignored doctests
for the targeted widget and engine suites plus the locked consumer smoke, with formatting and
strict all-target clippy green; it did not rerun the mutation controls.
Genuinely open AI gates: native Linux X11/Wayland and Windows focus/keyboard/clipboard/IME/rendering
screenshots; macOS IME/Metal/termination; physical-GPU qualification; the performance protocol; any