diff --git a/CHANGELOG-INTERNAL.md b/CHANGELOG-INTERNAL.md index 87cf8c1..369d08c 100644 --- a/CHANGELOG-INTERNAL.md +++ b/CHANGELOG-INTERNAL.md @@ -15,6 +15,32 @@ flags, exit codes, the NDJSON schema, on-disk file formats, or the facade's publ a `### BREAKING` section placed FIRST in that version, and each such line is prefixed with `**BREAKING:**`. A breaking change also requires a version bump by the owner (convention 34). +## [Unreleased] + +### Fixed + +- **A START-only field rendered by a WINDOW never locked.** `App._sync_running_ui` refreshed + `self.form` only, so `narrow_filter` - which lives on `SettingsWindow`'s own `ControlForm` + (`surface="settings"`, convention 42) - stayed `state="normal"` for the whole session whenever + the window was open BEFORE START. Opened AFTER a start it came up correctly disabled, because + the build path reads `is_locked` and nothing re-read it afterwards. Measured on the fake Tk both + ways before and after the fix. Two halves, both guarded: `SettingsWindow.refresh()` now calls + `form.refresh_field_states()` (self-healing on the 700 ms tick, whatever moved the state), and + `_sync_running_ui` ticks the open windows so the lock is immediate rather than a tick late. + Conventions 7, 21 and 42. + +### Tests + +- `tests/test_gui_release_fixes.py::test_start_only_fields_are_locked_while_a_session_runs` + rewritten to DERIVE its subjects from `fields.FIELD_DEFS` instead of naming `duration` and the + filter combobox by hand, and to resolve each field to the surface that renders it + (`SECTIONS[].surface`, with `CHOICE` belonging to `App`, not to a form). The old shape was a test + of two examples, so `narrow_filter` was added to the registry, shipped unlocked and left the + suite green - PROJECT_NOTES rule 2.6 (one value, several consumers, a guard on one of them) and + the same mistake convention 16 records. It now opens the Settings window BEFORE the session + starts, which is the order that was broken. Verified by mutation: removing either half of the + fix turns it red. + ## [0.4.0] - 2026-08-01 ### BREAKING diff --git a/CHANGELOG.md b/CHANGELOG.md index 249557a..0f975d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to Bean Network Tester. The format follows [Keep a Changelog](https://keepachangelog.com/); versions follow SemVer. +## [Unreleased] + +### Fixed + +- In the Settings window, "Capture only the targeted traffic" stayed clickable after you pressed + START, if the window was already open at the time. Ticking it did nothing until the next + session. It now greys out for as long as the session runs, with the same "locked while running" + note as every other option that is only read at start. + ## [0.4.0] - 2026-08-01 **The short version.** This release is about numbers you can trust and targeting that catches what diff --git a/beantester/gui/app.py b/beantester/gui/app.py index dfe5040..439e8c4 100644 --- a/beantester/gui/app.py +++ b/beantester/gui/app.py @@ -1708,6 +1708,13 @@ def _sync_running_ui(self): if self.form is not None: # every other START-only field (today: "Run time") locks with it self.form.refresh_field_states() + # ...and so do the ones rendered by a WINDOW. The Settings window has its + # own ControlForm carrying "narrow_filter", so refreshing only self.form + # left that checkbox live for the whole session whenever the window + # happened to be open before START. The tick would heal it within 700 ms; + # doing it here makes the lock immediate, like every other surface. + with crashlog.quiet("gui.app"): + self.windows.refresh() self._form_changed = True self._refresh_dirty() self._refresh_start_enabled() diff --git a/beantester/gui/panels/settings.py b/beantester/gui/panels/settings.py index cfa33ca..285ec75 100644 --- a/beantester/gui/panels/settings.py +++ b/beantester/gui/panels/settings.py @@ -198,8 +198,19 @@ def _sync_scope_status(self): label.pack_forget() def refresh(self): - """Ticked by the App while this window is open (PanelWindow.refresh).""" + """Ticked by the App while this window is open (PanelWindow.refresh). + + ``refresh_field_states`` is here because this window renders START-only + fields (``narrow_filter``) on its OWN ``ControlForm``, and + ``App._sync_running_ui`` only ever refreshed the Control page's form. A + window opened BEFORE the session started therefore kept the checkbox + clickable for the whole run, while the same field opened AFTER the start + came up correctly disabled - the build path reads the registry, nothing + re-read it afterwards. Ticking it here heals the state whatever moved it, + not just start/stop. + """ with crashlog.quiet("gui.panels.settings"): + self.form.refresh_field_states() self._sync_scope_status() # -- preference rows ------------------------------------------------------- # diff --git a/tests/test_gui_release_fixes.py b/tests/test_gui_release_fixes.py index 5f77814..673de0e 100644 --- a/tests/test_gui_release_fixes.py +++ b/tests/test_gui_release_fixes.py @@ -231,21 +231,52 @@ def test_a_target_that_matches_nothing_says_so_on_the_page(): def test_start_only_fields_are_locked_while_a_session_runs(): - """"Run time" is consumed by BeanEngine.start(), exactly like the traffic - filter - so, exactly like the filter, it must not look editable mid-session.""" + """EVERY field the registry marks start_only greys out mid-session, on + whichever surface renders it. + + This used to name ``duration`` and the filter combobox by hand, which made it + a test of two EXAMPLES rather than of the rule (convention 16's mistake, and + PROJECT_NOTES rule 2.6: one value, several consumers, a guard on one of them). + ``narrow_filter`` was added to the registry later, landed on the Settings + window's own ControlForm, and stayed clickable for an entire session whenever + that window was open before START - with this test green throughout. Deriving + the list from FIELD_DEFS means the fourth start_only field cannot repeat it. + + The window is opened BEFORE the session starts on purpose: that is the order + that was broken. Opened after, the build path already read the registry. + """ run_gui(""" - assert app.form.entries["duration"].kw.get("state") in (None, "normal") + from beantester.fields import CHOICE, FIELD_DEFS, FIELDS, SECTIONS + + surface_of = {key: sec.surface for sec in SECTIONS for key in sec.fields} + start_only = [f.key for f in FIELD_DEFS if f.start_only] + assert start_only, "no start_only field in the registry - the rule lost its subject" + + app.open_window("settings") + settings_form = app.windows._open["settings"].form + + def widget(key): + # the traffic filter is a CHOICE and belongs to App, not to a form + if FIELDS[key].kind == CHOICE: + return app.filter_cb + form = settings_form if surface_of[key] == "settings" else app.form + return form.entries[key] + + for key in start_only: + assert widget(key).kw.get("state") in (None, "normal", "readonly"), key app.running = True app._sync_running_ui() - assert app.form.entries["duration"].kw.get("state") == "disabled" - assert app.form.labels["duration"].kw.get("style") == "CardOff.TLabel" - assert app.filter_cb.kw.get("state") == "disabled" + for key in start_only: + assert widget(key).kw.get("state") == "disabled", ( + key + " stayed editable while a session was running") app.running = False app._sync_running_ui() - assert app.form.entries["duration"].kw.get("state") == "normal" - assert app.filter_cb.kw.get("state") == "readonly" + for key in start_only: + expected = ("readonly" if FIELDS[key].kind == CHOICE + else "normal") + assert widget(key).kw.get("state") == expected, key """)