agent-migrate-adapters 0.6.1

Codex, Pi, and Claude Code configuration adapters for agent-migrate
Documentation
use std::collections::BTreeMap;
use std::str::FromStr;

use agent_migrate_adapters::{AdapterError, RenderRequest, plan_conversion, render_bundle};
use agent_migrate_core::{
    AgentKind, Bundle, BundleId, BundleRelativePath, Compatibility, FileKind, Manifest,
    ManifestIdentity, SchemaVersion, Scope, SourceDescriptor, SourceEnvironment,
};
use jiff::Timestamp;
use uuid::Uuid;

#[test]
fn claude_to_pi_reports_exact_bridge_lossy_and_preserved_states()
-> Result<(), Box<dyn std::error::Error>> {
    // Given
    let bundle = claude_bundle()?;

    // When
    let report = plan_conversion(&bundle, AgentKind::Pi, Scope::Global)?;

    // Then
    assert!(report.resources.iter().any(|item| {
        item.kind == FileKind::Skill && item.compatibility == Compatibility::Exact
    }));
    assert!(report.resources.iter().any(|item| {
        item.kind == FileKind::Mcp && matches!(item.compatibility, Compatibility::Bridged { .. })
    }));
    assert!(report.resources.iter().any(|item| {
        item.kind == FileKind::Agent && matches!(item.compatibility, Compatibility::Lossy { .. })
    }));
    assert!(report.resources.iter().any(|item| {
        item.kind == FileKind::Hook
            && matches!(
                item.compatibility,
                Compatibility::PreservedUnavailable { .. }
            )
    }));
    Ok(())
}

#[test]
fn render_enforces_lossy_and_bridge_policy_before_creating_writes()
-> Result<(), Box<dyn std::error::Error>> {
    // Given
    let bundle = claude_bundle()?;
    let strict = RenderRequest::new(AgentKind::Pi, Scope::Global);
    let lossy = strict.clone().accept_lossy();
    let ready = lossy.clone().with_bridge_healthy();

    // When
    let strict_result = render_bundle(&bundle, &strict);
    let bridge_result = render_bundle(&bundle, &lossy);
    let rendered = render_bundle(&bundle, &ready)?;

    // Then
    assert!(matches!(
        strict_result,
        Err(AdapterError::LossyAcceptanceRequired { .. })
    ));
    assert!(matches!(
        bridge_result,
        Err(AdapterError::BridgeUnavailable { .. })
    ));
    let paths = rendered
        .plan
        .writes()
        .map(|write| write.path.to_string())
        .collect::<Vec<_>>();
    assert!(paths.contains(&".pi/agent/skills/demo/SKILL.md".to_owned()));
    assert!(paths.contains(&".pi/agent/agent-migrate-mcp.json".to_owned()));
    assert!(paths.contains(&".pi/agent/extensions/agent-migrate-mcp.ts".to_owned()));
    assert!(paths.contains(&".pi/agent/prompts/agents/reviewer.md".to_owned()));
    assert!(paths.contains(&".agent-migrate/preserved/hooks/hooks.json".to_owned()));
    Ok(())
}

#[test]
fn multiple_settings_resources_merge_without_data_loss() -> Result<(), Box<dyn std::error::Error>> {
    // Given
    let mut files = BTreeMap::new();
    files.insert(
        BundleRelativePath::from_str("settings/pi/settings.json")?,
        (FileKind::Settings, br#"{"theme":"dark"}"#.to_vec()),
    );
    files.insert(
        BundleRelativePath::from_str("settings/pi/models.json")?,
        (
            FileKind::Settings,
            br#"{"models":{"fixture":{"provider":"local"}}}"#.to_vec(),
        ),
    );
    let bundle = bundle(AgentKind::Pi, files)?;

    // When
    let rendered = render_bundle(
        &bundle,
        &RenderRequest::new(AgentKind::Claude, Scope::Global),
    )?;
    let settings = rendered
        .plan
        .writes()
        .find(|write| write.path.to_string() == ".claude/settings.json")
        .ok_or("missing merged settings")?;
    let value: serde_json::Value = serde_json::from_slice(&settings.content)?;

    // Then
    assert_eq!(value["theme"], "dark");
    assert_eq!(value["models"]["fixture"]["provider"], "local");
    assert_eq!(rendered.plan.len(), 1);
    Ok(())
}

fn claude_bundle() -> Result<Bundle, Box<dyn std::error::Error>> {
    let mut files = BTreeMap::new();
    files.insert(
        BundleRelativePath::from_str("skills/claude/demo/SKILL.md")?,
        (FileKind::Skill, b"---\nname: demo\n---\n".to_vec()),
    );
    files.insert(
        BundleRelativePath::from_str("mcp/claude/.mcp.json")?,
        (
            FileKind::Mcp,
            br#"{"mcpServers":{"docs":{"command":"node","args":["server.js"]}}}"#.to_vec(),
        ),
    );
    files.insert(
        BundleRelativePath::from_str("agents/claude/reviewer.md")?,
        (FileKind::Agent, b"Review changes.\n".to_vec()),
    );
    files.insert(
        BundleRelativePath::from_str("hooks/claude/hooks.json")?,
        (FileKind::Hook, b"{}".to_vec()),
    );
    bundle(AgentKind::Claude, files)
}

fn bundle(
    agent: AgentKind,
    files: BTreeMap<BundleRelativePath, (FileKind, Vec<u8>)>,
) -> Result<Bundle, Box<dyn std::error::Error>> {
    let identity = ManifestIdentity::new(
        SchemaVersion::V1,
        BundleId::from_uuid(Uuid::nil()),
        Timestamp::constant(0, 0),
    );
    let source = SourceDescriptor::new(
        agent,
        SourceEnvironment::new("2.1.220", Scope::Global, "macos-aarch64"),
    );
    Bundle::from_files(Manifest::new(identity, source), files).map_err(Into::into)
}