Commit Graph

4 Commits

Author SHA1 Message Date
c310861855 slots: minute-of-day consumers (phase 2 of 3: Python side)
Every Python consumer now speaks slot minutes:

- satisfaction is nearest-slot snapping (_slot_wire.satisfied_by_entries)
  at every "is this slot logged?" site: gate, CLI status, MCP, catering
  occupancy and dedup. Nothing on disk is rewritten; 07:00/10:00 entries
  keep covering 07:15/10:15 after a schedule move.
- log_meal writes slot_fields(minute) before signing (slot + slot_min).
- schedule store and sync encode via schedule_to_wire/from_wire; a
  whole-hour schedule stays byte-identical (f/l/n only).
- CLI ate --hour takes HH or HH:MM; MCP log_meal takes "HH:MM" or a
  legacy int hour, validated against today's slots; get_slots returns
  {minute, label}.
- gate History tab: first/last are HH:MM ttk.Comboboxes (quarter-hour
  list, typed times, Up/Down step without posting the popdown), locked
  as disabled; validation mirrors normalized().
- gate timer fires every 15 min; docs rewritten for minutes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015npsYVB2PdtWkG7MPAi4tn
2026-10-10 19:53:39 +02:00
df5e1272f1 Make _FakeTclError subclass the real tkinter.TclError
Found while auditing the `_NO_WIDGETS` exemption added two commits ago: the
comment claimed `_gatelock_widgetgroups` binds real `tk` only for its
`contextlib.suppress(tk.TclError)` dead-copy guards. That part is true -- it
constructs no widgets -- but the consequence was wrong.

`_FakeTclError` subclassed `Exception`, not `tkinter.TclError`, so a fake-tk
gate test raising it sailed straight through those suppressions. The
"a monitor vanished mid-update" paths were therefore only ever exercised
against the real error; with the fake they would have raised through the gate.
Reproduced directly before fixing: a `_FAKE_TK.TclError` escaped
`WidgetGroup.config` both with and without the module patched, so adding the
module to the patch set would not have fixed it.

`tkinter.TclError` subclasses `Exception`, so inheriting keeps every existing
`except _FakeTclError` working and makes both spellings catchable.
`test_fake_tclerror_is_catchable_as_the_real_one` pins it and asserts the live
copy still gets configured; reverting the base class fails it.

Verified: 707 Python tests / 100% branch coverage, pre-commit clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RA9ynjqXPnexRQnR5oAuCC
2026-08-15 20:24:11 +02:00
8f2d1011c7 Split the three gate test files; assert fake_tk really swaps tkinter
Seventh batch of the 250-line sweep (over-cap count 31 -> 28). The test files
mirror the source split from the previous commit one-for-one:

  test_gatelock_mealflow.py  379 -> 227  (+ test_gatelock_dashboard.py)
  test_gatelock_fanout.py    335 -> 128  (+ test_gatelock_groups.py)
  test_gatelock.py           331 -> 224  (+ test_gatelock_form.py)

`test_gatelock_dashboard.py` follows `_gatelock_dashboard`/the slot walk and
manual sync; `test_gatelock_groups.py` takes the group primitives that
`_gatelock_widgetgroups` now owns; `test_gatelock_form.py` takes the form and
basis/amount cases.

Also closes the remaining half of the fake-tk fail-open. The previous commit's
canary proved every widget-building module is *named* in `_GATE_TK_MODULES`;
it could not prove the patch installs something that is not real tkinter. If
`_FAKE_TK` were imported from the wrong place after the coming `_tk_fakes.py`
split, every module would still be "patched" and the suite would still pass
while building real widgets. `test_fake_tk_actually_replaces_real_tkinter`
now asserts `module.tk is _FAKE_TK` for every entry *inside* the `fake_tk()`
context, plus both `ttk` fakes and restoration afterwards. Verified it fails
closed by making one module skip the patch: it reported exactly that module.

Verified: 704 Python tests / 100% branch coverage, check_patch_targets 0,
pre-commit clean, ~/.local/share/diet_guard byte-identical (gate timer paused
for the batch).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RA9ynjqXPnexRQnR5oAuCC
2026-08-15 20:12:13 +02:00
e8f0ecaae1 Split the six Python gate files; guard the fake-tk patch set
Sixth batch of the 250-line sweep. All six gate sources named for this batch
are now under cap, plus _gate_layout_probe.py, which this batch's import
rewiring pulled into the changeset (over-cap count 38 -> 31).

  _gatelock_ui.py       477 -> 174  (palette/leaf-helper hub)
  _gate_layout_probe.py 346 -> 196
  _gatelock_groups.py   308 -> 137
  _gatelock_calendar_ui 295 ->  77
  _gatelock_calendar.py 286 -> 210
  _gatelock.py          263 -> 238
  _gatelock_mealflow.py 259 -> 207

New siblings: _gatelock_fields (input widgets), _gatelock_layout (assembly),
_gatelock_widgetgroups (per-widget mirroring), _gatelock_calendar_types
(bundles/palette), _gatelock_calendar_widgets (History builders),
_gatelock_dashboard and _gatelock_budgetedit (mixins), _gatelock_lockfile
(the stdlib-only flock), _gate_focus_probe and _gate_measurement.

The load-bearing part is the test seam, not the line counts.
`_gate_fixtures._GATE_TK_MODULES` patches `tk` on a hand-listed tuple, and
that list fails OPEN: a widget builder split into a new sibling that nobody
adds to it builds *real* Tk widgets against fake parents. Neither existing
detector sees that -- check_patch_targets.py only validates paths that still
resolve, and the ~8s runtime only catches network calls. It bit twice during
this split (real StringVars from make_calendar_vars returned MagicMocks
instead of the asserted text), so test_gate_tk_modules.py now asserts the
invariant directly: every module binding `tk` is in the patch set or named in
_NO_WIDGETS with a reason. It caught a pre-existing gap too --
_gatelock_support was never in the set.

Same fail-open shape drove two other choices: `escape_tab_trap` is a call-time
indirection so the keyboard test's patch of `_escape_text_tab_trap` still
bites, and the flock pair moved with its conftest patch target updated in the
same edit (that one fails closed). build_layout is imported from
_gatelock_layout directly rather than re-exported lazily, which avoids both a
cycle and an import-outside-toplevel suppression.

Verified: 703 Python tests / 100% branch coverage, the 5 layout-fits tests
under xvfb, 555 Flutter tests (unchanged), flutter analyze clean,
check_patch_targets 0, `sync` 0 (403 entries), pre-commit clean, and
`gate --demo` renders both tabs with live dashboard data.
~/.local/share/diet_guard byte-identical across the batch (gate timer paused,
restored after).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RA9ynjqXPnexRQnR5oAuCC
2026-08-15 20:08:03 +02:00