package server import ( "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/bus" "git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/plugins" "git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/profiles" "git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/runmgr" "git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/runtime" "git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/settings" "git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/store" "git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/tools" "git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/pkg/extension" ) type compiledListView struct { Manifest struct { ID string `json:"id"` Name string `json:"name"` Version string `json:"version"` APIVersion string `json:"apiVersion"` Provides struct { Commands []string `json:"commands"` Intents []string `json:"intents"` } `json:"provides"` Backend struct { Mode string `json:"mode"` } `json:"backend"` } `json:"manifest"` } // WP3-I4:/api/plugins 合并编译插件条目——backend.mode=inproc、apiVersion 取 // 扩展面版本、provides 来自注册面摘要;目录插件同名时目录条目权威不重复。 func TestListPluginsMergesCompiled(t *testing.T) { dir := t.TempDir() st, err := store.OpenSQLite(filepath.Join(dir, "test.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { st.Close() }) b := bus.New() reg := tools.NewRegistry() reg.RegisterBuiltin() setSvc := settings.NewService(st) profSvc := profiles.New(st) profSvc.EnsureDefault() mgr := runmgr.New(st, b, reg, profSvc, setSvc, &runtime.LLMRunner{}, &runtime.SubLLMRunner{}, &runtime.MockRunner{}, &runtime.MockSubagent{}) pluginsRoot := filepath.Join(dir, "plugins") // 目录插件与编译条目同名:目录条目权威,编译合成条目不重复列出。 if err := os.MkdirAll(filepath.Join(pluginsRoot, "generative-ui"), 0o755); err != nil { t.Fatal(err) } manifest := `{"id":"generative-ui","name":"目录版","version":"1.2.3","apiVersion":"1.0","provides":{}}` if err := os.WriteFile(filepath.Join(pluginsRoot, "generative-ui", "plugin.json"), []byte(manifest), 0o644); err != nil { t.Fatal(err) } ph := plugins.New(st, reg, setSvc, pluginsRoot) for id, err := range ph.LoadDir() { if err != nil { t.Fatalf("LoadDir %s: %v", id, err) } } srv := New(st, mgr, reg, profSvc, setSvc, ph) srv.Auth = NewAuthenticator(st, true) srv.CompiledPlugins = []CompiledPlugin{ {ID: "rdm", Commands: []string{"rdm.ingest", "rdm.inspect.capabilities"}, Intents: []string{"rdm.write-proposal"}}, {ID: "generative-ui", Commands: []string{"generative-ui.present"}}, } h := srv.Handler() key, err := st.CreateAPIKey("alice", "e2e") if err != nil { t.Fatal(err) } req := httptest.NewRequest(http.MethodGet, "/api/plugins", nil) req.Header.Set("Authorization", "Bearer "+key) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status %d: %s", rec.Code, rec.Body.String()) } var list []compiledListView if err := json.Unmarshal(rec.Body.Bytes(), &list); err != nil { t.Fatal(err) } var rdm, genui *compiledListView genuiCount := 0 for i := range list { switch list[i].Manifest.ID { case "rdm": rdm = &list[i] case "generative-ui": genui = &list[i] genuiCount++ } } if rdm == nil { t.Fatalf("compiled rdm entry missing: %s", rec.Body.String()) } if rdm.Manifest.Name != "rdm" || rdm.Manifest.Backend.Mode != "inproc" { t.Fatalf("rdm entry = %+v", rdm.Manifest) } if rdm.Manifest.APIVersion != extension.SurfaceVersion { t.Fatalf("rdm apiVersion = %q, want %q", rdm.Manifest.APIVersion, extension.SurfaceVersion) } if len(rdm.Manifest.Provides.Commands) != 2 || len(rdm.Manifest.Provides.Intents) != 1 { t.Fatalf("rdm provides = %+v", rdm.Manifest.Provides) } if genuiCount != 1 || genui == nil { t.Fatalf("generative-ui listed %d times", genuiCount) } // 目录条目权威:同名编译条目不重复、manifest 不被合成版覆盖。 if genui.Manifest.Name != "目录版" || genui.Manifest.Version != "1.2.3" || genui.Manifest.Backend.Mode != "" { t.Fatalf("genui entry = %+v", genui.Manifest) } }