use harn_session_store::{
AppendEvent, CreateSession, SessionEventKind, SessionStore, SqliteSessionStore,
};
use super::*;
fn transcript_row(kind: &str, role: &str, text: &str) -> serde_json::Value {
serde_json::json!({
"transcript_event": {
"id": format!("{role}-{text}"),
"kind": kind,
"role": role,
"visibility": "public",
"text": text,
}
})
}
async fn store_with_session(session_id: &str) -> SqliteSessionStore {
let store = SqliteSessionStore::open_in_memory().expect("in-memory canonical store");
store
.create(CreateSession {
id: Some(session_id.to_string()),
..CreateSession::default()
})
.await
.expect("create session");
store
}
#[tokio::test]
async fn a_stored_session_restores_its_public_transcript() {
let session_id = "01a003d0-1513-7271-90aa-4542d6059498";
let store = store_with_session(session_id).await;
for (kind, role, text) in [
("message", "user", "add a multiply function"),
("message", "assistant", "done, tests pass"),
] {
store
.append(
session_id,
AppendEvent::new(SessionEventKind::Message, transcript_row(kind, role, text))
.with_actor(role),
)
.await
.expect("append transcript row");
}
let restored = load_canonical_session_replay_events_from_store(&store, session_id)
.await
.expect("restore should not error")
.expect("the store knows this session");
let rendered: Vec<String> = restored
.iter()
.map(|entry| match &entry.event {
AgentEvent::UserMessage { content, .. } => format!("user: {content:?}"),
AgentEvent::AgentMessageChunk { content, .. } => format!("assistant: {content}"),
other => format!("other: {other:?}"),
})
.collect();
assert_eq!(restored.len(), 2, "both rows replay: {rendered:?}");
assert!(
rendered[0].contains("add a multiply function"),
"the user turn restores in order: {rendered:?}"
);
assert!(
rendered[1].starts_with("assistant: done, tests pass"),
"the assistant turn restores in order: {rendered:?}"
);
assert!(
restored[0].event_id < restored[1].event_id,
"replay keeps stored order: {rendered:?}"
);
}
#[tokio::test]
async fn an_unknown_id_reports_no_session_rather_than_an_empty_one() {
let store = store_with_session("known-session").await;
let restored = load_canonical_session_replay_events_from_store(&store, "never-existed")
.await
.expect("a missing session is not an error");
assert!(
restored.is_none(),
"an id the store does not hold must be reported absent, not empty"
);
}
#[tokio::test]
async fn a_session_with_no_transcript_yet_is_still_restorable() {
let store = store_with_session("brand-new").await;
let restored = load_canonical_session_replay_events_from_store(&store, "brand-new")
.await
.expect("restore should not error");
assert_eq!(
restored.map(|events| events.len()),
Some(0),
"an empty session restores as an empty transcript, not as absent"
);
}
#[tokio::test]
async fn a_replayed_tool_call_keeps_its_metadata_tool_name() {
let session_id = "with-tool-call";
let store = store_with_session(session_id).await;
let mut event = AppendEvent::new(
SessionEventKind::ToolCall,
serde_json::json!({
"transcript_event": {
"id": "event-call",
"kind": "tool_call",
"role": "assistant",
"visibility": "internal",
"metadata": {"tool_call_id": "tool-1", "tool_name": "look"},
}
}),
);
event
.headers
.insert("tool_call_id".to_string(), "tool-1".to_string());
store
.append(session_id, event)
.await
.expect("append tool call row");
let restored = load_canonical_session_replay_events_from_store(&store, session_id)
.await
.expect("restore should not error")
.expect("the store knows this session");
assert_eq!(restored.len(), 1, "the tool call replays: {restored:?}");
match &restored[0].event {
AgentEvent::ToolCall {
tool_call_id,
tool_name,
..
} => {
assert_eq!(tool_call_id, "tool-1");
assert_eq!(tool_name, "look");
}
other => panic!("expected a replayed tool call, got {other:?}"),
}
}
#[tokio::test]
async fn internal_rows_stay_out_of_the_restored_transcript() {
let session_id = "with-internals";
let store = store_with_session(session_id).await;
store
.append(
session_id,
AppendEvent::new(
SessionEventKind::Message,
serde_json::json!({
"transcript_event": {
"kind": "message",
"role": "assistant",
"visibility": "internal",
"text": "scratch reasoning",
}
}),
),
)
.await
.expect("append internal row");
store
.append(
session_id,
AppendEvent::new(
SessionEventKind::Custom {
custom_type: "usage_checkpoint".to_string(),
},
serde_json::json!({"usage": {"input_tokens": 72}}),
),
)
.await
.expect("append usage row");
store
.append(
session_id,
AppendEvent::new(
SessionEventKind::Message,
transcript_row("message", "assistant", "here is the answer"),
),
)
.await
.expect("append public row");
let restored = load_canonical_session_replay_events_from_store(&store, session_id)
.await
.expect("restore should not error")
.expect("the store knows this session");
assert_eq!(
restored.len(),
1,
"only the public turn replays: {restored:?}"
);
assert!(matches!(
&restored[0].event,
AgentEvent::AgentMessageChunk { content, .. } if content == "here is the answer"
));
}
#[tokio::test]
async fn journal_shaped_tool_rows_replay_and_internal_prose_stays_hidden() {
let session_id = "journal-shaped";
let store = store_with_session(session_id).await;
let call = |id: &str, file: &str| {
let mut event = AppendEvent::new(
SessionEventKind::ToolCall,
serde_json::json!({
"transcript_event": {
"id": format!("event-{id}"),
"kind": "tool_call",
"role": "assistant",
"visibility": "internal",
"text": "",
"metadata": {
"raw_input": {"file": file},
"status": "pending",
"tool_call_id": id,
"tool_name": "look",
},
}
}),
);
event
.headers
.insert("tool_call_id".to_string(), id.to_string());
event
};
let result = |id: &str, text: &str, is_error: bool| {
let mut event = AppendEvent::new(
SessionEventKind::ToolResult,
serde_json::json!({
"transcript_event": {
"id": format!("result-{id}"),
"kind": "tool_result",
"role": "tool_result",
"visibility": "internal",
"text": text,
},
"raw_message": {
"content": text,
"is_error": is_error,
"name": "look",
"role": "tool_result",
"tool_call_id": id,
},
}),
);
event
.headers
.insert("tool_call_id".to_string(), id.to_string());
event
};
let internal_prose = AppendEvent::new(
SessionEventKind::Message,
serde_json::json!({
"transcript_event": {
"kind": "message",
"role": "user",
"visibility": "internal",
"text": "<context-directives speaker=\"harness\">",
}
}),
);
for event in [
internal_prose,
call("call-1", "calc.py"),
call("call-2", "gone.py"),
result("call-1", "calc.py (3 lines)", false),
result("call-2", "no such file", true),
] {
store.append(session_id, event).await.expect("append row");
}
let restored = load_canonical_session_replay_events_from_store(&store, session_id)
.await
.expect("restore should not error")
.expect("the store knows this session");
let rows: Vec<String> = restored
.iter()
.map(|entry| match &entry.event {
AgentEvent::ToolCall {
tool_call_id,
tool_name,
raw_input,
..
} => format!("call {tool_call_id} {tool_name} {raw_input}"),
AgentEvent::ToolCallUpdate {
tool_call_id,
tool_name,
status,
..
} => format!("result {tool_call_id} {tool_name} {status:?}"),
other => format!("other {other:?}"),
})
.collect();
assert_eq!(
rows,
[
r#"call call-1 look {"file":"calc.py"}"#,
r#"call call-2 look {"file":"gone.py"}"#,
"result call-1 look Completed",
"result call-2 look Failed",
],
"every stored tool row replays, named and with its input; internal prose does not"
);
}