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>> {
let bundle = claude_bundle()?;
let report = plan_conversion(&bundle, AgentKind::Pi, Scope::Global)?;
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>> {
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();
let strict_result = render_bundle(&bundle, &strict);
let bridge_result = render_bundle(&bundle, &lossy);
let rendered = render_bundle(&bundle, &ready)?;
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>> {
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)?;
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)?;
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)
}