use super::*;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
fn setup_legacy(dir: &Path) {
fs::create_dir_all(dir.join("workspace/skills")).unwrap();
fs::create_dir_all(dir.join("agents/duncan/sessions")).unwrap();
let config = serde_json::json!({
"name": "Duncan Idaho",
"model": "gpt-4",
"provider": "openai",
"api_url": "https://api.openai.com/v1",
"temperature": 0.7,
"max_tokens": 4096,
"channels": {
"telegram": { "enabled": true, "token": "tg-token" },
"whatsapp": { "enabled": false, "token": "wa-token", "phone_id": "12345" }
},
"cron": [
{ "name": "heartbeat", "schedule": {"kind": "cron", "expr": "*/5 * * * *"}, "command": "ping", "enabled": true },
{ "name": "cleanup", "schedule": "0 3 * * *", "command": "cleanup", "enabled": false }
]
});
fs::write(
dir.join("legacy.json"),
serde_json::to_string_pretty(&config).unwrap(),
)
.unwrap();
fs::write(
dir.join("workspace/SOUL.md"),
"# Soul\n\n## Identity\nI am Duncan Idaho.\n\n## Traits\nLoyal, fierce, skilled.\n",
)
.unwrap();
fs::write(
dir.join("workspace/AGENTS.md"),
"# Agents\n\n## Capabilities\nFighting, strategy, leadership.\n",
)
.unwrap();
fs::write(
dir.join("workspace/skills/greet.sh"),
"#!/bin/bash\necho hello\n",
)
.unwrap();
fs::write(dir.join("workspace/skills/math.py"), "print(2+2)\n").unwrap();
let session = serde_json::json!([{
"id": "sess-001", "agent_id": "duncan", "created_at": "2025-01-01T00:00:00Z",
"messages": [
{ "role": "user", "content": "Hello", "timestamp": "2025-01-01T00:00:01Z" },
{ "role": "assistant", "content": "Hi there!", "timestamp": "2025-01-01T00:00:02Z" }
]
}]);
fs::write(
dir.join("sessions.json"),
serde_json::to_string_pretty(&session).unwrap(),
)
.unwrap();
let jsonl = "{\"role\":\"user\",\"content\":\"JSONL msg\",\"timestamp\":\"2025-01-02T00:00:00Z\"}\n{\"role\":\"assistant\",\"content\":\"Reply\",\"timestamp\":\"2025-01-02T00:00:01Z\"}";
fs::write(dir.join("agents/duncan/sessions/sess-002.jsonl"), jsonl).unwrap();
}
fn setup_roboticus(dir: &Path) {
fs::create_dir_all(dir.join("workspace")).unwrap();
fs::create_dir_all(dir.join("skills")).unwrap();
fs::write(dir.join("roboticus.toml"), "[agent]\nname = \"Duncan Idaho\"\nid = \"duncan\"\nworkspace = \"/tmp/workspace\"\n\n[server]\nhost = \"localhost\"\nport = 18789\n\n[database]\npath = \"/tmp/roboticus.db\"\n\n[models]\nprimary = \"gpt-4\"\nfallback = \"gpt-3.5-turbo\"\ntemperature = 0.7\nmax_tokens = 4096\n").unwrap();
fs::write(dir.join("channels.toml"), "[channels.telegram]\nenabled = true\ntoken_env = \"TELEGRAM_BOT_TOKEN\"\n\n[channels.whatsapp]\nenabled = false\ntoken_env = \"WHATSAPP_TOKEN\"\nphone_id = \"12345\"\n").unwrap();
fs::write(dir.join("workspace/OS.toml"), "[os]\nprompt_text = \"\"\"\\n# Soul\\n\\n## Identity\\nI am Duncan.\\n\"\"\"\nidentity = \"I am Duncan.\"\n").unwrap();
fs::write(
dir.join("workspace/FIRMWARE.toml"),
"[firmware]\ncapabilities = \"Fighting, strategy.\"\n",
)
.unwrap();
fs::write(dir.join("skills/greet.gosh"), "echo hello\n").unwrap();
fs::write(dir.join("skills/math.py"), "print(2+2)\n").unwrap();
}
#[test]
fn import_config_succeeds() {
let oc = TempDir::new().unwrap();
let ic = TempDir::new().unwrap();
setup_legacy(oc.path());
let r = import_config(oc.path(), ic.path());
assert!(r.success);
assert_eq!(r.items_processed, 1);
let content = fs::read_to_string(ic.path().join("roboticus.toml")).unwrap();
assert!(content.contains("Duncan Idaho"));
assert!(content.contains("gpt-4"));
assert!(
content.contains("[channels.telegram]"),
"roboticus.toml must include channel config"
);
assert!(content.contains("enabled = true"));
}
#[test]
fn import_config_missing_file() {
let r = import_config(Path::new("/nonexistent"), Path::new("/tmp"));
assert!(!r.success);
}
#[test]
fn export_config_succeeds() {
let ic = TempDir::new().unwrap();
let oc = TempDir::new().unwrap();
setup_roboticus(ic.path());
let r = export_config(ic.path(), oc.path());
assert!(r.success);
let content = fs::read_to_string(oc.path().join("legacy.json")).unwrap();
assert!(content.contains("Duncan Idaho"));
assert!(content.contains("gpt-4"));
}
#[test]
fn config_roundtrip() {
let oc = TempDir::new().unwrap();
let ic = TempDir::new().unwrap();
let oc2 = TempDir::new().unwrap();
setup_legacy(oc.path());
assert!(import_config(oc.path(), ic.path()).success);
assert!(export_config(ic.path(), oc2.path()).success);
let exported: serde_json::Value =
serde_json::from_str(&fs::read_to_string(oc2.path().join("legacy.json")).unwrap()).unwrap();
assert_eq!(exported["name"], "Duncan Idaho");
assert_eq!(exported["model"], "gpt-4");
}
#[test]
fn export_config_merge_preserves_unknown_fields() {
let ic = TempDir::new().unwrap();
let oc = TempDir::new().unwrap();
setup_roboticus(ic.path());
fs::write(
oc.path().join("legacy.json"),
r#"{"custom_field":"preserved","name":"old"}"#,
)
.unwrap();
let r = export_config(ic.path(), oc.path());
assert!(r.success);
let exported: serde_json::Value =
serde_json::from_str(&fs::read_to_string(oc.path().join("legacy.json")).unwrap()).unwrap();
assert_eq!(exported["custom_field"], "preserved");
assert_eq!(exported["name"], "Duncan Idaho");
}
#[test]
fn import_personality_succeeds() {
let oc = TempDir::new().unwrap();
let ic = TempDir::new().unwrap();
setup_legacy(oc.path());
let r = import_personality(oc.path(), ic.path());
assert!(r.success);
assert_eq!(r.items_processed, 2);
assert!(ic.path().join("workspace/OS.toml").exists());
assert!(ic.path().join("workspace/FIRMWARE.toml").exists());
}
#[test]
fn export_personality_succeeds() {
let ic = TempDir::new().unwrap();
let oc = TempDir::new().unwrap();
setup_roboticus(ic.path());
let r = export_personality(ic.path(), oc.path());
assert!(r.success);
assert_eq!(r.items_processed, 2);
assert!(oc.path().join("workspace/SOUL.md").exists());
assert!(oc.path().join("workspace/AGENTS.md").exists());
}
#[test]
fn personality_roundtrip_via_prompt_text() {
let oc = TempDir::new().unwrap();
let ic = TempDir::new().unwrap();
let oc2 = TempDir::new().unwrap();
setup_legacy(oc.path());
assert!(import_personality(oc.path(), ic.path()).success);
assert!(export_personality(ic.path(), oc2.path()).success);
let original = fs::read_to_string(oc.path().join("workspace/SOUL.md")).unwrap();
let exported = fs::read_to_string(oc2.path().join("workspace/SOUL.md")).unwrap();
assert_eq!(original.trim(), exported.trim());
}
#[test]
fn personality_missing_files_warns() {
let oc = TempDir::new().unwrap();
let ic = TempDir::new().unwrap();
fs::create_dir_all(oc.path().join("workspace")).unwrap();
let r = import_personality(oc.path(), ic.path());
assert!(r.success);
assert_eq!(r.items_processed, 0);
assert_eq!(r.warnings.len(), 2);
}
#[test]
fn import_skills_succeeds() {
let oc = TempDir::new().unwrap();
let ic = TempDir::new().unwrap();
setup_legacy(oc.path());
let r = import_skills(oc.path(), ic.path(), true);
assert!(r.success);
assert_eq!(r.items_processed, 2);
assert!(ic.path().join("skills/greet.sh").exists());
}
#[test]
fn import_skills_no_dir() {
let r = import_skills(Path::new("/nonexistent"), Path::new("/tmp"), true);
assert!(r.success);
assert_eq!(r.items_processed, 0);
}
#[test]
fn export_skills_succeeds() {
let ic = TempDir::new().unwrap();
let oc = TempDir::new().unwrap();
setup_roboticus(ic.path());
let r = export_skills(ic.path(), oc.path());
assert!(r.success);
assert_eq!(r.items_processed, 2);
}
#[test]
fn import_sessions_from_json() {
let oc = TempDir::new().unwrap();
let ic = TempDir::new().unwrap();
setup_legacy(oc.path());
let r = import_sessions(oc.path(), ic.path());
assert!(r.success);
assert!(r.items_processed >= 1);
assert!(ic.path().join("state.db").exists());
}
#[test]
fn import_sessions_from_jsonl() {
let oc = TempDir::new().unwrap();
let ic = TempDir::new().unwrap();
setup_legacy(oc.path());
fs::remove_file(oc.path().join("sessions.json")).unwrap();
let r = import_sessions(oc.path(), ic.path());
assert!(r.success);
assert_eq!(r.items_processed, 1);
}
#[test]
fn export_sessions_succeeds() {
let oc = TempDir::new().unwrap();
let ic = TempDir::new().unwrap();
setup_legacy(oc.path());
import_sessions(oc.path(), ic.path());
let out = TempDir::new().unwrap();
let r = export_sessions(ic.path(), out.path());
assert!(r.success);
assert!(r.items_processed >= 1);
assert!(out.path().join("sessions.json").exists());
}
#[test]
fn sessions_no_data() {
let oc = TempDir::new().unwrap();
let ic = TempDir::new().unwrap();
let r = import_sessions(oc.path(), ic.path());
assert!(r.success);
assert_eq!(r.items_processed, 0);
}
#[test]
fn import_cron_from_config() {
let oc = TempDir::new().unwrap();
let ic = TempDir::new().unwrap();
setup_legacy(oc.path());
let r = import_cron(oc.path(), ic.path());
assert!(r.success);
assert_eq!(r.items_processed, 2);
}
#[test]
fn import_cron_from_jobs_json() {
let oc = TempDir::new().unwrap();
let ic = TempDir::new().unwrap();
let jobs = serde_json::json!([{ "name": "daily", "schedule": "0 0 * * *", "command": "report", "enabled": true }]);
fs::write(
oc.path().join("jobs.json"),
serde_json::to_string(&jobs).unwrap(),
)
.unwrap();
let r = import_cron(oc.path(), ic.path());
assert!(r.success);
assert_eq!(r.items_processed, 1);
}
#[test]
fn export_cron_succeeds() {
let oc = TempDir::new().unwrap();
let ic = TempDir::new().unwrap();
setup_legacy(oc.path());
import_cron(oc.path(), ic.path());
let out = TempDir::new().unwrap();
let r = export_cron(ic.path(), out.path());
assert!(r.success);
assert_eq!(r.items_processed, 2);
assert!(out.path().join("jobs.json").exists());
}
#[test]
fn cron_no_data() {
let r = import_cron(Path::new("/nonexistent"), Path::new("/tmp"));
assert!(r.success);
assert_eq!(r.items_processed, 0);
}
#[test]
fn import_channels_succeeds() {
let oc = TempDir::new().unwrap();
let ic = TempDir::new().unwrap();
setup_legacy(oc.path());
let r = import_channels(oc.path(), ic.path());
assert!(r.success);
assert_eq!(r.items_processed, 2);
assert!(ic.path().join("channels.toml").exists());
}
#[test]
fn export_channels_succeeds() {
let ic = TempDir::new().unwrap();
let oc = TempDir::new().unwrap();
setup_roboticus(ic.path());
let r = export_channels(ic.path(), oc.path());
assert!(r.success);
assert_eq!(r.items_processed, 2);
}
#[test]
fn channels_no_config() {
let r = import_channels(Path::new("/nonexistent"), Path::new("/tmp"));
assert!(r.success);
assert_eq!(r.items_processed, 0);
}
#[test]
fn markdown_to_toml_has_prompt_text() {
let md = "# Title\n\n## Identity\nI am Duncan.\n";
let toml = markdown_to_personality_toml(md, "os");
assert!(toml.contains("[os]"));
assert!(toml.contains("prompt_text"));
assert!(toml.contains("identity"));
}
#[test]
fn toml_to_markdown_uses_prompt_text() {
let toml_str = "[os]\nprompt_text = \"\"\"\n# Soul\n\n## Identity\nI am Duncan.\n\"\"\"\n";
let md = personality_toml_to_markdown(toml_str, "SOUL");
assert!(md.contains("# Soul"));
assert!(md.contains("Duncan"));
}
#[test]
fn qt_escapes() {
assert_eq!(qt("hello"), "\"hello\"");
assert_eq!(qt("he\"llo"), "\"he\\\"llo\"");
assert_eq!(qt("a\\b"), "\"a\\\\b\"");
}
#[test]
fn qt_empty_string() {
assert_eq!(qt(""), "\"\"");
}
#[test]
fn qt_ml_wraps_in_triple_quotes() {
let result = qt_ml("hello\nworld");
assert!(result.starts_with("\"\"\""));
assert!(result.ends_with("\"\"\""));
assert!(result.contains("hello\nworld"));
}
#[test]
fn titlecase_converts_underscored_keys() {
assert_eq!(titlecase("prompt_text"), "Prompt Text");
assert_eq!(titlecase("identity"), "Identity");
assert_eq!(titlecase("core_values"), "Core Values");
}
#[test]
fn titlecase_empty_string() {
assert_eq!(titlecase(""), "");
}
#[test]
fn titlecase_single_word() {
assert_eq!(titlecase("traits"), "Traits");
}
#[test]
fn titlecase_multiple_underscores() {
assert_eq!(titlecase("my_great_key_name"), "My Great Key Name");
}
#[test]
fn err_helper_sets_fields_correctly() {
let result = err(MigrationArea::Config, "something failed".to_string());
assert!(!result.success);
assert_eq!(result.items_processed, 0);
assert!(result.warnings.is_empty());
assert_eq!(result.error, Some("something failed".to_string()));
}
#[test]
fn markdown_to_personality_toml_empty_input() {
let toml = markdown_to_personality_toml("", "soul");
assert!(toml.contains("[soul]"));
assert!(toml.contains("prompt_text"));
}
#[test]
fn markdown_to_personality_toml_with_multiple_sections() {
let md = "# Soul\n\n## Identity\nI am Duncan.\n\n## Traits\nLoyal and fierce.\n\n## Values\nHonor above all.\n";
let toml = markdown_to_personality_toml(md, "soul");
assert!(toml.contains("[soul]"));
assert!(toml.contains("identity"));
assert!(toml.contains("traits"));
assert!(toml.contains("values"));
}
#[test]
fn personality_toml_to_markdown_invalid_toml_wraps_raw() {
let invalid = "this is not valid toml {{{{";
let md = personality_toml_to_markdown(invalid, "Test");
assert!(md.contains("# Test"));
assert!(md.contains(invalid));
}
#[test]
fn personality_toml_to_markdown_structured_fields_without_prompt_text() {
let toml_str = "[soul]\nidentity = \"I am Duncan.\"\ntraits = \"Loyal, fierce.\"\n";
let md = personality_toml_to_markdown(toml_str, "SOUL");
assert!(md.contains("# SOUL"));
assert!(md.contains("## Identity"));
assert!(md.contains("I am Duncan."));
assert!(md.contains("## Traits"));
}