ci / vet-and-test (pull_request) Successful in 47s
Why: compiled-in plugins (generative-ui, rdm) never appeared in the admin plugins list: GET /api/plugins only scanned the directory host, so binary- level capabilities were invisible to operators (user-reported gap). Evidence: go test ./internal/app/ ./internal/server/ green locally incl. new TestApplyCompiledRecordsSummary and TestListPluginsMergesCompiled; CI (make test) is authoritative. Impact: applyCompiled records each plugin's registered commands/intents/ skills into a summary handed to the server; /api/plugins merges synthesized inproc entries (backend.mode=inproc, apiVersion=extension surface version, provides from the registration record). Directory entries win on id collision. No public extension-surface change. Refs: WP3-I4; task card feature-plugins-compiled-listing.
125 lines
4.0 KiB
Go
125 lines
4.0 KiB
Go
package server
|
||
|
||
import (
|
||
"encoding/json"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"os"
|
||
"path/filepath"
|
||
"testing"
|
||
|
||
"agent-system-backend/internal/bus"
|
||
"agent-system-backend/internal/plugins"
|
||
"agent-system-backend/internal/profiles"
|
||
"agent-system-backend/internal/runmgr"
|
||
"agent-system-backend/internal/runtime"
|
||
"agent-system-backend/internal/settings"
|
||
"agent-system-backend/internal/store"
|
||
"agent-system-backend/internal/tools"
|
||
"agent-system-backend/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)
|
||
}
|
||
}
|