mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
dea06c391c
* Improve the web launcher and gateway integration across backend and frontend. - add runtime model availability checks for local and OAuth-backed models - support launcher-driven gateway host overrides and websocket URL resolution - add gateway log clearing and keep incremental log sync consistent after resets - migrate session history APIs to JSONL metadata-backed storage with legacy fallback - expose session titles and improve chat history loading and error handling - move shared backend runtime helpers into the web utils package - avoid blocking web startup when automatic onboard initialization fails - add backend tests covering gateway readiness, host resolution, models, logs, and sessions * feat(agent): add skills and tools management APIs and UI - add backend APIs to list, view, import, and delete skills - add tool status and toggle endpoints with dependency-aware config updates - add agent skills/tools pages, routes, sidebar entries, and i18n strings - add backend tests for the new skills and tools flows * chore(frontend): upgrade shadcn to 4.0.5 and refresh lockfile * chore(web): keep backend dist placeholder tracked
199 lines
6.3 KiB
Go
199 lines
6.3 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"runtime"
|
|
"testing"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/config"
|
|
)
|
|
|
|
func TestHandleListTools(t *testing.T) {
|
|
configPath, cleanup := setupOAuthTestEnv(t)
|
|
defer cleanup()
|
|
|
|
cfg, err := config.LoadConfig(configPath)
|
|
if err != nil {
|
|
t.Fatalf("LoadConfig() error = %v", err)
|
|
}
|
|
cfg.Tools.ReadFile.Enabled = true
|
|
cfg.Tools.WriteFile.Enabled = false
|
|
cfg.Tools.Cron.Enabled = true
|
|
cfg.Tools.FindSkills.Enabled = true
|
|
cfg.Tools.Skills.Enabled = true
|
|
cfg.Tools.Spawn.Enabled = true
|
|
cfg.Tools.Subagent.Enabled = false
|
|
cfg.Tools.MCP.Enabled = true
|
|
cfg.Tools.MCP.Discovery.Enabled = true
|
|
cfg.Tools.MCP.Discovery.UseRegex = true
|
|
cfg.Tools.MCP.Discovery.UseBM25 = false
|
|
err = config.SaveConfig(configPath, cfg)
|
|
if err != nil {
|
|
t.Fatalf("SaveConfig() error = %v", err)
|
|
}
|
|
|
|
h := NewHandler(configPath)
|
|
mux := http.NewServeMux()
|
|
h.RegisterRoutes(mux)
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/tools", nil)
|
|
mux.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
|
}
|
|
|
|
var resp toolSupportResponse
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("Unmarshal() error = %v", err)
|
|
}
|
|
gotTools := make(map[string]toolSupportItem, len(resp.Tools))
|
|
for _, tool := range resp.Tools {
|
|
gotTools[tool.Name] = tool
|
|
}
|
|
if gotTools["read_file"].Status != "enabled" {
|
|
t.Fatalf("read_file status = %q, want enabled", gotTools["read_file"].Status)
|
|
}
|
|
if gotTools["write_file"].Status != "disabled" {
|
|
t.Fatalf("write_file status = %q, want disabled", gotTools["write_file"].Status)
|
|
}
|
|
if gotTools["cron"].Status != "enabled" {
|
|
t.Fatalf("cron status = %q, want enabled", gotTools["cron"].Status)
|
|
}
|
|
if gotTools["spawn"].Status != "blocked" || gotTools["spawn"].ReasonCode != "requires_subagent" {
|
|
t.Fatalf("spawn = %#v, want blocked/requires_subagent", gotTools["spawn"])
|
|
}
|
|
if gotTools["find_skills"].Status != "enabled" {
|
|
t.Fatalf("find_skills status = %q, want enabled", gotTools["find_skills"].Status)
|
|
}
|
|
if gotTools["tool_search_tool_regex"].Status != "enabled" {
|
|
t.Fatalf("tool_search_tool_regex status = %q, want enabled", gotTools["tool_search_tool_regex"].Status)
|
|
}
|
|
if gotTools["tool_search_tool_regex"].ConfigKey != "mcp.discovery.use_regex" {
|
|
t.Fatalf(
|
|
"tool_search_tool_regex config_key = %q, want mcp.discovery.use_regex",
|
|
gotTools["tool_search_tool_regex"].ConfigKey,
|
|
)
|
|
}
|
|
if gotTools["tool_search_tool_bm25"].Status != "disabled" {
|
|
t.Fatalf("tool_search_tool_bm25 status = %q, want disabled", gotTools["tool_search_tool_bm25"].Status)
|
|
}
|
|
if gotTools["tool_search_tool_bm25"].ConfigKey != "mcp.discovery.use_bm25" {
|
|
t.Fatalf(
|
|
"tool_search_tool_bm25 config_key = %q, want mcp.discovery.use_bm25",
|
|
gotTools["tool_search_tool_bm25"].ConfigKey,
|
|
)
|
|
}
|
|
if runtime.GOOS == "linux" {
|
|
if gotTools["i2c"].Status != "disabled" {
|
|
t.Fatalf("i2c status = %q, want disabled on linux when config is off", gotTools["i2c"].Status)
|
|
}
|
|
} else {
|
|
cfg.Tools.I2C.Enabled = true
|
|
cfg.Tools.SPI.Enabled = true
|
|
if err := config.SaveConfig(configPath, cfg); err != nil {
|
|
t.Fatalf("SaveConfig() error = %v", err)
|
|
}
|
|
|
|
rec = httptest.NewRecorder()
|
|
req = httptest.NewRequest(http.MethodGet, "/api/tools", nil)
|
|
mux.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
|
}
|
|
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("Unmarshal() error = %v", err)
|
|
}
|
|
gotTools = make(map[string]toolSupportItem, len(resp.Tools))
|
|
for _, tool := range resp.Tools {
|
|
gotTools[tool.Name] = tool
|
|
}
|
|
|
|
if gotTools["i2c"].Status != "blocked" || gotTools["i2c"].ReasonCode != "requires_linux" {
|
|
t.Fatalf("i2c = %#v, want blocked/requires_linux", gotTools["i2c"])
|
|
}
|
|
if gotTools["spi"].Status != "blocked" || gotTools["spi"].ReasonCode != "requires_linux" {
|
|
t.Fatalf("spi = %#v, want blocked/requires_linux", gotTools["spi"])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHandleUpdateToolState(t *testing.T) {
|
|
configPath, cleanup := setupOAuthTestEnv(t)
|
|
defer cleanup()
|
|
|
|
cfg, err := config.LoadConfig(configPath)
|
|
if err != nil {
|
|
t.Fatalf("LoadConfig() error = %v", err)
|
|
}
|
|
cfg.Tools.Spawn.Enabled = false
|
|
cfg.Tools.Subagent.Enabled = false
|
|
cfg.Tools.Cron.Enabled = false
|
|
cfg.Tools.MCP.Enabled = false
|
|
cfg.Tools.MCP.Discovery.Enabled = false
|
|
cfg.Tools.MCP.Discovery.UseRegex = false
|
|
err = config.SaveConfig(configPath, cfg)
|
|
if err != nil {
|
|
t.Fatalf("SaveConfig() error = %v", err)
|
|
}
|
|
|
|
h := NewHandler(configPath)
|
|
mux := http.NewServeMux()
|
|
h.RegisterRoutes(mux)
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(
|
|
http.MethodPut,
|
|
"/api/tools/spawn/state",
|
|
bytes.NewBufferString(`{"enabled":true}`),
|
|
)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
mux.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("spawn status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
|
}
|
|
|
|
rec2 := httptest.NewRecorder()
|
|
req2 := httptest.NewRequest(
|
|
http.MethodPut,
|
|
"/api/tools/tool_search_tool_regex/state",
|
|
bytes.NewBufferString(`{"enabled":true}`),
|
|
)
|
|
req2.Header.Set("Content-Type", "application/json")
|
|
mux.ServeHTTP(rec2, req2)
|
|
if rec2.Code != http.StatusOK {
|
|
t.Fatalf("regex status = %d, want %d, body=%s", rec2.Code, http.StatusOK, rec2.Body.String())
|
|
}
|
|
|
|
rec3 := httptest.NewRecorder()
|
|
req3 := httptest.NewRequest(
|
|
http.MethodPut,
|
|
"/api/tools/cron/state",
|
|
bytes.NewBufferString(`{"enabled":true}`),
|
|
)
|
|
req3.Header.Set("Content-Type", "application/json")
|
|
mux.ServeHTTP(rec3, req3)
|
|
if rec3.Code != http.StatusOK {
|
|
t.Fatalf("cron status = %d, want %d, body=%s", rec3.Code, http.StatusOK, rec3.Body.String())
|
|
}
|
|
|
|
updated, err := config.LoadConfig(configPath)
|
|
if err != nil {
|
|
t.Fatalf("LoadConfig(updated) error = %v", err)
|
|
}
|
|
if !updated.Tools.Spawn.Enabled || !updated.Tools.Subagent.Enabled {
|
|
t.Fatalf("spawn/subagent should both be enabled: %#v", updated.Tools)
|
|
}
|
|
if !updated.Tools.MCP.Enabled || !updated.Tools.MCP.Discovery.Enabled || !updated.Tools.MCP.Discovery.UseRegex {
|
|
t.Fatalf("mcp regex discovery should be enabled: %#v", updated.Tools.MCP)
|
|
}
|
|
if !updated.Tools.Cron.Enabled {
|
|
t.Fatalf("cron should be enabled: %#v", updated.Tools.Cron)
|
|
}
|
|
}
|