Skip to content

Commit 158e9ed

Browse files
authored
test: cover seven previously-untested code paths in src/runtime/ (#15)
Targeted coverage uplift: 7 new test files covering 97 previously-uncovered lines across llm/graph/orchestrator/service/api. Tests passing 1265 -> 1310; src/runtime coverage 87.21% -> 88.90%. Closes #15.
1 parent d658fe8 commit 158e9ed

7 files changed

Lines changed: 982 additions & 0 deletions

tests/test_envelope_recovery.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""Coverage tests for ``runtime.graph._try_recover_envelope_from_raw`` (graph.py:583-610).
2+
3+
The recovery helper is invoked by the agent runner when LangGraph's
4+
structured-output pass raises ``OutputParserException`` — it tries
5+
several candidate substrings to dig an :class:`AgentTurnOutput` out of
6+
free-form LLM text. The function is pure and behaves identically for
7+
every input, so a table-driven test suite pins each branch.
8+
"""
9+
from __future__ import annotations
10+
11+
import json
12+
13+
import pytest
14+
15+
from runtime.agents.turn_output import AgentTurnOutput
16+
from runtime.graph import _try_recover_envelope_from_raw
17+
18+
19+
def _envelope_dict(*, content: str = "ok", confidence: float = 0.85,
20+
rationale: str = "stub", signal: str | None = None) -> dict:
21+
return {
22+
"content": content,
23+
"confidence": confidence,
24+
"confidence_rationale": rationale,
25+
"signal": signal,
26+
}
27+
28+
29+
def _envelope_json(**overrides) -> str:
30+
return json.dumps(_envelope_dict(**overrides))
31+
32+
33+
class TestEmptyInput:
34+
@pytest.mark.parametrize("raw", ["", " ", "\n\n \t\n"])
35+
def test_empty_or_whitespace_returns_none(self, raw):
36+
assert _try_recover_envelope_from_raw(raw) is None
37+
38+
39+
class TestPlainJsonInput:
40+
def test_valid_envelope_json_parses(self):
41+
out = _try_recover_envelope_from_raw(_envelope_json(content="hello"))
42+
assert isinstance(out, AgentTurnOutput)
43+
assert out.content == "hello"
44+
45+
def test_valid_envelope_with_signal(self):
46+
out = _try_recover_envelope_from_raw(_envelope_json(signal="reconcile"))
47+
assert out is not None
48+
assert out.signal == "reconcile"
49+
50+
51+
class TestMarkdownFencedJson:
52+
def test_fenced_with_json_tag(self):
53+
raw = f"```json\n{_envelope_json()}\n```"
54+
out = _try_recover_envelope_from_raw(raw)
55+
assert isinstance(out, AgentTurnOutput)
56+
57+
def test_fenced_without_json_tag(self):
58+
raw = f"```\n{_envelope_json(confidence=0.42)}\n```"
59+
out = _try_recover_envelope_from_raw(raw)
60+
assert out is not None
61+
assert out.confidence == 0.42
62+
63+
def test_fenced_with_surrounding_chatter(self):
64+
raw = (
65+
"Here is my structured response:\n\n"
66+
f"```json\n{_envelope_json(content='fenced')}\n```\n\n"
67+
"Hope that helps!"
68+
)
69+
out = _try_recover_envelope_from_raw(raw)
70+
assert out is not None
71+
assert out.content == "fenced"
72+
73+
74+
class TestGreedyBraceMatch:
75+
def test_chatter_then_json_then_chatter(self):
76+
# No fences — should fall through to the greedy first-{...-last-} scan.
77+
raw = (
78+
f"Sure, here's the answer: {_envelope_json(content='greedy')} "
79+
"Let me know if you need more!"
80+
)
81+
out = _try_recover_envelope_from_raw(raw)
82+
assert out is not None
83+
assert out.content == "greedy"
84+
85+
86+
class TestUnrecoverableInput:
87+
def test_invalid_json_returns_none(self):
88+
assert _try_recover_envelope_from_raw("{not valid json}") is None
89+
90+
def test_no_braces_returns_none(self):
91+
assert _try_recover_envelope_from_raw("Just a plain sentence.") is None
92+
93+
def test_json_array_not_dict_returns_none(self):
94+
# Greedy match would still find a substring, but `[1, 2, 3]`
95+
# has no braces. Use a real array text.
96+
assert _try_recover_envelope_from_raw('["a", "b"]') is None
97+
98+
def test_valid_dict_missing_required_fields_returns_none(self):
99+
# `{"foo": "bar"}` parses but fails AgentTurnOutput validation.
100+
assert _try_recover_envelope_from_raw('{"foo": "bar"}') is None
101+
102+
def test_dict_with_invalid_field_types_returns_none(self):
103+
# confidence must be 0..1; this should fail validation on every candidate.
104+
assert _try_recover_envelope_from_raw(
105+
'{"content": "x", "confidence": 5.0, "confidence_rationale": "y"}'
106+
) is None

tests/test_handle_agent_failure.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""Coverage tests for ``runtime.graph._handle_agent_failure`` (graph.py:613-644).
2+
3+
This helper is invoked by the agent runner when an agent body raises a
4+
non-pause exception (anything other than ``GraphInterrupt``). It reloads
5+
the session (absorbing partial tool writes), appends a failure
6+
``AgentRun``, marks the session ``status='error'``, persists, and returns
7+
the state dict the LangGraph node yields.
8+
"""
9+
from __future__ import annotations
10+
11+
import pytest
12+
13+
from runtime.config import EmbeddingConfig, MetadataConfig, ProviderConfig
14+
from runtime.graph import _handle_agent_failure
15+
from runtime.state import AgentRun, Session
16+
from runtime.storage.embeddings import build_embedder
17+
from runtime.storage.engine import build_engine
18+
from runtime.storage.models import Base
19+
from runtime.storage.session_store import SessionStore
20+
21+
22+
@pytest.fixture
23+
def store(tmp_path) -> SessionStore:
24+
eng = build_engine(MetadataConfig(url=f"sqlite:///{tmp_path}/test.db"))
25+
Base.metadata.create_all(eng)
26+
embedder = build_embedder(
27+
EmbeddingConfig(provider="s", model="x", dim=1024),
28+
{"s": ProviderConfig(kind="stub")},
29+
)
30+
return SessionStore(engine=eng, embedder=embedder)
31+
32+
33+
def _seed_session(store: SessionStore, *, agents_run: list[AgentRun] | None = None) -> Session:
34+
"""Create + persist a baseline session and return it."""
35+
inc = store.create(query="probe", environment="dev",
36+
reporter_id="u1", reporter_team="t")
37+
if agents_run:
38+
inc.agents_run.extend(agents_run)
39+
store.save(inc)
40+
inc = store.load(inc.id)
41+
return inc
42+
43+
44+
class TestHappyPath:
45+
def test_failure_run_appended_and_status_set_to_error(self, store):
46+
inc = _seed_session(store)
47+
result = _handle_agent_failure(
48+
skill_name="triage",
49+
started_at="2026-05-15T00:00:00Z",
50+
exc=RuntimeError("upstream blew up"),
51+
inc_id=inc.id,
52+
store=store,
53+
fallback=inc,
54+
)
55+
# Returned state dict
56+
assert result["last_agent"] == "triage"
57+
assert result["next_route"] is None
58+
assert result["error"] == "upstream blew up"
59+
assert isinstance(result["session"], Session)
60+
# Persisted session reflects the failure
61+
loaded = store.load(inc.id)
62+
assert loaded.status == "error"
63+
assert len(loaded.agents_run) == 1
64+
run = loaded.agents_run[0]
65+
assert run.agent == "triage"
66+
assert run.summary == "agent failed: upstream blew up"
67+
68+
def test_appends_to_existing_run_history(self, store):
69+
prior = AgentRun(
70+
agent="intake",
71+
started_at="2026-05-15T00:00:00Z",
72+
ended_at="2026-05-15T00:00:01Z",
73+
summary="completed: routed to triage",
74+
)
75+
inc = _seed_session(store, agents_run=[prior])
76+
_handle_agent_failure(
77+
skill_name="triage",
78+
started_at="2026-05-15T00:00:02Z",
79+
exc=TimeoutError("provider hung"),
80+
inc_id=inc.id,
81+
store=store,
82+
fallback=inc,
83+
)
84+
loaded = store.load(inc.id)
85+
assert [r.agent for r in loaded.agents_run] == ["intake", "triage"]
86+
assert "agent failed: provider hung" in loaded.agents_run[1].summary
87+
88+
def test_preserves_partial_tool_writes_via_reload(self, store):
89+
"""If a tool wrote to the session before the agent raised,
90+
the reload-then-append pattern must keep that tool's write."""
91+
inc = _seed_session(store)
92+
# Simulate a tool write that already persisted.
93+
from runtime.state import ToolCall
94+
inc.tool_calls.append(ToolCall(
95+
agent="triage",
96+
tool="lookup_similar_incidents",
97+
args={"query": "x"},
98+
result={"hits": []},
99+
ts="2026-05-15T00:00:00Z",
100+
))
101+
store.save(inc)
102+
# Caller's stale `fallback` reference does not have the tool call.
103+
stale = inc.model_copy(deep=True)
104+
stale.tool_calls = []
105+
_handle_agent_failure(
106+
skill_name="triage",
107+
started_at="2026-05-15T00:00:02Z",
108+
exc=RuntimeError("oops"),
109+
inc_id=inc.id,
110+
store=store,
111+
fallback=stale,
112+
)
113+
loaded = store.load(inc.id)
114+
# Tool call survived because _handle_agent_failure reloaded
115+
# before appending its failure run.
116+
assert len(loaded.tool_calls) == 1
117+
assert loaded.tool_calls[0].tool == "lookup_similar_incidents"
118+
119+
120+
class TestFallbackPath:
121+
def test_uses_fallback_when_session_missing_from_store(self, store):
122+
# Session never persisted; store.load(inc_id) raises FileNotFoundError.
123+
from runtime.state import Session
124+
ghost = Session(
125+
id="INC-20260515-999",
126+
status="in_progress",
127+
created_at="2026-05-15T00:00:00Z",
128+
updated_at="2026-05-15T00:00:00Z",
129+
)
130+
result = _handle_agent_failure(
131+
skill_name="intake",
132+
started_at="2026-05-15T00:00:00Z",
133+
exc=RuntimeError("dropped on the floor"),
134+
inc_id="INC-20260515-999",
135+
store=store,
136+
fallback=ghost,
137+
)
138+
# The fallback was used, the failure run was appended,
139+
# and the now-populated fallback was saved.
140+
assert result["session"].status == "error"
141+
loaded = store.load("INC-20260515-999")
142+
assert loaded.id == "INC-20260515-999"
143+
assert loaded.status == "error"
144+
assert len(loaded.agents_run) == 1
145+
assert loaded.agents_run[0].agent == "intake"
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""Coverage tests for ``StubChatModel.with_structured_output`` (llm.py:141-160, 171-177).
2+
3+
The structured-output runnable was previously only exercised indirectly
4+
via ``langchain.agents.create_agent``. These tests pin the direct
5+
contract: the stub returns a Runnable-like that yields a valid schema
6+
instance per ``invoke`` / ``ainvoke``, populated from the canned text and
7+
``stub_envelope_*`` parameters.
8+
9+
The permissive ``model_validate`` fallback (lines 161-169) is genuinely
10+
defensive: pydantic v2's ``model_validate`` internally calls ``__init__``
11+
too, so any schema whose constructor raises also fails the fallback.
12+
The fallback exists for hypothetical schemas with custom
13+
``__pydantic_validator__`` overrides, which the framework doesn't ship
14+
and tests can't construct without monkey-patching pydantic internals.
15+
"""
16+
from __future__ import annotations
17+
18+
import pytest
19+
20+
from runtime.agents.turn_output import AgentTurnOutput
21+
from runtime.llm import StubChatModel
22+
23+
24+
def _stub(*, confidence: float = 0.85, rationale: str = "stub rationale",
25+
signal: str | None = None, role: str = "intake",
26+
canned: dict[str, str] | None = None) -> StubChatModel:
27+
return StubChatModel(
28+
role=role,
29+
canned_responses=canned if canned is not None else {role: "stub body text"},
30+
stub_envelope_confidence=confidence,
31+
stub_envelope_rationale=rationale,
32+
stub_envelope_signal=signal,
33+
)
34+
35+
36+
class TestStubStructuredOutputHappyPath:
37+
"""Happy path: schema(...) keyword constructor succeeds."""
38+
39+
def test_invoke_returns_schema_instance(self):
40+
runnable = _stub().with_structured_output(AgentTurnOutput)
41+
out = runnable.invoke("any input")
42+
assert isinstance(out, AgentTurnOutput)
43+
assert out.content == "stub body text"
44+
assert out.confidence == 0.85
45+
assert out.confidence_rationale == "stub rationale"
46+
assert out.signal is None
47+
48+
@pytest.mark.asyncio
49+
async def test_ainvoke_returns_schema_instance(self):
50+
runnable = _stub(confidence=0.42, rationale="hedge", signal="retry").with_structured_output(AgentTurnOutput)
51+
out = await runnable.ainvoke("any input")
52+
assert isinstance(out, AgentTurnOutput)
53+
assert out.confidence == 0.42
54+
assert out.confidence_rationale == "hedge"
55+
assert out.signal == "retry"
56+
57+
def test_canned_response_missing_uses_default_marker(self):
58+
runnable = _stub(role="ghost", canned={}).with_structured_output(AgentTurnOutput)
59+
out = runnable.invoke("x")
60+
assert out.content.startswith("[stub:ghost]")
61+
62+
def test_include_raw_kwarg_is_accepted(self):
63+
# langchain passes include_raw=True/False on the call site; the stub
64+
# accepts the kwarg but doesn't change behaviour.
65+
runnable = _stub().with_structured_output(AgentTurnOutput, include_raw=True)
66+
assert runnable.invoke("x").content == "stub body text"
67+
68+
def test_extra_kwargs_are_swallowed(self):
69+
runnable = _stub().with_structured_output(AgentTurnOutput, method="json_mode", strict=True)
70+
assert runnable.invoke("x").confidence == 0.85
71+
72+

0 commit comments

Comments
 (0)