855 lines
23 KiB
Go
855 lines
23 KiB
Go
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))
|
|
}
|
|
}
|