harn-vm 0.10.147

Async bytecode virtual machine for the Harn programming language
Documentation
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
}

/// The defect this module exists to close (burin#6267): a session the canonical
/// store holds must be restorable, whether or not the observability event log
/// ever observed it.
#[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:?}"
    );
}

/// An id no store holds is the one case that should still fail loudly, so a
/// typo or a stale id stays distinguishable from a real restore.
#[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"
    );
}

/// A freshly created session with no turns yet is a real session. Reporting it
/// unknown is what made every zero-event launch row unopenable.
#[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"
    );
}

/// The journal persists a tool lifecycle event with its identity under
/// `metadata`, and stamps the call id into a header. Replay must restore the
/// tool's real name from that shape, not a generic placeholder.
#[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:?}"),
    }
}

/// Internal bookkeeping rows (usage checkpoints, audit annotations) are not
/// conversation, and must not surface in a restored transcript.
#[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"
    ));
}

/// The journal writes every tool call and tool result as an internal row, with
/// the name under `metadata` on the call and on the stored provider message
/// beside the result. This is that shape, copied from a real store. Replay
/// dropped all four rows as internal bookkeeping, so a resumed session showed
/// no tool rows at all (harn#8920). Internal prose still stays hidden.
#[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"
    );
}