pidgin-lang 0.1.1

A compact agent handoff protocol runtime — parse, validate, resolve, and expand Pidgin packets
Documentation
#[cfg(test)]
#[allow(clippy::module_inception)]
mod expander_tests {
    use std::collections::BTreeMap;
    use std::path::Path;

    use crate::ast::{Directive, FieldValue, PgnPacket};
    use crate::expander::{expand_to_approval_request, expand_to_run_packet, ExpandedRef};
    use crate::registry::{load_workflow_registry, WorkflowRegistry};
    use crate::resolver::{ResolvedRef, ResolutionStatus};
    use crate::safety::SafetyResult;

    fn test_workflows() -> WorkflowRegistry {
        let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../configs/WORKFLOW_REGISTRY.yaml");
        load_workflow_registry(&path).unwrap()
    }

    fn default_safety() -> SafetyResult {
        SafetyResult {
            allowed: true,
            blocked: false,
            fired_rules: vec![],
            human_required: false,
            effective_risk: "med".to_string(),
        }
    }

    fn make_packet() -> PgnPacket {
        let mut fields = BTreeMap::new();
        fields.insert(
            "wf".to_string(),
            FieldValue::Scalar("generic_review".to_string()),
        );
        fields.insert("mode".to_string(), FieldValue::Scalar("draft".to_string()));
        fields.insert(
            "in".to_string(),
            FieldValue::List(vec!["primary_subject".to_string(), "source_refs".to_string()]),
        );
        fields.insert(
            "out".to_string(),
            FieldValue::List(vec!["review_notes".to_string()]),
        );
        fields.insert(
            "do".to_string(),
            FieldValue::List(vec!["draft".to_string(), "review".to_string()]),
        );
        fields.insert(
            "deny".to_string(),
            FieldValue::List(vec!["publish".to_string()]),
        );
        fields.insert("risk".to_string(), FieldValue::Scalar("med".to_string()));
        PgnPacket {
            directive: Directive::Run,
            run_id: "test.expand".to_string(),
            fields,
        }
    }

    #[test]
    fn expand_run_packet_includes_metadata() {
        let packet = make_packet();
        let resolved = vec![];
        let safety = default_safety();
        let workflows = test_workflows();

        let expanded = expand_to_run_packet(&packet, &resolved, &safety, &workflows);

        assert_eq!(expanded.spec_version, "1.0");
        assert_eq!(expanded.run_id, "test.expand");
        assert_eq!(expanded.workflow, "generic_review");
        assert_eq!(expanded.mode, "draft");
    }

    #[test]
    fn expand_run_packet_includes_actions() {
        let packet = make_packet();
        let resolved = vec![];
        let safety = default_safety();
        let workflows = test_workflows();

        let expanded = expand_to_run_packet(&packet, &resolved, &safety, &workflows);

        assert_eq!(expanded.do_actions, vec!["draft", "review"]);
        assert_eq!(expanded.deny_actions, vec!["publish"]);
    }

    #[test]
    fn expand_run_packet_includes_resolved_refs() {
        let packet = make_packet();
        let resolved = vec![
            ResolvedRef {
                original: "primary_subject".to_string(),
                namespace: String::new(),
                ref_id: "primary_subject".to_string(),
                resolved_path: None,
                confidence: 0.0,
                required: true,
                status: ResolutionStatus::Unresolved,
            },
            ResolvedRef {
                original: "source_refs".to_string(),
                namespace: String::new(),
                ref_id: "source_refs".to_string(),
                resolved_path: None,
                confidence: 0.0,
                required: true,
                status: ResolutionStatus::Unresolved,
            },
        ];
        let safety = default_safety();
        let workflows = test_workflows();

        let expanded = expand_to_run_packet(&packet, &resolved, &safety, &workflows);

        assert_eq!(expanded.inputs.len(), 2);
        assert_eq!(expanded.inputs[0].reference, "primary_subject");
    }

    #[test]
    fn expand_run_packet_uses_safety_risk() {
        let packet = make_packet();
        let resolved = vec![];
        let safety = SafetyResult {
            effective_risk: "high".to_string(),
            ..default_safety()
        };
        let workflows = test_workflows();

        let expanded = expand_to_run_packet(&packet, &resolved, &safety, &workflows);

        assert_eq!(expanded.effective_risk, "high");
    }

    #[test]
    fn expand_run_packet_includes_executor_from_workflow() {
        let packet = make_packet();
        let resolved = vec![];
        let safety = default_safety();
        let workflows = test_workflows();

        let expanded = expand_to_run_packet(&packet, &resolved, &safety, &workflows);

        assert_eq!(expanded.recommended_executor, "claude-code");
        assert_eq!(expanded.fallback_executor, "opencode");
        assert_eq!(expanded.ttl, "24h");
    }

    #[test]
    fn expand_approval_request_includes_actions_and_risk() {
        let packet = make_packet();
        let safety = SafetyResult {
            effective_risk: "crit".to_string(),
            human_required: true,
            ..default_safety()
        };
        let workflows = test_workflows();

        let approval = expand_to_approval_request(&packet, &safety, &workflows);

        assert_eq!(approval.spec_version, "1.0");
        assert_eq!(approval.run_id, "test.expand");
        assert_eq!(approval.workflow, "generic_review");
        assert_eq!(approval.risk, "crit");
        assert!(approval.human_required);
        assert_eq!(approval.actions, vec!["draft", "review"]);
    }

    #[test]
    fn expanded_ref_from_resolved_ref() {
        let r = ResolvedRef {
            original: "file:Cargo.toml".to_string(),
            namespace: "file".to_string(),
            ref_id: "Cargo.toml".to_string(),
            resolved_path: Some(Path::new("Cargo.toml").to_path_buf()),
            confidence: 1.0,
            required: true,
            status: ResolutionStatus::Resolved,
        };
        let er = ExpandedRef::from(&r);
        assert_eq!(er.reference, "file:Cargo.toml");
        assert_eq!(er.status, "Resolved");
        assert!((er.confidence - 1.0).abs() < f32::EPSILON);
        assert_eq!(er.path, Some("Cargo.toml".to_string()));
    }

    #[test]
    fn expand_with_note() {
        let mut packet = make_packet();
        packet
            .fields
            .insert("note".to_string(), FieldValue::Scalar("test note".to_string()));
        let resolved = vec![];
        let safety = default_safety();
        let workflows = test_workflows();

        let expanded = expand_to_run_packet(&packet, &resolved, &safety, &workflows);

        assert_eq!(expanded.note, Some("test note".to_string()));
    }

    #[test]
    fn expand_without_note() {
        let packet = make_packet();
        let resolved = vec![];
        let safety = default_safety();
        let workflows = test_workflows();

        let expanded = expand_to_run_packet(&packet, &resolved, &safety, &workflows);

        assert_eq!(expanded.note, None);
    }
}