| """Tool Registry + Evidence Accumulator tests.""" |
|
|
| from __future__ import annotations |
|
|
| import pytest |
|
|
| from orchestrator.tools import ( |
| EvidenceAccumulator, |
| Tool, |
| ToolContext, |
| ToolName, |
| ToolRegistry, |
| ToolResult, |
| ) |
|
|
|
|
| def _result(tool: ToolName = "policy_match", **overrides: object) -> ToolResult: |
| base: dict[str, object] = { |
| "tool": tool, |
| "status": "success", |
| "summary": "matched rule 2", |
| "latency_ms": 142, |
| "detail": {}, |
| "error": None, |
| } |
| base.update(overrides) |
| return ToolResult(**base) |
|
|
|
|
| class _FakeTool: |
| """In-test Tool implementation. Satisfies the runtime_checkable Protocol.""" |
|
|
| def __init__(self, name: ToolName, summary: str = "ok", status: str = "success") -> None: |
| self._name = name |
| self._summary = summary |
| self._status = status |
|
|
| @property |
| def name(self) -> ToolName: |
| return self._name |
|
|
| async def run(self, context: ToolContext) -> ToolResult: |
| return ToolResult( |
| tool=self._name, |
| status=self._status, |
| summary=self._summary, |
| latency_ms=10, |
| ) |
|
|
|
|
| |
|
|
|
|
| def test_register_then_get() -> None: |
| reg = ToolRegistry() |
| t = _FakeTool("policy_match") |
| reg.register(t) |
| assert reg.get("policy_match") is t |
| assert reg.has("policy_match") |
| assert len(reg) == 1 |
| assert "policy_match" in reg |
|
|
|
|
| def test_register_duplicate_raises() -> None: |
| reg = ToolRegistry() |
| reg.register(_FakeTool("policy_match")) |
| with pytest.raises(ValueError, match="already registered"): |
| reg.register(_FakeTool("policy_match")) |
|
|
|
|
| def test_get_unknown_raises_keyerror() -> None: |
| reg = ToolRegistry() |
| with pytest.raises(KeyError, match="unknown tool"): |
| reg.get("user_history") |
|
|
|
|
| def test_names_preserves_insertion_order() -> None: |
| reg = ToolRegistry() |
| reg.register(_FakeTool("policy_match")) |
| reg.register(_FakeTool("report_velocity")) |
| reg.register(_FakeTool("user_history")) |
| assert reg.names() == ["policy_match", "report_velocity", "user_history"] |
|
|
|
|
| def test_has_returns_false_for_unregistered() -> None: |
| reg = ToolRegistry() |
| assert not reg.has("thread_context") |
| assert "thread_context" not in reg |
|
|
|
|
| def test_fake_tool_satisfies_runtime_protocol() -> None: |
| """The runtime_checkable @Protocol lets us isinstance() at construction time.""" |
| assert isinstance(_FakeTool("policy_match"), Tool) |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_registered_tool_run_returns_result() -> None: |
| reg = ToolRegistry() |
| reg.register(_FakeTool("policy_match", summary="hello")) |
| result = await reg.get("policy_match").run( |
| ToolContext( |
| subreddit_id="t5_x", |
| correlation_id="inv-1", |
| target_kind="post", |
| target_id="t3_x", |
| ) |
| ) |
| assert result.tool == "policy_match" |
| assert result.status == "success" |
| assert result.summary == "hello" |
|
|
|
|
| |
|
|
|
|
| def test_accumulator_mints_monotonic_ids() -> None: |
| acc = EvidenceAccumulator() |
| e1 = acc.append(_result(tool="policy_match")) |
| e2 = acc.append(_result(tool="report_velocity")) |
| e3 = acc.append(_result(tool="user_history")) |
| assert [e.id for e in (e1, e2, e3)] == ["ev-1", "ev-2", "ev-3"] |
| assert len(acc) == 3 |
|
|
|
|
| def test_accumulator_distinct_instances_have_separate_counters() -> None: |
| """A new investigation = a new accumulator = fresh ev-1 counter.""" |
| a = EvidenceAccumulator() |
| b = EvidenceAccumulator() |
| a.append(_result()) |
| a.append(_result()) |
| first_b = b.append(_result()) |
| assert first_b.id == "ev-1" |
|
|
|
|
| def test_by_id_lookup() -> None: |
| acc = EvidenceAccumulator() |
| acc.append(_result(tool="policy_match")) |
| acc.append(_result(tool="report_velocity", summary="z=6")) |
| hit = acc.by_id("ev-2") |
| assert hit is not None |
| assert hit.tool == "report_velocity" |
| assert hit.summary == "z=6" |
|
|
|
|
| def test_by_id_miss_returns_none() -> None: |
| acc = EvidenceAccumulator() |
| acc.append(_result()) |
| assert acc.by_id("ev-7") is None |
| assert acc.has("ev-7") is False |
| assert acc.has("ev-1") is True |
|
|
|
|
| def test_failures_get_evidence_id_but_excluded_from_successful() -> None: |
| """Failures appear in the Timeline (for transparency) but the Reasoner |
| is not allowed to cite them — ADR-0003.""" |
| acc = EvidenceAccumulator() |
| acc.append(_result(tool="policy_match", status="success")) |
| acc.append(_result(tool="thread_context", status="timeout", summary="reddit api slow")) |
| acc.append(_result(tool="user_history", status="failure", summary="db error")) |
| assert len(acc) == 3 |
| successful = acc.successful_entries() |
| assert len(successful) == 1 |
| assert successful[0].tool == "policy_match" |
|
|
|
|
| def test_entries_returns_defensive_copy() -> None: |
| acc = EvidenceAccumulator() |
| acc.append(_result()) |
| snapshot = acc.entries() |
| snapshot.clear() |
| assert len(acc) == 1, "mutating the returned list must not affect the accumulator" |
|
|
|
|
| def test_detail_is_copied_on_append() -> None: |
| """Mutating the original detail dict after append should not corrupt evidence.""" |
| original_detail: dict[str, object] = {"z": 6.2} |
| acc = EvidenceAccumulator() |
| entry = acc.append(_result(detail=original_detail)) |
| original_detail["z"] = 999 |
| assert entry.detail == {"z": 6.2} |
|
|
|
|
| def test_iteration_protocol() -> None: |
| acc = EvidenceAccumulator() |
| acc.append(_result(tool="policy_match")) |
| acc.append(_result(tool="report_velocity")) |
| ids_via_iter = [e.id for e in acc] |
| assert ids_via_iter == ["ev-1", "ev-2"] |
|
|
|
|
| |
|
|
|
|
| @pytest.mark.parametrize( |
| ("status", "expected"), |
| [ |
| ("success", False), |
| ("skipped", False), |
| ("failure", True), |
| ("timeout", True), |
| ], |
| ) |
| def test_is_terminal_failure(status: str, expected: bool) -> None: |
| assert _result(status=status).is_terminal_failure() is expected |
|
|