"""Per-worker Redis key namespacing for the AI-spending rollup tests. The documented worktree/session isolation covers the DATABASE. Redis is only isolated as far as ``tests/conftest.py`` takes it: it rewrites ``REDIS_URL`` to logical DB ``int(digits(PYTEST_XDIST_WORKER)) % 16``, so two concurrent sessions whose worker names carry the same digits (or a session that forgets ``PYTEST_XDIST_WORKER`` entirely) land on the SAME logical DB. These tests then fight over two GLOBAL keys: * ``helix:lock:ai_spending:period_rollup_write`` — one session's real acquire makes the other's "lock is free" test see ``RollupLockBusy`` ("skipped"/409 where a write was expected); * ``helix:ai_spending:cost_drift_check:heal_defer_streak`` and ``helix:ai_spending:period_rollup_refresh:defer_streak`` — the streak assertions are exact (``consecutive_deferrals == 1``) and the per-test cleanup DELETEs the keys, so each session wipes the other's counter mid-sequence. All three constants are read as module globals at call time, so patching the module attribute with a per-process suffix is sufficient and needs no production-code seam. Tests must therefore read the key through the module (or the fixtures below), never via a from-import taken at collection time. """ from __future__ import annotations import os from unittest.mock import patch import pytest from app.domains.ai_spending import locks as _locks from app.scheduled_jobs import ai_spending_handlers as _handlers # Worker name AND pid: the worker name alone repeats across sessions (both # may use "gw0"/"fx2"), the pid alone is stable enough within one session. _KEY_SUFFIX = f"{os.environ.get('PYTEST_XDIST_WORKER') or 'local'}-{os.getpid()}" @pytest.fixture(autouse=True) def rollup_test_redis_keys(): """Namespace the rollup lock key + both defer-streak keys for this process. Yields ``(lock_key, heal_streak_key, refresh_streak_key)``.""" lock_key = f"{_locks.ROLLUP_WRITE_LOCK}:test-{_KEY_SUFFIX}" streak_key = f"{_handlers._HEAL_DEFER_STREAK_KEY}:test-{_KEY_SUFFIX}" refresh_streak_key = ( f"{_handlers._REFRESH_DEFER_STREAK_KEY}:test-{_KEY_SUFFIX}" ) with ( patch.object(_locks, "ROLLUP_WRITE_LOCK", lock_key), patch.object(_handlers, "_HEAL_DEFER_STREAK_KEY", streak_key), patch.object( _handlers, "_REFRESH_DEFER_STREAK_KEY", refresh_streak_key, ), ): yield lock_key, streak_key, refresh_streak_key