mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-13 21:57:29 +08:00
379 lines
11 KiB
Go
379 lines
11 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
cfgpkg "github.com/YspCoder/clawgo/pkg/config"
|
|
)
|
|
|
|
func TestHandleWebUIConfigRequiresConfirmForProviderAPIBaseChange(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfgPath := filepath.Join(tmp, "config.json")
|
|
|
|
cfg := cfgpkg.DefaultConfig()
|
|
cfg.Logging.Enabled = false
|
|
pc := cfg.Models.Providers["openai"]
|
|
pc.APIBase = "https://old.example/v1"
|
|
pc.APIKey = "test-key"
|
|
cfg.Models.Providers["openai"] = pc
|
|
if err := cfgpkg.SaveConfig(cfgPath, cfg); err != nil {
|
|
t.Fatalf("save config: %v", err)
|
|
}
|
|
|
|
bodyCfg := cfgpkg.DefaultConfig()
|
|
bodyCfg.Logging.Enabled = false
|
|
bodyPC := bodyCfg.Models.Providers["openai"]
|
|
bodyPC.APIBase = "https://new.example/v1"
|
|
bodyPC.APIKey = "test-key"
|
|
bodyCfg.Models.Providers["openai"] = bodyPC
|
|
body, err := json.Marshal(bodyCfg)
|
|
if err != nil {
|
|
t.Fatalf("marshal body: %v", err)
|
|
}
|
|
|
|
srv := NewServer("127.0.0.1", 0, "", nil)
|
|
srv.SetConfigPath(cfgPath)
|
|
srv.SetConfigAfterHook(func() error { return nil })
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/config", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.handleWebUIConfig(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if !strings.Contains(rec.Body.String(), `"requires_confirm":true`) {
|
|
t.Fatalf("expected requires_confirm response, got: %s", rec.Body.String())
|
|
}
|
|
if !strings.Contains(rec.Body.String(), `models.providers.openai.api_base`) {
|
|
t.Fatalf("expected models.providers.openai.api_base in changed_fields, got: %s", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestHandleWebUIConfigAcceptsStringConfirmRisky(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfgPath := filepath.Join(tmp, "config.json")
|
|
|
|
cfg := cfgpkg.DefaultConfig()
|
|
cfg.Logging.Enabled = false
|
|
pc := cfg.Models.Providers["openai"]
|
|
pc.APIBase = "https://old.example/v1"
|
|
pc.APIKey = "test-key"
|
|
cfg.Models.Providers["openai"] = pc
|
|
if err := cfgpkg.SaveConfig(cfgPath, cfg); err != nil {
|
|
t.Fatalf("save config: %v", err)
|
|
}
|
|
|
|
bodyCfg := cfgpkg.DefaultConfig()
|
|
bodyCfg.Logging.Enabled = false
|
|
bodyPC := bodyCfg.Models.Providers["openai"]
|
|
bodyPC.APIBase = "https://new.example/v1"
|
|
bodyPC.APIKey = "test-key"
|
|
bodyCfg.Models.Providers["openai"] = bodyPC
|
|
bodyMap := map[string]interface{}{}
|
|
raw, err := json.Marshal(bodyCfg)
|
|
if err != nil {
|
|
t.Fatalf("marshal body: %v", err)
|
|
}
|
|
if err := json.Unmarshal(raw, &bodyMap); err != nil {
|
|
t.Fatalf("unmarshal body map: %v", err)
|
|
}
|
|
bodyMap["confirm_risky"] = "true"
|
|
body, err := json.Marshal(bodyMap)
|
|
if err != nil {
|
|
t.Fatalf("marshal request body: %v", err)
|
|
}
|
|
|
|
srv := NewServer("127.0.0.1", 0, "", nil)
|
|
srv.SetConfigPath(cfgPath)
|
|
srv.SetConfigAfterHook(func() error { return nil })
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/config", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.handleWebUIConfig(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestNormalizeCronJobParsesStringScheduleValues(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
job := normalizeCronJob(map[string]interface{}{
|
|
"schedule": map[string]interface{}{
|
|
"kind": "every",
|
|
"everyMs": "60000",
|
|
},
|
|
"payload": map[string]interface{}{
|
|
"message": "hello",
|
|
},
|
|
})
|
|
if got, _ := job["expr"].(string); got == "" || !strings.Contains(got, "@every") {
|
|
t.Fatalf("expected normalized @every expr, got %#v", job["expr"])
|
|
}
|
|
}
|
|
|
|
func TestHandleWebUIConfigRequiresConfirmForCustomProviderSecretChange(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfgPath := filepath.Join(tmp, "config.json")
|
|
|
|
cfg := cfgpkg.DefaultConfig()
|
|
cfg.Logging.Enabled = false
|
|
cfg.Models.Providers["backup"] = cfgpkg.ProviderConfig{
|
|
APIBase: "https://backup.example/v1",
|
|
APIKey: "old-secret",
|
|
Models: []string{"backup-model"},
|
|
Auth: "bearer",
|
|
TimeoutSec: 30,
|
|
}
|
|
if err := cfgpkg.SaveConfig(cfgPath, cfg); err != nil {
|
|
t.Fatalf("save config: %v", err)
|
|
}
|
|
|
|
bodyCfg := cfgpkg.DefaultConfig()
|
|
bodyCfg.Logging.Enabled = false
|
|
bodyCfg.Models.Providers["backup"] = cfgpkg.ProviderConfig{
|
|
APIBase: "https://backup.example/v1",
|
|
APIKey: "new-secret",
|
|
Models: []string{"backup-model"},
|
|
Auth: "bearer",
|
|
TimeoutSec: 30,
|
|
}
|
|
body, err := json.Marshal(bodyCfg)
|
|
if err != nil {
|
|
t.Fatalf("marshal body: %v", err)
|
|
}
|
|
|
|
srv := NewServer("127.0.0.1", 0, "", nil)
|
|
srv.SetConfigPath(cfgPath)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/config", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.handleWebUIConfig(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if !strings.Contains(rec.Body.String(), `"requires_confirm":true`) {
|
|
t.Fatalf("expected requires_confirm response, got: %s", rec.Body.String())
|
|
}
|
|
if !strings.Contains(rec.Body.String(), `models.providers.backup.api_key`) {
|
|
t.Fatalf("expected models.providers.backup.api_key in changed_fields, got: %s", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestHandleWebUIConfigRunsReloadHookSynchronously(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfgPath := filepath.Join(tmp, "config.json")
|
|
cfg := cfgpkg.DefaultConfig()
|
|
cfg.Logging.Enabled = false
|
|
if err := cfgpkg.SaveConfig(cfgPath, cfg); err != nil {
|
|
t.Fatalf("save config: %v", err)
|
|
}
|
|
|
|
body, err := json.Marshal(cfg)
|
|
if err != nil {
|
|
t.Fatalf("marshal body: %v", err)
|
|
}
|
|
|
|
srv := NewServer("127.0.0.1", 0, "", nil)
|
|
srv.SetConfigPath(cfgPath)
|
|
called := false
|
|
srv.SetConfigAfterHook(func() error {
|
|
called = true
|
|
return nil
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/config", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.handleWebUIConfig(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if !called {
|
|
t.Fatalf("expected reload hook to run")
|
|
}
|
|
}
|
|
|
|
func TestHandleWebUIConfigReturnsReloadHookError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfgPath := filepath.Join(tmp, "config.json")
|
|
cfg := cfgpkg.DefaultConfig()
|
|
cfg.Logging.Enabled = false
|
|
if err := cfgpkg.SaveConfig(cfgPath, cfg); err != nil {
|
|
t.Fatalf("save config: %v", err)
|
|
}
|
|
|
|
body, err := json.Marshal(cfg)
|
|
if err != nil {
|
|
t.Fatalf("marshal body: %v", err)
|
|
}
|
|
|
|
srv := NewServer("127.0.0.1", 0, "", nil)
|
|
srv.SetConfigPath(cfgPath)
|
|
srv.SetConfigAfterHook(func() error {
|
|
return fmt.Errorf("reload boom")
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/config", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.handleWebUIConfig(rec, req)
|
|
|
|
if rec.Code != http.StatusInternalServerError {
|
|
t.Fatalf("expected 500, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if !strings.Contains(rec.Body.String(), "reload failed") {
|
|
t.Fatalf("expected reload failure in body, got: %s", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestHandleWebUIConfigNormalizedGet(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfgPath := filepath.Join(tmp, "config.json")
|
|
cfg := cfgpkg.DefaultConfig()
|
|
cfg.Logging.Enabled = false
|
|
cfg.Agents.Subagents["coder"] = cfgpkg.SubagentConfig{
|
|
Enabled: true,
|
|
Role: "coding",
|
|
SystemPromptFile: "agents/coder/AGENT.md",
|
|
}
|
|
if err := cfgpkg.SaveConfig(cfgPath, cfg); err != nil {
|
|
t.Fatalf("save config: %v", err)
|
|
}
|
|
|
|
srv := NewServer("127.0.0.1", 0, "", nil)
|
|
srv.SetConfigPath(cfgPath)
|
|
req := httptest.NewRequest(http.MethodGet, "/api/config?mode=normalized", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.handleWebUIConfig(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
var payload map[string]interface{}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if payload["ok"] != true {
|
|
t.Fatalf("expected ok=true, got %#v", payload)
|
|
}
|
|
configMap, _ := payload["config"].(map[string]interface{})
|
|
coreMap, _ := configMap["core"].(map[string]interface{})
|
|
if strings.TrimSpace(fmt.Sprintf("%v", coreMap["main_agent_id"])) != "main" {
|
|
t.Fatalf("unexpected normalized config: %#v", payload)
|
|
}
|
|
}
|
|
|
|
func TestHandleWebUIConfigNormalizedPost(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
cfgPath := filepath.Join(tmp, "config.json")
|
|
cfg := cfgpkg.DefaultConfig()
|
|
cfg.Logging.Enabled = false
|
|
if err := cfgpkg.SaveConfig(cfgPath, cfg); err != nil {
|
|
t.Fatalf("save config: %v", err)
|
|
}
|
|
|
|
body := map[string]interface{}{
|
|
"confirm_risky": true,
|
|
"core": map[string]interface{}{
|
|
"default_provider": "openai",
|
|
"default_model": "gpt-5.4",
|
|
"main_agent_id": "main",
|
|
"subagents": map[string]interface{}{
|
|
"reviewer": map[string]interface{}{
|
|
"enabled": true,
|
|
"role": "testing",
|
|
"prompt": "agents/reviewer/AGENT.md",
|
|
"provider": "openai",
|
|
"tool_allowlist": []interface{}{"shell"},
|
|
"runtime_class": "provider_bound",
|
|
},
|
|
},
|
|
"tools": map[string]interface{}{"shell_enabled": true, "mcp_enabled": false},
|
|
"gateway": map[string]interface{}{"host": "127.0.0.1", "port": float64(18790)},
|
|
},
|
|
"runtime": map[string]interface{}{
|
|
"router": map[string]interface{}{
|
|
"enabled": true,
|
|
"strategy": "rules_first",
|
|
"allow_direct_agent_chat": false,
|
|
"max_hops": float64(6),
|
|
"default_timeout_sec": float64(600),
|
|
"default_wait_reply": true,
|
|
"sticky_thread_owner": true,
|
|
"rules": []interface{}{
|
|
map[string]interface{}{"agent_id": "reviewer", "keywords": []interface{}{"review"}},
|
|
},
|
|
},
|
|
"providers": map[string]interface{}{
|
|
"openai": map[string]interface{}{
|
|
"auth": "bearer",
|
|
"api_base": "https://api.openai.com/v1",
|
|
"timeout_sec": float64(30),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
raw, err := json.Marshal(body)
|
|
if err != nil {
|
|
t.Fatalf("marshal body: %v", err)
|
|
}
|
|
|
|
srv := NewServer("127.0.0.1", 0, "", nil)
|
|
srv.SetConfigPath(cfgPath)
|
|
srv.SetConfigAfterHook(func() error { return nil })
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/config?mode=normalized", bytes.NewReader(raw))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
srv.handleWebUIConfig(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
loaded, err := cfgpkg.LoadConfig(cfgPath)
|
|
if err != nil {
|
|
t.Fatalf("reload config: %v", err)
|
|
}
|
|
if !loaded.Agents.Router.Enabled {
|
|
t.Fatalf("expected router to be enabled")
|
|
}
|
|
if _, ok := loaded.Agents.Subagents["reviewer"]; !ok {
|
|
t.Fatalf("expected reviewer subagent, got %+v", loaded.Agents.Subagents)
|
|
}
|
|
}
|