Download tests/test_persistence.py from FlyingNunchucks/09-agentic-workflow-sys: direct link, hf CLI and curl.
- Browser
- Download file 8.14 kB
-
https://huggingface.co/spaces/FlyingNunchucks/09-agentic-workflow-sys/resolve/main/tests/test_persistence.py
- Command line
-
hf download hf://spaces/FlyingNunchucks/09-agentic-workflow-sys/tests/test_persistence.py
-
curl -L -o test_persistence.py https://huggingface.co/spaces/FlyingNunchucks/09-agentic-workflow-sys/resolve/main/tests/test_persistence.py
8.14 kB
| from pathlib import Path | |
| import pytest | |
| from src.events import record_event | |
| from src.executor import ( | |
| WorkflowResumeError, | |
| execute_workflow, | |
| resume_workflow, | |
| ) | |
| from src.persistence import ( | |
| InMemoryStateStore, | |
| RunNotFoundError, | |
| SQLiteStateStore, | |
| ) | |
| from src.registry import HandlerRegistry | |
| from src.schemas import ( | |
| EventType, | |
| NodeDefinition, | |
| NodeRun, | |
| NodeStatus, | |
| NodeType, | |
| WorkflowDefinition, | |
| WorkflowRun, | |
| WorkflowStatus, | |
| ) | |
| def make_two_step_workflow(*, version: str = "1.0") -> WorkflowDefinition: | |
| return WorkflowDefinition( | |
| workflow_id="persistence-workflow", | |
| name="Persistence Workflow", | |
| version=version, | |
| nodes=[ | |
| NodeDefinition( | |
| node_id="first", | |
| name="First", | |
| node_type=NodeType.TASK, | |
| handler="first_handler", | |
| ), | |
| NodeDefinition( | |
| node_id="second", | |
| name="Second", | |
| node_type=NodeType.TASK, | |
| handler="second_handler", | |
| depends_on=["first"], | |
| ), | |
| ], | |
| ) | |
| def test_in_memory_store_returns_detached_snapshot() -> None: | |
| store = InMemoryStateStore() | |
| run = WorkflowRun( | |
| run_id="run-1", | |
| workflow_id="workflow-1", | |
| context={"value": 1}, | |
| ) | |
| store.save(run) | |
| run.context["value"] = 99 | |
| loaded = store.load("run-1") | |
| assert loaded.context == {"value": 1} | |
| loaded.context["value"] = 7 | |
| assert store.load("run-1").context == {"value": 1} | |
| def test_in_memory_store_rejects_unknown_run() -> None: | |
| store = InMemoryStateStore() | |
| with pytest.raises(RunNotFoundError, match="was not found"): | |
| store.load("missing") | |
| def test_sqlite_store_round_trips_nested_workflow_state(tmp_path: Path) -> None: | |
| store = SQLiteStateStore(tmp_path / "workflow.db") | |
| run = WorkflowRun( | |
| run_id="sqlite-run", | |
| workflow_id="workflow-1", | |
| workflow_version="2.1", | |
| status=WorkflowStatus.RUNNING, | |
| node_runs={ | |
| "task": NodeRun( | |
| node_id="task", | |
| status=NodeStatus.COMPLETED, | |
| attempt=1, | |
| output={"ok": True}, | |
| ) | |
| }, | |
| context={"request": "synthetic"}, | |
| ) | |
| record_event(run, EventType.WORKFLOW_STARTED) | |
| record_event(run, EventType.NODE_COMPLETED, node_id="task") | |
| store.save(run) | |
| loaded = store.load("sqlite-run") | |
| assert loaded == run | |
| assert loaded.events[0].event_type is EventType.WORKFLOW_STARTED | |
| assert loaded.node_runs["task"].output == {"ok": True} | |
| def test_sqlite_store_upserts_latest_checkpoint(tmp_path: Path) -> None: | |
| store = SQLiteStateStore(tmp_path / "workflow.db") | |
| run = WorkflowRun(run_id="run-1", workflow_id="workflow-1") | |
| store.save(run) | |
| run.status = WorkflowStatus.COMPLETED | |
| run.final_output = {"done": True} | |
| store.save(run) | |
| loaded = store.load("run-1") | |
| assert loaded.status is WorkflowStatus.COMPLETED | |
| assert loaded.final_output == {"done": True} | |
| def test_sqlite_store_rejects_unknown_run(tmp_path: Path) -> None: | |
| store = SQLiteStateStore(tmp_path / "workflow.db") | |
| with pytest.raises(RunNotFoundError, match="was not found"): | |
| store.load("missing") | |
| class SimulatedProcessStop(BaseException): | |
| pass | |
| class StopAfterFirstCompletionStore: | |
| """Save a safe checkpoint, then simulate process termination once.""" | |
| def __init__(self) -> None: | |
| self.inner = InMemoryStateStore() | |
| self.triggered = False | |
| def save(self, run: WorkflowRun) -> None: | |
| self.inner.save(run) | |
| first = run.node_runs.get("first") | |
| second = run.node_runs.get("second") | |
| if ( | |
| not self.triggered | |
| and first is not None | |
| and second is not None | |
| and first.status is NodeStatus.COMPLETED | |
| and second.status is NodeStatus.PENDING | |
| ): | |
| self.triggered = True | |
| raise SimulatedProcessStop() | |
| def load(self, run_id: str) -> WorkflowRun: | |
| return self.inner.load(run_id) | |
| def test_safe_checkpoint_resume_skips_completed_node_after_process_stop() -> None: | |
| registry = HandlerRegistry() | |
| first_calls = 0 | |
| second_calls = 0 | |
| def first_handler(payload: dict) -> dict: | |
| nonlocal first_calls | |
| first_calls += 1 | |
| return {"first": "done"} | |
| def second_handler(payload: dict) -> dict: | |
| nonlocal second_calls | |
| second_calls += 1 | |
| assert payload["dependencies"] == {"first": {"first": "done"}} | |
| return {"second": "done"} | |
| registry.register("first_handler", first_handler) | |
| registry.register("second_handler", second_handler) | |
| workflow = make_two_step_workflow() | |
| store = StopAfterFirstCompletionStore() | |
| with pytest.raises(SimulatedProcessStop): | |
| execute_workflow( | |
| workflow, | |
| registry, | |
| run_id="resume-run", | |
| state_store=store, | |
| ) | |
| checkpoint = store.load("resume-run") | |
| assert checkpoint.status is WorkflowStatus.RUNNING | |
| assert checkpoint.node_runs["first"].status is NodeStatus.COMPLETED | |
| assert checkpoint.node_runs["second"].status is NodeStatus.PENDING | |
| assert first_calls == 1 | |
| assert second_calls == 0 | |
| resumed = resume_workflow( | |
| workflow, | |
| registry, | |
| run_id="resume-run", | |
| state_store=store, | |
| ) | |
| assert resumed.status is WorkflowStatus.COMPLETED | |
| assert resumed.node_runs["first"].attempt == 1 | |
| assert resumed.node_runs["second"].attempt == 1 | |
| assert first_calls == 1 | |
| assert second_calls == 1 | |
| assert EventType.WORKFLOW_RESUMED in [event.event_type for event in resumed.events] | |
| assert resumed.final_output == {"second": {"second": "done"}} | |
| def test_completed_run_resume_is_idempotent() -> None: | |
| registry = HandlerRegistry() | |
| calls = 0 | |
| def handler(payload: dict) -> dict: | |
| nonlocal calls | |
| calls += 1 | |
| return {"ok": True} | |
| registry.register("first_handler", handler) | |
| registry.register("second_handler", handler) | |
| workflow = make_two_step_workflow() | |
| store = InMemoryStateStore() | |
| original = execute_workflow( | |
| workflow, | |
| registry, | |
| run_id="completed-run", | |
| state_store=store, | |
| ) | |
| original_event_ids = [event.event_id for event in original.events] | |
| resumed = resume_workflow( | |
| workflow, | |
| registry, | |
| run_id="completed-run", | |
| state_store=store, | |
| ) | |
| assert calls == 2 | |
| assert resumed.status is WorkflowStatus.COMPLETED | |
| assert [event.event_id for event in resumed.events] == original_event_ids | |
| def test_resume_rejects_changed_workflow_version() -> None: | |
| store = InMemoryStateStore() | |
| run = WorkflowRun( | |
| run_id="run-1", | |
| workflow_id="persistence-workflow", | |
| workflow_version="1.0", | |
| status=WorkflowStatus.RUNNING, | |
| node_runs={ | |
| "first": NodeRun(node_id="first", status=NodeStatus.COMPLETED), | |
| "second": NodeRun(node_id="second", status=NodeStatus.PENDING), | |
| }, | |
| ) | |
| store.save(run) | |
| with pytest.raises(WorkflowResumeError, match="version"): | |
| resume_workflow( | |
| make_two_step_workflow(version="2.0"), | |
| HandlerRegistry(), | |
| run_id="run-1", | |
| state_store=store, | |
| ) | |
| def test_resume_rejects_ambiguous_in_flight_node() -> None: | |
| store = InMemoryStateStore() | |
| run = WorkflowRun( | |
| run_id="run-1", | |
| workflow_id="persistence-workflow", | |
| workflow_version="1.0", | |
| status=WorkflowStatus.RUNNING, | |
| node_runs={ | |
| "first": NodeRun(node_id="first", status=NodeStatus.COMPLETED), | |
| "second": NodeRun(node_id="second", status=NodeStatus.RUNNING, attempt=1), | |
| }, | |
| ) | |
| store.save(run) | |
| with pytest.raises(WorkflowResumeError, match="ambiguous or non-resumable"): | |
| resume_workflow( | |
| make_two_step_workflow(), | |
| HandlerRegistry(), | |
| run_id="run-1", | |
| state_store=store, | |
| ) | |