mirror of
https://github.com/ollama/ollama.git
synced 2026-04-18 16:54:13 +02:00
When launching OpenClaw without prior onboarding, run the onboarding wizard instead of going straight to gateway. This ensures proper gateway configuration (mode, token, etc.) before first use. - Add onboarded() to check for wizard.lastRunAt marker in config - Run onboard with --auth-choice skip --gateway-token ollama for fresh installs - Existing installs (onboarding completed) run gateway directly
879 lines
25 KiB
Go
879 lines
25 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestOpenclawIntegration(t *testing.T) {
|
|
c := &Openclaw{}
|
|
|
|
t.Run("String", func(t *testing.T) {
|
|
if got := c.String(); got != "OpenClaw" {
|
|
t.Errorf("String() = %q, want %q", got, "OpenClaw")
|
|
}
|
|
})
|
|
|
|
t.Run("implements Runner", func(t *testing.T) {
|
|
var _ Runner = c
|
|
})
|
|
|
|
t.Run("implements Editor", func(t *testing.T) {
|
|
var _ Editor = c
|
|
})
|
|
}
|
|
|
|
func TestOpenclawEdit(t *testing.T) {
|
|
c := &Openclaw{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
|
|
configDir := filepath.Join(tmpDir, ".openclaw")
|
|
configPath := filepath.Join(configDir, "openclaw.json")
|
|
|
|
cleanup := func() { os.RemoveAll(configDir) }
|
|
|
|
t.Run("fresh install", func(t *testing.T) {
|
|
cleanup()
|
|
if err := c.Edit([]string{"llama3.2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertOpenclawModelExists(t, configPath, "llama3.2")
|
|
assertOpenclawPrimaryModel(t, configPath, "ollama/llama3.2")
|
|
})
|
|
|
|
t.Run("multiple models - first is primary", func(t *testing.T) {
|
|
cleanup()
|
|
if err := c.Edit([]string{"llama3.2", "mistral"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertOpenclawModelExists(t, configPath, "llama3.2")
|
|
assertOpenclawModelExists(t, configPath, "mistral")
|
|
assertOpenclawPrimaryModel(t, configPath, "ollama/llama3.2")
|
|
})
|
|
|
|
t.Run("preserve other providers", func(t *testing.T) {
|
|
cleanup()
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(configPath, []byte(`{"models":{"providers":{"anthropic":{"apiKey":"xxx"}}}}`), 0o644)
|
|
if err := c.Edit([]string{"llama3.2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, _ := os.ReadFile(configPath)
|
|
var cfg map[string]any
|
|
json.Unmarshal(data, &cfg)
|
|
models := cfg["models"].(map[string]any)
|
|
providers := models["providers"].(map[string]any)
|
|
if providers["anthropic"] == nil {
|
|
t.Error("anthropic provider was removed")
|
|
}
|
|
})
|
|
|
|
t.Run("preserve top-level keys", func(t *testing.T) {
|
|
cleanup()
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(configPath, []byte(`{"theme":"dark","mcp":{"servers":{}}}`), 0o644)
|
|
if err := c.Edit([]string{"llama3.2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, _ := os.ReadFile(configPath)
|
|
var cfg map[string]any
|
|
json.Unmarshal(data, &cfg)
|
|
if cfg["theme"] != "dark" {
|
|
t.Error("theme was removed")
|
|
}
|
|
if cfg["mcp"] == nil {
|
|
t.Error("mcp was removed")
|
|
}
|
|
})
|
|
|
|
t.Run("preserve user customizations on models", func(t *testing.T) {
|
|
cleanup()
|
|
c.Edit([]string{"llama3.2"})
|
|
|
|
// User adds custom field
|
|
data, _ := os.ReadFile(configPath)
|
|
var cfg map[string]any
|
|
json.Unmarshal(data, &cfg)
|
|
models := cfg["models"].(map[string]any)
|
|
providers := models["providers"].(map[string]any)
|
|
ollama := providers["ollama"].(map[string]any)
|
|
modelList := ollama["models"].([]any)
|
|
entry := modelList[0].(map[string]any)
|
|
entry["customField"] = "user-value"
|
|
configData, _ := json.MarshalIndent(cfg, "", " ")
|
|
os.WriteFile(configPath, configData, 0o644)
|
|
|
|
// Re-run Edit
|
|
c.Edit([]string{"llama3.2"})
|
|
|
|
data, _ = os.ReadFile(configPath)
|
|
json.Unmarshal(data, &cfg)
|
|
models = cfg["models"].(map[string]any)
|
|
providers = models["providers"].(map[string]any)
|
|
ollama = providers["ollama"].(map[string]any)
|
|
modelList = ollama["models"].([]any)
|
|
entry = modelList[0].(map[string]any)
|
|
if entry["customField"] != "user-value" {
|
|
t.Error("custom field was lost")
|
|
}
|
|
})
|
|
|
|
t.Run("edit replaces models list", func(t *testing.T) {
|
|
cleanup()
|
|
c.Edit([]string{"llama3.2", "mistral"})
|
|
c.Edit([]string{"llama3.2"})
|
|
|
|
assertOpenclawModelExists(t, configPath, "llama3.2")
|
|
assertOpenclawModelNotExists(t, configPath, "mistral")
|
|
})
|
|
|
|
t.Run("empty models is no-op", func(t *testing.T) {
|
|
cleanup()
|
|
os.MkdirAll(configDir, 0o755)
|
|
original := `{"existing":"data"}`
|
|
os.WriteFile(configPath, []byte(original), 0o644)
|
|
|
|
c.Edit([]string{})
|
|
|
|
data, _ := os.ReadFile(configPath)
|
|
if string(data) != original {
|
|
t.Error("empty models should not modify file")
|
|
}
|
|
})
|
|
|
|
t.Run("corrupted JSON treated as empty", func(t *testing.T) {
|
|
cleanup()
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(configPath, []byte(`{corrupted`), 0o644)
|
|
|
|
if err := c.Edit([]string{"llama3.2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
data, _ := os.ReadFile(configPath)
|
|
var cfg map[string]any
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
t.Error("result should be valid JSON")
|
|
}
|
|
})
|
|
|
|
t.Run("wrong type models section", func(t *testing.T) {
|
|
cleanup()
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(configPath, []byte(`{"models":"not a map"}`), 0o644)
|
|
|
|
if err := c.Edit([]string{"llama3.2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertOpenclawModelExists(t, configPath, "llama3.2")
|
|
})
|
|
}
|
|
|
|
func TestOpenclawModels(t *testing.T) {
|
|
c := &Openclaw{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
|
|
t.Run("no config returns nil", func(t *testing.T) {
|
|
if models := c.Models(); len(models) > 0 {
|
|
t.Errorf("expected nil/empty, got %v", models)
|
|
}
|
|
})
|
|
|
|
t.Run("returns all ollama models", func(t *testing.T) {
|
|
configDir := filepath.Join(tmpDir, ".openclaw")
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{
|
|
"models":{"providers":{"ollama":{"models":[
|
|
{"id":"llama3.2"},
|
|
{"id":"mistral"}
|
|
]}}}
|
|
}`), 0o644)
|
|
|
|
models := c.Models()
|
|
if len(models) != 2 {
|
|
t.Errorf("expected 2 models, got %v", models)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Helper functions
|
|
func assertOpenclawModelExists(t *testing.T, path, model string) {
|
|
t.Helper()
|
|
data, _ := os.ReadFile(path)
|
|
var cfg map[string]any
|
|
json.Unmarshal(data, &cfg)
|
|
models := cfg["models"].(map[string]any)
|
|
providers := models["providers"].(map[string]any)
|
|
ollama := providers["ollama"].(map[string]any)
|
|
modelList := ollama["models"].([]any)
|
|
for _, m := range modelList {
|
|
if entry, ok := m.(map[string]any); ok {
|
|
if entry["id"] == model {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
t.Errorf("model %s not found", model)
|
|
}
|
|
|
|
func assertOpenclawModelNotExists(t *testing.T, path, model string) {
|
|
t.Helper()
|
|
data, _ := os.ReadFile(path)
|
|
var cfg map[string]any
|
|
json.Unmarshal(data, &cfg)
|
|
models, _ := cfg["models"].(map[string]any)
|
|
providers, _ := models["providers"].(map[string]any)
|
|
ollama, _ := providers["ollama"].(map[string]any)
|
|
modelList, _ := ollama["models"].([]any)
|
|
for _, m := range modelList {
|
|
if entry, ok := m.(map[string]any); ok {
|
|
if entry["id"] == model {
|
|
t.Errorf("model %s should not exist", model)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func assertOpenclawPrimaryModel(t *testing.T, path, expected string) {
|
|
t.Helper()
|
|
data, _ := os.ReadFile(path)
|
|
var cfg map[string]any
|
|
json.Unmarshal(data, &cfg)
|
|
agents := cfg["agents"].(map[string]any)
|
|
defaults := agents["defaults"].(map[string]any)
|
|
model := defaults["model"].(map[string]any)
|
|
if model["primary"] != expected {
|
|
t.Errorf("primary model = %v, want %v", model["primary"], expected)
|
|
}
|
|
}
|
|
|
|
func TestOpenclawPaths(t *testing.T) {
|
|
c := &Openclaw{}
|
|
|
|
t.Run("returns path when config exists", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
configDir := filepath.Join(tmpDir, ".openclaw")
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{}`), 0o644)
|
|
|
|
paths := c.Paths()
|
|
if len(paths) != 1 {
|
|
t.Errorf("expected 1 path, got %d", len(paths))
|
|
}
|
|
})
|
|
|
|
t.Run("returns nil when config missing", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
if paths := c.Paths(); paths != nil {
|
|
t.Errorf("expected nil, got %v", paths)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestOpenclawModelsEdgeCases(t *testing.T) {
|
|
c := &Openclaw{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
configDir := filepath.Join(tmpDir, ".openclaw")
|
|
configPath := filepath.Join(configDir, "openclaw.json")
|
|
cleanup := func() { os.RemoveAll(configDir) }
|
|
|
|
t.Run("corrupted JSON returns nil", func(t *testing.T) {
|
|
cleanup()
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(configPath, []byte(`{corrupted`), 0o644)
|
|
if models := c.Models(); models != nil {
|
|
t.Errorf("expected nil, got %v", models)
|
|
}
|
|
})
|
|
|
|
t.Run("wrong type at models level", func(t *testing.T) {
|
|
cleanup()
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(configPath, []byte(`{"models":"string"}`), 0o644)
|
|
if models := c.Models(); models != nil {
|
|
t.Errorf("expected nil, got %v", models)
|
|
}
|
|
})
|
|
|
|
t.Run("wrong type at providers level", func(t *testing.T) {
|
|
cleanup()
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(configPath, []byte(`{"models":{"providers":"string"}}`), 0o644)
|
|
if models := c.Models(); models != nil {
|
|
t.Errorf("expected nil, got %v", models)
|
|
}
|
|
})
|
|
|
|
t.Run("wrong type at ollama level", func(t *testing.T) {
|
|
cleanup()
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(configPath, []byte(`{"models":{"providers":{"ollama":"string"}}}`), 0o644)
|
|
if models := c.Models(); models != nil {
|
|
t.Errorf("expected nil, got %v", models)
|
|
}
|
|
})
|
|
|
|
t.Run("model entry missing id", func(t *testing.T) {
|
|
cleanup()
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(configPath, []byte(`{"models":{"providers":{"ollama":{"models":[{"name":"test"}]}}}}`), 0o644)
|
|
if len(c.Models()) != 0 {
|
|
t.Error("expected empty for missing id")
|
|
}
|
|
})
|
|
|
|
t.Run("model id is not string", func(t *testing.T) {
|
|
cleanup()
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(configPath, []byte(`{"models":{"providers":{"ollama":{"models":[{"id":123}]}}}}`), 0o644)
|
|
if len(c.Models()) != 0 {
|
|
t.Error("expected empty for non-string id")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestOpenclawEditSchemaFields(t *testing.T) {
|
|
c := &Openclaw{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
configPath := filepath.Join(tmpDir, ".openclaw", "openclaw.json")
|
|
|
|
if err := c.Edit([]string{"llama3.2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
data, _ := os.ReadFile(configPath)
|
|
var cfg map[string]any
|
|
json.Unmarshal(data, &cfg)
|
|
models := cfg["models"].(map[string]any)
|
|
providers := models["providers"].(map[string]any)
|
|
ollama := providers["ollama"].(map[string]any)
|
|
modelList := ollama["models"].([]any)
|
|
entry := modelList[0].(map[string]any)
|
|
|
|
// Verify required schema fields
|
|
if entry["reasoning"] != false {
|
|
t.Error("reasoning should be false")
|
|
}
|
|
if entry["input"] == nil {
|
|
t.Error("input should be set")
|
|
}
|
|
if entry["contextWindow"] == nil {
|
|
t.Error("contextWindow should be set")
|
|
}
|
|
if entry["maxTokens"] == nil {
|
|
t.Error("maxTokens should be set")
|
|
}
|
|
cost := entry["cost"].(map[string]any)
|
|
if cost["cacheRead"] == nil {
|
|
t.Error("cost.cacheRead should be set")
|
|
}
|
|
if cost["cacheWrite"] == nil {
|
|
t.Error("cost.cacheWrite should be set")
|
|
}
|
|
}
|
|
|
|
func TestOpenclawEditModelNames(t *testing.T) {
|
|
c := &Openclaw{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
configPath := filepath.Join(tmpDir, ".openclaw", "openclaw.json")
|
|
cleanup := func() { os.RemoveAll(filepath.Join(tmpDir, ".openclaw")) }
|
|
|
|
t.Run("model with colon tag", func(t *testing.T) {
|
|
cleanup()
|
|
if err := c.Edit([]string{"llama3.2:70b"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertOpenclawModelExists(t, configPath, "llama3.2:70b")
|
|
assertOpenclawPrimaryModel(t, configPath, "ollama/llama3.2:70b")
|
|
})
|
|
|
|
t.Run("model with slash", func(t *testing.T) {
|
|
cleanup()
|
|
if err := c.Edit([]string{"library/model:tag"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertOpenclawModelExists(t, configPath, "library/model:tag")
|
|
assertOpenclawPrimaryModel(t, configPath, "ollama/library/model:tag")
|
|
})
|
|
|
|
t.Run("model with hyphen", func(t *testing.T) {
|
|
cleanup()
|
|
if err := c.Edit([]string{"test-model"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertOpenclawModelExists(t, configPath, "test-model")
|
|
})
|
|
}
|
|
|
|
func TestOpenclawEditAgentsPreservation(t *testing.T) {
|
|
c := &Openclaw{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
configDir := filepath.Join(tmpDir, ".openclaw")
|
|
configPath := filepath.Join(configDir, "openclaw.json")
|
|
cleanup := func() { os.RemoveAll(configDir) }
|
|
|
|
t.Run("preserve other agent defaults", func(t *testing.T) {
|
|
cleanup()
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(configPath, []byte(`{"agents":{"defaults":{"model":{"primary":"old"},"temperature":0.7}}}`), 0o644)
|
|
|
|
c.Edit([]string{"llama3.2"})
|
|
|
|
data, _ := os.ReadFile(configPath)
|
|
var cfg map[string]any
|
|
json.Unmarshal(data, &cfg)
|
|
agents := cfg["agents"].(map[string]any)
|
|
defaults := agents["defaults"].(map[string]any)
|
|
if defaults["temperature"] != 0.7 {
|
|
t.Error("temperature setting was lost")
|
|
}
|
|
})
|
|
|
|
t.Run("preserve other agents besides defaults", func(t *testing.T) {
|
|
cleanup()
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(configPath, []byte(`{"agents":{"defaults":{},"custom-agent":{"foo":"bar"}}}`), 0o644)
|
|
|
|
c.Edit([]string{"llama3.2"})
|
|
|
|
data, _ := os.ReadFile(configPath)
|
|
var cfg map[string]any
|
|
json.Unmarshal(data, &cfg)
|
|
agents := cfg["agents"].(map[string]any)
|
|
if agents["custom-agent"] == nil {
|
|
t.Error("custom-agent was lost")
|
|
}
|
|
})
|
|
}
|
|
|
|
const testOpenclawFixture = `{
|
|
"theme": "dark",
|
|
"mcp": {"servers": {"custom": {"enabled": true}}},
|
|
"models": {
|
|
"providers": {
|
|
"anthropic": {"apiKey": "xxx"},
|
|
"ollama": {
|
|
"baseUrl": "http://127.0.0.1:11434/v1",
|
|
"models": [{"id": "old-model", "customField": "preserved"}]
|
|
}
|
|
}
|
|
},
|
|
"agents": {
|
|
"defaults": {"model": {"primary": "old"}, "temperature": 0.7},
|
|
"custom-agent": {"foo": "bar"}
|
|
}
|
|
}`
|
|
|
|
func TestOpenclawEdit_RoundTrip(t *testing.T) {
|
|
c := &Openclaw{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
configDir := filepath.Join(tmpDir, ".openclaw")
|
|
configPath := filepath.Join(configDir, "openclaw.json")
|
|
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(configPath, []byte(testOpenclawFixture), 0o644)
|
|
|
|
if err := c.Edit([]string{"llama3.2", "mistral"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
data, _ := os.ReadFile(configPath)
|
|
var cfg map[string]any
|
|
json.Unmarshal(data, &cfg)
|
|
|
|
// Verify top-level preserved
|
|
if cfg["theme"] != "dark" {
|
|
t.Error("theme not preserved")
|
|
}
|
|
mcp := cfg["mcp"].(map[string]any)
|
|
servers := mcp["servers"].(map[string]any)
|
|
if servers["custom"] == nil {
|
|
t.Error("mcp.servers.custom not preserved")
|
|
}
|
|
|
|
// Verify other providers preserved
|
|
models := cfg["models"].(map[string]any)
|
|
providers := models["providers"].(map[string]any)
|
|
if providers["anthropic"] == nil {
|
|
t.Error("anthropic provider not preserved")
|
|
}
|
|
|
|
// Verify agents preserved
|
|
agents := cfg["agents"].(map[string]any)
|
|
if agents["custom-agent"] == nil {
|
|
t.Error("custom-agent not preserved")
|
|
}
|
|
defaults := agents["defaults"].(map[string]any)
|
|
if defaults["temperature"] != 0.7 {
|
|
t.Error("temperature not preserved")
|
|
}
|
|
}
|
|
|
|
func TestOpenclawEdit_Idempotent(t *testing.T) {
|
|
c := &Openclaw{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
configDir := filepath.Join(tmpDir, ".openclaw")
|
|
configPath := filepath.Join(configDir, "openclaw.json")
|
|
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(configPath, []byte(testOpenclawFixture), 0o644)
|
|
|
|
c.Edit([]string{"llama3.2", "mistral"})
|
|
firstData, _ := os.ReadFile(configPath)
|
|
|
|
c.Edit([]string{"llama3.2", "mistral"})
|
|
secondData, _ := os.ReadFile(configPath)
|
|
|
|
if string(firstData) != string(secondData) {
|
|
t.Error("repeated edits with same models produced different results")
|
|
}
|
|
}
|
|
|
|
func TestOpenclawEdit_MultipleConsecutiveEdits(t *testing.T) {
|
|
c := &Openclaw{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
configDir := filepath.Join(tmpDir, ".openclaw")
|
|
configPath := filepath.Join(configDir, "openclaw.json")
|
|
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(configPath, []byte(testOpenclawFixture), 0o644)
|
|
|
|
for i := range 10 {
|
|
models := []string{"model-a", "model-b"}
|
|
if i%2 == 0 {
|
|
models = []string{"model-x", "model-y", "model-z"}
|
|
}
|
|
if err := c.Edit(models); err != nil {
|
|
t.Fatalf("edit %d failed: %v", i, err)
|
|
}
|
|
}
|
|
|
|
data, _ := os.ReadFile(configPath)
|
|
var cfg map[string]any
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
t.Fatalf("file is not valid JSON after multiple edits: %v", err)
|
|
}
|
|
|
|
if cfg["theme"] != "dark" {
|
|
t.Error("theme lost after multiple edits")
|
|
}
|
|
}
|
|
|
|
func TestOpenclawEdit_BackupCreated(t *testing.T) {
|
|
c := &Openclaw{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
configDir := filepath.Join(tmpDir, ".openclaw")
|
|
configPath := filepath.Join(configDir, "openclaw.json")
|
|
backupDir := filepath.Join(os.TempDir(), "ollama-backups")
|
|
|
|
os.MkdirAll(configDir, 0o755)
|
|
uniqueMarker := fmt.Sprintf("test-marker-%d", os.Getpid())
|
|
original := fmt.Sprintf(`{"theme": "%s"}`, uniqueMarker)
|
|
os.WriteFile(configPath, []byte(original), 0o644)
|
|
|
|
if err := c.Edit([]string{"model-a"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
backups, _ := filepath.Glob(filepath.Join(backupDir, "openclaw.json.*"))
|
|
foundBackup := false
|
|
for _, backup := range backups {
|
|
data, _ := os.ReadFile(backup)
|
|
if string(data) == original {
|
|
foundBackup = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !foundBackup {
|
|
t.Error("backup with original content not found")
|
|
}
|
|
}
|
|
|
|
func TestOpenclawClawdbotAlias(t *testing.T) {
|
|
for _, alias := range []string{"clawdbot", "moltbot"} {
|
|
t.Run(alias+" alias resolves to Openclaw runner", func(t *testing.T) {
|
|
r, ok := integrations[alias]
|
|
if !ok {
|
|
t.Fatalf("%s not found in integrations", alias)
|
|
}
|
|
if _, ok := r.(*Openclaw); !ok {
|
|
t.Errorf("%s integration is %T, want *Openclaw", alias, r)
|
|
}
|
|
})
|
|
|
|
t.Run(alias+" is hidden from selector", func(t *testing.T) {
|
|
if !integrationAliases[alias] {
|
|
t.Errorf("%s should be in integrationAliases", alias)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOpenclawLegacyPaths(t *testing.T) {
|
|
c := &Openclaw{}
|
|
|
|
t.Run("falls back to legacy clawdbot path", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
legacyDir := filepath.Join(tmpDir, ".clawdbot")
|
|
os.MkdirAll(legacyDir, 0o755)
|
|
os.WriteFile(filepath.Join(legacyDir, "clawdbot.json"), []byte(`{}`), 0o644)
|
|
|
|
paths := c.Paths()
|
|
if len(paths) != 1 {
|
|
t.Fatalf("expected 1 path, got %d", len(paths))
|
|
}
|
|
if paths[0] != filepath.Join(legacyDir, "clawdbot.json") {
|
|
t.Errorf("expected legacy path, got %s", paths[0])
|
|
}
|
|
})
|
|
|
|
t.Run("prefers new path over legacy", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
newDir := filepath.Join(tmpDir, ".openclaw")
|
|
legacyDir := filepath.Join(tmpDir, ".clawdbot")
|
|
os.MkdirAll(newDir, 0o755)
|
|
os.MkdirAll(legacyDir, 0o755)
|
|
os.WriteFile(filepath.Join(newDir, "openclaw.json"), []byte(`{}`), 0o644)
|
|
os.WriteFile(filepath.Join(legacyDir, "clawdbot.json"), []byte(`{}`), 0o644)
|
|
|
|
paths := c.Paths()
|
|
if len(paths) != 1 {
|
|
t.Fatalf("expected 1 path, got %d", len(paths))
|
|
}
|
|
if paths[0] != filepath.Join(newDir, "openclaw.json") {
|
|
t.Errorf("expected new path, got %s", paths[0])
|
|
}
|
|
})
|
|
|
|
t.Run("Models reads from legacy path", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
legacyDir := filepath.Join(tmpDir, ".clawdbot")
|
|
os.MkdirAll(legacyDir, 0o755)
|
|
os.WriteFile(filepath.Join(legacyDir, "clawdbot.json"), []byte(`{
|
|
"models":{"providers":{"ollama":{"models":[{"id":"llama3.2"}]}}}
|
|
}`), 0o644)
|
|
|
|
models := c.Models()
|
|
if len(models) != 1 || models[0] != "llama3.2" {
|
|
t.Errorf("expected [llama3.2], got %v", models)
|
|
}
|
|
})
|
|
|
|
t.Run("Models prefers new path over legacy", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
newDir := filepath.Join(tmpDir, ".openclaw")
|
|
legacyDir := filepath.Join(tmpDir, ".clawdbot")
|
|
os.MkdirAll(newDir, 0o755)
|
|
os.MkdirAll(legacyDir, 0o755)
|
|
os.WriteFile(filepath.Join(newDir, "openclaw.json"), []byte(`{
|
|
"models":{"providers":{"ollama":{"models":[{"id":"new-model"}]}}}
|
|
}`), 0o644)
|
|
os.WriteFile(filepath.Join(legacyDir, "clawdbot.json"), []byte(`{
|
|
"models":{"providers":{"ollama":{"models":[{"id":"legacy-model"}]}}}
|
|
}`), 0o644)
|
|
|
|
models := c.Models()
|
|
if len(models) != 1 || models[0] != "new-model" {
|
|
t.Errorf("expected [new-model], got %v", models)
|
|
}
|
|
})
|
|
|
|
t.Run("Edit reads new path over legacy when both exist", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
newDir := filepath.Join(tmpDir, ".openclaw")
|
|
legacyDir := filepath.Join(tmpDir, ".clawdbot")
|
|
os.MkdirAll(newDir, 0o755)
|
|
os.MkdirAll(legacyDir, 0o755)
|
|
os.WriteFile(filepath.Join(newDir, "openclaw.json"), []byte(`{"theme":"new"}`), 0o644)
|
|
os.WriteFile(filepath.Join(legacyDir, "clawdbot.json"), []byte(`{"theme":"legacy"}`), 0o644)
|
|
|
|
if err := c.Edit([]string{"llama3.2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
data, _ := os.ReadFile(filepath.Join(newDir, "openclaw.json"))
|
|
var cfg map[string]any
|
|
json.Unmarshal(data, &cfg)
|
|
if cfg["theme"] != "new" {
|
|
t.Errorf("expected theme from new config, got %v", cfg["theme"])
|
|
}
|
|
})
|
|
|
|
t.Run("Edit migrates from legacy config", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
legacyDir := filepath.Join(tmpDir, ".clawdbot")
|
|
os.MkdirAll(legacyDir, 0o755)
|
|
os.WriteFile(filepath.Join(legacyDir, "clawdbot.json"), []byte(`{"theme":"dark"}`), 0o644)
|
|
|
|
if err := c.Edit([]string{"llama3.2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Should write to new path
|
|
newPath := filepath.Join(tmpDir, ".openclaw", "openclaw.json")
|
|
data, err := os.ReadFile(newPath)
|
|
if err != nil {
|
|
t.Fatal("expected new config file to be created")
|
|
}
|
|
var cfg map[string]any
|
|
json.Unmarshal(data, &cfg)
|
|
if cfg["theme"] != "dark" {
|
|
t.Error("legacy theme setting was not migrated")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestOpenclawEdit_CreatesDirectoryIfMissing(t *testing.T) {
|
|
c := &Openclaw{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
configDir := filepath.Join(tmpDir, ".openclaw")
|
|
|
|
if _, err := os.Stat(configDir); !os.IsNotExist(err) {
|
|
t.Fatal("directory should not exist before test")
|
|
}
|
|
|
|
if err := c.Edit([]string{"model-a"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := os.Stat(configDir); os.IsNotExist(err) {
|
|
t.Fatal("directory was not created")
|
|
}
|
|
}
|
|
|
|
func TestOpenclawOnboarded(t *testing.T) {
|
|
c := &Openclaw{}
|
|
|
|
t.Run("returns false when no config exists", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
if c.onboarded() {
|
|
t.Error("expected false when no config exists")
|
|
}
|
|
})
|
|
|
|
t.Run("returns false when config exists but no wizard section", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
configDir := filepath.Join(tmpDir, ".openclaw")
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{"theme":"dark"}`), 0o644)
|
|
|
|
if c.onboarded() {
|
|
t.Error("expected false when no wizard section")
|
|
}
|
|
})
|
|
|
|
t.Run("returns false when wizard section exists but no lastRunAt", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
configDir := filepath.Join(tmpDir, ".openclaw")
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{"wizard":{}}`), 0o644)
|
|
|
|
if c.onboarded() {
|
|
t.Error("expected false when wizard.lastRunAt is missing")
|
|
}
|
|
})
|
|
|
|
t.Run("returns false when wizard.lastRunAt is empty string", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
configDir := filepath.Join(tmpDir, ".openclaw")
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{"wizard":{"lastRunAt":""}}`), 0o644)
|
|
|
|
if c.onboarded() {
|
|
t.Error("expected false when wizard.lastRunAt is empty")
|
|
}
|
|
})
|
|
|
|
t.Run("returns true when wizard.lastRunAt is set", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
configDir := filepath.Join(tmpDir, ".openclaw")
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{"wizard":{"lastRunAt":"2024-01-01T00:00:00Z"}}`), 0o644)
|
|
|
|
if !c.onboarded() {
|
|
t.Error("expected true when wizard.lastRunAt is set")
|
|
}
|
|
})
|
|
|
|
t.Run("checks legacy clawdbot path", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
legacyDir := filepath.Join(tmpDir, ".clawdbot")
|
|
os.MkdirAll(legacyDir, 0o755)
|
|
os.WriteFile(filepath.Join(legacyDir, "clawdbot.json"), []byte(`{"wizard":{"lastRunAt":"2024-01-01T00:00:00Z"}}`), 0o644)
|
|
|
|
if !c.onboarded() {
|
|
t.Error("expected true when legacy config has wizard.lastRunAt")
|
|
}
|
|
})
|
|
|
|
t.Run("prefers new path over legacy", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
newDir := filepath.Join(tmpDir, ".openclaw")
|
|
legacyDir := filepath.Join(tmpDir, ".clawdbot")
|
|
os.MkdirAll(newDir, 0o755)
|
|
os.MkdirAll(legacyDir, 0o755)
|
|
// New path has no wizard marker
|
|
os.WriteFile(filepath.Join(newDir, "openclaw.json"), []byte(`{}`), 0o644)
|
|
// Legacy has wizard marker
|
|
os.WriteFile(filepath.Join(legacyDir, "clawdbot.json"), []byte(`{"wizard":{"lastRunAt":"2024-01-01T00:00:00Z"}}`), 0o644)
|
|
|
|
if c.onboarded() {
|
|
t.Error("expected false - should prefer new path which has no wizard marker")
|
|
}
|
|
})
|
|
|
|
t.Run("handles corrupted JSON gracefully", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
configDir := filepath.Join(tmpDir, ".openclaw")
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{corrupted`), 0o644)
|
|
|
|
if c.onboarded() {
|
|
t.Error("expected false for corrupted JSON")
|
|
}
|
|
})
|
|
|
|
t.Run("handles wrong type for wizard section", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
configDir := filepath.Join(tmpDir, ".openclaw")
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(filepath.Join(configDir, "openclaw.json"), []byte(`{"wizard":"not a map"}`), 0o644)
|
|
|
|
if c.onboarded() {
|
|
t.Error("expected false when wizard is wrong type")
|
|
}
|
|
})
|
|
}
|