Explorar el Código

fix(api): keep in-flight starts fail-closed

SamLee hace 1 día
padre
commit
66b3ac7e56

+ 0 - 17
script_build_host/src/script_build_host/api/routes.py

@@ -21,7 +21,6 @@ from script_build_host.application.observation_views import (
     mission_snapshot_view,
     task_detail_view,
 )
-from script_build_host.domain.errors import BuildNotFound
 from script_build_host.domain.ports import (
     MissionBindingRepository,
     PublicationRepository,
@@ -142,21 +141,6 @@ def create_script_build_router(
         async def execute() -> tuple[int, dict[str, Any]]:
             return await execute_with_root(preallocated_root_trace_id)
 
-        async def resume_reserved(
-            root_trace_id: str | None, _resource_id: int | None
-        ) -> tuple[int, dict[str, Any]]:
-            if root_trace_id is None:
-                raise problem(409, "IDEMPOTENCY_RECOVERY_REQUIRED", "reserved start has no Root")
-            try:
-                result = await mission_service.start_result_by_root(root_trace_id)
-            except BuildNotFound:
-                return await execute_with_root(root_trace_id)
-            return 200, {
-                "script_build_id": result.script_build_id,
-                "status": result.status.value,
-                "root_trace_id": result.root_trace_id,
-            }
-
         if idempotency_key and http_command_journal is not None:
             _, payload = await http_command_journal.execute(
                 principal=actor,
@@ -164,7 +148,6 @@ def create_script_build_router(
                 idempotency_key=idempotency_key,
                 request_payload=body.model_dump(mode="json"),
                 command=execute,
-                resume_reserved=resume_reserved,
                 preallocated_root_trace_id=preallocated_root_trace_id,
             )
         else:

+ 41 - 0
script_build_host/tests/test_http_commands.py

@@ -207,3 +207,44 @@ async def test_oversized_idempotent_response_is_failed_not_left_reserved(
             )
             == "failed"
         )
+
+
+@pytest.mark.asyncio
+async def test_idempotency_key_is_scoped_by_principal_and_route(
+    database: tuple[AsyncEngine, async_sessionmaker[AsyncSession]],
+) -> None:
+    _, sessions = database
+    journal = HttpCommandJournal(sessions)
+    calls: list[str] = []
+
+    async def execute(label: str) -> tuple[int, dict[str, Any]]:
+        calls.append(label)
+        return 200, {"label": label}
+
+    common = {
+        "idempotency_key": "same-key",
+        "request_payload": {"script_build_id": 7},
+    }
+    first = await journal.execute(
+        principal=Principal("subject", "tenant-a"),
+        route_family="script-build:phase-three-advance",
+        command=lambda: execute("advance-a"),
+        **common,
+    )
+    second = await journal.execute(
+        principal=Principal("subject", "tenant-b"),
+        route_family="script-build:phase-three-advance",
+        command=lambda: execute("advance-b"),
+        **common,
+    )
+    third = await journal.execute(
+        principal=Principal("subject", "tenant-a"),
+        route_family="script-build:finalize",
+        command=lambda: execute("finalize-a"),
+        **common,
+    )
+
+    assert first[1]["label"] == "advance-a"
+    assert second[1]["label"] == "advance-b"
+    assert third[1]["label"] == "finalize-a"
+    assert calls == ["advance-a", "advance-b", "finalize-a"]