mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
Feat/update migrate (#910)
* * update migrate * * rename handlers to sources * * delete dead code * * fix go test error
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
package openclaw
|
||||
|
||||
var migrateableFiles = []string{
|
||||
"AGENTS.md",
|
||||
"SOUL.md",
|
||||
"USER.md",
|
||||
"TOOLS.md",
|
||||
"HEARTBEAT.md",
|
||||
}
|
||||
|
||||
var migrateableDirs = []string{
|
||||
"memory",
|
||||
"skills",
|
||||
}
|
||||
|
||||
var supportedChannels = map[string]bool{
|
||||
"whatsapp": true,
|
||||
"telegram": true,
|
||||
"feishu": true,
|
||||
"discord": true,
|
||||
"maixcam": true,
|
||||
"qq": true,
|
||||
"dingtalk": true,
|
||||
"slack": true,
|
||||
"line": true,
|
||||
"onebot": true,
|
||||
"wecom": true,
|
||||
"wecom_app": true,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,714 @@
|
||||
package openclaw
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadOpenClawConfig(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "openclaw.json")
|
||||
|
||||
testConfig := `{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"model": {
|
||||
"primary": "anthropic/claude-sonnet-4-20250514"
|
||||
},
|
||||
"workspace": "~/.openclaw/workspace"
|
||||
},
|
||||
"list": [
|
||||
{
|
||||
"id": "main",
|
||||
"name": "Main Agent",
|
||||
"model": {
|
||||
"primary": "openai/gpt-4o",
|
||||
"fallbacks": ["claude-3-opus"]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"channels": {
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"botToken": "test-token",
|
||||
"allowFrom": ["user1", "user2"]
|
||||
},
|
||||
"discord": {
|
||||
"enabled": true,
|
||||
"token": "discord-token"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
"providers": {
|
||||
"anthropic": {
|
||||
"api_key": "sk-ant-test",
|
||||
"base_url": "https://api.anthropic.com"
|
||||
},
|
||||
"openai": {
|
||||
"api_key": "sk-test"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
err := os.WriteFile(configPath, []byte(testConfig), 0o644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadOpenClawConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Agents == nil {
|
||||
t.Error("agents should not be nil")
|
||||
}
|
||||
|
||||
if cfg.Agents.Defaults == nil {
|
||||
t.Error("agents.defaults should not be nil")
|
||||
}
|
||||
|
||||
provider, model := cfg.GetDefaultModel()
|
||||
if provider != "anthropic" {
|
||||
t.Errorf("expected provider 'anthropic', got '%s'", provider)
|
||||
}
|
||||
if model != "claude-sonnet-4-20250514" {
|
||||
t.Errorf("expected model 'claude-sonnet-4-20250514', got '%s'", model)
|
||||
}
|
||||
|
||||
workspace := cfg.GetDefaultWorkspace()
|
||||
if workspace != "~/.picoclaw/workspace" {
|
||||
t.Errorf("expected workspace '~/.picoclaw/workspace', got '%s'", workspace)
|
||||
}
|
||||
|
||||
agents := cfg.GetAgents()
|
||||
if len(agents) != 1 {
|
||||
t.Errorf("expected 1 agent, got %d", len(agents))
|
||||
}
|
||||
if agents[0].ID != "main" {
|
||||
t.Errorf("expected agent id 'main', got '%s'", agents[0].ID)
|
||||
}
|
||||
|
||||
if cfg.Channels == nil {
|
||||
t.Error("channels should not be nil")
|
||||
}
|
||||
if cfg.Channels.Telegram == nil {
|
||||
t.Error("telegram channel should not be nil")
|
||||
}
|
||||
if cfg.Channels.Telegram.BotToken == nil || *cfg.Channels.Telegram.BotToken != "test-token" {
|
||||
t.Error("telegram bot token not parsed correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetProviderConfig(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "openclaw.json")
|
||||
|
||||
testConfig := `{
|
||||
"models": {
|
||||
"providers": {
|
||||
"anthropic": {
|
||||
"api_key": "sk-ant-test",
|
||||
"base_url": "https://api.anthropic.com",
|
||||
"max_tokens": 4096
|
||||
},
|
||||
"openai": {
|
||||
"api_key": "sk-test",
|
||||
"base_url": "https://api.openai.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
err := os.WriteFile(configPath, []byte(testConfig), 0o644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadOpenClawConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
providers := GetProviderConfig(cfg.Models)
|
||||
if len(providers) != 2 {
|
||||
t.Errorf("expected 2 providers, got %d", len(providers))
|
||||
}
|
||||
|
||||
if anthropic, ok := providers["anthropic"]; ok {
|
||||
if anthropic.APIKey != "sk-ant-test" {
|
||||
t.Errorf("expected anthropic api_key 'sk-ant-test', got '%s'", anthropic.APIKey)
|
||||
}
|
||||
if anthropic.BaseURL != "https://api.anthropic.com" {
|
||||
t.Errorf("expected anthropic base_url 'https://api.anthropic.com', got '%s'", anthropic.BaseURL)
|
||||
}
|
||||
} else {
|
||||
t.Error("anthropic provider not found")
|
||||
}
|
||||
|
||||
if openai, ok := providers["openai"]; ok {
|
||||
if openai.APIKey != "sk-test" {
|
||||
t.Errorf("expected openai api_key 'sk-test', got '%s'", openai.APIKey)
|
||||
}
|
||||
} else {
|
||||
t.Error("openai provider not found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToPicoClaw(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "openclaw.json")
|
||||
|
||||
testConfig := `{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"model": {
|
||||
"primary": "anthropic/claude-sonnet-4-20250514"
|
||||
},
|
||||
"workspace": "~/.openclaw/workspace"
|
||||
},
|
||||
"list": [
|
||||
{
|
||||
"id": "main",
|
||||
"name": "Main Agent"
|
||||
},
|
||||
{
|
||||
"id": "assistant",
|
||||
"name": "Assistant",
|
||||
"skills": ["skill1", "skill2"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"channels": {
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"botToken": "test-token",
|
||||
"allowFrom": ["user1", "user2"]
|
||||
},
|
||||
"discord": {
|
||||
"enabled": false,
|
||||
"token": "discord-token"
|
||||
},
|
||||
"whatsapp": {
|
||||
"enabled": true,
|
||||
"bridgeUrl": "http://localhost:3000"
|
||||
},
|
||||
"feishu": {
|
||||
"enabled": true,
|
||||
"appId": "app-id",
|
||||
"appSecret": "app-secret",
|
||||
"allowFrom": ["user3"]
|
||||
},
|
||||
"signal": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
"providers": {
|
||||
"anthropic": {
|
||||
"api_key": "sk-ant-test"
|
||||
},
|
||||
"openai": {
|
||||
"api_key": "sk-test"
|
||||
}
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
"entries": {
|
||||
"skill1": {}
|
||||
}
|
||||
},
|
||||
"memory": {"enabled": true},
|
||||
"cron": {"enabled": true}
|
||||
}`
|
||||
|
||||
err := os.WriteFile(configPath, []byte(testConfig), 0o644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadOpenClawConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
picoCfg, warnings, err := cfg.ConvertToPicoClaw("")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to convert config: %v", err)
|
||||
}
|
||||
|
||||
if picoCfg.Agents.Defaults.ModelName != "claude-sonnet-4-20250514" {
|
||||
t.Errorf("expected model 'claude-sonnet-4-20250514', got '%s'", picoCfg.Agents.Defaults.ModelName)
|
||||
}
|
||||
if picoCfg.Agents.Defaults.Workspace != "~/.picoclaw/workspace" {
|
||||
t.Errorf("expected workspace '~/.picoclaw/workspace', got '%s'", picoCfg.Agents.Defaults.Workspace)
|
||||
}
|
||||
|
||||
if len(picoCfg.Agents.List) != 2 {
|
||||
t.Errorf("expected 2 agents, got %d", len(picoCfg.Agents.List))
|
||||
}
|
||||
if picoCfg.Agents.List[0].ID != "main" {
|
||||
t.Errorf("expected first agent id 'main', got '%s'", picoCfg.Agents.List[0].ID)
|
||||
}
|
||||
if picoCfg.Agents.List[1].Skills == nil || len(picoCfg.Agents.List[1].Skills) != 2 {
|
||||
t.Errorf("expected 2 skills for assistant agent")
|
||||
}
|
||||
|
||||
if !picoCfg.Channels.Telegram.Enabled {
|
||||
t.Error("telegram should be enabled")
|
||||
}
|
||||
if picoCfg.Channels.Telegram.Token != "test-token" {
|
||||
t.Errorf("expected telegram token 'test-token', got '%s'", picoCfg.Channels.Telegram.Token)
|
||||
}
|
||||
|
||||
if picoCfg.Channels.WhatsApp.BridgeURL != "http://localhost:3000" {
|
||||
t.Errorf("expected whatsapp bridge URL 'http://localhost:3000', got '%s'", picoCfg.Channels.WhatsApp.BridgeURL)
|
||||
}
|
||||
|
||||
if picoCfg.Channels.Feishu.AppID != "app-id" {
|
||||
t.Errorf("expected feishu app ID 'app-id', got '%s'", picoCfg.Channels.Feishu.AppID)
|
||||
}
|
||||
|
||||
if len(picoCfg.ModelList) != 1 {
|
||||
t.Errorf("expected 1 model config (no models.json provided), got %d", len(picoCfg.ModelList))
|
||||
}
|
||||
|
||||
foundWarning := false
|
||||
for _, w := range warnings {
|
||||
if len(w) > 0 {
|
||||
foundWarning = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundWarning {
|
||||
t.Log("warnings should be generated for skills, memory, cron, and unsupported channels")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToPicoClawWithQQAndDingTalk(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "openclaw.json")
|
||||
|
||||
testConfig := `{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"model": {
|
||||
"primary": "anthropic/claude-sonnet-4-20250514"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels": {
|
||||
"qq": {
|
||||
"enabled": true,
|
||||
"appId": "qq-app-id",
|
||||
"appSecret": "qq-app-secret"
|
||||
},
|
||||
"dingtalk": {
|
||||
"enabled": true,
|
||||
"appId": "ding-app-id",
|
||||
"appSecret": "ding-app-secret"
|
||||
},
|
||||
"maixcam": {
|
||||
"enabled": true,
|
||||
"host": "192.168.1.100",
|
||||
"port": 9000
|
||||
},
|
||||
"slack": {
|
||||
"enabled": true,
|
||||
"botToken": "xoxb-test",
|
||||
"appToken": "xapp-test"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
err := os.WriteFile(configPath, []byte(testConfig), 0o644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadOpenClawConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
picoCfg, _, err := cfg.ConvertToPicoClaw("")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to convert config: %v", err)
|
||||
}
|
||||
|
||||
if !picoCfg.Channels.QQ.Enabled {
|
||||
t.Error("qq should be enabled")
|
||||
}
|
||||
if picoCfg.Channels.QQ.AppID != "qq-app-id" {
|
||||
t.Errorf("expected qq app ID 'qq-app-id', got '%s'", picoCfg.Channels.QQ.AppID)
|
||||
}
|
||||
|
||||
if !picoCfg.Channels.DingTalk.Enabled {
|
||||
t.Error("dingtalk should be enabled")
|
||||
}
|
||||
if picoCfg.Channels.DingTalk.ClientID != "ding-app-id" {
|
||||
t.Errorf("expected dingtalk client ID 'ding-app-id', got '%s'", picoCfg.Channels.DingTalk.ClientID)
|
||||
}
|
||||
|
||||
if !picoCfg.Channels.MaixCam.Enabled {
|
||||
t.Error("maixcam should be enabled")
|
||||
}
|
||||
if picoCfg.Channels.MaixCam.Host != "192.168.1.100" {
|
||||
t.Errorf("expected maixcam host '192.168.1.100', got '%s'", picoCfg.Channels.MaixCam.Host)
|
||||
}
|
||||
if picoCfg.Channels.MaixCam.Port != 9000 {
|
||||
t.Errorf("expected maixcam port 9000, got %d", picoCfg.Channels.MaixCam.Port)
|
||||
}
|
||||
|
||||
if !picoCfg.Channels.Slack.Enabled {
|
||||
t.Error("slack should be enabled")
|
||||
}
|
||||
if picoCfg.Channels.Slack.BotToken != "xoxb-test" {
|
||||
t.Errorf("expected slack bot token 'xoxb-test', got '%s'", picoCfg.Channels.Slack.BotToken)
|
||||
}
|
||||
if picoCfg.Channels.Slack.AppToken != "xapp-test" {
|
||||
t.Errorf("expected slack app token 'xapp-test', got '%s'", picoCfg.Channels.Slack.AppToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenClawAgentModel(t *testing.T) {
|
||||
model := &OpenClawAgentModel{
|
||||
Primary: strPtr("anthropic/claude-3-opus"),
|
||||
Fallbacks: []string{"claude-3-sonnet", "claude-3-haiku"},
|
||||
}
|
||||
|
||||
primary := model.GetPrimary()
|
||||
if primary != "anthropic/claude-3-opus" {
|
||||
t.Errorf("expected primary 'anthropic/claude-3-opus', got '%s'", primary)
|
||||
}
|
||||
|
||||
fallbacks := model.GetFallbacks()
|
||||
if len(fallbacks) != 2 {
|
||||
t.Errorf("expected 2 fallbacks, got %d", len(fallbacks))
|
||||
}
|
||||
|
||||
model2 := &OpenClawAgentModel{
|
||||
Simple: "claude-3-opus",
|
||||
}
|
||||
|
||||
primary2 := model2.GetPrimary()
|
||||
if primary2 != "claude-3-opus" {
|
||||
t.Errorf("expected primary 'claude-3-opus' from Simple, got '%s'", primary2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelEnabled(t *testing.T) {
|
||||
cfg := &OpenClawConfig{
|
||||
Channels: &OpenClawChannels{
|
||||
Telegram: &OpenClawTelegramConfig{
|
||||
Enabled: boolPtr(true),
|
||||
},
|
||||
Discord: &OpenClawDiscordConfig{
|
||||
Enabled: boolPtr(false),
|
||||
},
|
||||
Slack: &OpenClawSlackConfig{
|
||||
Enabled: boolPtr(true),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if !cfg.IsChannelEnabled("telegram") {
|
||||
t.Error("telegram should be enabled")
|
||||
}
|
||||
if cfg.IsChannelEnabled("discord") {
|
||||
t.Error("discord should be disabled")
|
||||
}
|
||||
if !cfg.IsChannelEnabled("slack") {
|
||||
t.Error("slack should be enabled (explicitly set)")
|
||||
}
|
||||
if cfg.IsChannelEnabled("line") {
|
||||
t.Error("line should return false (not in switch cases)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDefaultModel(t *testing.T) {
|
||||
cfg := &OpenClawConfig{
|
||||
Agents: &OpenClawAgents{
|
||||
Defaults: &OpenClawAgentDefaults{
|
||||
Model: &OpenClawAgentModel{
|
||||
Primary: strPtr("openai/gpt-4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
provider, model := cfg.GetDefaultModel()
|
||||
if provider != "openai" {
|
||||
t.Errorf("expected provider 'openai', got '%s'", provider)
|
||||
}
|
||||
if model != "gpt-4" {
|
||||
t.Errorf("expected model 'gpt-4', got '%s'", model)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDefaultModelWithNoDefaults(t *testing.T) {
|
||||
cfg := &OpenClawConfig{}
|
||||
|
||||
provider, model := cfg.GetDefaultModel()
|
||||
if provider != "anthropic" {
|
||||
t.Errorf("expected default provider 'anthropic', got '%s'", provider)
|
||||
}
|
||||
if model != "claude-sonnet-4-20250514" {
|
||||
t.Errorf("expected default model 'claude-sonnet-4-20250514', got '%s'", model)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasFunctions(t *testing.T) {
|
||||
cfg := &OpenClawConfig{
|
||||
Skills: &OpenClawSkills{Entries: map[string]json.RawMessage{"skill1": nil}},
|
||||
Memory: json.RawMessage(`{"enabled": true}`),
|
||||
Cron: json.RawMessage(`{"enabled": true}`),
|
||||
Hooks: json.RawMessage(`{"enabled": true}`),
|
||||
Session: json.RawMessage(`{"enabled": true}`),
|
||||
Auth: &OpenClawAuth{Profiles: json.RawMessage(`{"profile1": {}}`)},
|
||||
}
|
||||
|
||||
if !cfg.HasSkills() {
|
||||
t.Error("should have skills")
|
||||
}
|
||||
if !cfg.HasMemory() {
|
||||
t.Error("should have memory")
|
||||
}
|
||||
if !cfg.HasCron() {
|
||||
t.Error("should have cron")
|
||||
}
|
||||
if !cfg.HasHooks() {
|
||||
t.Error("should have hooks")
|
||||
}
|
||||
if !cfg.HasSession() {
|
||||
t.Error("should have session")
|
||||
}
|
||||
if !cfg.HasAuthProfiles() {
|
||||
t.Error("should have auth profiles")
|
||||
}
|
||||
|
||||
cfg2 := &OpenClawConfig{}
|
||||
if cfg2.HasSkills() {
|
||||
t.Error("should not have skills")
|
||||
}
|
||||
if cfg2.HasMemory() {
|
||||
t.Error("should not have memory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadOpenClawConfigFromDir(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "openclaw.json")
|
||||
|
||||
testConfig := `{"agents": {}}`
|
||||
err := os.WriteFile(configPath, []byte(testConfig), 0o644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadOpenClawConfigFromDir(tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config from dir: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Agents == nil {
|
||||
t.Error("agents should not be nil")
|
||||
}
|
||||
|
||||
_, err = LoadOpenClawConfigFromDir("/nonexistent/dir")
|
||||
if err == nil {
|
||||
t.Error("should return error for nonexistent dir")
|
||||
}
|
||||
}
|
||||
|
||||
func TestToStandardConfig(t *testing.T) {
|
||||
picoCfg := &PicoClawConfig{
|
||||
Agents: AgentsConfig{
|
||||
Defaults: AgentDefaults{
|
||||
Provider: "anthropic",
|
||||
ModelName: "claude-sonnet-4-20250514",
|
||||
Workspace: "~/.picoclaw/workspace",
|
||||
},
|
||||
List: []AgentConfig{
|
||||
{
|
||||
ID: "main",
|
||||
Name: "Main Agent",
|
||||
Default: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
ModelList: []ModelConfig{
|
||||
{
|
||||
ModelName: "claude-sonnet-4-20250514",
|
||||
Model: "anthropic/claude-sonnet-4-20250514",
|
||||
APIKey: "sk-ant-test",
|
||||
},
|
||||
},
|
||||
Channels: ChannelsConfig{
|
||||
Telegram: TelegramConfig{
|
||||
Enabled: true,
|
||||
Token: "test-token",
|
||||
AllowFrom: []string{"user1"},
|
||||
},
|
||||
WhatsApp: WhatsAppConfig{
|
||||
Enabled: true,
|
||||
BridgeURL: "http://localhost:3000",
|
||||
},
|
||||
},
|
||||
Gateway: GatewayConfig{
|
||||
Host: "0.0.0.0",
|
||||
Port: 8080,
|
||||
},
|
||||
}
|
||||
|
||||
stdCfg := picoCfg.ToStandardConfig()
|
||||
|
||||
if stdCfg.Agents.Defaults.Provider != "anthropic" {
|
||||
t.Errorf("expected provider 'anthropic', got '%s'", stdCfg.Agents.Defaults.Provider)
|
||||
}
|
||||
if stdCfg.Agents.Defaults.ModelName != "claude-sonnet-4-20250514" {
|
||||
t.Errorf("expected model name 'claude-sonnet-4-20250514', got '%s'", stdCfg.Agents.Defaults.ModelName)
|
||||
}
|
||||
if stdCfg.Agents.Defaults.Workspace != "~/.picoclaw/workspace" {
|
||||
t.Errorf("expected workspace '~/.picoclaw/workspace', got '%s'", stdCfg.Agents.Defaults.Workspace)
|
||||
}
|
||||
|
||||
if len(stdCfg.Agents.List) != 1 {
|
||||
t.Errorf("expected 1 agent, got %d", len(stdCfg.Agents.List))
|
||||
}
|
||||
if stdCfg.Agents.List[0].ID != "main" {
|
||||
t.Errorf("expected agent id 'main', got '%s'", stdCfg.Agents.List[0].ID)
|
||||
}
|
||||
|
||||
foundModel := false
|
||||
var foundAPIKey string
|
||||
for _, m := range stdCfg.ModelList {
|
||||
if m.ModelName == "claude-sonnet-4-20250514" {
|
||||
foundModel = true
|
||||
foundAPIKey = m.APIKey
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundModel {
|
||||
t.Error("expected to find claude-sonnet-4-20250514 model config")
|
||||
}
|
||||
if foundAPIKey != "sk-ant-test" {
|
||||
t.Errorf("expected api key 'sk-ant-test', got '%s'", foundAPIKey)
|
||||
}
|
||||
|
||||
if !stdCfg.Channels.Telegram.Enabled {
|
||||
t.Error("telegram should be enabled")
|
||||
}
|
||||
if stdCfg.Channels.Telegram.Token != "test-token" {
|
||||
t.Errorf("expected token 'test-token', got '%s'", stdCfg.Channels.Telegram.Token)
|
||||
}
|
||||
|
||||
if stdCfg.Gateway.Port != 8080 {
|
||||
t.Errorf("expected gateway port 8080, got %d", stdCfg.Gateway.Port)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadProviderConfigFromAgentsDir(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
agentsDir := filepath.Join(tmpDir, "agents", "main", "agent")
|
||||
err := os.MkdirAll(agentsDir, 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create agents dir: %v", err)
|
||||
}
|
||||
|
||||
modelsJSON := `{
|
||||
"providers": {
|
||||
"anthropic": {
|
||||
"baseUrl": "https://api.anthropic.com",
|
||||
"api": "anthropic",
|
||||
"apiKey": "sk-ant-from-models",
|
||||
"models": [
|
||||
{
|
||||
"id": "claude-sonnet-4-20250514",
|
||||
"name": "Claude Sonnet 4"
|
||||
}
|
||||
]
|
||||
},
|
||||
"openai": {
|
||||
"baseUrl": "https://api.openai.com",
|
||||
"api": "openai",
|
||||
"apiKey": "sk-from-models",
|
||||
"models": [
|
||||
{
|
||||
"id": "gpt-4o",
|
||||
"name": "GPT-4o"
|
||||
}
|
||||
]
|
||||
},
|
||||
"zhipu": {
|
||||
"baseUrl": "https://open.bigmodel.cn/api/paas/v4",
|
||||
"api": "openai",
|
||||
"apiKey": "zhipu-key",
|
||||
"models": []
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
err = os.WriteFile(filepath.Join(agentsDir, "models.json"), []byte(modelsJSON), 0o644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write models.json: %v", err)
|
||||
}
|
||||
|
||||
providers := GetProviderConfigFromDir(tmpDir)
|
||||
if len(providers) != 3 {
|
||||
t.Errorf("expected 3 providers, got %d", len(providers))
|
||||
}
|
||||
|
||||
if anthropic, ok := providers["anthropic"]; ok {
|
||||
if anthropic.ApiKey != "sk-ant-from-models" {
|
||||
t.Errorf("expected anthropic apiKey 'sk-ant-from-models', got '%s'", anthropic.ApiKey)
|
||||
}
|
||||
if anthropic.BaseUrl != "https://api.anthropic.com" {
|
||||
t.Errorf("expected anthropic baseUrl 'https://api.anthropic.com', got '%s'", anthropic.BaseUrl)
|
||||
}
|
||||
} else {
|
||||
t.Error("anthropic provider not found")
|
||||
}
|
||||
|
||||
if openai, ok := providers["openai"]; ok {
|
||||
if openai.ApiKey != "sk-from-models" {
|
||||
t.Errorf("expected openai apiKey 'sk-from-models', got '%s'", openai.ApiKey)
|
||||
}
|
||||
if openai.BaseUrl != "https://api.openai.com" {
|
||||
t.Errorf("expected openai baseUrl 'https://api.openai.com', got '%s'", openai.BaseUrl)
|
||||
}
|
||||
} else {
|
||||
t.Error("openai provider not found")
|
||||
}
|
||||
|
||||
if zhipu, ok := providers["zhipu"]; ok {
|
||||
if zhipu.ApiKey != "zhipu-key" {
|
||||
t.Errorf("expected zhipu apiKey 'zhipu-key', got '%s'", zhipu.ApiKey)
|
||||
}
|
||||
if zhipu.BaseUrl != "https://open.bigmodel.cn/api/paas/v4" {
|
||||
t.Errorf("expected zhipu baseUrl 'https://open.bigmodel.cn/api/paas/v4', got '%s'", zhipu.BaseUrl)
|
||||
}
|
||||
} else {
|
||||
t.Error("zhipu provider not found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetProviderConfigFromDirNotExist(t *testing.T) {
|
||||
providers := GetProviderConfigFromDir("/nonexistent/path")
|
||||
if len(providers) != 0 {
|
||||
t.Errorf("expected 0 providers for nonexistent path, got %d", len(providers))
|
||||
}
|
||||
}
|
||||
|
||||
func strPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func boolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package openclaw
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
"github.com/sipeed/picoclaw/pkg/migrate/internal"
|
||||
)
|
||||
|
||||
var providerMapping = map[string]string{
|
||||
"anthropic": "anthropic",
|
||||
"claude": "anthropic",
|
||||
"openai": "openai",
|
||||
"gpt": "openai",
|
||||
"groq": "groq",
|
||||
"ollama": "ollama",
|
||||
"openrouter": "openrouter",
|
||||
"deepseek": "deepseek",
|
||||
"together": "together",
|
||||
"mistral": "mistral",
|
||||
"fireworks": "fireworks",
|
||||
"google": "google",
|
||||
"gemini": "google",
|
||||
"xai": "xai",
|
||||
"grok": "xai",
|
||||
"cerebras": "cerebras",
|
||||
"sambanova": "sambanova",
|
||||
}
|
||||
|
||||
type OpenclawHandler struct {
|
||||
opts Options
|
||||
sourceConfigFile string
|
||||
sourceWorkspace string
|
||||
}
|
||||
|
||||
type (
|
||||
Options = internal.Options
|
||||
Action = internal.Action
|
||||
Result = internal.Result
|
||||
Operation = internal.Operation
|
||||
)
|
||||
|
||||
func NewOpenclawHandler(opts Options) (Operation, error) {
|
||||
home, err := resolveSourceHome(opts.SourceHome)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts.SourceHome = home
|
||||
|
||||
configFile, err := findSourceConfig(home)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &OpenclawHandler{
|
||||
opts: opts,
|
||||
sourceWorkspace: filepath.Join(opts.SourceHome, "workspace"),
|
||||
sourceConfigFile: configFile,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (o *OpenclawHandler) GetSourceName() string {
|
||||
return "openclaw"
|
||||
}
|
||||
|
||||
func (o *OpenclawHandler) GetSourceHome() (string, error) {
|
||||
return o.opts.SourceHome, nil
|
||||
}
|
||||
|
||||
func (o *OpenclawHandler) GetSourceWorkspace() (string, error) {
|
||||
return o.sourceWorkspace, nil
|
||||
}
|
||||
|
||||
func (o *OpenclawHandler) GetSourceConfigFile() (string, error) {
|
||||
return o.sourceConfigFile, nil
|
||||
}
|
||||
|
||||
func (o *OpenclawHandler) GetMigrateableFiles() []string {
|
||||
return migrateableFiles
|
||||
}
|
||||
|
||||
func (o *OpenclawHandler) GetMigrateableDirs() []string {
|
||||
return migrateableDirs
|
||||
}
|
||||
|
||||
func (o *OpenclawHandler) ExecuteConfigMigration(srcConfigPath, dstConfigPath string) error {
|
||||
openclawCfg, err := LoadOpenClawConfig(srcConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
picoCfg, warnings, err := openclawCfg.ConvertToPicoClaw(o.opts.SourceHome)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, w := range warnings {
|
||||
fmt.Printf(" Warning: %s\n", w)
|
||||
}
|
||||
|
||||
incoming := picoCfg.ToStandardConfig()
|
||||
if err := os.MkdirAll(filepath.Dir(dstConfigPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return config.SaveConfig(dstConfigPath, incoming)
|
||||
}
|
||||
|
||||
func resolveSourceHome(override string) (string, error) {
|
||||
if override != "" {
|
||||
return internal.ExpandHome(override), nil
|
||||
}
|
||||
if envHome := os.Getenv("OPENCLAW_HOME"); envHome != "" {
|
||||
return internal.ExpandHome(envHome), nil
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolving home directory: %w", err)
|
||||
}
|
||||
return filepath.Join(home, ".openclaw"), nil
|
||||
}
|
||||
|
||||
func findSourceConfig(sourceHome string) (string, error) {
|
||||
candidates := []string{
|
||||
filepath.Join(sourceHome, "openclaw.json"),
|
||||
filepath.Join(sourceHome, "config.json"),
|
||||
}
|
||||
for _, p := range candidates {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no config file found in %s (tried openclaw.json, config.json)", sourceHome)
|
||||
}
|
||||
|
||||
func rewriteWorkspacePath(path string) string {
|
||||
path = strings.Replace(path, ".openclaw", ".picoclaw", 1)
|
||||
return path
|
||||
}
|
||||
|
||||
func mapProvider(provider string) string {
|
||||
if mapped, ok := providerMapping[strings.ToLower(provider)]; ok {
|
||||
return mapped
|
||||
}
|
||||
return strings.ToLower(provider)
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
package openclaw
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewOpenclawHandler(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "openclaw.json")
|
||||
err := os.WriteFile(configPath, []byte("{}"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
handler, err := NewOpenclawHandler(Options{
|
||||
SourceHome: tmpDir,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, handler)
|
||||
}
|
||||
|
||||
func TestNewOpenclawHandlerNoConfig(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
_, err := NewOpenclawHandler(Options{
|
||||
SourceHome: tmpDir,
|
||||
})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestOpenclawHandlerGetSourceName(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "openclaw.json")
|
||||
err := os.WriteFile(configPath, []byte("{}"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
handler, err := NewOpenclawHandler(Options{
|
||||
SourceHome: tmpDir,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "openclaw", handler.GetSourceName())
|
||||
}
|
||||
|
||||
func TestOpenclawHandlerGetSourceHome(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "openclaw.json")
|
||||
err := os.WriteFile(configPath, []byte("{}"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
handler, err := NewOpenclawHandler(Options{
|
||||
SourceHome: tmpDir,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
home, err := handler.GetSourceHome()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tmpDir, home)
|
||||
}
|
||||
|
||||
func TestOpenclawHandlerGetSourceWorkspace(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "openclaw.json")
|
||||
err := os.WriteFile(configPath, []byte("{}"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
handler, err := NewOpenclawHandler(Options{
|
||||
SourceHome: tmpDir,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
workspace, err := handler.GetSourceWorkspace()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, filepath.Join(tmpDir, "workspace"), workspace)
|
||||
}
|
||||
|
||||
func TestOpenclawHandlerGetSourceConfigFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "openclaw.json")
|
||||
err := os.WriteFile(configPath, []byte("{}"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
handler, err := NewOpenclawHandler(Options{
|
||||
SourceHome: tmpDir,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
configFile, err := handler.GetSourceConfigFile()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, configPath, configFile)
|
||||
}
|
||||
|
||||
func TestOpenclawHandlerGetSourceConfigFileWithConfigJson(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.json")
|
||||
err := os.WriteFile(configPath, []byte("{}"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
handler, err := NewOpenclawHandler(Options{
|
||||
SourceHome: tmpDir,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
configFile, err := handler.GetSourceConfigFile()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, configPath, configFile)
|
||||
}
|
||||
|
||||
func TestOpenclawHandlerGetMigrateableFiles(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "openclaw.json")
|
||||
err := os.WriteFile(configPath, []byte("{}"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
handler, err := NewOpenclawHandler(Options{
|
||||
SourceHome: tmpDir,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
files := handler.GetMigrateableFiles()
|
||||
assert.NotEmpty(t, files)
|
||||
assert.Contains(t, files, "AGENTS.md")
|
||||
assert.Contains(t, files, "SOUL.md")
|
||||
assert.Contains(t, files, "USER.md")
|
||||
}
|
||||
|
||||
func TestOpenclawHandlerGetMigrateableDirs(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "openclaw.json")
|
||||
err := os.WriteFile(configPath, []byte("{}"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
handler, err := NewOpenclawHandler(Options{
|
||||
SourceHome: tmpDir,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
dirs := handler.GetMigrateableDirs()
|
||||
assert.NotEmpty(t, dirs)
|
||||
assert.Contains(t, dirs, "memory")
|
||||
assert.Contains(t, dirs, "skills")
|
||||
}
|
||||
|
||||
func TestResolveSourceHome(t *testing.T) {
|
||||
result, err := resolveSourceHome("/custom/path")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "/custom/path", result)
|
||||
}
|
||||
|
||||
func TestResolveSourceHomeWithEnvVar(t *testing.T) {
|
||||
t.Setenv("OPENCLAW_HOME", "/env/path")
|
||||
|
||||
result, err := resolveSourceHome("")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "/env/path", result)
|
||||
}
|
||||
|
||||
func TestResolveSourceHomeWithTilde(t *testing.T) {
|
||||
home, err := os.UserHomeDir()
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := resolveSourceHome("~/openclaw")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, filepath.Join(home, "openclaw"), result)
|
||||
}
|
||||
|
||||
func TestFindSourceConfig(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "openclaw.json")
|
||||
err := os.WriteFile(configPath, []byte("{}"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := findSourceConfig(tmpDir)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, configPath, result)
|
||||
}
|
||||
|
||||
func TestFindSourceConfigWithConfigJson(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.json")
|
||||
err := os.WriteFile(configPath, []byte("{}"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := findSourceConfig(tmpDir)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, configPath, result)
|
||||
}
|
||||
|
||||
func TestFindSourceConfigNotFound(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
_, err := findSourceConfig(tmpDir)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no config file found")
|
||||
}
|
||||
|
||||
func TestMapProvider(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"anthropic", "anthropic"},
|
||||
{"claude", "anthropic"},
|
||||
{"openai", "openai"},
|
||||
{"gpt", "openai"},
|
||||
{"groq", "groq"},
|
||||
{"ollama", "ollama"},
|
||||
{"openrouter", "openrouter"},
|
||||
{"deepseek", "deepseek"},
|
||||
{"together", "together"},
|
||||
{"mistral", "mistral"},
|
||||
{"fireworks", "fireworks"},
|
||||
{"google", "google"},
|
||||
{"gemini", "google"},
|
||||
{"xai", "xai"},
|
||||
{"grok", "xai"},
|
||||
{"cerebras", "cerebras"},
|
||||
{"sambanova", "sambanova"},
|
||||
{"unknown", "unknown"},
|
||||
{"", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := mapProvider(tt.input)
|
||||
assert.Equal(t, tt.expected, result, "mapProvider(%q)", tt.input)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteWorkspacePath(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"~/.openclaw/workspace", "~/.picoclaw/workspace"},
|
||||
{"/home/user/.openclaw/workspace", "/home/user/.picoclaw/workspace"},
|
||||
{"/path/without/openclaw/change", "/path/without/openclaw/change"},
|
||||
{"", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := rewriteWorkspacePath(tt.input)
|
||||
assert.Equal(t, tt.expected, result, "rewriteWorkspacePath(%q)", tt.input)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user