"""Pipeline run-id context and the scanner stamping it into scan markers.""" from __future__ import annotations import asyncio import pytest from app.services import pipeline_run def test_no_run_id_by_default(): assert pipeline_run.current() is None def test_bind_and_release_restore_previous(): assert pipeline_run.current() is None token = pipeline_run.bind("run-1") try: assert pipeline_run.current() == "run-1" finally: pipeline_run.release(token) assert pipeline_run.current() is None def test_new_run_ids_are_unique(): ids = {pipeline_run.new_run_id() for _ in range(100)} assert len(ids) == 100 @pytest.mark.asyncio async def test_run_id_propagates_to_awaited_coroutines(): """The scan and shadow steps are awaited inside the pipeline's task, so they must observe the id the pipeline bound.""" async def step() -> str | None: return pipeline_run.current() token = pipeline_run.bind("run-42") try: assert await step() == "run-42" finally: pipeline_run.release(token) @pytest.mark.asyncio async def test_run_id_does_not_leak_into_an_independent_task(): """A manual scan is a separate APScheduler job, started independently of the pipeline. Modelled here as a task created before the bind: it captures its own context and never observes the id the pipeline binds afterwards.""" seen: dict[str, str | None] = {} manual_started = asyncio.Event() let_manual_finish = asyncio.Event() async def manual_job() -> None: manual_started.set() await let_manual_finish.wait() seen["manual"] = pipeline_run.current() # Created with no id in context — the manual job predates the pipeline bind. task = asyncio.create_task(manual_job()) await manual_started.wait() token = pipeline_run.bind("pipeline") try: assert pipeline_run.current() == "pipeline" let_manual_finish.set() await task finally: pipeline_run.release(token) assert seen["manual"] is None