Files
next-tjwater-server/tests/unit/test_contaminant_source_trace.py
T
jiang 2e4f6cb93b feat(investigations): add event impact and work-order feedback flows
Add isolated burst and pollution impact evidence, parallel work-order workflows, persisted notifications and graph updates.

Checkpoint current code. Known follow-ups remain for large pollution result previews and graceful geometry failure.
2026-11-14 15:14:02 +08:00

74 lines
3.8 KiB
Python

import asyncio
from types import SimpleNamespace
from unittest.mock import AsyncMock
from uuid import uuid4
import pytest
from fastapi import HTTPException
from pydantic import ValidationError
from app.mcp import query_service
from app.mcp.analysis_models import ContaminantSourceTraceInput
from app.mcp.calculation_jobs import should_queue
from app.mcp.catalog import TOOLS
from app.services import contaminant_source_trace as trace
def arguments(**extra):
return ContaminantSourceTraceInput(context_handle="a" * 43,
**({"candidate_node_ids": ["J2", "J1", "missing"],
"start": "2026-07-16T08:00:00+08:00", "end": "2026-07-16T08:15:00+08:00"} | extra))
@pytest.mark.parametrize("nodes", [[{"id": "J2"}, {"id": "J1"}], [{"id": "J1"}, {"id": "J2"}]])
def test_trace_is_immediate_project_scoped_stable_and_explicit_about_algorithm(monkeypatch, nodes):
read = AsyncMock(return_value=nodes)
monkeypatch.setattr(trace, "read_rows", read)
connection = object()
args = arguments()
result = asyncio.run(trace.trace_contaminant_source(connection, args))
assert result["located_node"] == "J1"
assert result["missing_node_ids"] == ["missing"]
assert result["top_candidates"] == [{"node_id": "J1"}]
assert result["algorithm_status"] == "provisional"
assert result["selection_basis"] == "lexicographically_first_existing_candidate_node"
assert "相似度" not in result and "confidence" not in result
assert read.await_args.args[0] is connection
assert "network.nodes" in read.await_args.args[1] and "ANY(%s)" in read.await_args.args[1]
assert read.await_args.args[2] == (args.candidate_node_ids,)
assert not should_queue(TOOLS["run_contaminant_source_trace"], args)
@pytest.mark.parametrize("change", [{"candidate_node_ids": []}, {"candidate_node_ids": ["J1", "J1"]},
{"candidate_node_ids": [str(index) for index in range(21)]},
{"start": "2026-07-16T08:00:00"}, {"end": "2026-07-16T08:00:00+08:00"},
{"end": "2026-07-18T08:00:00+08:00"}])
def test_trace_rejects_unbounded_or_ambiguous_inputs(change):
with pytest.raises(ValidationError):
arguments(**change)
@pytest.mark.parametrize("nodes", [[], [{"id": "J1"}]])
def test_dispatch_preserves_provenance_and_reports_only_actual_node_gaps(monkeypatch, nodes):
monkeypatch.setattr(trace, "read_rows", AsyncMock(return_value=nodes))
monkeypatch.setattr(query_service, "permissions_for_context", lambda _: {"simulation.run"})
scope = SimpleNamespace(connection=object(), project=SimpleNamespace(project_role="member"))
binding = SimpleNamespace(tool_cap=["run_contaminant_source_trace"], purpose="task_execution",
permission_cap=["simulation.run"], task_id=uuid4(), task_scope_cap=[], replay_context=None)
result = asyncio.run(query_service.execute_query(scope, binding, TOOLS["run_contaminant_source_trace"], arguments()))
assert result.origin == "synthetic" and result.semantics == "analysis"
assert (result.data["result"]["located_node"] is None) == (not nodes)
assert bool(result.data_gaps) == (not nodes)
assert "context_handle" not in result.data["parameters"]
def test_replay_trace_cannot_escape_frozen_window(monkeypatch):
monkeypatch.setattr(query_service, "permissions_for_context", lambda _: {"simulation.run"})
scope = SimpleNamespace(project=SimpleNamespace(project_role="member"))
binding = SimpleNamespace(tool_cap=["run_contaminant_source_trace"], purpose="task_execution",
permission_cap=["simulation.run"], task_id=uuid4(), task_scope_cap=[],
replay_context={"knowledge_cutoff": "2026-07-16T08:00:00+08:00"})
with pytest.raises(HTTPException) as caught:
asyncio.run(query_service.authorize_query(scope, binding, TOOLS["run_contaminant_source_trace"], arguments()))
assert caught.value.status_code == 422