From 414c8efa9839f4f1aebcdf9e4d195eb68d142d21 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sun, 19 Jul 2026 19:04:25 -0400 Subject: [PATCH] fix(test): avoid autobegin-vs-begin collision in the log toggle/exclude test [M79 step 5] test_persist_logs_respects_toggle_and_exclude read via s.execute(SELECT) between two `async with s.begin()` blocks; that SELECT autobegins a transaction, so the next begin() raised "A transaction is already begun". Move both reads after the final begin block (the toggle-off persist stores nothing, so end-state still reflects the exclude phase), and pass a SimpleNamespace stand-in host to sidestep ORM attribute-expiry across the commit boundaries. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01CAGR73DUowdVFVvYzLXC5C --- tests/integration/test_docker.py | 35 ++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/tests/integration/test_docker.py b/tests/integration/test_docker.py index 3dfae2a..48b7479 100644 --- a/tests/integration/test_docker.py +++ b/tests/integration/test_docker.py @@ -214,6 +214,7 @@ def test_persist_logs_stores_lines(app): def test_persist_logs_respects_toggle_and_exclude(app): """Server-side controls (Settings): an excluded container's lines are dropped while others persist; the global toggle off stores nothing new.""" + from types import SimpleNamespace from sqlalchemy import text from steward.models.hosts import Host from steward.core.settings import set_setting @@ -225,33 +226,41 @@ def test_persist_logs_respects_toggle_and_exclude(app): {"container": "noisy", "stream": "stdout", "ts": None, "line": "drop-me"}, ])] + hid = str(uuid.uuid4()) + # Stand-in host: the logs-only persist path reads only host.id, so a plain + # object sidesteps ORM attribute-expiry across the commit boundaries below. + host = SimpleNamespace(id=hid, name="loghost3") + async def _go(): async with app.db_sessionmaker() as s: + # Back-to-back begin blocks with NO read in between — a SELECT between + # them would autobegin a txn and make the next begin() collide. async with s.begin(): await s.execute(text("DELETE FROM docker_logs")) await set_setting(s, "docker.logs.enabled", True) await set_setting(s, "docker.logs.exclude", ["noisy"]) - h = Host(id=str(uuid.uuid4()), name="loghost3", address="10.7.7.10") - s.add(h) - await s.flush() - hid = h.id - await persist(s, h, [], None, None, batch) - kept = {r[0] for r in (await s.execute(text( - "SELECT DISTINCT container_name FROM docker_logs WHERE host_id=:h"), - {"h": hid})).all()} - async with s.begin(): # flip the global kill-switch off + s.add(Host(id=hid, name="loghost3", address="10.7.7.10")) + await s.flush() # host row before its log rows (FK order) + await persist(s, host, [], None, None, batch) # web kept, noisy excluded + async with s.begin(): # global kill-switch off → nothing stored await set_setting(s, "docker.logs.enabled", False) - await persist(s, h, [], None, None, batch) - after_off = (await s.execute(text( - "SELECT COUNT(*) FROM docker_logs WHERE host_id=:h"), {"h": hid})).scalar() + await persist(s, host, [], None, None, batch) async with s.begin(): # restore defaults for other tests await set_setting(s, "docker.logs.enabled", True) await set_setting(s, "docker.logs.exclude", []) + # Reads only after the last begin block (they autobegin, but nothing + # opens a begin() after them). Toggle-off added nothing, so the state + # here still reflects the exclude phase. + kept = {r[0] for r in (await s.execute(text( + "SELECT DISTINCT container_name FROM docker_logs WHERE host_id=:h"), + {"h": hid})).all()} + after_off = (await s.execute(text( + "SELECT COUNT(*) FROM docker_logs WHERE host_id=:h"), {"h": hid})).scalar() return kept, after_off kept, after_off = asyncio.run(_go()) assert kept == {"web"} # excluded 'noisy' dropped on ingest - assert after_off == 1 # toggle off → no new rows (still just the one 'web') + assert after_off == 1 # toggle off added nothing → still just the one 'web' line @_NEEDS_DB