From d8140c2c5d0809f2ffcf36369a538e6b0bdb106d Mon Sep 17 00:00:00 2001 From: phall Date: Mon, 10 Aug 2026 22:32:36 -0400 Subject: [PATCH] fix(automation): drop closed windows from the snapshot's window list A closed window keeps its runtime table slot until its label or id is re-created -- `removeWindowAt` runs at re-creation, not at close -- so `self.windows[0..window_count]` still carries every window the app has ever declared, with `info.open = false` on the dead ones. The snapshot walked that span unfiltered and published a `window @wN` line for each, while `listViews` and `appendAutomationWidgets` correctly found nothing under them: a ghost window with no views. That misleads exactly the assertion a smoke test wants to make. A test that opens a settings window, closes it, and asserts the window COUNT returned to one passed against a window that is not on screen, and a test asserting the reconciled window set matched saw closed windows in it forever. Filter on `info.open` and advance the output index only for the windows that survive, so the published slice is the windows that are actually up. Co-Authored-By: Claude Opus 5 (1M context) --- src/runtime/automation_snapshot.zig | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/runtime/automation_snapshot.zig b/src/runtime/automation_snapshot.zig index 67eaa556c..05a2a3527 100644 --- a/src/runtime/automation_snapshot.zig +++ b/src/runtime/automation_snapshot.zig @@ -46,16 +46,31 @@ pub fn RuntimeAutomationSnapshot(comptime Runtime: type) type { .text_layout_line_budget = canvas_limits.max_canvas_text_layout_lines_per_view, }; } + var window_count: usize = 0; var view_count: usize = 0; var widget_count: usize = 0; var menu_item_count: usize = 0; - for (self.windows[0..count], 0..) |window, index| { - self.automation_windows[index] = .{ + // CLOSED windows are skipped, not reported. A closed window + // keeps its runtime table slot (with `info.open = false`) + // until its label or id is re-created — `removeWindowAt` runs + // at re-creation, not at close — so `self.windows[0..count]` + // still carries every window the app has ever declared. Their + // views are gone from `self.views` the moment they close, so + // an unfiltered pass published a window line with no views + // under it: a ghost that made every automation assertion on + // window COUNT (a settings window that must close, a + // reconciled window set) pass against a window that is not on + // screen. The output index advances only for the windows that + // survive the filter. + for (self.windows[0..count]) |window| { + if (!window.info.open) continue; + self.automation_windows[window_count] = .{ .id = window.info.id, .title = if (window.info.title.len > 0) window.info.title else title, .bounds = window.info.frame, .focused = window.info.focused, }; + window_count += 1; if (view_count < self.automation_views.len) { const views = self.listViews(window.info.id, self.automation_views[view_count..]); view_count += views.len; @@ -63,7 +78,7 @@ pub fn RuntimeAutomationSnapshot(comptime Runtime: type) type { appendAutomationWidgets(self, window.info.id, &widget_count, &menu_item_count); } return .{ - .windows = self.automation_windows[0..count], + .windows = self.automation_windows[0..window_count], .views = self.automation_views[0..view_count], .widgets = self.automation_widgets[0..widget_count], .commands = self.options.commands,