heddle-thread-api 0.25.1

Native Thread clients and durable peer replication over Iroh
Documentation
#![cfg(feature = "semantic-analysis")]

use heddle_thread_api::{behavior::BehaviorAnalyzer, contract as api};
use prost::Message;

fn revision(oid: &str) -> api::RevisionRef {
    api::RevisionRef {
        spool: Some(api::SpoolRef {
            id: "review-spool".into(),
        }),
        revision: Some(api::revision_ref::Revision::GitCommitOid(oid.into())),
    }
}

#[test]
fn real_revision_comparison_round_trips_as_renderable_typed_facts() {
    let base = revision("92a9b0705370d2c04f8bcaf9b7141937085d0ad9");
    let source = revision("2a67bcaf7446def0bd01e4b9b79b2cbb9e203401");
    let analyzer = BehaviorAnalyzer::new(base.clone(), source.clone()).expect("exact comparison");
    let base_text = include_str!("../../semantic/tests/fixtures/behavior/capture-base.rs");
    let source_text = include_str!("../../semantic/tests/fixtures/behavior/capture-source.rs");
    let result = analyzer.compare(
        "crates/verbs/src/save.rs",
        Some(base_text),
        Some(source_text),
        &["capture".into()],
    );
    let change = result
        .changes
        .iter()
        .find(|c| {
            c.assignments
                .iter()
                .any(|a| a.target == "SavePlan.git_scope")
        })
        .expect("real change");
    let event = api::AnalysisEvent {
        payload: Some(api::analysis_event::Payload::BehaviorChange(change.clone())),
        ..Default::default()
    };
    let decoded =
        api::AnalysisEvent::decode(event.encode_to_vec().as_slice()).expect("protobuf round trip");
    assert_eq!(decoded, event);
    assert_eq!(change.base.as_ref(), Some(&base));
    assert_eq!(change.source.as_ref(), Some(&source));
    let expression = |id: &str| {
        change
            .expressions
            .iter()
            .find(|e| e.id == id)
            .expect("in-record expression")
    };
    let condition = change
        .expressions
        .iter()
        .find_map(|e| e.conditional.as_ref())
        .expect("conditional");
    assert_eq!(condition.branches.len(), 2);
    assert_eq!(
        condition.branches[0].label,
        api::behavior_branch::Label::True as i32
    );
    assert_eq!(
        condition.branches[1].label,
        api::behavior_branch::Label::False as i32
    );
    let origin = &change.correspondences[0].base_ids[0];
    assert!(
        change
            .correspondences
            .iter()
            .all(|c| c.base_ids == [origin.clone()])
    );
    assert_eq!(
        expression(origin).source.as_ref().expect("source").text,
        "GitScope::None"
    );
    for e in &change.expressions {
        let r = e.source.as_ref().expect("source reference");
        let text = if r.revision.as_ref() == Some(&base) {
            base_text
        } else {
            assert_eq!(r.revision.as_ref(), Some(&source));
            source_text
        };
        assert_eq!(&text[r.start_byte as usize..r.end_byte as usize], r.text);
        assert_eq!(r.blob_hash.len(), 32);
        assert_eq!(e.provenance, api::BehaviorProvenance::Syntax as i32);
    }
    assert!(
        change
            .bindings
            .iter()
            .all(|b| b.provenance == api::BehaviorProvenance::BindingResolution as i32)
    );
    assert!(
        change
            .correspondences
            .iter()
            .all(|c| c.provenance == api::BehaviorProvenance::StructuralComparison as i32)
    );
    assert!(!result.coverage.omitted.is_empty());
    let deps = change.dependencies.as_ref().expect("cache dependencies");
    assert_ne!(deps.base_extraction, deps.source_extraction);
    let moved = analyzer.compare(
        "crates/verbs/src/save.rs",
        Some(base_text),
        Some(&format!("// move\n{source_text}")),
        &["capture".into()],
    );
    let moved = moved
        .changes
        .iter()
        .find(|c| {
            c.assignments
                .iter()
                .any(|a| a.target == "SavePlan.git_scope")
        })
        .expect("moved map");
    assert_ne!(change.id, moved.id);
    assert_ne!(
        deps.comparison,
        moved
            .dependencies
            .as_ref()
            .expect("new dependencies")
            .comparison
    );
}

#[test]
fn unavailable_is_distinct_from_analyzed_empty_and_identity_binds_exact_revisions() {
    let base = revision(&"a".repeat(40));
    let source = revision(&"b".repeat(40));
    let analyzer = BehaviorAnalyzer::new(base.clone(), source.clone()).expect("exact revisions");
    let missing = analyzer.compare("save.rs", None, Some("fn f() {}"), &[]);
    assert_eq!(
        missing.coverage.omitted[0].support,
        api::BehaviorSupport::Unavailable as i32
    );
    let empty = analyzer.compare("save.rs", Some("fn f() {}"), Some("fn f() {}"), &[]);
    assert!(empty.changes.is_empty());
    assert!(empty.coverage.selection_exhausted);
    assert!(empty.coverage.omitted.is_empty());
    assert!(!empty.coverage.analyzed.is_empty());
    assert_ne!(
        analyzer.analysis,
        BehaviorAnalyzer::new(source, base)
            .expect("reverse")
            .analysis
    );
    assert!(BehaviorAnalyzer::new(revision("main"), revision(&"b".repeat(40))).is_err());
}