Launching the app died with "'dict' object has no attribute 'readiness'".
TeamView.__init__ forwarded its legacy app_state dict into update_data(snapshot),
whose guard only handled None, so {} reached snapshot.readiness. TeamView is the
default view, so the window never appeared.
Fixes the call site and hardens every snapshot guard to fall back on anything
that is not a HubSnapshot. Adds tests/test_view_startup_contract.py, which
checks both conditions statically and therefore runs headless; verified to fail
on the pre-fix sources.
The GUI-import invariant previously matched the literal string "customtkinter",
which missed modules pulling it transitively. Rewritten over the AST, it
immediately found test_codex_opencode_wizard.py importing router.ui.components
without pytest.importorskip — the same defect that took the release gate down
once before, hidden until now behind conftest's name-based skip.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
71 lines
3.1 KiB
Python
71 lines
3.1 KiB
Python
"""Startup contract for UI views — runs without customtkinter.
|
|
|
|
Guards the crash that took the app down on launch:
|
|
|
|
TeamView.__init__ forwarded its legacy ``app_state`` dict into
|
|
``update_data(snapshot)``. The guard there only handled ``None``, so ``{}``
|
|
slipped through and ``snapshot.readiness`` raised
|
|
``'dict' object has no attribute 'readiness'``. "Команда" is the default
|
|
view, so the failure happened before the window ever appeared.
|
|
|
|
These checks are static: they read the view sources rather than build widgets,
|
|
so they run in headless environments where the GUI toolkit is absent.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
VIEWS_DIR = Path(__file__).resolve().parent.parent / "src" / "antigravity_provider" / "router" / "ui" / "views"
|
|
|
|
|
|
def _view_files() -> list[Path]:
|
|
return sorted(VIEWS_DIR.glob("*_view.py"))
|
|
|
|
|
|
def _self_update_data_calls(tree: ast.AST, inside: str) -> list[ast.Call]:
|
|
"""Return self.update_data(...) calls made from the named method."""
|
|
calls: list[ast.Call] = []
|
|
for cls in (n for n in ast.walk(tree) if isinstance(n, ast.ClassDef)):
|
|
for fn in (n for n in cls.body if isinstance(n, ast.FunctionDef) and n.name == inside):
|
|
for node in ast.walk(fn):
|
|
if (
|
|
isinstance(node, ast.Call)
|
|
and isinstance(node.func, ast.Attribute)
|
|
and node.func.attr == "update_data"
|
|
and isinstance(node.func.value, ast.Name)
|
|
and node.func.value.id == "self"
|
|
):
|
|
calls.append(node)
|
|
return calls
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("view_path", _view_files(), ids=lambda p: p.stem.replace("_view", ""))
|
|
def test_constructor_does_not_forward_app_state_to_update_data(view_path: Path) -> None:
|
|
"""A view constructor must not pass its legacy app_state into update_data."""
|
|
tree = ast.parse(view_path.read_text(encoding="utf-8"))
|
|
for call in _self_update_data_calls(tree, inside="__init__"):
|
|
assert not call.args and not call.keywords, (
|
|
f"{view_path.name}: __init__ calls self.update_data(...) with an argument. "
|
|
"update_data expects a HubSnapshot; constructors hold app_state dicts. "
|
|
"Call self.update_data() and let it pull the current snapshot."
|
|
)
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("view_path", _view_files(), ids=lambda p: p.stem.replace("_view", ""))
|
|
def test_update_data_guard_rejects_non_snapshot(view_path: Path) -> None:
|
|
"""update_data must fall back on anything that is not a HubSnapshot, not just None."""
|
|
source = view_path.read_text(encoding="utf-8")
|
|
if "def update_data" not in source or "snapshot" not in source:
|
|
pytest.skip("view has no snapshot-driven update_data")
|
|
if "HubStateStore.get().get_snapshot()" not in source:
|
|
pytest.skip("view does not read the snapshot store")
|
|
|
|
assert "isinstance(snapshot, HubSnapshot)" in source, (
|
|
f"{view_path.name}: update_data guards with `snapshot is None` only. "
|
|
"A legacy dict passes that check and then fails on attribute access."
|
|
)
|