harn-vm 0.10.173

Async bytecode virtual machine for the Harn programming language
use serde_json::json;

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

#[test]
fn historical_view_producer_defaults_remain_unknown() {
    let run: RunView = serde_json::from_value(json!({})).unwrap();
    let session: SessionView = serde_json::from_value(json!({})).unwrap();
    assert_eq!(run.producer, ViewProducer::unknown());
    assert_eq!(session.producer, ViewProducer::unknown());
    let producer: ViewProducer = serde_json::from_value(json!({"name":"historical"})).unwrap();
    assert_eq!(producer.name, "historical");
    assert!(producer.version.is_empty());
    let current = build_run_view(&RunRecord::default()).producer;
    assert_eq!(current.name, "harn");
    assert_eq!(current.version, env!("CARGO_PKG_VERSION"));
}

#[test]
fn public_view_schema_preserves_native_presence_and_scalar_rules() {
    let schema = run_session_view_schema();
    assert!(!schema.to_string().contains(env!("CARGO_PKG_VERSION")));
    let validator = jsonschema::draft202012::new(&schema).unwrap();
    let sparse = json!({"schema":RUN_VIEW_SCHEMA,"schema_version":RUN_VIEW_SCHEMA_VERSION});
    assert!(validator.is_valid(&sparse));
    let mut malformed = sparse;
    malformed["run"] = json!({"started_at":null});
    assert!(!validator.is_valid(&malformed));
    malformed["run"] = json!({"duration_ms":-1});
    assert!(!validator.is_valid(&malformed));
    malformed.as_object_mut().unwrap().remove("run");
    malformed["evidence"] = json!({"flight_recording":{"retained_events":1}});
    assert!(!validator.is_valid(&malformed));
    malformed["evidence"] = json!({"reasoning_receipts":[],"evaluation_receipts":null});
    assert!(validator.is_valid(&malformed));
    assert!(!validator.is_valid(&json!({"schema":RUN_VIEW_SCHEMA,"schema_version":2})));
}

fn sample_run() -> RunRecord {
    RunRecord {
        type_name: "run_record".to_string(),
        id: "run_1".to_string(),
        workflow_id: "wf".to_string(),
        workflow_name: Some("Workflow".to_string()),
        task: "do work".to_string(),
        status: "completed".to_string(),
        started_at: "2026-01-01T00:00:00Z".to_string(),
        finished_at: Some("2026-01-01T00:00:02Z".to_string()),
        stages: vec![RunStageRecord {
            id: "stage_1".to_string(),
            node_id: "plan".to_string(),
            kind: "llm".to_string(),
            status: "completed".to_string(),
            outcome: "ok".to_string(),
            started_at: "2026-01-01T00:00:00Z".to_string(),
            finished_at: Some("2026-01-01T00:00:01Z".to_string()),
            visible_text: Some("done".to_string()),
            usage: Some(LlmUsageRecord {
                input_tokens: 10,
                output_tokens: 5,
                total_duration_ms: 1000,
                call_count: 1,
                cost_usd: Some(0.01),
                known_cost_usd: 0.01,
                total_cost: 0.01,
                models: vec!["model-a".to_string()],
                ..LlmUsageRecord::default()
            }),
            metadata: BTreeMap::from([
                ("session_id".to_string(), json!("session_1")),
                ("provider".to_string(), json!("test")),
                ("model".to_string(), json!("model-a")),
            ]),
            ..RunStageRecord::default()
        }],
        evidence: ExecutionEvidenceRecord {
            trace_spans: vec![RunTraceSpanRecord {
                kind: "llm_call".to_string(),
                metadata: BTreeMap::from([
                    ("provider".to_string(), json!("test")),
                    ("model".to_string(), json!("model-a")),
                    ("input_tokens".to_string(), json!(10)),
                    ("output_tokens".to_string(), json!(5)),
                    ("cost_usd".to_string(), json!(0.01)),
                    ("api_key".to_string(), json!("must-not-project")),
                ]),
                ..RunTraceSpanRecord::default()
            }],
            ..ExecutionEvidenceRecord::default()
        },
        transcript: Some(json!({
            "source": "inline",
            "summary": "short",
            "messages": [{"role": "assistant"}],
            "events": [{"kind": "output"}]
        })),
        ..RunRecord::default()
    }
}

#[test]
fn build_run_view_projects_stable_public_fields() {
    const EXECUTION_ID: &str = "hxe-019c13e0-8080-7000-8000-000000000001";
    let mut run = sample_run();
    run.evidence.schema_version = crate::orchestration::EXECUTION_EVIDENCE_SCHEMA_VERSION;
    run.evidence.execution_id = Some(EXECUTION_ID.to_string());
    let view = build_run_view_with_path(&run, Some("runs/run_1.json"));
    assert_eq!(view.schema, RUN_VIEW_SCHEMA);
    assert_eq!(view.schema_version, RUN_VIEW_SCHEMA_VERSION);
    assert_eq!(view.run.run_id, "run_1");
    assert_eq!(view.run.session_id.as_deref(), Some("session_1"));
    assert_eq!(view.run.run_path.as_deref(), Some("runs/run_1.json"));
    assert_eq!(view.run.duration_ms, Some(2000));
    assert_eq!(view.evidence.execution_id.as_deref(), Some(EXECUTION_ID));
    assert_eq!(
        view.evidence.trace_spans[0].metadata["api_key"],
        json!(crate::redact::REDACTED_PLACEHOLDER)
    );
    assert_eq!(view.visible_text.as_deref(), Some("done"));
    assert_eq!(view.transcript.message_count, 1);
    assert_eq!(view.usage.input_tokens, 10);
    assert_eq!(view.providers.len(), 1);
    assert!(view.projection.projection_id.starts_with("run_view:run_1:"));
    assert!(view.projection.projection_hash.is_some());
}

#[test]
fn build_run_view_falls_back_to_public_assistant_transcript_blocks() {
    let secret = "sk-proj-test-abcdefghijklmnopqrstuvwxyz123456";
    let run = RunRecord {
        type_name: "run_record".to_string(),
        id: "transcript_only".to_string(),
        status: "completed".to_string(),
        transcript: Some(json!({
            "events": [
                {
                    "kind": "message",
                    "role": "user",
                    "visibility": "public",
                    "blocks": [{
                        "type": "text",
                        "text": "user prompt must not become output",
                        "visibility": "public"
                    }]
                },
                {
                    "kind": "message",
                    "role": "assistant",
                    "visibility": "public",
                    "text": "event-level text is not an authority",
                    "blocks": [
                        {"type": "output_text", "text": "visible answer ", "visibility": "public"},
                        {"type": "text", "text": format!("with {secret}"), "visibility": "public"},
                        {"type": "reasoning", "text": "public reasoning stays private", "visibility": "public"},
                        {"type": "output_text", "text": "private output", "visibility": "private"}
                    ]
                },
                {
                    "kind": "message",
                    "role": "assistant",
                    "visibility": "private",
                    "blocks": [{
                        "type": "output_text",
                        "text": "private event output",
                        "visibility": "public"
                    }]
                }
            ]
        })),
        ..RunRecord::default()
    };

    let view = build_run_view(&run);
    let visible = view.visible_text.expect("public assistant output");
    assert!(visible.starts_with("visible answer with "));
    assert!(visible.contains("<redacted:openai_key:"));
    assert!(!visible.contains(secret));
    assert!(!visible.contains("user prompt"));
    assert!(!visible.contains("event-level text"));
    assert!(!visible.contains("reasoning"));
    assert!(!visible.contains("private output"));
    assert!(!visible.contains("private event output"));
}

#[test]
fn build_run_view_tolerates_sparse_legacy_records() {
    let run = RunRecord {
        type_name: "run_record".to_string(),
        id: "legacy".to_string(),
        status: "failed".to_string(),
        ..RunRecord::default()
    };
    let view = build_run_view(&run);
    assert_eq!(view.run.run_id, "legacy");
    assert_eq!(view.run.session_id, None);
    assert!(!view.transcript.present);
    assert_eq!(
        view.failure.as_ref().map(|failure| failure.status.as_str()),
        Some("failed")
    );
}

#[test]
fn build_session_view_aggregates_runs() {
    let run = build_run_view(&sample_run());
    let view = build_session_view_from_run_views(
        vec![run],
        SessionViewOptions {
            session_id: Some("session_1".to_string()),
            last_event_id: Some(7),
            chain_root_hash: Some("sha256:abc".to_string()),
            ..SessionViewOptions::default()
        },
    );
    assert_eq!(view.schema, SESSION_VIEW_SCHEMA);
    assert_eq!(view.session.session_id.as_deref(), Some("session_1"));
    assert_eq!(view.session.last_event_id, Some(7));
    assert_eq!(view.session.chain_root_hash.as_deref(), Some("sha256:abc"));
    assert_eq!(view.history.len(), 1);
    assert_eq!(view.usage.call_count, 1);
    assert!(view
        .projection
        .projection_id
        .starts_with("session_view:session_1:"));
}

#[test]
fn build_run_view_summarizes_stage_only_transcripts() {
    let mut run = sample_run();
    run.transcript = None;
    run.stages[0].transcript = Some(json!({
        "summary": "stage transcript only",
        "messages": [{"role": "assistant"}, {"role": "tool"}],
        "events": [{"kind": "tool_result"}]
    }));

    let view = build_run_view(&run);
    assert!(view.transcript.present);
    assert_eq!(view.transcript.source.as_deref(), Some("stages"));
    assert_eq!(view.transcript.message_count, 2);
    assert_eq!(view.transcript.event_count, 1);
    assert_eq!(
        view.transcript.summary.as_deref(),
        Some("plan: stage transcript only")
    );
}

#[test]
fn build_run_view_redacts_child_tasks_and_approvals() {
    let mut run = sample_run();
    run.child_runs.push(RunChildRecord {
        worker_id: "worker_1".to_string(),
        worker_name: "worker".to_string(),
        task: "inspect AKIAABCDEFGHIJKLMNOP".to_string(),
        ..RunChildRecord::default()
    });
    run.hitl_questions.push(RunHitlQuestionRecord {
        request_id: "approval_1".to_string(),
        prompt: "approve AKIAABCDEFGHIJKLMNOP".to_string(),
        ..RunHitlQuestionRecord::default()
    });

    let view = build_run_view(&run);
    assert!(!view.run.child_runs[0].task.contains("AKIAABCDEFGHIJKLMNOP"));
    assert!(!view.pending.approvals[0]
        .prompt
        .contains("AKIAABCDEFGHIJKLMNOP"));
}