bonsai-ninja-trace 0.2.2

Trace result + renderers.
use super::*;

fn source_span() -> SourceSpan {
    SourceSpan {
        file: "app.py".to_string(),
        start_line: 1,
        start_col: 1,
        end_line: 1,
        end_col: 1,
        start_byte: 0,
        end_byte: 1,
    }
}

fn step(id: u64, kind: TraceStepKind, precision: Precision) -> TraceStep {
    TraceStep {
        id,
        path_id: 1,
        order: id + 1,
        kind,
        message: format!("step {id}"),
        function: "handle".to_string(),
        module: "app.py".to_string(),
        file: "app.py".to_string(),
        span: source_span(),
        code: String::new(),
        state_before: None,
        state_after: None,
        precision,
        notes: Vec::new(),
    }
}

#[test]
fn trace_locations_are_workspace_relative_without_rewriting_module_names() {
    assert_eq!(
        portable_trace_path("/workspace", "/workspace/src/app.rs"),
        "src/app.rs"
    );
    assert_eq!(
        portable_trace_path("/workspace", "crate.runtime.Handle"),
        "crate.runtime.Handle"
    );
    assert_eq!(portable_trace_path("", "/external/lib.rs"), "/external/lib.rs");
}

#[test]
fn truncate_after_step_rebuilds_derived_trace_sections() {
    let mut trace = TraceResult {
        trace_id: "trace-test".to_string(),
        query: TraceQuery::default(),
        summary: TraceSummary {
            analysis_complete: true,
            total_steps: 4,
            total_paths: 1,
            explored_paths: 1,
            precision: Precision::Narrowed,
            ..TraceSummary::default()
        },
        paths: vec![PathSummary {
            path_id: 1,
            first_step: 0,
            last_step: 3,
            path_constraints: Vec::new(),
            terminated_by: PathTermination::Return,
            precision: Precision::Narrowed,
        }],
        steps: vec![
            step(0, TraceStepKind::EnterFunction, Precision::Exact),
            step(1, TraceStepKind::Call, Precision::Narrowed),
            step(2, TraceStepKind::EvalExpr, Precision::Exact),
            step(3, TraceStepKind::Return, Precision::Exact),
        ],
        edges: vec![
            TraceEdge {
                from_step: 0,
                to_step: 1,
                kind: TraceEdgeKind::Next,
            },
            TraceEdge {
                from_step: 1,
                to_step: 2,
                kind: TraceEdgeKind::CallEnter,
            },
            TraceEdge {
                from_step: 2,
                to_step: 3,
                kind: TraceEdgeKind::Next,
            },
        ],
        states: Vec::new(),
        diagnostics: Vec::new(),
        metadata: TraceMetadata::default(),
    };

    truncate_after_step(&mut trace, 1);

    assert_eq!(trace.steps.len(), 2);
    assert_eq!(trace.edges.len(), 1);
    assert_eq!(trace.edges[0].from_step, 0);
    assert_eq!(trace.edges[0].to_step, 1);
    assert_eq!(trace.summary.total_steps, 2);
    assert_eq!(trace.summary.total_paths, 1);
    assert_eq!(trace.summary.explored_paths, 1);
    assert_eq!(trace.summary.precision, Precision::Narrowed);
    assert_eq!(trace.paths.len(), 1);
    assert_eq!(trace.paths[0].last_step, 1);
    assert_eq!(trace.paths[0].precision, Precision::Narrowed);
}

#[test]
fn selected_sink_marks_the_retained_path_as_an_intentional_stop() {
    let mut trace = TraceResult {
        steps: vec![
            step(0, TraceStepKind::EnterFunction, Precision::Exact),
            step(1, TraceStepKind::EnterFunction, Precision::Exact),
        ],
        paths: vec![PathSummary {
            path_id: 1,
            first_step: 0,
            last_step: 1,
            path_constraints: Vec::new(),
            terminated_by: PathTermination::Unknown,
            precision: Precision::Exact,
        }],
        ..TraceResult::default()
    };

    mark_last_step_termination(&mut trace, PathTermination::ReachedTarget);

    assert_eq!(trace.paths[0].terminated_by, PathTermination::ReachedTarget);
}

#[test]
fn path_summary_marks_unresolved_call_diagnostic_termination() {
    let mut unresolved = step(1, TraceStepKind::Diagnostic, Precision::Exact);
    unresolved.message = "Unresolved call dynamic_target".to_string();
    let paths = path_summaries(
        &[
            step(0, TraceStepKind::EnterFunction, Precision::Exact),
            unresolved,
            step(2, TraceStepKind::Return, Precision::Exact),
        ],
        false,
    );

    assert_eq!(paths.len(), 1);
    assert_eq!(paths[0].terminated_by, PathTermination::UnknownCall);
    assert_eq!(paths[0].precision, Precision::Exact);
}

#[test]
fn public_semantic_step_suppresses_diagnostic_precision() {
    let raw = RawStep {
        id: bonsai_common::TraceStepId::new(0),
        path_id: 0,
        kind: StepKind::Call,
        span: bonsai_common::Span::new(bonsai_common::FileId::new(0), 0, 1),
        func: bonsai_common::FuncId::new(1),
        precision: Precision::Unknown,
        message: "Call dynamic_target".to_string(),
    };
    let mut reasons = Vec::new();

    let public = public_semantic_step(&raw, &mut reasons);

    assert_eq!(public.kind, TraceStepKind::Diagnostic);
    assert_eq!(public.precision, Precision::Exact);
    assert!(
        public.message.contains("Suppressed diagnostic-precision Call"),
        "diagnostic precision should be metadata, not call evidence: {}",
        public.message
    );
    assert_eq!(reasons, vec!["diagnostic-precision-step:Call"]);
}