Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions CHANGELOG-INTERNAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions beantester/gui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
13 changes: 12 additions & 1 deletion beantester/gui/panels/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ------------------------------------------------------- #
Expand Down
47 changes: 39 additions & 8 deletions tests/test_gui_release_fixes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
""")


Expand Down