From 055aaab20c0df92ad3c1a3387399cb5d742f4964 Mon Sep 17 00:00:00 2001 From: owenpkent <20529132+owenpkent@users.noreply.github.com> Date: Mon, 21 Sep 2026 18:05:10 -0400 Subject: [PATCH 1/2] Stub the drive scan the panel actually calls, and fail if it is gone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The screenshot tool assigned `drives.list_volumes`. That was the panel's scan until it was rewritten around `scan_batches`, after which the assignment created a new unused attribute instead of overriding anything, and nothing said so. The real scan then ran on every render: whether real drive labels and free space reached the PNGs came down to which write landed last, since the network batch arrives seconds after the local one. Patch the funnel every scan goes through, exit non-zero if that name ever disappears too, and assert before each picture that only the invented volumes are on screen — a check that reads the panel rather than trusting a monkeypatch, so the next rewrite of the scan cannot silently undo it. --- tools/screenshots.py | 49 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/tools/screenshots.py b/tools/screenshots.py index b9c9b9f..66d358a 100644 --- a/tools/screenshots.py +++ b/tools/screenshots.py @@ -89,6 +89,48 @@ ] +def stub_volume_scan() -> None: + """Replace the drive panel's scan, and refuse to run if there is nothing + to replace. + + This used to assign `drives.list_volumes`. The panel's scan was later + rewritten around `scan_batches`, and since nothing verified the target + still existed, the assignment quietly began creating a new unused attribute + instead of overriding anything. The real scan then ran on every render, and + real drive labels and free space — including network shares — could reach + the PNGs that go in a public README. Whether they did came down to which + write landed last, because the network batch arrives seconds after the + local one. + + So: patch the funnel every scan goes through, and make a missing name stop + the run rather than hand the panel back to the machine. + """ + if not callable(getattr(drives, "scan_batches", None)): + raise SystemExit( + "tools/screenshots.py: drives.scan_batches is gone, so the drive " + "panel would scan this machine and put its real volumes in the " + "screenshots. Point the stub at whatever the panel calls now.") + # One final batch: no second delivery to race the first, and the panel + # never sees a non-final batch it would merge network shares into. + drives.scan_batches = lambda: iter([(list(VOLUMES), True)]) + + +def assert_no_real_volumes(window: MainWindow) -> None: + """Fail if anything but the invented volumes reached the panel. + + The stub above is the guard; this is the check that the guard worked. It + reads what is actually on screen, so it survives the next rewrite of the + scan in a way that patching a function name did not. + """ + invented = {volume.label for volume in VOLUMES} + showing = {row.volume.label for row in window.drives._rows} + leaked = showing - invented + if leaked: + raise SystemExit( + "tools/screenshots.py: the drive panel is showing real volumes " + f"({', '.join(sorted(leaked))}); refusing to write screenshots.") + + def seed_config() -> None: (config_dir() / "presets.json").write_text( json.dumps([p.to_dict() for p in PRESETS], indent=2), encoding="utf-8") @@ -139,8 +181,7 @@ def main(argv: list[str] | None = None) -> int: out.mkdir(parents=True, exist_ok=True) seed_config() - # The panel scans real volumes on a worker thread; give it ours instead. - drives.list_volumes = lambda: list(VOLUMES) + stub_volume_scan() app = QApplication([]) theme.apply(app) @@ -162,6 +203,7 @@ def main(argv: list[str] | None = None) -> int: window._set_mode(0) settle(app) + assert_no_real_volumes(window) shoot(window, out, "app-preset-mode.png") window._set_mode(1) @@ -171,6 +213,9 @@ def main(argv: list[str] | None = None) -> int: window.simple.destinations.set_paths([Path(r"D:\Archive\2026"), Path(r"N:\cold\2026")]) settle(app) + # Checked again: the panel polls every few seconds, so a scan that slipped + # past the stub would land between the two pictures. + assert_no_real_volumes(window) shoot(window, out, "app-simple-mode.png") editor = PresetEditor(PRESETS[2]) From f070dfffc33b50d8a49fed3fff8fa7b149921686 Mon Sep 17 00:00:00 2001 From: owenpkent <20529132+owenpkent@users.noreply.github.com> Date: Mon, 21 Sep 2026 18:18:37 -0400 Subject: [PATCH 2/2] Test the guard that keeps real volumes out of the screenshots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The failure being guarded against was a patch that silently stopped patching, so a guard with no test of its own is the same shape of risk. Covers both halves: that the stub replaces the function the panel actually calls and yields a single final batch, leaving nothing to race; and that a missing — or merely non-callable — target stops the run instead of handing the panel back to the machine. The check itself is tested against a leaked network-share label, against several, and against an empty panel, which is the state before the first batch arrives and must not read as a leak. --- tests/test_screenshot_tool.py | 126 ++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 tests/test_screenshot_tool.py diff --git a/tests/test_screenshot_tool.py b/tests/test_screenshot_tool.py new file mode 100644 index 0000000..fc0861d --- /dev/null +++ b/tests/test_screenshot_tool.py @@ -0,0 +1,126 @@ +"""The screenshot tool's guard against rendering real volumes. + +`tools/screenshots.py` is not shipped, but it writes the PNGs in a public +README, and the thing it is guarding against already happened once: it patched +a function the drive panel had stopped calling, so the patch became a no-op, +the real scan ran, and whether a real drive label reached the images came down +to which write landed last. A guard that can rot silently is worth a test. +""" + +from __future__ import annotations + +import importlib.util +import os +from pathlib import Path +from types import SimpleNamespace + +import pytest + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") +pytest.importorskip("PySide6", reason="GUI extra not installed") + +TOOL = Path(__file__).resolve().parent.parent / "tools" / "screenshots.py" + + +@pytest.fixture(scope="module") +def tool(): + """The tool, imported with its environment put back afterwards. + + Importing it redirects `APPDATA` and `XDG_CONFIG_HOME` to a sandbox at + module scope — deliberate in the tool, and not something to leave behind + for the rest of the suite, which reads those to find the config directory. + """ + saved = {name: os.environ.get(name) + for name in ("APPDATA", "XDG_CONFIG_HOME")} + try: + spec = importlib.util.spec_from_file_location("_screenshots", TOOL) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + yield module + finally: + for name, value in saved.items(): + if value is None: + os.environ.pop(name, None) + else: + os.environ[name] = value + + +def _window(*labels: str): + """The shape `assert_no_real_volumes` reads: rows carrying volumes.""" + rows = [SimpleNamespace(volume=SimpleNamespace(label=label)) + for label in labels] + return SimpleNamespace(drives=SimpleNamespace(_rows=rows)) + + +# ------------------------------------------------------------------- the stub + + +def test_the_stub_replaces_the_scan_the_panel_calls(tool, monkeypatch): + """Not a name it used to call. The previous version assigned + `drives.list_volumes`, which no longer exists, so nothing was overridden + and the real scan kept running.""" + monkeypatch.setattr(tool.drives, "scan_batches", + lambda: iter([("real volumes", True)])) + tool.stub_volume_scan() + + batches = list(tool.drives.scan_batches()) + assert [volumes for volumes, _ in batches] == [tool.VOLUMES] + + +def test_the_stub_yields_one_final_batch(tool, monkeypatch): + """A non-final batch tells the panel to keep network shares from the + previous scan, and a second delivery is what raced the invented list in the + first place. One batch, marked final, leaves neither opening.""" + monkeypatch.setattr(tool.drives, "scan_batches", lambda: iter([])) + tool.stub_volume_scan() + + batches = list(tool.drives.scan_batches()) + assert len(batches) == 1 + assert batches[0][1] is True + + +def test_a_missing_scan_function_stops_the_run(tool, monkeypatch): + """The regression this guard exists for: the tool must not fall back to + letting the panel scan the machine.""" + monkeypatch.delattr(tool.drives, "scan_batches") + + with pytest.raises(SystemExit, match="scan_batches"): + tool.stub_volume_scan() + + +def test_a_non_callable_scan_attribute_is_not_good_enough(tool, monkeypatch): + """`hasattr` would have accepted this, and a stub assigned over a + non-function is the same silent no-op in a different disguise.""" + monkeypatch.setattr(tool.drives, "scan_batches", "not a function") + + with pytest.raises(SystemExit, match="scan_batches"): + tool.stub_volume_scan() + + +# ------------------------------------------------------------------ the check + + +def test_invented_volumes_pass_the_check(tool): + tool.assert_no_real_volumes(_window(*[v.label for v in tool.VOLUMES])) + + +def test_a_real_volume_refuses_to_render(tool): + """What a leak looks like: a network share's own label on screen.""" + with pytest.raises(SystemExit, match="NAS_1"): + tool.assert_no_real_volumes(_window("A001 (PYXIS)", "NAS_1")) + + +def test_the_refusal_names_every_leaked_volume(tool): + """So the person running it can see it was the whole scan that got + through, not one stray row.""" + with pytest.raises(SystemExit) as raised: + tool.assert_no_real_volumes(_window("home", "Marketing")) + + message = str(raised.value) + assert "home" in message and "Marketing" in message + + +def test_an_empty_panel_is_not_treated_as_a_leak(tool): + """The panel is empty until the first batch arrives, and a guard that + failed the run for that would fire on timing rather than on a leak.""" + tool.assert_no_real_volumes(_window())