harn-vm 0.8.14

Async bytecode virtual machine for the Harn programming language
Documentation
use std::fs;

use serde_json::json;

use super::*;
use crate::orchestration::{LlmUsageRecord, RunStageRecord};

fn repo_fixture_path(name: &str) -> String {
    format!(
        "{}/../../spec/session-bundles/fixtures/{name}",
        env!("CARGO_MANIFEST_DIR")
    )
}

fn fixture_run() -> RunRecord {
    let synthetic_api_key = format!("{}{}", "sk-test_", "1234567890abcdefghijklmnop");
    let synthetic_bearer = format!("{} {}", "Bearer", "abcDEFghi12345");
    RunRecord {
        type_name: "run_record".to_string(),
        id: "run_123".to_string(),
        workflow_id: "wf_123".to_string(),
        workflow_name: Some("Review".to_string()),
        task: "Review the failing deployment".to_string(),
        status: "completed".to_string(),
        started_at: "2026-05-01T00:00:00Z".to_string(),
        finished_at: Some("2026-05-01T00:01:00Z".to_string()),
        transcript: Some(json!({
            "_type": "transcript",
            "messages": [
                {"role": "user", "content": format!("token {synthetic_api_key}")}
            ],
            "events": [
                {"kind": "permission_denied", "reason": "policy", "arguments": {"api_key": "abc"}},
                {"type": "tool_schemas", "schemas": [{"name": "read_file"}]}
            ],
            "assets": [],
            "summary": "done",
            "metadata": {"system_prompt": "be careful"}
        })),
        usage: Some(LlmUsageRecord {
            input_tokens: 10,
            output_tokens: 4,
            call_count: 1,
            total_duration_ms: 100,
            total_cost: 0.01,
            models: vec!["mock/model".to_string()],
        }),
        stages: vec![RunStageRecord {
            id: "stage_1".to_string(),
            node_id: "answer".to_string(),
            transcript: Some(json!({
                "messages": [{"role": "assistant", "content": "done"}],
                "events": [],
                "assets": []
            })),
            ..RunStageRecord::default()
        }],
        tool_recordings: vec![ToolCallRecord {
            tool_name: "read_file".to_string(),
            tool_use_id: "toolu_1".to_string(),
            args_hash: "abc123".to_string(),
            result: synthetic_bearer,
            is_rejected: false,
            duration_ms: 7,
            iteration: 1,
            timestamp: "2026-05-01T00:00:30Z".to_string(),
        }],
        hitl_questions: vec![RunHitlQuestionRecord {
            request_id: "hitl_1".to_string(),
            prompt: "Approve?".to_string(),
            agent: "agent".to_string(),
            trace_id: None,
            asked_at: "2026-05-01T00:00:10Z".to_string(),
        }],
        ..RunRecord::default()
    }
}

#[test]
fn sanitized_export_redacts_secrets_and_records_manifest() {
    let bundle =
        export_run_record_bundle(&fixture_run(), &SessionBundleExportOptions::default()).unwrap();
    let rendered = serde_json::to_string(&bundle).unwrap();
    assert!(!rendered.contains("1234567890abcdefghijklmnop"));
    assert!(!rendered.contains("abcDEFghi12345"));
    assert!(rendered.contains(REDACTED_PLACEHOLDER));
    assert!(bundle
        .redaction
        .entries
        .iter()
        .any(|entry| entry.class == "secret_pattern_or_url"));
    assert_eq!(bundle.transcript.sections.len(), 2);
    assert_eq!(bundle.tools.schemas.len(), 1);
    assert_eq!(bundle.permissions.len(), 2);
}

#[test]
fn replay_only_export_withholds_prompt_and_tool_payloads() {
    let options = SessionBundleExportOptions {
        mode: SessionBundleExportMode::ReplayOnly,
        ..SessionBundleExportOptions::default()
    };
    let bundle = export_run_record_bundle(&fixture_run(), &options).unwrap();
    let rendered = serde_json::to_string(&bundle).unwrap();
    assert!(!rendered.contains("Review the failing deployment"));
    assert!(!rendered.contains("done"));
    assert!(rendered.contains(REPLAY_ONLY_PLACEHOLDER));
    assert!(bundle
        .redaction
        .entries
        .iter()
        .any(|entry| entry.action == "withheld"));
}

#[test]
fn validation_rejects_missing_required_fields() {
    let err = validate_session_bundle_value(&json!({}), &SessionBundleValidationOptions::default())
        .unwrap_err();
    assert_eq!(
        err,
        SessionBundleError::MissingRequired("$._type".to_string())
    );

    let bundle =
        export_run_record_bundle(&fixture_run(), &SessionBundleExportOptions::default()).unwrap();
    let mut value = serde_json::to_value(bundle).unwrap();
    value.as_object_mut().unwrap().remove("producer");
    let err = validate_session_bundle_value(&value, &SessionBundleValidationOptions::default())
        .unwrap_err();
    assert_eq!(
        err,
        SessionBundleError::MissingRequired("$.producer".to_string())
    );

    let bundle =
        export_run_record_bundle(&fixture_run(), &SessionBundleExportOptions::default()).unwrap();
    let mut value = serde_json::to_value(bundle).unwrap();
    value["source"]
        .as_object_mut()
        .unwrap()
        .remove("run_record_id");
    let err = validate_session_bundle_value(&value, &SessionBundleValidationOptions::default())
        .unwrap_err();
    assert_eq!(
        err,
        SessionBundleError::MissingRequired("$.source.run_record_id".to_string())
    );
}

#[test]
fn validation_rejects_future_versions() {
    let bundle =
        export_run_record_bundle(&fixture_run(), &SessionBundleExportOptions::default()).unwrap();
    let mut value = serde_json::to_value(bundle).unwrap();
    value["schema_version"] = json!(999);
    let err = validate_session_bundle_value(&value, &SessionBundleValidationOptions::default())
        .unwrap_err();
    assert_eq!(
        err,
        SessionBundleError::UnsupportedSchemaVersion {
            found: 999,
            supported: SESSION_BUNDLE_SCHEMA_VERSION,
        }
    );
}

#[test]
fn validation_rejects_unredacted_secret_markers() {
    let options = SessionBundleExportOptions {
        mode: SessionBundleExportMode::Local,
        ..SessionBundleExportOptions::default()
    };
    let bundle = export_run_record_bundle(&fixture_run(), &options).unwrap();
    let value = serde_json::to_value(bundle).unwrap();
    let err = validate_session_bundle_value(&value, &SessionBundleValidationOptions::default())
        .unwrap_err();
    assert!(matches!(err, SessionBundleError::UnsafeSecretMarker { .. }));
}

#[test]
fn imported_run_record_prefers_embedded_run_record() {
    let bundle =
        export_run_record_bundle(&fixture_run(), &SessionBundleExportOptions::default()).unwrap();
    let run_record = import_run_record_value(&bundle).unwrap();
    assert_eq!(run_record["_type"], "run_record");
    assert_eq!(run_record["id"], "run_123");
}

#[test]
fn schema_is_self_describing() {
    let schema = session_bundle_schema();
    assert_eq!(schema["properties"]["_type"]["const"], SESSION_BUNDLE_TYPE);
    assert_eq!(
        schema["properties"]["schema_version"]["maximum"],
        SESSION_BUNDLE_SCHEMA_VERSION
    );
}

#[test]
fn checked_session_bundle_fixtures_validate_and_import() {
    for fixture in [
        "sample-local.bundle.json",
        "sample-sanitized.bundle.json",
        "sample-replay-only.bundle.json",
    ] {
        let path = repo_fixture_path(fixture);
        let content = fs::read_to_string(&path).unwrap();
        let bundle =
            validate_session_bundle_str(&content, &SessionBundleValidationOptions::default())
                .unwrap();
        assert_eq!(bundle.source.run_record_id, "run_fixture_session_bundle");

        let imported = import_run_record_value(&bundle).unwrap();
        assert_eq!(imported["_type"], "run_record");
        assert_eq!(imported["id"], "run_fixture_session_bundle");
    }
}