oxicode/foundation/
compat_import.rs1use std::path::Path;
15
16use crate::foundation::FoundationError;
17use crate::foundation::profiles::{CompatibilityImport, Profile};
18
19pub fn migration_enabled() -> bool {
21 matches!(
22 std::env::var("OXICODE_FOUNDATION_MIGRATION").as_deref(),
23 Ok("1") | Ok("true") | Ok("TRUE")
24 )
25}
26
27pub fn read_compatibility_shim(
32 path: &Path,
33) -> Result<Option<CompatibilityImport>, FoundationError> {
34 if !path.is_file() {
35 return Ok(None);
36 }
37 let raw = std::fs::read_to_string(path)?;
38 if raw.trim().is_empty() {
39 return Ok(None);
40 }
41 let profile: Profile = serde_json::from_str(&raw)?;
42 profile.credential.validate()?;
43 Ok(Some(CompatibilityImport { profile }))
44}
45
46pub fn write_migration_marker(root: &Path, profile_id: &str) -> Result<(), FoundationError> {
50 let path = root.join("migration.marker.json");
51 let body = serde_json::json!({
52 "profile_id": profile_id,
53 "migrated_at": chrono::Utc::now().to_rfc3339(),
54 "migrated_by": "oxicode",
55 });
56 std::fs::write(
57 &path,
58 serde_json::to_string_pretty(&body).map_err(|e| FoundationError::Parse(e.to_string()))?,
59 )?;
60 Ok(())
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66 use crate::foundation::profiles::CredentialLocator;
67
68 #[test]
69 fn migration_disabled_by_default() {
70 let original = std::env::var("OXICODE_FOUNDATION_MIGRATION").ok();
72 unsafe {
73 std::env::remove_var("OXICODE_FOUNDATION_MIGRATION");
74 }
75 assert!(!migration_enabled());
76 if let Some(value) = original {
77 unsafe {
78 std::env::set_var("OXICODE_FOUNDATION_MIGRATION", value);
79 }
80 }
81 }
82
83 #[test]
84 fn migrate_marker_roundtrip() {
85 let tmp = tempfile::tempdir().unwrap();
86 write_migration_marker(tmp.path(), "legacy").unwrap();
87 let body = std::fs::read_to_string(tmp.path().join("migration.marker.json")).unwrap();
88 assert!(body.contains("legacy"));
89 }
90
91 #[test]
92 fn shim_returns_none_when_missing() {
93 let tmp = tempfile::tempdir().unwrap();
94 let opt = read_compatibility_shim(&tmp.path().join("missing.json")).unwrap();
95 assert!(opt.is_none());
96 }
97
98 #[test]
99 fn shim_parses_minimal_profile() {
100 let tmp = tempfile::tempdir().unwrap();
101 let path = tmp.path().join("compatibility.json");
102 let raw = r#"{
103 "id": "legacy",
104 "provider": "anthropic",
105 "model": "claude-sonnet",
106 "roles": ["coding.primary"],
107 "credential": { "service": "dev.oxi.foundation", "account": "legacy" }
108 }"#;
109 std::fs::write(&path, raw).unwrap();
110 let import = read_compatibility_shim(&path).unwrap().unwrap();
111 assert_eq!(import.profile.provider, "anthropic");
112 assert_eq!(
113 import.profile.credential,
114 CredentialLocator {
115 service: "dev.oxi.foundation".to_string(),
116 account: "legacy".to_string(),
117 }
118 );
119 }
120}