Additional agent DAG tests (#750)

- test_agent_provenance.py: test_session_parent_uri,
  test_session_no_parent_uri, and 6 synthesis tests (types,
  single/multiple parents, document, label)
- test_on_action_callback.py: 3 tests — fires before tool, skipped
  for Final, works when None
- test_callback_message_id.py: 7 tests — message_id on think/observe/
  answer callbacks (streaming + non-streaming) and
  send_final_response
- test_parse_chunk_message_id.py (5 tests) - _parse_chunk propagates
  message_id for thought, observation, answer; handles missing
  gracefully
- test_explainability_parsing.py (+1) -
  test_dispatches_analysis_with_tooluse - Analysis+ToolUse mixin still
  dispatches to Analysis
- test_explainability.py (+1) -
  test_observation_found_via_subtrace_synthesis
- chain walker follows from sub-trace Synthesis to find Observation
  and
  Conclusion in correct order
- test_agent_provenance.py (+8) - session parent_uri (2), synthesis
  single/multiple parents, types, document, label (6)
This commit is contained in:
cybermaggedon 2026-04-01 13:59:34 +01:00 committed by Cyber MacGeddon
parent 3ba6a3238f
commit dbf8daa74a
7 changed files with 733 additions and 1 deletions

View file

@ -558,3 +558,96 @@ class TestExplainabilityClientDetectSessionType:
mock_flow = MagicMock()
client = ExplainabilityClient(mock_flow, retry_delay=0.0)
assert client.detect_session_type("urn:trustgraph:docrag:abc") == "docrag"
class TestChainWalkerFollowsSubTraceTerminal:
"""Test that _follow_provenance_chain continues from a sub-trace's
Synthesis to find downstream entities like Observation."""
def test_observation_found_via_subtrace_synthesis(self):
"""
DAG: Question -> Analysis -> GraphRAG Question -> Synthesis -> Observation
The walker should find Analysis, the sub-trace, then follow from
Synthesis to discover Observation.
"""
# Entity triples (s, p, o)
entity_data = {
"urn:agent:q": [
("urn:agent:q", RDF_TYPE, TG_AGENT_QUESTION),
("urn:agent:q", TG_QUERY, "test"),
],
"urn:agent:analysis": [
("urn:agent:analysis", RDF_TYPE, TG_ANALYSIS),
("urn:agent:analysis", PROV_WAS_DERIVED_FROM, "urn:agent:q"),
],
"urn:graphrag:q": [
("urn:graphrag:q", RDF_TYPE, TG_QUESTION),
("urn:graphrag:q", RDF_TYPE, TG_GRAPH_RAG_QUESTION),
("urn:graphrag:q", TG_QUERY, "test"),
("urn:graphrag:q", PROV_WAS_DERIVED_FROM, "urn:agent:analysis"),
],
"urn:graphrag:synth": [
("urn:graphrag:synth", RDF_TYPE, TG_SYNTHESIS),
("urn:graphrag:synth", PROV_WAS_DERIVED_FROM, "urn:graphrag:q"),
],
"urn:agent:obs": [
("urn:agent:obs", RDF_TYPE, TG_OBSERVATION_TYPE),
("urn:agent:obs", PROV_WAS_DERIVED_FROM, "urn:graphrag:synth"),
],
"urn:agent:conclusion": [
("urn:agent:conclusion", RDF_TYPE, TG_CONCLUSION),
("urn:agent:conclusion", PROV_WAS_DERIVED_FROM, "urn:agent:obs"),
],
}
# Build a mock flow that answers triples queries
# Query by s= returns that entity's triples
# Query by p=wasDerivedFrom, o=X returns entities derived from X
def mock_triples_query(s=None, p=None, o=None, **kwargs):
if s and not p:
# Fetch entity triples
tuples = entity_data.get(s, [])
return _make_wire_triples(tuples)
elif p == PROV_WAS_DERIVED_FROM and o:
# Find entities derived from o
results = []
for uri, tuples in entity_data.items():
for _, pred, obj in tuples:
if pred == PROV_WAS_DERIVED_FROM and obj == o:
results.append((uri, pred, obj))
return _make_wire_triples(results)
return []
mock_flow = MagicMock()
mock_flow.triples_query.side_effect = mock_triples_query
client = ExplainabilityClient(mock_flow, retry_delay=0.0, max_retries=2)
# Mock fetch_graphrag_trace to return a trace with a synthesis
synth_entity = Synthesis(uri="urn:graphrag:synth", entity_type="synthesis")
client.fetch_graphrag_trace = MagicMock(return_value={
"question": Question(uri="urn:graphrag:q", entity_type="question",
question_type="graph-rag"),
"synthesis": synth_entity,
})
trace = client.fetch_agent_trace(
"urn:agent:q",
graph="urn:graph:retrieval",
)
# Should have found all steps
step_types = [
type(s).__name__ if not isinstance(s, dict) else s.get("type")
for s in trace["steps"]
]
assert "Analysis" in step_types, f"Missing Analysis in {step_types}"
assert "sub-trace" in step_types, f"Missing sub-trace in {step_types}"
assert "Observation" in step_types, f"Missing Observation in {step_types}"
assert "Conclusion" in step_types, f"Missing Conclusion in {step_types}"
# Observation should come after the sub-trace
subtrace_idx = step_types.index("sub-trace")
obs_idx = step_types.index("Observation")
assert obs_idx > subtrace_idx, "Observation should appear after sub-trace"