feat: extend operational investigation workflows
Generic Container CI/CD / test-build-publish (push) Successful in 3m53s
Server CI/CD v2 / build-test-publish-and-deploy (push) Successful in 3m52s

This commit is contained in:
2026-10-30 17:30:19 +08:00
parent 63af998045
commit 05183aff93
82 changed files with 4644 additions and 330 deletions
+6 -2
View File
@@ -254,14 +254,16 @@ def test_committed_write_refresh_failure_has_explicit_http_contract() -> None:
assert response.json()["code"] == "materialized_view_refresh_failed_after_commit"
def test_valve_isolation_runtime_accepts_frontend_query(monkeypatch) -> None:
@pytest.mark.parametrize("element_type", ["auto", "link", "node"])
def test_valve_isolation_runtime_accepts_frontend_query(monkeypatch, element_type) -> None:
captured: dict[str, object] = {}
def fake_analyze_valve_isolation(network, accident_element, disabled_valves):
def fake_analyze_valve_isolation(network, accident_element, disabled_valves, accident_element_type):
captured.update(
network=network,
accident_element=accident_element,
disabled_valves=disabled_valves,
accident_element_type=accident_element_type,
business_dsn=get_project_pgconn_string(network),
timescale_dsn=get_project_timescale_pgconn_string(network),
)
@@ -288,6 +290,7 @@ def test_valve_isolation_runtime_accepts_frontend_query(monkeypatch) -> None:
("accident_element", "P-1"),
("accident_element", "P-2"),
("disabled_valves", "V-9"),
*([] if element_type == "auto" else [("accident_element_type", element_type)]),
],
)
@@ -297,6 +300,7 @@ def test_valve_isolation_runtime_accepts_frontend_query(monkeypatch) -> None:
"network": "fengyang",
"accident_element": ["P-1", "P-2"],
"disabled_valves": ["V-9"],
"accident_element_type": element_type,
"business_dsn": "postgresql://user:password@biz/fengyang",
"timescale_dsn": "postgresql://user:password@timescale/fengyang",
}
@@ -33,6 +33,7 @@ def test_dynamic_investigation_routes_are_in_the_public_contract():
assert "get" in paths["/api/v1/operational-tasks/{task_id}/reports"]
assert "get" in paths["/api/v1/operational-tasks/{task_id}/reports/{revision}"]
assert "post" in paths["/api/v1/operational-tasks/{task_id}/presentation-events"]
assert "get" in paths["/api/v1/operational-tasks/{task_id}/presentation-events"]
assert "get" in paths["/api/v1/operational-tasks/{task_id}/presentation-cursor"]
context_params = paths["/api/v1/operational-tasks/{task_id}/agent-context"]["get"]["parameters"]
assert any(item["name"] == "report_revision" and item["in"] == "query" for item in context_params)
@@ -123,3 +124,21 @@ async def test_report_endpoints_preserve_interactive_blocks_and_version_context(
schemas = app.openapi()["components"]["schemas"]
assert "ui_blocks" in schemas["InvestigationReportInput"]["properties"]
assert "ui_blocks" in schemas["InvestigationReport"]["properties"]
@pytest.mark.anyio
async def test_presentation_history_route_forwards_read_only_paging_and_domain_errors(monkeypatch):
service = SimpleNamespace(presentation_history=AsyncMock(return_value={"items": []}))
monkeypatch.setattr(endpoints, "_service", lambda _ctx, _conn: service)
task_id = uuid4()
await endpoints.get_operational_task_presentation_history(
task_id, after=7, through=30, limit=20, ctx=_service_context(), conn=object(),
)
service.presentation_history.assert_awaited_once_with(task_id, after=7, through=30, limit=20)
from app.domain.domain_errors import DomainError
service.presentation_history.side_effect = DomainError("TASK_NOT_FOUND", "missing", status=404)
with pytest.raises(HTTPException) as error:
await endpoints.get_operational_task_presentation_history(
task_id, after=0, through=None, limit=100, ctx=_service_context(), conn=object(),
)
assert error.value.status_code == 404
+1 -1
View File
@@ -9,7 +9,7 @@ def test_main_server_mounts_peripheral_health_endpoint():
assert response.status_code == 200
assert response.json() == {
"service": "peripheral-sample",
"origin": "synthetic",
"origin": "system_data",
"storage": "postgresql",
"status": "up",
"storage_verified": False,
+39
View File
@@ -0,0 +1,39 @@
import asyncio
from uuid import uuid4
from fastapi import FastAPI
from fastapi.testclient import TestClient
from app.api.v1.endpoints import operational_tasks as endpoints
from tests.unit.test_task_questions import setup, finish
def test_question_http_contract_waits_and_resumes_original_task(monkeypatch):
service, repo, task_id, command = setup()
app = FastAPI()
app.include_router(endpoints.router)
monkeypatch.setattr(endpoints, "_service", lambda *_args: service)
# Exercise HTTP parsing and response contracts; service tests cover authorization.
for route in app.routes:
if getattr(route, "name", "") in {"create_operational_task_question", "answer_operational_task_question"}:
for dependency in route.dependant.dependencies:
app.dependency_overrides[dependency.call] = lambda: None
with TestClient(app) as client:
path = f"/operational-tasks/{task_id}/questions"
invalid = client.post(path, json={**command.model_dump(mode="json"), "project_id": str(uuid4())})
assert invalid.status_code == 422
created = client.post(path, json=command.model_dump(mode="json"))
assert created.status_code == 200, created.text
question_id = created.json()["question_request_id"]
asyncio.run(finish(service, repo, task_id))
answered = client.post(f"{path}/{question_id}/answer", json={
"request_id": str(uuid4()), "expected_version": 1,
"answers": [{"question_id": "flow", "selected_option_ids": ["unknown"], "custom_text": ""}],
})
assert answered.status_code == 200, answered.text
result = answered.json()
assert result["task_id"] == str(task_id) and result["execution_generation"] == 2
assert result["active_run"]["status"] == "queued"
assert result["questions"][0]["status"] == "answered"
assert result["questions"][0]["answered_by"] == service._actor()
assert result["evidence"] == []