fix(tests+mutator): unbreak the docker-shadow test env + let mutator delete from active

Two related fixes that came out of running the W5 tests locally:

1. tests/__init__.py — empty file, makes 'tests/' a package so pytest
   stops inserting it into sys.path.  Without it, 'tests/docker/'
   (the docker-image test category) shadowed the installed docker SDK
   on every engine-touching test in the repo:

     module 'docker' has no attribute 'DockerClient'

   Pytest's default --import-mode=prepend was the culprit; making
   tests/ a package is the cheapest fix and doesn't change
   --import-mode for the whole tree.

2. delete_topology_decky / delete_topology_edge / delete_lan grow an
   'enforce_pending: bool = True' kwarg.  Default preserves the HTTP
   CRUD guard (api_decky_crud / api_edge_crud / api_lan_crud get the
   409 for free).  apply_remove_decky / apply_detach_decky /
   apply_remove_lan now pass enforce_pending=False — the mutator
   queue is the live-editing surface and has its own active-topology
   gating; the repo's pending-only guard was for design-time CRUD
   that mustn't bypass it.  Without this, apply_remove_decky was
   silently broken on active topologies pre-W5; W5's new test
   surfaced it on first run.

10/10 new W5 tests pass; 58/58 across mutator + topology suites.
This commit is contained in:
2026-04-29 00:24:17 -04:00
parent 98c929894c
commit a27e3f5e0f
6 changed files with 52 additions and 11 deletions

View File

@@ -61,12 +61,18 @@ class LansMixin:
lan_id: str,
*,
expected_version: Optional[int] = None,
enforce_pending: bool = True,
) -> None:
"""Cascade-delete a LAN from a pending topology.
"""Cascade-delete a LAN.
Rejects if any decky declares this LAN as its home (i.e. has a
non-bridge edge to it — the only LAN that decky lives in). The
caller must delete or reassign the home-deckies first.
``enforce_pending=True`` by default keeps the HTTP CRUD guard
intact; the mutator's ``apply_remove_lan`` opts out (it has
already gated on topology status and the live-LAN docker
materialisation runs after).
"""
from decnet.topology.status import TopologyNotEditable # noqa: F401
@@ -75,7 +81,8 @@ class LansMixin:
lan = result.scalar_one_or_none()
if lan is None:
return
await self._assert_pending(session, lan.topology_id)
if enforce_pending:
await self._assert_pending(session, lan.topology_id)
# Home-decky check: any decky whose only edge lands here?
edges_result = await session.execute(