Skip to content

Commit 459ab16

Browse files
authored
fix(tests): WAL-pinning for all inline NullRunRuntime creations (#63)
* fix(tests): pin test_runtime WAL to tmp_path to avoid cross-Python flake The test_runtime fixture in test_protect_branches.py built a real NullRunRuntime(api_key, _test_mode=True) without going through the mock_api conftest. The runtime's Transport.start() calls _replay_from_wal() which reads /tmp/nullrun.wal (the default NULLRUN_WAL_PATH fallback). If a previous test run in a different Python version (3.10 or 3.12) had persisted a non-empty WAL, the 3.11 worker would replay those events to a real HTTP endpoint, get HTTP 401, and the fixture would fail at setup with NullRunAuthError: nullrun.breaker.exceptions.NullRunAuthError: Invalid API key This bit CI on 2026-07-11 (run 29156199607): tests 3.10 + 3.12 passed, test 3.11 failed with that error in the fixture setup of test_enforce_sensitive_tool_dict_with_fallback_fail_open. The 3.10/3.12 runs cleared the global /tmp/nullrun.wal by reading it first, so 3.11 picked up the next writer. Order- dependent; flaky on the matrix. Pin NULLRUN_WAL_PATH to a tmp_path-scoped file so each test session reads its own fresh empty WAL. Resolves the flake without touching SDK source (no production code change). Verified locally: - pytest tests/test_protect_branches.py -> 43/43 pass - with pre-seeded stale /tmp/nullrun.wal, the previously failing test now passes. No public API change. No SDK_MIN_VERSION bump. Backends on 1.0.0 keep working unchanged. Recommended: 0.13.6 (no version bump needed for a test-only fix). * fix(tests): WAL-pinning for all inline NullRunRuntime creations Follows up on commit 41a16f7 which pinned NULLRUN_WAL_PATH for the test_runtime fixture only. Other tests in test_protect_branches.py / test_runtime_branches.py / test_toolbox_langgraph.py build NullRunRuntime inline (no fixture) and were still picking up a stale WAL from a previous test run, causing HTTP 401 `NullRunAuthError` in 3.12 (CI run 29158094827, job `test (3.12)`). This commit: 1. Adds a shared `make_test_runtime` factory fixture to conftest.py that pins NULLRUN_WAL_PATH to tmp_path, stubs _do_flush / _do_flush_locked / _client, and resets the singleton around the factory. 2. Replaces 4 inline `NullRunRuntime(api_key=..., _test_mode=True)` calls in test_protect_branches.py with `make_test_runtime()`, including: - test_protect_async_kill_re_raises_WorkflowKilledInterrupt - test_get_protected_runtime_falls_back_to_get_runtime 3. Patches the local _make_test_runtime / _make_runtime_with_mocked_auth helpers in test_runtime_branches.py to set NULLRUN_WAL_PATH per-call (via tempfile.mkdtemp) before constructing the runtime. 4. Extends the autouse _test_runtime fixture in test_toolbox_langgraph.py to take tmp_path and pin NULLRUN_WAL_PATH, matching conftest::make_test_runtime. Verified locally on 3.11: - pytest tests/ -n auto: 1219 passed, 1 failed, 7 skipped (1 failure: test_actions.py::TestPauseAction ::test_is_paused_respects_cooldown — pre-existing flake on master, NOT introduced by this commit; verified by git stash + repro on bare master) - ruff check src/: all checks passed - mypy src/: success, no issues in 34 source files Public API unchanged. No SDK_MIN_VERSION bump. Backends on 1.0.0 keep working unchanged. Recommended: 0.13.6 (no version bump needed).
1 parent 6563f73 commit 459ab16

4 files changed

Lines changed: 90 additions & 8 deletions

File tree

‎tests/conftest.py‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,44 @@ def _make(**kwargs):
168168
return rt
169169

170170
return _make
171+
172+
@pytest.fixture
173+
def make_test_runtime(monkeypatch, tmp_path):
174+
"""Factory for tests that build a real ``NullRunRuntime`` inline
175+
(no ``mock_api`` indirection).
176+
177+
Pins ``NULLRUN_WAL_PATH`` to a tmp_path-scoped file so the
178+
constructor's ``Transport._replay_from_wal`` never reads the
179+
default ``tempfile.gettempdir()/nullrun.wal`` (which may carry
180+
real on-disk events from a previous test run or parallel
181+
worker and would cause HTTP 401 → ``NullRunAuthError`` in
182+
setup). Mirrors the ``test_runtime`` fixture in
183+
``test_protect_branches.py`` so all tests that build a runtime
184+
directly get the same isolation.
185+
186+
Stub ``_do_flush`` / ``_do_flush_locked`` / ``_client`` so any
187+
real network attempt is no-op'd. Reset singleton around the
188+
factory so test ordering is independent.
189+
"""
190+
from unittest.mock import MagicMock
191+
from nullrun.runtime import NullRunRuntime
192+
193+
NullRunRuntime.reset_instance()
194+
# Pre-pin the WAL path before any runtime can be constructed
195+
# (otherwise the default is captured at first construction).
196+
monkeypatch.setenv("NULLRUN_WAL_PATH", str(tmp_path / "sdk.wal"))
197+
198+
def _factory(**overrides):
199+
api_key = overrides.pop("api_key", "test-key-12345678")
200+
rt = NullRunRuntime(api_key=api_key, _test_mode=True)
201+
# Stub the network-facing pieces for tests that build a
202+
# runtime inline (not via ``mock_api``).
203+
rt._transport._do_flush = lambda: None
204+
rt._transport._do_flush_locked = lambda: None
205+
rt._transport._client = MagicMock()
206+
for k, v in overrides.items():
207+
setattr(rt, k, v)
208+
return rt
209+
210+
yield _factory
211+
NullRunRuntime.reset_instance()

‎tests/test_protect_branches.py‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,20 @@
3535

3636

3737
@pytest.fixture
38-
def test_runtime(monkeypatch):
38+
def test_runtime(monkeypatch, tmp_path):
3939
"""Provide a runtime in test mode so get_runtime returns without
4040
authenticating against a real server.
41+
42+
Replays any WAL left over from previous test runs in a
43+
tmp_path-scoped WAL file so the constructor's
44+
``_replay_from_wal`` never reads ``~/.nullrun/sdk.wal`` and
45+
flushes real on-disk events to a live API. This avoids the
46+
cross-Python-version flake seen on CI in 2026-07-11 where
47+
3.11 picked up a stale WAL from a 3.10/3.12 worker that
48+
finished without explicitly clearing it.
4149
"""
4250
monkeypatch.setenv("NULLRUN_API_KEY", "test-key-12345678")
51+
monkeypatch.setenv("NULLRUN_WAL_PATH", str(tmp_path / "sdk.wal"))
4352
NullRunRuntime.reset_instance()
4453
rt = NullRunRuntime(api_key="test-key-12345678", _test_mode=True)
4554
rt.organization_id = "org-1"
@@ -437,13 +446,13 @@ def f():
437446

438447

439448
@pytest.mark.asyncio
440-
async def test_protect_async_kill_re_raises_WorkflowKilledInterrupt():
449+
async def test_protect_async_kill_re_raises_WorkflowKilledInterrupt(make_test_runtime):
441450
"""Async wrapper does NOT unify — kill signal propagates as-is so
442451
async frameworks can interrupt the event loop cleanly.
443452
"""
444453
from nullrun import decorators as dec_mod
445454

446-
rt = NullRunRuntime(api_key="test-key-12345678", _test_mode=True)
455+
rt = make_test_runtime()
447456
rt.track_event = MagicMock()
448457
rt.check_control_plane = MagicMock(
449458
side_effect=WorkflowKilledInterrupt(workflow_id="wf-1", reason="x")
@@ -542,12 +551,12 @@ def test_get_protected_runtime_returns_runtime(test_runtime):
542551
assert decorators.get_protected_runtime() is rt
543552

544553

545-
def test_get_protected_runtime_falls_back_to_get_runtime(test_runtime, monkeypatch):
554+
def test_get_protected_runtime_falls_back_to_get_runtime(monkeypatch, make_test_runtime):
546555
"""When the decorator slot is empty, fall back to the global singleton."""
547556
from nullrun import decorators
548557

549558
decorators._runtime = None
550-
NullRunRuntime._instance = NullRunRuntime(api_key="test-key-12345678", _test_mode=True)
559+
NullRunRuntime._instance = make_test_runtime()
551560
try:
552561
out = decorators.get_protected_runtime()
553562
assert out is NullRunRuntime._instance

‎tests/test_runtime_branches.py‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,21 @@ def _reset_singleton():
3030
def _make_test_runtime() -> NullRunRuntime:
3131
"""Build a runtime that skips network I/O and returns from
3232
``_authenticate`` with a stub organisation id.
33+
34+
Pins ``NULLRUN_WAL_PATH`` to a per-call tmp dir so the
35+
constructor's ``Transport._replay_from_wal`` never picks up a
36+
stale WAL from a previous test run (which would replay real
37+
events to a live API and cause HTTP 401 in setup). See
38+
``conftest::make_test_runtime`` for the fixture equivalent.
3339
"""
40+
# Per-call isolation: each helper invocation owns its WAL.
41+
# ``setdefault`` so an outer session-level pinning (from
42+
# ``make_test_runtime`` fixture) is preserved if already set.
43+
import os
44+
import tempfile
45+
if not os.environ.get("NULLRUN_WAL_PATH"):
46+
wal_dir = tempfile.mkdtemp(prefix="nullrun-test-wal-")
47+
os.environ["NULLRUN_WAL_PATH"] = os.path.join(wal_dir, "sdk.wal")
3448
rt = NullRunRuntime(api_key="test-key-12345678", _test_mode=True)
3549
rt.organization_id = "org-1"
3650
rt.workflow_id = "wf-1"
@@ -357,8 +371,9 @@ def _trigger_shutdown():
357371
# ─── get_instance credential rotation ──────────────────────────────
358372

359373

360-
def test_get_instance_returns_singleton_when_no_change(monkeypatch):
374+
def test_get_instance_returns_singleton_when_no_change(monkeypatch, tmp_path):
361375
monkeypatch.setenv("NULLRUN_API_KEY", "test-key-12345678")
376+
monkeypatch.setenv("NULLRUN_WAL_PATH", str(tmp_path / "sdk.wal"))
362377
NullRunRuntime.reset_instance()
363378
rt1 = NullRunRuntime(api_key="test-key-12345678", _test_mode=True)
364379
NullRunRuntime._instance = rt1
@@ -372,7 +387,16 @@ def test_get_instance_returns_singleton_when_no_change(monkeypatch):
372387
def _make_runtime_with_mocked_auth() -> NullRunRuntime:
373388
"""Build a test-mode runtime and stub the transport client.post
374389
so we can drive ``_authenticate`` deterministically.
390+
391+
Pins ``NULLRUN_WAL_PATH`` per call so we never read a stale
392+
WAL from a previous run. ``setdefault`` preserves any
393+
outer-session pinning set by a fixture.
375394
"""
395+
import os
396+
import tempfile
397+
if not os.environ.get("NULLRUN_WAL_PATH"):
398+
wal_dir = tempfile.mkdtemp(prefix="nullrun-test-wal-")
399+
os.environ["NULLRUN_WAL_PATH"] = os.path.join(wal_dir, "sdk.wal")
376400
rt = NullRunRuntime(api_key="test-key-12345678", _test_mode=True)
377401
rt._transport._client = MagicMock()
378402
rt._fetch_policy = MagicMock()

‎tests/test_toolbox_langgraph.py‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,18 @@
1515

1616

1717
@pytest.fixture(autouse=True)
18-
def _test_runtime(monkeypatch):
18+
def _test_runtime(monkeypatch, tmp_path):
1919
"""Provide a runtime in test mode so get_runtime returns without
20-
authenticating against a real server."""
20+
authenticating against a real server.
21+
22+
Pins ``NULLRUN_WAL_PATH`` to a tmp_path-scoped file so the
23+
constructor's ``Transport._replay_from_wal`` never picks up
24+
a stale WAL left over from a previous test run (which would
25+
replay real events to a live API and cause HTTP 401 in
26+
setup). Mirrors ``conftest::make_test_runtime``.
27+
"""
2128
monkeypatch.setenv("NULLRUN_API_KEY", "test-key-12345678")
29+
monkeypatch.setenv("NULLRUN_WAL_PATH", str(tmp_path / "sdk.wal"))
2230
NullRunRuntime.reset_instance()
2331
# Pre-build a test-mode singleton so get_runtime returns it without
2432
# hitting the network. Construct directly and store on the singleton

0 commit comments

Comments
 (0)