From 46d509beb11894384443dc158f5a393e7efafec6 Mon Sep 17 00:00:00 2001 From: Juan Date: Mon, 11 May 2026 10:31:12 +0200 Subject: [PATCH] =?UTF-8?q?test(integration):=20resume=20HITL=20tras=20rec?= =?UTF-8?q?reaci=C3=B3n=20del=20app=20(checkpointing=20SQLite)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Invoca el escenario SIP hasta awaiting_approval, descarta el TestClient (y las caches DI) y crea otro sobre el mismo DATA_DIR; el /approve posterior reanuda hasta completed gracias a execution_index.json + checkpoints.sqlite. Desviación del plan: el Task 29 Step 2 ("persistir índice de ejecuciones") ya se implementó en el Task 24 (api/executions.py: _record_execution/_load_index/ _resolve sobre data_dir/execution_index.json), así que este commit solo añade el test. Co-Authored-By: Claude Opus 4.7 --- .../test_invoke_resume_after_restart.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/integration/test_invoke_resume_after_restart.py diff --git a/tests/integration/test_invoke_resume_after_restart.py b/tests/integration/test_invoke_resume_after_restart.py new file mode 100644 index 0000000..024a916 --- /dev/null +++ b/tests/integration/test_invoke_resume_after_restart.py @@ -0,0 +1,61 @@ +"""Integration: el estado HITL sobrevive a un reinicio del proceso (checkpointing SQLite). + +Tras una pausa ``awaiting_approval`` se descarta el ``TestClient`` (y con él las caches +DI en memoria) y se crea otro apuntando al mismo ``DATA_DIR``. El ``/approve`` posterior +debe encontrar la ejecución vía ``execution_index.json`` + ``checkpoints.sqlite`` y +reanudarla hasta ``completed``. +""" + +from pathlib import Path + +import pytest + +EXAMPLES = ( + Path(__file__).parent.parent.parent / "agents" / "incident_analyzer" / "examples" +) + + +@pytest.mark.integration +def test_resume_tras_recreacion_del_app(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + from fastapi.testclient import TestClient + + from agentforge_core.api import deps + from agentforge_core.main import create_app + + repo_root = Path(__file__).parent.parent.parent + monkeypatch.setenv("DATA_DIR", str(tmp_path)) + monkeypatch.setenv("AGENTS_DIR", str(repo_root / "agents")) + monkeypatch.setenv("POLICIES_DIR", str(repo_root / "policies")) + monkeypatch.setenv("LLM_PROVIDER", "mock") + + def _fresh() -> TestClient: + for fn in ( + deps.get_settings, + deps.get_registry, + deps.get_policy_store, + deps.get_orchestrator, + deps.get_llm_provider, + deps.get_guardrail_engine, + ): + fn.cache_clear() + return TestClient(create_app()) + + payload = (EXAMPLES / "01_sip_registration_drop.txt").read_text(encoding="utf-8") + + # 1) Primera "ejecución" del core: invoca y queda en awaiting_approval. + client_a = _fresh() + r = client_a.post("/agents/incident_analyzer/invoke", json={"input": payload}) + body = r.json() + assert body["status"] == "awaiting_approval" + trace_id = body["trace_id"] + pending = [a["id"] for a in body["needs_human_for"]] + client_a.close() + + # 2) Recreamos el app (simula restart del contenedor) — DATA_DIR persiste. + client_b = _fresh() + r2 = client_b.post( + f"/executions/{trace_id}/approve", + json={"approved_action_ids": pending}, + ) + assert r2.status_code == 200 + assert r2.json()["status"] == "completed"