mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-14 01:27:28 +08:00
262 lines
8.9 KiB
Go
262 lines
8.9 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
cfgpkg "github.com/YspCoder/clawgo/pkg/config"
|
|
"github.com/YspCoder/clawgo/pkg/nodes"
|
|
)
|
|
|
|
func TestHandleSubagentRPCSpawn(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := NewServer("127.0.0.1", 0, "", nodes.NewManager())
|
|
srv.SetSubagentHandler(func(ctx context.Context, action string, args map[string]interface{}) (interface{}, error) {
|
|
if action != "spawn" {
|
|
t.Fatalf("unexpected action: %s", action)
|
|
}
|
|
if fmt.Sprint(args["agent_id"]) != "coder" || fmt.Sprint(args["task"]) != "ship it" {
|
|
t.Fatalf("unexpected args: %+v", args)
|
|
}
|
|
return map[string]interface{}{"message": "spawned"}, nil
|
|
})
|
|
|
|
body := `{"method":"subagent.spawn","request_id":"req-1","params":{"agent_id":"coder","task":"ship it","channel":"webui","chat_id":"group"}}`
|
|
req := httptest.NewRequest(http.MethodPost, "/api/rpc/subagent", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.handleSubagentRPC(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if !strings.Contains(rec.Body.String(), `"request_id":"req-1"`) || !strings.Contains(rec.Body.String(), `"message":"spawned"`) {
|
|
t.Fatalf("unexpected rpc body: %s", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestHandleNodeRPCDispatch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := NewServer("127.0.0.1", 0, "", nodes.NewManager())
|
|
srv.SetNodeDispatchHandler(func(ctx context.Context, req nodes.Request, mode string) (nodes.Response, error) {
|
|
if req.Node != "edge-a" || req.Action != "screen_snapshot" || mode != "relay" {
|
|
t.Fatalf("unexpected request: %+v mode=%s", req, mode)
|
|
}
|
|
return nodes.Response{
|
|
OK: true,
|
|
Node: req.Node,
|
|
Action: req.Action,
|
|
Payload: map[string]interface{}{
|
|
"used_transport": "relay",
|
|
},
|
|
}, nil
|
|
})
|
|
|
|
body := `{"method":"node.dispatch","request_id":"req-2","params":{"node":"edge-a","action":"screen_snapshot","mode":"relay","args":{"quality":"high"}}}`
|
|
req := httptest.NewRequest(http.MethodPost, "/api/rpc/node", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.handleNodeRPC(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if !strings.Contains(rec.Body.String(), `"request_id":"req-2"`) || !strings.Contains(rec.Body.String(), `"used_transport":"relay"`) {
|
|
t.Fatalf("unexpected rpc body: %s", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestHandleProviderRPCListModels(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://example.invalid/v1"
|
|
pc.APIKey = "test-key"
|
|
pc.Models = []string{"gpt-5.4", "gpt-5.4-mini"}
|
|
cfg.Models.Providers["openai"] = pc
|
|
if err := cfgpkg.SaveConfig(cfgPath, cfg); err != nil {
|
|
t.Fatalf("save config: %v", err)
|
|
}
|
|
|
|
srv := NewServer("127.0.0.1", 0, "", nodes.NewManager())
|
|
srv.SetConfigPath(cfgPath)
|
|
|
|
body := `{"method":"provider.list_models","request_id":"req-p1","params":{"provider":"openai"}}`
|
|
req := httptest.NewRequest(http.MethodPost, "/api/rpc/provider", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.handleProviderRPC(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if !strings.Contains(rec.Body.String(), `"gpt-5.4"`) || !strings.Contains(rec.Body.String(), `"request_id":"req-p1"`) {
|
|
t.Fatalf("unexpected provider rpc body: %s", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestHandleProviderRPCCountTokensUnavailable(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://example.invalid/v1"
|
|
pc.APIKey = "test-key"
|
|
pc.Models = []string{"gpt-5.4"}
|
|
cfg.Models.Providers["openai"] = pc
|
|
if err := cfgpkg.SaveConfig(cfgPath, cfg); err != nil {
|
|
t.Fatalf("save config: %v", err)
|
|
}
|
|
|
|
srv := NewServer("127.0.0.1", 0, "", nodes.NewManager())
|
|
srv.SetConfigPath(cfgPath)
|
|
|
|
body := `{"method":"provider.count_tokens","request_id":"req-p2","params":{"provider":"openai","messages":[{"role":"user","content":"hello"}]}}`
|
|
req := httptest.NewRequest(http.MethodPost, "/api/rpc/provider", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.handleProviderRPC(rec, req)
|
|
if rec.Code != http.StatusServiceUnavailable {
|
|
t.Fatalf("expected 503, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if !strings.Contains(rec.Body.String(), `"code":"unavailable"`) {
|
|
t.Fatalf("expected unavailable rpc error, got: %s", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestHandleSkillsRPCView(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
workspace := filepath.Join(tmp, "workspace")
|
|
skillsDir := filepath.Join(workspace, "skills", "demo")
|
|
if err := os.MkdirAll(skillsDir, 0755); err != nil {
|
|
t.Fatalf("mkdir skills dir: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(skillsDir, "SKILL.md"), []byte(buildSkillMarkdown("demo", "Demo skill", []string{"shell"}, "Be useful")), 0644); err != nil {
|
|
t.Fatalf("write skill: %v", err)
|
|
}
|
|
|
|
srv := NewServer("127.0.0.1", 0, "", nodes.NewManager())
|
|
srv.SetWorkspacePath(workspace)
|
|
|
|
body := `{"method":"skills.view","request_id":"req-s1","params":{"id":"demo","files":true}}`
|
|
req := httptest.NewRequest(http.MethodPost, "/api/rpc/skills", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.handleSkillsRPC(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if !strings.Contains(rec.Body.String(), `"request_id":"req-s1"`) || !strings.Contains(rec.Body.String(), `SKILL.md`) {
|
|
t.Fatalf("unexpected skills rpc body: %s", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestHandleWebUISkillsUsesRPCFacade(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tmp := t.TempDir()
|
|
workspace := filepath.Join(tmp, "workspace")
|
|
skillsDir := filepath.Join(workspace, "skills")
|
|
if err := os.MkdirAll(skillsDir, 0755); err != nil {
|
|
t.Fatalf("mkdir skills dir: %v", err)
|
|
}
|
|
|
|
srv := NewServer("127.0.0.1", 0, "", nodes.NewManager())
|
|
srv.SetWorkspacePath(workspace)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/skills", strings.NewReader(`{"action":"create","name":"demo","description":"Demo skill","system_prompt":"Be useful","tools":["shell"]}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.handleWebUISkills(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if _, err := os.Stat(filepath.Join(skillsDir, "demo", "SKILL.md")); err != nil {
|
|
t.Fatalf("expected created skill file: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHandleWebUIProviderModelsUsesRPCFacade(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://example.invalid/v1"
|
|
pc.APIKey = "test-key"
|
|
pc.Models = []string{"gpt-5.4-mini"}
|
|
cfg.Models.Providers["openai"] = pc
|
|
if err := cfgpkg.SaveConfig(cfgPath, cfg); err != nil {
|
|
t.Fatalf("save config: %v", err)
|
|
}
|
|
|
|
srv := NewServer("127.0.0.1", 0, "", nodes.NewManager())
|
|
srv.SetConfigPath(cfgPath)
|
|
srv.SetConfigAfterHook(func() error { return nil })
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/provider/models", strings.NewReader(`{"provider":"openai","model":"gpt-5.4"}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.handleWebUIProviderModels(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if !strings.Contains(rec.Body.String(), `"gpt-5.4"`) {
|
|
t.Fatalf("unexpected response: %s", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestHandleWebUIProviderRuntimeUsesRPCFacade(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://example.invalid/v1"
|
|
pc.APIKey = "test-key"
|
|
pc.Models = []string{"gpt-5.4-mini"}
|
|
cfg.Models.Providers["openai"] = pc
|
|
if err := cfgpkg.SaveConfig(cfgPath, cfg); err != nil {
|
|
t.Fatalf("save config: %v", err)
|
|
}
|
|
|
|
srv := NewServer("127.0.0.1", 0, "", nodes.NewManager())
|
|
srv.SetConfigPath(cfgPath)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/provider/runtime?provider=openai&limit=5", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.handleWebUIProviderRuntime(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if !strings.Contains(rec.Body.String(), `"view"`) {
|
|
t.Fatalf("unexpected response: %s", rec.Body.String())
|
|
}
|
|
}
|