refactor: stabilize runtime and unify config

This commit is contained in:
lpf
2026-03-14 21:40:12 +08:00
parent 60eee65fec
commit 341e578c9f
75 changed files with 3081 additions and 1627 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -269,6 +269,7 @@ func TestHandleWebUIConfigRequiresConfirmForProviderAPIBaseChange(t *testing.T)
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")
@@ -287,6 +288,74 @@ func TestHandleWebUIConfigRequiresConfirmForProviderAPIBaseChange(t *testing.T)
}
}
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()
@@ -414,6 +483,127 @@ func TestHandleWebUIConfigReturnsReloadHookError(t *testing.T) {
}
}
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)
}
}
func TestHandleNodeConnectRegistersAndHeartbeatsNode(t *testing.T) {
t.Parallel()
@@ -627,62 +817,6 @@ func TestHandleWebUISessionsHidesInternalSessionsByDefault(t *testing.T) {
}
}
func TestHandleWebUISubagentsRuntimeLive(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) {
switch action {
case "thread":
return map[string]interface{}{
"thread": map[string]interface{}{"thread_id": "thread-1"},
"messages": []map[string]interface{}{
{"message_id": "msg-1", "content": "hello"},
},
}, nil
case "inbox":
return map[string]interface{}{
"messages": []map[string]interface{}{
{"message_id": "msg-2", "content": "reply"},
},
}, nil
case "stream":
return map[string]interface{}{
"task": map[string]interface{}{"id": "subagent-1"},
"items": []map[string]interface{}{
{"kind": "event", "message": "progress"},
},
}, nil
default:
return map[string]interface{}{}, nil
}
})
mux := http.NewServeMux()
mux.HandleFunc("/api/subagents_runtime/live", srv.handleWebUISubagentsRuntimeLive)
httpSrv := httptest.NewServer(mux)
defer httpSrv.Close()
wsURL := "ws" + strings.TrimPrefix(httpSrv.URL, "http") + "/api/subagents_runtime/live?task_id=subagent-1&preview_task_id=subagent-1"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer conn.Close()
var msg map[string]interface{}
if err := conn.ReadJSON(&msg); err != nil {
t.Fatalf("read live snapshot: %v", err)
}
payload, _ := msg["payload"].(map[string]interface{})
thread, _ := payload["thread"].(map[string]interface{})
inbox, _ := payload["inbox"].(map[string]interface{})
preview, _ := payload["preview"].(map[string]interface{})
if thread == nil || inbox == nil || preview == nil {
t.Fatalf("expected thread/inbox/preview payload, got: %+v", msg)
}
}
func TestHandleWebUIChatLive(t *testing.T) {
t.Parallel()