Files
hkuds--deeptutor/tests/capabilities/test_explore_context_capability.py
wehub-resource-sync e4dcfc49aa
Tests / Lint and Format (push) Waiting to run
Tests / Web Node Tests (push) Waiting to run
Tests / Import Check (Python 3.11) (push) Waiting to run
Tests / Import Check (Python 3.12) (push) Waiting to run
Tests / Import Check (Python 3.13) (push) Waiting to run
Tests / Import Check (Python 3.14) (push) Waiting to run
Tests / Python Tests (Python 3.11) (push) Blocked by required conditions
Tests / Python Tests (Python 3.12) (push) Blocked by required conditions
Tests / Python Tests (Python 3.13) (push) Blocked by required conditions
Tests / Python Tests (Python 3.14) (push) Blocked by required conditions
Tests / Test Summary (push) Blocked by required conditions
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

316 lines
11 KiB
Python

"""Unit tests for the explore-context loop capability and its explorer."""
from __future__ import annotations
from typing import Any
import pytest
from deeptutor.capabilities.explore_context import ExploreContextCapability
from deeptutor.capabilities.explore_context import explorer as explorer_mod
from deeptutor.capabilities.protocol import PromptBlock
from deeptutor.core.context import Attachment, UnifiedContext
from deeptutor.core.stream import StreamEventType
from deeptutor.core.stream_bus import StreamBus
def _ctx(**metadata: Any) -> UnifiedContext:
attachments = metadata.pop("_attachments", [])
manifest = metadata.pop("_manifest", "")
return UnifiedContext(
session_id="s1",
user_message="what did this chat do?",
attachments=attachments,
source_manifest=manifest,
metadata=metadata,
)
# ---------------------------------------------------------------------------
# is_active
# ---------------------------------------------------------------------------
def test_is_active_requires_readable_sources() -> None:
cap = ExploreContextCapability()
# A readable source attached this turn → active.
assert cap.is_active(_ctx(source_index={"hs-x": "t"}, history_references=["hs-x"])) is True
# A source carried over from an earlier turn (no fresh ref this turn) is
# ALSO active now — the investigation runs query-driven on every turn that
# has sources to read.
assert cap.is_active(_ctx(source_index={"hs-x": "t"})) is True
# A fresh reference but no readable source index (e.g. non-chat turn) → skip.
assert cap.is_active(_ctx(history_references=["hs-x"])) is False
# Empty turn → skip.
assert cap.is_active(_ctx()) is False
def test_is_active_image_only_turn_has_no_readable_source() -> None:
cap = ExploreContextCapability()
# Image attachments never enter the source index, so an image-only turn
# carries no readable source and the pre-pass stays dormant.
img = Attachment(type="image", id="i1")
assert cap.is_active(_ctx(_attachments=[img])) is False
def test_capability_owns_no_tools_and_no_system_block() -> None:
cap = ExploreContextCapability()
assert cap.owned_tools == ()
assert cap.pre_loop_seed(_ctx()) == ""
assert cap.system_block(_ctx(), language="en", prompts={}) is None
# ---------------------------------------------------------------------------
# Single-pass fallback (non-native tool-calling models)
# ---------------------------------------------------------------------------
def _fake_stream(briefing_chunks: list[str]):
async def _stream(*_args: Any, **_kwargs: Any):
for chunk in briefing_chunks:
yield chunk
return _stream
@pytest.fixture
def _force_single_pass(monkeypatch: pytest.MonkeyPatch) -> None:
"""Pin the explorer onto its single-pass (llm_stream) fallback path."""
monkeypatch.setattr(explorer_mod, "can_use_native_tool_calling", lambda **_kwargs: False)
@pytest.mark.asyncio
async def test_single_pass_returns_investigation_block(
monkeypatch: pytest.MonkeyPatch, _force_single_pass: None
) -> None:
monkeypatch.setattr(
explorer_mod,
"llm_stream",
_fake_stream(["In this transcript, the user and ", "the other agent updated the nav."]),
)
cap = ExploreContextCapability()
ctx = _ctx(
source_index={"hs-imported_claude_code_x": "## Claude Code\nI updated the nav."},
history_references=["imported_claude_code_x"],
_manifest="[Attached Sources]\n- id=hs-imported_claude_code_x type=history name='nav'",
)
block = await cap.pre_loop(ctx, StreamBus(), usage=None)
assert isinstance(block, PromptBlock)
assert block.name == "explore_context"
# Header framing + the model's third-person account are both present.
assert "Context Investigation" in block.content
assert "the other agent updated the nav." in block.content
@pytest.mark.asyncio
async def test_pre_loop_emits_no_content_events(
monkeypatch: pytest.MonkeyPatch, _force_single_pass: None
) -> None:
"""The pre-pass must never stream CONTENT — only that channel is captured
as the turn's user-facing answer."""
monkeypatch.setattr(explorer_mod, "llm_stream", _fake_stream(["briefing"]))
cap = ExploreContextCapability()
ctx = _ctx(source_index={"hs-x": "transcript"}, history_references=["x"])
bus = StreamBus()
await cap.pre_loop(ctx, bus, usage=None)
kinds = [e.type for e in bus._history]
assert StreamEventType.CONTENT not in kinds
assert StreamEventType.STAGE_START in kinds
assert StreamEventType.THINKING in kinds
@pytest.mark.asyncio
async def test_pre_loop_inactive_returns_none() -> None:
cap = ExploreContextCapability()
# No readable sources → inactive → no pre-pass, no LLM call.
assert await cap.pre_loop(_ctx(history_references=["x"]), StreamBus(), usage=None) is None
@pytest.mark.asyncio
async def test_pre_loop_empty_investigation_returns_none(
monkeypatch: pytest.MonkeyPatch, _force_single_pass: None
) -> None:
monkeypatch.setattr(explorer_mod, "llm_stream", _fake_stream([" "]))
cap = ExploreContextCapability()
ctx = _ctx(source_index={"hs-x": "t"}, history_references=["x"])
assert await cap.pre_loop(ctx, StreamBus(), usage=None) is None
@pytest.mark.asyncio
async def test_pre_loop_swallows_llm_failure(
monkeypatch: pytest.MonkeyPatch, _force_single_pass: None
) -> None:
def _boom(*_args: Any, **_kwargs: Any):
raise RuntimeError("provider down")
monkeypatch.setattr(explorer_mod, "llm_stream", _boom)
cap = ExploreContextCapability()
ctx = _ctx(source_index={"hs-x": "t"}, history_references=["x"])
# A failed pre-pass degrades to no investigation, never raises.
assert await cap.pre_loop(ctx, StreamBus(), usage=None) is None
# ---------------------------------------------------------------------------
# Agentic loop (native tool-calling models)
# ---------------------------------------------------------------------------
class _FakeFunc:
def __init__(self, name: str | None = None, arguments: str | None = None) -> None:
self.name = name
self.arguments = arguments
class _FakeToolCall:
def __init__(self, index: int, call_id: str, name: str, arguments: str) -> None:
self.index = index
self.id = call_id
self.function = _FakeFunc(name=name, arguments=arguments)
class _FakeDelta:
def __init__(
self,
content: str | None = None,
tool_calls: list[_FakeToolCall] | None = None,
) -> None:
self.content = content
self.tool_calls = tool_calls
self.reasoning_content = None
self.reasoning = None
class _FakeChoice:
def __init__(self, delta: _FakeDelta) -> None:
self.delta = delta
self.finish_reason = None
class _FakeChunk:
def __init__(self, delta: _FakeDelta) -> None:
self.choices = [_FakeChoice(delta)]
self.usage = None
class _FakeResponseStream:
def __init__(self, chunks: list[_FakeChunk]) -> None:
self._chunks = chunks
async def __aiter__(self):
for chunk in self._chunks:
yield chunk
class _FakeCompletions:
"""Returns one queued response per ``create`` call (one per loop round)."""
def __init__(self, rounds: list[list[_FakeChunk]]) -> None:
self._rounds = list(rounds)
self.calls: list[dict[str, Any]] = []
async def create(self, **kwargs: Any) -> _FakeResponseStream:
self.calls.append(kwargs)
chunks = self._rounds.pop(0) if self._rounds else []
return _FakeResponseStream(chunks)
class _FakeClient:
def __init__(self, rounds: list[list[_FakeChunk]]) -> None:
self.chat = type("Chat", (), {"completions": _FakeCompletions(rounds)})()
@pytest.fixture
def _force_loop(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(explorer_mod, "can_use_native_tool_calling", lambda **_kwargs: True)
@pytest.mark.asyncio
async def test_loop_reads_source_then_writes_investigation(
monkeypatch: pytest.MonkeyPatch, _force_loop: None
) -> None:
"""The investigator calls read_source on a source, then writes an
investigation grounded in the loaded full text."""
rounds = [
# Round 0: ask to read the attached transcript.
[
_FakeChunk(
_FakeDelta(
tool_calls=[_FakeToolCall(0, "call_1", "read_source", '{"source_id": "hs-x"}')]
)
)
],
# Round 1: no tool calls → this text is the investigation.
[_FakeChunk(_FakeDelta(content="The other agent rewrote the nav and shipped it."))],
]
fake_client = _FakeClient(rounds)
monkeypatch.setattr(explorer_mod, "build_openai_client", lambda _cfg: fake_client)
cap = ExploreContextCapability()
ctx = _ctx(
source_index={"hs-x": "## Claude Code\nI rewrote the nav and shipped it."},
history_references=["x"],
_manifest="[Attached Sources]\n- id=hs-x type=history name='nav'",
)
bus = StreamBus()
block = await cap.pre_loop(ctx, bus, usage=None)
assert isinstance(block, PromptBlock)
assert "Context Investigation" in block.content
assert "rewrote the nav" in block.content
# read_source was dispatched (the model's source_id reached the create
# call only via the tool round, and a tool result fed the second round).
completions = fake_client.chat.completions
assert len(completions.calls) == 2
# Never streamed CONTENT — the investigation rides the thinking channel.
kinds = [e.type for e in bus._history]
assert StreamEventType.CONTENT not in kinds
assert StreamEventType.TOOL_CALL in kinds
@pytest.mark.asyncio
async def test_loop_falls_back_to_single_pass_on_client_error(
monkeypatch: pytest.MonkeyPatch, _force_loop: None
) -> None:
"""A loop that errors before producing anything degrades to the robust
single-pass dump-and-brief so the answer loop still gets grounding."""
def _boom_client(_cfg: Any) -> Any:
raise RuntimeError("schema rejected")
monkeypatch.setattr(explorer_mod, "build_openai_client", _boom_client)
monkeypatch.setattr(explorer_mod, "llm_stream", _fake_stream(["fallback briefing text"]))
cap = ExploreContextCapability()
ctx = _ctx(source_index={"hs-x": "transcript body"}, history_references=["x"])
block = await cap.pre_loop(ctx, StreamBus(), usage=None)
assert isinstance(block, PromptBlock)
assert "fallback briefing text" in block.content
# ---------------------------------------------------------------------------
# ContextExplorer source assembly (single-pass helpers)
# ---------------------------------------------------------------------------
def test_explorer_clips_and_caps_sources() -> None:
explorer = explorer_mod.ContextExplorer(language="en", prompts={})
big = {f"nb-{i}": "y" * 100_000 for i in range(explorer_mod.MAX_SOURCES + 5)}
blocks = explorer._render_source_blocks(big)
assert blocks.count("### [") <= explorer_mod.MAX_SOURCES
assert "(truncated)" in blocks
assert len(blocks) <= explorer_mod.TOTAL_CHARS + 5_000 # blocks + headers/notes
def test_explorer_kind_label_from_prefix() -> None:
explorer = explorer_mod.ContextExplorer(language="en", prompts={})
assert explorer._kind_label("hs-abc") == "Conversation transcript"
assert explorer._kind_label("at-abc") == "Document"
assert explorer._kind_label("zz-abc") == "Source"