Spaces:
Sleeping
Sleeping
| package migrate | |
| import ( | |
| "encoding/json" | |
| "os" | |
| "path/filepath" | |
| "testing" | |
| "github.com/sipeed/picoclaw/pkg/config" | |
| ) | |
| func TestCamelToSnake(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| input string | |
| want string | |
| }{ | |
| {"simple", "apiKey", "api_key"}, | |
| {"two words", "apiBase", "api_base"}, | |
| {"three words", "maxToolIterations", "max_tool_iterations"}, | |
| {"already snake", "api_key", "api_key"}, | |
| {"single word", "enabled", "enabled"}, | |
| {"all lower", "model", "model"}, | |
| {"consecutive caps", "apiURL", "api_url"}, | |
| {"starts upper", "Model", "model"}, | |
| {"bridge url", "bridgeUrl", "bridge_url"}, | |
| {"client id", "clientId", "client_id"}, | |
| {"app secret", "appSecret", "app_secret"}, | |
| {"verification token", "verificationToken", "verification_token"}, | |
| {"allow from", "allowFrom", "allow_from"}, | |
| } | |
| for _, tt := range tests { | |
| t.Run(tt.name, func(t *testing.T) { | |
| got := camelToSnake(tt.input) | |
| if got != tt.want { | |
| t.Errorf("camelToSnake(%q) = %q, want %q", tt.input, got, tt.want) | |
| } | |
| }) | |
| } | |
| } | |
| func TestConvertKeysToSnake(t *testing.T) { | |
| input := map[string]interface{}{ | |
| "apiKey": "test-key", | |
| "apiBase": "https://example.com", | |
| "nested": map[string]interface{}{ | |
| "maxTokens": float64(8192), | |
| "allowFrom": []interface{}{"user1", "user2"}, | |
| "deeperLevel": map[string]interface{}{ | |
| "clientId": "abc", | |
| }, | |
| }, | |
| } | |
| result := convertKeysToSnake(input) | |
| m, ok := result.(map[string]interface{}) | |
| if !ok { | |
| t.Fatal("expected map[string]interface{}") | |
| } | |
| if _, ok := m["api_key"]; !ok { | |
| t.Error("expected key 'api_key' after conversion") | |
| } | |
| if _, ok := m["api_base"]; !ok { | |
| t.Error("expected key 'api_base' after conversion") | |
| } | |
| nested, ok := m["nested"].(map[string]interface{}) | |
| if !ok { | |
| t.Fatal("expected nested map") | |
| } | |
| if _, ok := nested["max_tokens"]; !ok { | |
| t.Error("expected key 'max_tokens' in nested map") | |
| } | |
| if _, ok := nested["allow_from"]; !ok { | |
| t.Error("expected key 'allow_from' in nested map") | |
| } | |
| deeper, ok := nested["deeper_level"].(map[string]interface{}) | |
| if !ok { | |
| t.Fatal("expected deeper_level map") | |
| } | |
| if _, ok := deeper["client_id"]; !ok { | |
| t.Error("expected key 'client_id' in deeper level") | |
| } | |
| } | |
| func TestLoadOpenClawConfig(t *testing.T) { | |
| tmpDir := t.TempDir() | |
| configPath := filepath.Join(tmpDir, "openclaw.json") | |
| openclawConfig := map[string]interface{}{ | |
| "providers": map[string]interface{}{ | |
| "anthropic": map[string]interface{}{ | |
| "apiKey": "sk-ant-test123", | |
| "apiBase": "https://api.anthropic.com", | |
| }, | |
| }, | |
| "agents": map[string]interface{}{ | |
| "defaults": map[string]interface{}{ | |
| "maxTokens": float64(4096), | |
| "model": "claude-3-opus", | |
| }, | |
| }, | |
| } | |
| data, err := json.Marshal(openclawConfig) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if err := os.WriteFile(configPath, data, 0644); err != nil { | |
| t.Fatal(err) | |
| } | |
| result, err := LoadOpenClawConfig(configPath) | |
| if err != nil { | |
| t.Fatalf("LoadOpenClawConfig: %v", err) | |
| } | |
| providers, ok := result["providers"].(map[string]interface{}) | |
| if !ok { | |
| t.Fatal("expected providers map") | |
| } | |
| anthropic, ok := providers["anthropic"].(map[string]interface{}) | |
| if !ok { | |
| t.Fatal("expected anthropic map") | |
| } | |
| if anthropic["api_key"] != "sk-ant-test123" { | |
| t.Errorf("api_key = %v, want sk-ant-test123", anthropic["api_key"]) | |
| } | |
| agents, ok := result["agents"].(map[string]interface{}) | |
| if !ok { | |
| t.Fatal("expected agents map") | |
| } | |
| defaults, ok := agents["defaults"].(map[string]interface{}) | |
| if !ok { | |
| t.Fatal("expected defaults map") | |
| } | |
| if defaults["max_tokens"] != float64(4096) { | |
| t.Errorf("max_tokens = %v, want 4096", defaults["max_tokens"]) | |
| } | |
| } | |
| func TestConvertConfig(t *testing.T) { | |
| t.Run("providers mapping", func(t *testing.T) { | |
| data := map[string]interface{}{ | |
| "providers": map[string]interface{}{ | |
| "anthropic": map[string]interface{}{ | |
| "api_key": "sk-ant-test", | |
| "api_base": "https://api.anthropic.com", | |
| }, | |
| "openrouter": map[string]interface{}{ | |
| "api_key": "sk-or-test", | |
| }, | |
| "groq": map[string]interface{}{ | |
| "api_key": "gsk-test", | |
| }, | |
| }, | |
| } | |
| cfg, warnings, err := ConvertConfig(data) | |
| if err != nil { | |
| t.Fatalf("ConvertConfig: %v", err) | |
| } | |
| if len(warnings) != 0 { | |
| t.Errorf("expected no warnings, got %v", warnings) | |
| } | |
| if cfg.Providers.Anthropic.APIKey != "sk-ant-test" { | |
| t.Errorf("Anthropic.APIKey = %q, want %q", cfg.Providers.Anthropic.APIKey, "sk-ant-test") | |
| } | |
| if cfg.Providers.OpenRouter.APIKey != "sk-or-test" { | |
| t.Errorf("OpenRouter.APIKey = %q, want %q", cfg.Providers.OpenRouter.APIKey, "sk-or-test") | |
| } | |
| if cfg.Providers.Groq.APIKey != "gsk-test" { | |
| t.Errorf("Groq.APIKey = %q, want %q", cfg.Providers.Groq.APIKey, "gsk-test") | |
| } | |
| }) | |
| t.Run("unsupported provider warning", func(t *testing.T) { | |
| data := map[string]interface{}{ | |
| "providers": map[string]interface{}{ | |
| "deepseek": map[string]interface{}{ | |
| "api_key": "sk-deep-test", | |
| }, | |
| }, | |
| } | |
| _, warnings, err := ConvertConfig(data) | |
| if err != nil { | |
| t.Fatalf("ConvertConfig: %v", err) | |
| } | |
| if len(warnings) != 1 { | |
| t.Fatalf("expected 1 warning, got %d", len(warnings)) | |
| } | |
| if warnings[0] != "Provider 'deepseek' not supported in PicoClaw, skipping" { | |
| t.Errorf("unexpected warning: %s", warnings[0]) | |
| } | |
| }) | |
| t.Run("channels mapping", func(t *testing.T) { | |
| data := map[string]interface{}{ | |
| "channels": map[string]interface{}{ | |
| "telegram": map[string]interface{}{ | |
| "enabled": true, | |
| "token": "tg-token-123", | |
| "allow_from": []interface{}{"user1"}, | |
| }, | |
| "discord": map[string]interface{}{ | |
| "enabled": true, | |
| "token": "disc-token-456", | |
| }, | |
| }, | |
| } | |
| cfg, _, err := ConvertConfig(data) | |
| if err != nil { | |
| t.Fatalf("ConvertConfig: %v", err) | |
| } | |
| if !cfg.Channels.Telegram.Enabled { | |
| t.Error("Telegram should be enabled") | |
| } | |
| if cfg.Channels.Telegram.Token != "tg-token-123" { | |
| t.Errorf("Telegram.Token = %q, want %q", cfg.Channels.Telegram.Token, "tg-token-123") | |
| } | |
| if len(cfg.Channels.Telegram.AllowFrom) != 1 || cfg.Channels.Telegram.AllowFrom[0] != "user1" { | |
| t.Errorf("Telegram.AllowFrom = %v, want [user1]", cfg.Channels.Telegram.AllowFrom) | |
| } | |
| if !cfg.Channels.Discord.Enabled { | |
| t.Error("Discord should be enabled") | |
| } | |
| }) | |
| t.Run("unsupported channel warning", func(t *testing.T) { | |
| data := map[string]interface{}{ | |
| "channels": map[string]interface{}{ | |
| "email": map[string]interface{}{ | |
| "enabled": true, | |
| }, | |
| }, | |
| } | |
| _, warnings, err := ConvertConfig(data) | |
| if err != nil { | |
| t.Fatalf("ConvertConfig: %v", err) | |
| } | |
| if len(warnings) != 1 { | |
| t.Fatalf("expected 1 warning, got %d", len(warnings)) | |
| } | |
| if warnings[0] != "Channel 'email' not supported in PicoClaw, skipping" { | |
| t.Errorf("unexpected warning: %s", warnings[0]) | |
| } | |
| }) | |
| t.Run("agent defaults", func(t *testing.T) { | |
| data := map[string]interface{}{ | |
| "agents": map[string]interface{}{ | |
| "defaults": map[string]interface{}{ | |
| "model": "claude-3-opus", | |
| "max_tokens": float64(4096), | |
| "temperature": 0.5, | |
| "max_tool_iterations": float64(10), | |
| "workspace": "~/.openclaw/workspace", | |
| }, | |
| }, | |
| } | |
| cfg, _, err := ConvertConfig(data) | |
| if err != nil { | |
| t.Fatalf("ConvertConfig: %v", err) | |
| } | |
| if cfg.Agents.Defaults.Model != "claude-3-opus" { | |
| t.Errorf("Model = %q, want %q", cfg.Agents.Defaults.Model, "claude-3-opus") | |
| } | |
| if cfg.Agents.Defaults.MaxTokens != 4096 { | |
| t.Errorf("MaxTokens = %d, want %d", cfg.Agents.Defaults.MaxTokens, 4096) | |
| } | |
| if cfg.Agents.Defaults.Temperature != 0.5 { | |
| t.Errorf("Temperature = %f, want %f", cfg.Agents.Defaults.Temperature, 0.5) | |
| } | |
| if cfg.Agents.Defaults.Workspace != "~/.picoclaw/workspace" { | |
| t.Errorf("Workspace = %q, want %q", cfg.Agents.Defaults.Workspace, "~/.picoclaw/workspace") | |
| } | |
| }) | |
| t.Run("empty config", func(t *testing.T) { | |
| data := map[string]interface{}{} | |
| cfg, warnings, err := ConvertConfig(data) | |
| if err != nil { | |
| t.Fatalf("ConvertConfig: %v", err) | |
| } | |
| if len(warnings) != 0 { | |
| t.Errorf("expected no warnings, got %v", warnings) | |
| } | |
| if cfg.Agents.Defaults.Model != "glm-4.7" { | |
| t.Errorf("default model should be glm-4.7, got %q", cfg.Agents.Defaults.Model) | |
| } | |
| }) | |
| } | |
| func TestMergeConfig(t *testing.T) { | |
| t.Run("fills empty fields", func(t *testing.T) { | |
| existing := config.DefaultConfig() | |
| incoming := config.DefaultConfig() | |
| incoming.Providers.Anthropic.APIKey = "sk-ant-incoming" | |
| incoming.Providers.OpenRouter.APIKey = "sk-or-incoming" | |
| result := MergeConfig(existing, incoming) | |
| if result.Providers.Anthropic.APIKey != "sk-ant-incoming" { | |
| t.Errorf("Anthropic.APIKey = %q, want %q", result.Providers.Anthropic.APIKey, "sk-ant-incoming") | |
| } | |
| if result.Providers.OpenRouter.APIKey != "sk-or-incoming" { | |
| t.Errorf("OpenRouter.APIKey = %q, want %q", result.Providers.OpenRouter.APIKey, "sk-or-incoming") | |
| } | |
| }) | |
| t.Run("preserves existing non-empty fields", func(t *testing.T) { | |
| existing := config.DefaultConfig() | |
| existing.Providers.Anthropic.APIKey = "sk-ant-existing" | |
| incoming := config.DefaultConfig() | |
| incoming.Providers.Anthropic.APIKey = "sk-ant-incoming" | |
| incoming.Providers.OpenAI.APIKey = "sk-oai-incoming" | |
| result := MergeConfig(existing, incoming) | |
| if result.Providers.Anthropic.APIKey != "sk-ant-existing" { | |
| t.Errorf("Anthropic.APIKey should be preserved, got %q", result.Providers.Anthropic.APIKey) | |
| } | |
| if result.Providers.OpenAI.APIKey != "sk-oai-incoming" { | |
| t.Errorf("OpenAI.APIKey should be filled, got %q", result.Providers.OpenAI.APIKey) | |
| } | |
| }) | |
| t.Run("merges enabled channels", func(t *testing.T) { | |
| existing := config.DefaultConfig() | |
| incoming := config.DefaultConfig() | |
| incoming.Channels.Telegram.Enabled = true | |
| incoming.Channels.Telegram.Token = "tg-token" | |
| result := MergeConfig(existing, incoming) | |
| if !result.Channels.Telegram.Enabled { | |
| t.Error("Telegram should be enabled after merge") | |
| } | |
| if result.Channels.Telegram.Token != "tg-token" { | |
| t.Errorf("Telegram.Token = %q, want %q", result.Channels.Telegram.Token, "tg-token") | |
| } | |
| }) | |
| t.Run("preserves existing enabled channels", func(t *testing.T) { | |
| existing := config.DefaultConfig() | |
| existing.Channels.Telegram.Enabled = true | |
| existing.Channels.Telegram.Token = "existing-token" | |
| incoming := config.DefaultConfig() | |
| incoming.Channels.Telegram.Enabled = true | |
| incoming.Channels.Telegram.Token = "incoming-token" | |
| result := MergeConfig(existing, incoming) | |
| if result.Channels.Telegram.Token != "existing-token" { | |
| t.Errorf("Telegram.Token should be preserved, got %q", result.Channels.Telegram.Token) | |
| } | |
| }) | |
| } | |
| func TestPlanWorkspaceMigration(t *testing.T) { | |
| t.Run("copies available files", func(t *testing.T) { | |
| srcDir := t.TempDir() | |
| dstDir := t.TempDir() | |
| os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents"), 0644) | |
| os.WriteFile(filepath.Join(srcDir, "SOUL.md"), []byte("# Soul"), 0644) | |
| os.WriteFile(filepath.Join(srcDir, "USER.md"), []byte("# User"), 0644) | |
| actions, err := PlanWorkspaceMigration(srcDir, dstDir, false) | |
| if err != nil { | |
| t.Fatalf("PlanWorkspaceMigration: %v", err) | |
| } | |
| copyCount := 0 | |
| skipCount := 0 | |
| for _, a := range actions { | |
| if a.Type == ActionCopy { | |
| copyCount++ | |
| } | |
| if a.Type == ActionSkip { | |
| skipCount++ | |
| } | |
| } | |
| if copyCount != 3 { | |
| t.Errorf("expected 3 copies, got %d", copyCount) | |
| } | |
| if skipCount != 2 { | |
| t.Errorf("expected 2 skips (TOOLS.md, HEARTBEAT.md), got %d", skipCount) | |
| } | |
| }) | |
| t.Run("plans backup for existing destination files", func(t *testing.T) { | |
| srcDir := t.TempDir() | |
| dstDir := t.TempDir() | |
| os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents from OpenClaw"), 0644) | |
| os.WriteFile(filepath.Join(dstDir, "AGENTS.md"), []byte("# Existing Agents"), 0644) | |
| actions, err := PlanWorkspaceMigration(srcDir, dstDir, false) | |
| if err != nil { | |
| t.Fatalf("PlanWorkspaceMigration: %v", err) | |
| } | |
| backupCount := 0 | |
| for _, a := range actions { | |
| if a.Type == ActionBackup && filepath.Base(a.Destination) == "AGENTS.md" { | |
| backupCount++ | |
| } | |
| } | |
| if backupCount != 1 { | |
| t.Errorf("expected 1 backup action for AGENTS.md, got %d", backupCount) | |
| } | |
| }) | |
| t.Run("force skips backup", func(t *testing.T) { | |
| srcDir := t.TempDir() | |
| dstDir := t.TempDir() | |
| os.WriteFile(filepath.Join(srcDir, "AGENTS.md"), []byte("# Agents"), 0644) | |
| os.WriteFile(filepath.Join(dstDir, "AGENTS.md"), []byte("# Existing"), 0644) | |
| actions, err := PlanWorkspaceMigration(srcDir, dstDir, true) | |
| if err != nil { | |
| t.Fatalf("PlanWorkspaceMigration: %v", err) | |
| } | |
| for _, a := range actions { | |
| if a.Type == ActionBackup { | |
| t.Error("expected no backup actions with force=true") | |
| } | |
| } | |
| }) | |
| t.Run("handles memory directory", func(t *testing.T) { | |
| srcDir := t.TempDir() | |
| dstDir := t.TempDir() | |
| memDir := filepath.Join(srcDir, "memory") | |
| os.MkdirAll(memDir, 0755) | |
| os.WriteFile(filepath.Join(memDir, "MEMORY.md"), []byte("# Memory"), 0644) | |
| actions, err := PlanWorkspaceMigration(srcDir, dstDir, false) | |
| if err != nil { | |
| t.Fatalf("PlanWorkspaceMigration: %v", err) | |
| } | |
| hasCopy := false | |
| hasDir := false | |
| for _, a := range actions { | |
| if a.Type == ActionCopy && filepath.Base(a.Source) == "MEMORY.md" { | |
| hasCopy = true | |
| } | |
| if a.Type == ActionCreateDir { | |
| hasDir = true | |
| } | |
| } | |
| if !hasCopy { | |
| t.Error("expected copy action for memory/MEMORY.md") | |
| } | |
| if !hasDir { | |
| t.Error("expected create dir action for memory/") | |
| } | |
| }) | |
| t.Run("handles skills directory", func(t *testing.T) { | |
| srcDir := t.TempDir() | |
| dstDir := t.TempDir() | |
| skillDir := filepath.Join(srcDir, "skills", "weather") | |
| os.MkdirAll(skillDir, 0755) | |
| os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte("# Weather"), 0644) | |
| actions, err := PlanWorkspaceMigration(srcDir, dstDir, false) | |
| if err != nil { | |
| t.Fatalf("PlanWorkspaceMigration: %v", err) | |
| } | |
| hasCopy := false | |
| for _, a := range actions { | |
| if a.Type == ActionCopy && filepath.Base(a.Source) == "SKILL.md" { | |
| hasCopy = true | |
| } | |
| } | |
| if !hasCopy { | |
| t.Error("expected copy action for skills/weather/SKILL.md") | |
| } | |
| }) | |
| } | |
| func TestFindOpenClawConfig(t *testing.T) { | |
| t.Run("finds openclaw.json", func(t *testing.T) { | |
| tmpDir := t.TempDir() | |
| configPath := filepath.Join(tmpDir, "openclaw.json") | |
| os.WriteFile(configPath, []byte("{}"), 0644) | |
| found, err := findOpenClawConfig(tmpDir) | |
| if err != nil { | |
| t.Fatalf("findOpenClawConfig: %v", err) | |
| } | |
| if found != configPath { | |
| t.Errorf("found %q, want %q", found, configPath) | |
| } | |
| }) | |
| t.Run("falls back to config.json", func(t *testing.T) { | |
| tmpDir := t.TempDir() | |
| configPath := filepath.Join(tmpDir, "config.json") | |
| os.WriteFile(configPath, []byte("{}"), 0644) | |
| found, err := findOpenClawConfig(tmpDir) | |
| if err != nil { | |
| t.Fatalf("findOpenClawConfig: %v", err) | |
| } | |
| if found != configPath { | |
| t.Errorf("found %q, want %q", found, configPath) | |
| } | |
| }) | |
| t.Run("prefers openclaw.json over config.json", func(t *testing.T) { | |
| tmpDir := t.TempDir() | |
| openclawPath := filepath.Join(tmpDir, "openclaw.json") | |
| os.WriteFile(openclawPath, []byte("{}"), 0644) | |
| os.WriteFile(filepath.Join(tmpDir, "config.json"), []byte("{}"), 0644) | |
| found, err := findOpenClawConfig(tmpDir) | |
| if err != nil { | |
| t.Fatalf("findOpenClawConfig: %v", err) | |
| } | |
| if found != openclawPath { | |
| t.Errorf("should prefer openclaw.json, got %q", found) | |
| } | |
| }) | |
| t.Run("error when no config found", func(t *testing.T) { | |
| tmpDir := t.TempDir() | |
| _, err := findOpenClawConfig(tmpDir) | |
| if err == nil { | |
| t.Fatal("expected error when no config found") | |
| } | |
| }) | |
| } | |
| func TestRewriteWorkspacePath(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| input string | |
| want string | |
| }{ | |
| {"default path", "~/.openclaw/workspace", "~/.picoclaw/workspace"}, | |
| {"custom path", "/custom/path", "/custom/path"}, | |
| {"empty", "", ""}, | |
| } | |
| for _, tt := range tests { | |
| t.Run(tt.name, func(t *testing.T) { | |
| got := rewriteWorkspacePath(tt.input) | |
| if got != tt.want { | |
| t.Errorf("rewriteWorkspacePath(%q) = %q, want %q", tt.input, got, tt.want) | |
| } | |
| }) | |
| } | |
| } | |
| func TestRunDryRun(t *testing.T) { | |
| openclawHome := t.TempDir() | |
| picoClawHome := t.TempDir() | |
| wsDir := filepath.Join(openclawHome, "workspace") | |
| os.MkdirAll(wsDir, 0755) | |
| os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0644) | |
| os.WriteFile(filepath.Join(wsDir, "AGENTS.md"), []byte("# Agents"), 0644) | |
| configData := map[string]interface{}{ | |
| "providers": map[string]interface{}{ | |
| "anthropic": map[string]interface{}{ | |
| "apiKey": "test-key", | |
| }, | |
| }, | |
| } | |
| data, _ := json.Marshal(configData) | |
| os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644) | |
| opts := Options{ | |
| DryRun: true, | |
| OpenClawHome: openclawHome, | |
| PicoClawHome: picoClawHome, | |
| } | |
| result, err := Run(opts) | |
| if err != nil { | |
| t.Fatalf("Run: %v", err) | |
| } | |
| picoWs := filepath.Join(picoClawHome, "workspace") | |
| if _, err := os.Stat(filepath.Join(picoWs, "SOUL.md")); !os.IsNotExist(err) { | |
| t.Error("dry run should not create files") | |
| } | |
| if _, err := os.Stat(filepath.Join(picoClawHome, "config.json")); !os.IsNotExist(err) { | |
| t.Error("dry run should not create config") | |
| } | |
| _ = result | |
| } | |
| func TestRunFullMigration(t *testing.T) { | |
| openclawHome := t.TempDir() | |
| picoClawHome := t.TempDir() | |
| wsDir := filepath.Join(openclawHome, "workspace") | |
| os.MkdirAll(wsDir, 0755) | |
| os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul from OpenClaw"), 0644) | |
| os.WriteFile(filepath.Join(wsDir, "AGENTS.md"), []byte("# Agents from OpenClaw"), 0644) | |
| os.WriteFile(filepath.Join(wsDir, "USER.md"), []byte("# User from OpenClaw"), 0644) | |
| memDir := filepath.Join(wsDir, "memory") | |
| os.MkdirAll(memDir, 0755) | |
| os.WriteFile(filepath.Join(memDir, "MEMORY.md"), []byte("# Memory notes"), 0644) | |
| configData := map[string]interface{}{ | |
| "providers": map[string]interface{}{ | |
| "anthropic": map[string]interface{}{ | |
| "apiKey": "sk-ant-migrate-test", | |
| }, | |
| "openrouter": map[string]interface{}{ | |
| "apiKey": "sk-or-migrate-test", | |
| }, | |
| }, | |
| "channels": map[string]interface{}{ | |
| "telegram": map[string]interface{}{ | |
| "enabled": true, | |
| "token": "tg-migrate-test", | |
| }, | |
| }, | |
| } | |
| data, _ := json.Marshal(configData) | |
| os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644) | |
| opts := Options{ | |
| Force: true, | |
| OpenClawHome: openclawHome, | |
| PicoClawHome: picoClawHome, | |
| } | |
| result, err := Run(opts) | |
| if err != nil { | |
| t.Fatalf("Run: %v", err) | |
| } | |
| picoWs := filepath.Join(picoClawHome, "workspace") | |
| soulData, err := os.ReadFile(filepath.Join(picoWs, "SOUL.md")) | |
| if err != nil { | |
| t.Fatalf("reading SOUL.md: %v", err) | |
| } | |
| if string(soulData) != "# Soul from OpenClaw" { | |
| t.Errorf("SOUL.md content = %q, want %q", string(soulData), "# Soul from OpenClaw") | |
| } | |
| agentsData, err := os.ReadFile(filepath.Join(picoWs, "AGENTS.md")) | |
| if err != nil { | |
| t.Fatalf("reading AGENTS.md: %v", err) | |
| } | |
| if string(agentsData) != "# Agents from OpenClaw" { | |
| t.Errorf("AGENTS.md content = %q", string(agentsData)) | |
| } | |
| memData, err := os.ReadFile(filepath.Join(picoWs, "memory", "MEMORY.md")) | |
| if err != nil { | |
| t.Fatalf("reading memory/MEMORY.md: %v", err) | |
| } | |
| if string(memData) != "# Memory notes" { | |
| t.Errorf("MEMORY.md content = %q", string(memData)) | |
| } | |
| picoConfig, err := config.LoadConfig(filepath.Join(picoClawHome, "config.json")) | |
| if err != nil { | |
| t.Fatalf("loading PicoClaw config: %v", err) | |
| } | |
| if picoConfig.Providers.Anthropic.APIKey != "sk-ant-migrate-test" { | |
| t.Errorf("Anthropic.APIKey = %q, want %q", picoConfig.Providers.Anthropic.APIKey, "sk-ant-migrate-test") | |
| } | |
| if picoConfig.Providers.OpenRouter.APIKey != "sk-or-migrate-test" { | |
| t.Errorf("OpenRouter.APIKey = %q, want %q", picoConfig.Providers.OpenRouter.APIKey, "sk-or-migrate-test") | |
| } | |
| if !picoConfig.Channels.Telegram.Enabled { | |
| t.Error("Telegram should be enabled") | |
| } | |
| if picoConfig.Channels.Telegram.Token != "tg-migrate-test" { | |
| t.Errorf("Telegram.Token = %q, want %q", picoConfig.Channels.Telegram.Token, "tg-migrate-test") | |
| } | |
| if result.FilesCopied < 3 { | |
| t.Errorf("expected at least 3 files copied, got %d", result.FilesCopied) | |
| } | |
| if !result.ConfigMigrated { | |
| t.Error("config should have been migrated") | |
| } | |
| if len(result.Errors) > 0 { | |
| t.Errorf("expected no errors, got %v", result.Errors) | |
| } | |
| } | |
| func TestRunOpenClawNotFound(t *testing.T) { | |
| opts := Options{ | |
| OpenClawHome: "/nonexistent/path/to/openclaw", | |
| PicoClawHome: t.TempDir(), | |
| } | |
| _, err := Run(opts) | |
| if err == nil { | |
| t.Fatal("expected error when OpenClaw not found") | |
| } | |
| } | |
| func TestRunMutuallyExclusiveFlags(t *testing.T) { | |
| opts := Options{ | |
| ConfigOnly: true, | |
| WorkspaceOnly: true, | |
| } | |
| _, err := Run(opts) | |
| if err == nil { | |
| t.Fatal("expected error for mutually exclusive flags") | |
| } | |
| } | |
| func TestBackupFile(t *testing.T) { | |
| tmpDir := t.TempDir() | |
| filePath := filepath.Join(tmpDir, "test.md") | |
| os.WriteFile(filePath, []byte("original content"), 0644) | |
| if err := backupFile(filePath); err != nil { | |
| t.Fatalf("backupFile: %v", err) | |
| } | |
| bakPath := filePath + ".bak" | |
| bakData, err := os.ReadFile(bakPath) | |
| if err != nil { | |
| t.Fatalf("reading backup: %v", err) | |
| } | |
| if string(bakData) != "original content" { | |
| t.Errorf("backup content = %q, want %q", string(bakData), "original content") | |
| } | |
| } | |
| func TestCopyFile(t *testing.T) { | |
| tmpDir := t.TempDir() | |
| srcPath := filepath.Join(tmpDir, "src.md") | |
| dstPath := filepath.Join(tmpDir, "dst.md") | |
| os.WriteFile(srcPath, []byte("file content"), 0644) | |
| if err := copyFile(srcPath, dstPath); err != nil { | |
| t.Fatalf("copyFile: %v", err) | |
| } | |
| data, err := os.ReadFile(dstPath) | |
| if err != nil { | |
| t.Fatalf("reading copy: %v", err) | |
| } | |
| if string(data) != "file content" { | |
| t.Errorf("copy content = %q, want %q", string(data), "file content") | |
| } | |
| } | |
| func TestRunConfigOnly(t *testing.T) { | |
| openclawHome := t.TempDir() | |
| picoClawHome := t.TempDir() | |
| wsDir := filepath.Join(openclawHome, "workspace") | |
| os.MkdirAll(wsDir, 0755) | |
| os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0644) | |
| configData := map[string]interface{}{ | |
| "providers": map[string]interface{}{ | |
| "anthropic": map[string]interface{}{ | |
| "apiKey": "sk-config-only", | |
| }, | |
| }, | |
| } | |
| data, _ := json.Marshal(configData) | |
| os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644) | |
| opts := Options{ | |
| Force: true, | |
| ConfigOnly: true, | |
| OpenClawHome: openclawHome, | |
| PicoClawHome: picoClawHome, | |
| } | |
| result, err := Run(opts) | |
| if err != nil { | |
| t.Fatalf("Run: %v", err) | |
| } | |
| if !result.ConfigMigrated { | |
| t.Error("config should have been migrated") | |
| } | |
| picoWs := filepath.Join(picoClawHome, "workspace") | |
| if _, err := os.Stat(filepath.Join(picoWs, "SOUL.md")); !os.IsNotExist(err) { | |
| t.Error("config-only should not copy workspace files") | |
| } | |
| } | |
| func TestRunWorkspaceOnly(t *testing.T) { | |
| openclawHome := t.TempDir() | |
| picoClawHome := t.TempDir() | |
| wsDir := filepath.Join(openclawHome, "workspace") | |
| os.MkdirAll(wsDir, 0755) | |
| os.WriteFile(filepath.Join(wsDir, "SOUL.md"), []byte("# Soul"), 0644) | |
| configData := map[string]interface{}{ | |
| "providers": map[string]interface{}{ | |
| "anthropic": map[string]interface{}{ | |
| "apiKey": "sk-ws-only", | |
| }, | |
| }, | |
| } | |
| data, _ := json.Marshal(configData) | |
| os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644) | |
| opts := Options{ | |
| Force: true, | |
| WorkspaceOnly: true, | |
| OpenClawHome: openclawHome, | |
| PicoClawHome: picoClawHome, | |
| } | |
| result, err := Run(opts) | |
| if err != nil { | |
| t.Fatalf("Run: %v", err) | |
| } | |
| if result.ConfigMigrated { | |
| t.Error("workspace-only should not migrate config") | |
| } | |
| picoWs := filepath.Join(picoClawHome, "workspace") | |
| soulData, err := os.ReadFile(filepath.Join(picoWs, "SOUL.md")) | |
| if err != nil { | |
| t.Fatalf("reading SOUL.md: %v", err) | |
| } | |
| if string(soulData) != "# Soul" { | |
| t.Errorf("SOUL.md content = %q", string(soulData)) | |
| } | |
| } | |