use serde_json::json;
use yoagent_state::{
ActorRef, EventStore, MemoryEventStore, NodeId, RunId, YoAgentModelCalled,
YoAgentModelFinished, YoAgentRunFinished, YoAgentRunStarted, YoAgentState, YoAgentStateAdapter,
YoAgentStateSink, YoAgentToolCalled, YoAgentToolFinished,
};
#[tokio::test]
async fn adapter_emits_conformant_log() {
let state = YoAgentState::load(MemoryEventStore::new()).await.unwrap();
let adapter = YoAgentStateAdapter::new(state.clone(), ActorRef::agent("yoyo"));
let run = RunId::new("run_a");
adapter
.on_run_started(YoAgentRunStarted {
run_id: run.clone(),
task: "task".into(),
metadata: json!({}),
})
.await
.unwrap();
adapter
.on_model_called(YoAgentModelCalled {
run_id: run.clone(),
model: "claude-opus-4-6".into(),
prompt_summary: "p".into(),
})
.await
.unwrap();
adapter
.on_model_finished(YoAgentModelFinished {
run_id: run.clone(),
model: "claude-opus-4-6".into(),
output_summary: "o".into(),
})
.await
.unwrap();
adapter
.on_tool_called(YoAgentToolCalled {
run_id: run.clone(),
tool: "cargo test".into(),
input_summary: "i".into(),
})
.await
.unwrap();
adapter
.on_tool_finished(YoAgentToolFinished {
run_id: run.clone(),
tool: "cargo test".into(),
output_summary: "2 failed".into(),
success: false,
})
.await
.unwrap();
adapter
.on_tool_called(YoAgentToolCalled {
run_id: run.clone(),
tool: "cargo build".into(),
input_summary: "i".into(),
})
.await
.unwrap();
adapter
.on_tool_finished(YoAgentToolFinished {
run_id: run.clone(),
tool: "cargo build".into(),
output_summary: "ok".into(),
success: true,
})
.await
.unwrap();
let err = adapter
.on_run_started(YoAgentRunStarted {
run_id: RunId::new("run_b"),
task: "task".into(),
metadata: json!({}),
})
.await
.unwrap_err();
assert!(err.to_string().contains("already open"), "{err}");
adapter
.on_run_finished(YoAgentRunFinished {
run_id: run.clone(),
outcome: "reverted".into(),
metadata: json!({}),
})
.await
.unwrap();
let events = state.store().scan().await.unwrap();
let mut seen_ids = std::collections::HashSet::new();
for event in &events {
match &event.causation_id {
Some(cause) => assert!(
seen_ids.contains(cause.as_str()),
"{} cites a later/unknown event",
event.kind
),
None => assert!(
event.kind.ends_with(".started") || event.kind.ends_with(".created"),
"bad root kind: {}",
event.kind
),
}
seen_ids.insert(event.id.as_str().to_string());
if event.kind == "state.ops_applied" {
let cause = event.causation_id.as_ref().expect("paired ops");
let domain = events.iter().find(|e| &e.id == cause).unwrap();
assert_ne!(domain.kind, "state.ops_applied");
}
assert_eq!(
event.correlation_id.as_deref(),
Some("run_a"),
"{} lacks run correlation",
event.kind
);
}
let failures: Vec<_> = events
.iter()
.filter(|e| e.kind == "failure.observed")
.collect();
assert_eq!(failures.len(), 1, "success=true must not emit a failure");
let failure = failures[0];
for key in ["id", "failure_id", "title", "summary"] {
assert!(
failure.payload.get(key).is_some(),
"failure payload lacks `{key}`"
);
}
assert_eq!(failure.payload["title"], json!("tool cargo test failed"));
for kind in ["failure.observed", "run.started", "run.finished"] {
let domain = events.iter().find(|e| e.kind == kind).unwrap();
assert!(
events
.iter()
.any(|e| e.kind == "state.ops_applied"
&& e.causation_id.as_ref() == Some(&domain.id)),
"{kind} has no ops pair"
);
}
let node = state.get_node(NodeId::new("run_a")).await.unwrap();
assert_eq!(node.props["status"], json!("finished"));
assert_eq!(node.props["outcome"], json!("reverted"));
let produced = state
.incoming(NodeId::new("run_a"), Some("produced_by"))
.await;
assert_eq!(produced.len(), 3, "model_call + 2 tool_call nodes");
}