feat(core): "Internet only", the mirror of LAN mode, plus GUI fixes - #142
Merged
Conversation
The right-click menu on Statistics came up in the SYSTEM colours - a white box in the middle of a dark program - while the connection table's menu was dark. ttk styles do not reach a classic tk.Menu (on Windows it is a native Win32 popup), so the only difference was one missing call: the table wraps its menu in theme.style_menu, this one did not. Nothing in the suite could see it: the fake tkinter records colours and never renders them. So two guards, because "the wrapper is called" and "the menu is dark" are two separate claims: - test_repo_conventions.py::test_every_context_menu_is_handed_to_the_dark_theme walks the AST of beantester/gui/** and requires every Menu(...) call to sit inside style_menu(...). The rule, not the example - a test naming today's two menus would pass on the day a third arrives bare, which is how this one got in. - test_gui_release_fixes.py::test_the_statistics_copy_menu_is_dark_like_every_other_context_menu checks the built menu carries the theme's background, foreground and accent, and that the cached instance is reused. Both proven by mutation (two entries in MUTATIONS, both caught), and confirmed on a real Tk window: the menu now renders dark on both Live and Session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Settings -> Display gains "Show the search box on the Control page", on by default. Turning it off takes the bar away immediately, not on the next tick: a widget that appears or disappears up to 0.7 s after the click reads as a broken checkbox rather than a slow one. - gui/prefs.py: one Pref, worded positively - an unticked "Hide ..." box is a double negative, and it would be the only switch in that window where a tick means less than an untick. - gui/pages/__init__.py: pref_changed(app, key) broadcasts to any page that exposes on_pref_changed. It sits beside focus_search because addressing a page belongs to the page registry - and because gui/app.py is on the size ratchet with zero headroom, so a Pref naming an App method had nowhere to go. - gui/panels/settings.py: both row kinds now write through one _store(), so "a preference changed" has a single meaning. - gui/pages/control.py: hiding CLEARS. The marks are painted on the form, so a standing query would leave fields highlighted with no box left to clear them from, the sections the search unfolded would stay unfolded, and a debounce in flight would repaint both after the bar was gone. Bringing it back passes before=, because pack hands out space in call order and the bar would otherwise return under the whole page body. - Ctrl+F declines when the box is hidden and falls back to the connection table, instead of focusing an unmapped widget and swallowing the keystrokes that follow the shortcut. Guards: four tests in test_prefs.py, the hidden-box case added to the existing Ctrl+F guard, three entries in MUTATIONS (all caught). The pack order is the one claim the fake tkinter cannot make - it keeps children in creation order - so that half was verified on live Tk, together with the layout after the bar is gone and the Ctrl+F fallback. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SettingsWindow._build_pref_row handles BOOL and ACTION first and returns, so only a NUMBER row ever reaches the line that draws Pref.hint. scope_view_to_target declared one anyway: 250 characters, written, translated into both languages and shown to nobody. Nothing raises when that happens, which is how it survived. The field registry has had a guard for exactly this since narrow_filter did the same thing, and its docstring records the consequence: an invisible hint makes the TOOLTIP swell into a wall trying to carry the same explanation. That is what happened here too - tips.scope_view is the longest string in the language files. Nothing is lost by deleting the key. Checked line by line: the tooltip already says what the hint said - which surfaces the preference narrows, and that it changes the VIEW and never the capture or the impairment - plus three things the hint did not. Rendering hints for BOOL rows was rejected: it would leave the two registries with opposite rules about the same widget kind, and the Scope card's two switches would stop reading as a pair. Guard: test_prefs.py::test_only_prefs_that_can_show_a_hint_declare_one, the mirror of the field one, with a MUTATIONS entry that reddens it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A second checkbox under "Traffic to modify": it cuts the local network and leaves the internet up, for testing an app whose intranet server, NAS or printer has gone away. CLI: --internet-only. Pipeline step 2b, reason "internet_only", counter drop_internet_only. LOOPBACK SURVIVES IT. utils.is_lan_ip is deliberately not the mirror of is_local_ip: a machine talking to itself is not "the local network", and cutting 127.x would take down a local development server on the very machine running the tool. Said in the tooltip and in both READMEs, together with the half nobody expects - the router is on the local network, so DNS asked of it stops with everything else, and the internet then LOOKS broken. Measured while writing the predicate, because it reads the other way round: IPv4 multicast is globally routable as far as ipaddress is concerned, so mDNS and SSDP sit on the internet side of both switches. Both switches on is allowed and said out loud in the log. They judge the same packet from opposite sides and can never both fire on it, so each counter reports its own half and only loopback gets through. Refusing the combination was rejected: it is the union of two impairments, like --loss 100. The flag may not start with "lan-". Measured: a second --lan-* option makes the --lan abbreviation ambiguous and argparse then refuses it outright, silently breaking a documented shortcut of --lan-mode. decide() sat exactly on the complexity ceiling, so the two gates went behind one statement calling _address_class_cut, guarded so an ordinary session still pays two attribute reads and no call. Measured: ruff counts branch statements and not the boolean operators inside them, so the obvious nested pair reads as 30 and this reads as 29. The ceiling was not touched. Guards: the gate, the predicate (loopback, multicast, unclassifiable input), the both-on split, GATES in the property test - which had to learn a per-gate remote address, since the two 2b gates want opposite address classes - the sixth row in the remote-endpoint direction table, and a new test asking both address-class gates to leave the opposite class AND loopback alone in both directions. Five MUTATIONS entries, all caught. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One of the pair public and the other not is a trap for anyone reading the facade: the obvious question about these two is which of them counts loopback, and an asymmetric export invites guessing at it. Additive, and the launcher facade re-exports it with the rest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three things that were asked for, plus two defects the work uncovered on the way.
"Internet only" - the mirror of LAN mode
A second checkbox under "Traffic to modify", and
--internet-onlyon the command line: itcuts the local network and leaves the internet up, for testing an application whose intranet
server, NAS or printer has gone away. Pipeline step 2b, reason
internet_only, counterdrop_internet_only. The numbered pipeline 1..12 is unchanged.Loopback survives it.
utils.is_lan_ipis deliberately not the mirror ofis_local_ip:a machine talking to itself is not "the local network", and cutting 127.x would take down a
local development server on the very machine running the tool. Both the tooltip and the
READMEs say so, together with the half nobody expects - the router is on the local network,
so DNS asked of it stops with everything else and the internet then looks broken.
Measured while writing the predicate, because it reads the other way round: IPv4 multicast is
globally routable as far as
ipaddressis concerned, so mDNS and SSDP sit on the internetside of both switches.
Both switches on at once is allowed and said out loud in the log. They judge the same packet
from opposite sides and can never both fire on it, so each counter reports its own half and
only loopback gets through. Refusing the combination was rejected: it is the union of two
impairments, like
--loss 100.Two constraints that shaped the change:
lan-. Measured: a second--lan-*option makes the--lanabbreviation ambiguous and argparse then refuses it outright, silently breaking a documented
shortcut of
--lan-mode.decide()sat exactly on the complexity ceiling. The two gates went behind one statementcalling
_address_class_cut, guarded so an ordinary session still pays two attribute readsand no call. Measured: the metric counts branch statements and not the boolean operators
inside them, so the obvious nested pair reads as 30 while this reads as 29. The ceiling was
not raised.
A switch to hide the Control page's search box
Settings -> Display, on by default. It applies immediately rather than on the next tick: a
widget that appears or disappears up to 0.7 s after the click reads as a broken checkbox.
Hiding clears. The marks are painted on the form, so a standing query would leave fields
highlighted with no box left to clear them from, the sections the search unfolded would stay
unfolded, and a debounce in flight would repaint both after the bar was gone. Bringing the bar
back names what to sit before, because pack hands out space in call order. Ctrl+F declines
when the box is hidden and falls back to the connection table, instead of focusing an unmapped
widget and swallowing the keystrokes that follow the shortcut.
The Statistics copy menu was white
ttk styles do not reach a classic menu - on Windows it is a native popup - and the connection
table wrapped its menu in the dark theme while this one did not, two files apart. Nothing in
the suite could see it: the fake tkinter records colours and never renders them. Two guards,
because "the wrapper is called" and "the menu is dark" are two claims, and the first one walks
the AST so the next menu somebody adds cannot repeat this.
Two defects found on the way, not asked for
--narrow-filter. Found by running the CLI and readingits own
Reproduce:line, not by a test - the suite was green.settings_to_cliis ahand-written list that had fallen behind the registry by two flags, so a session started
with a narrowed capture produced a command that re-runs a wider one: the packet counts of
the re-run cannot match the report they came from. The reproduction report carried the fact
faithfully all along, which is why nobody noticed. New guard walks the field registry and
demands each flag appear in the command.
Pref.hintwas dead text for checkbox rows. The Settings window returns before the hintis drawn for anything but a numeric row, so a 250-character explanation on "Show only the
targeted traffic" was written, translated into both languages and shown to nobody. The field
registry has had a guard for exactly this since a field did the same thing; the preference
registry now has its mirror. Nothing was lost by deleting the key - checked line by line, the
tooltip already carried everything it said.
Contracts
NDJSON, the CSV export and the reproduction report all gained a key. Additive, so this is not
a breaking change and the version is untouched.
Verification
The full suite runs here, on both platforms. Locally: the guards of everything touched (501
of them),
ruff,mypyand the GUI smoke, plus nine entries added to the mutation registry,all caught.
Beyond the suite, because it is the part the suite cannot reach:
the new counter cell, the search box disappearing and coming back in its own place, and
Ctrl+F falling through to the connections table;
command carrying both flags, and the same command run twice;
loopback echo it owns - through four phases, twice, with the drop counts repeating exactly.
It was then shown able to fail: with the loopback carve-out mutated out, it exits non-zero
and names exactly the two phases whose loopback column broke.