Files
Agent-System-Backend/internal/plugins/host_v2_test.go
T
weichenandCursor cd5b82186d Add the public extension surface and platform services
Why: Downstream products must extend the base through a public package and host API instead of editing internal code.
Evidence: go test ./... passed, including plugin v2 assets, sidecar startup, migration v13, pending confirms, HITL timeout, hash embedder, command pairs, and the extension surface snapshot.
Impact: apiVersion 2.0-experimental rejects empty network allowlists for declarative HTTP. Hash embeddings stay opt-in via models.embedding.provider=hash. Plugin secrets resolve per owner at execution time.
Refs: docs/proposals/2026-10-01-plugin-contract-v2.md
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-01 10:35:02 +00:00

98 lines
3.7 KiB
Go

package plugins
import (
"os"
"path/filepath"
"testing"
"time"
core "agent-system-core"
)
func TestV2AssetsAndNetwork(t *testing.T) {
host, _, dir := newHost(t)
skills := filepath.Join(t.TempDir(), "skills")
host.SetSkillsRoot(skills)
root := filepath.Join(dir, "v2")
for _, sub := range []string{"skills", "mcp", "knowledge", "parsers", "agents", "channels", "pages", "tools"} {
if err := os.MkdirAll(filepath.Join(root, sub), 0o755); err != nil {
t.Fatal(err)
}
}
write := func(rel, body string) {
t.Helper()
if err := os.WriteFile(filepath.Join(root, rel), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
write("plugin.json", `{
"id":"v2","name":"V2","version":"0.1.0","apiVersion":"`+core.PluginAPIVersionExperimental+`",
"requires":{"network":["api.example.com"]},
"scope":{"tenants":["acme"]},
"backend":{"schemaVersion":1},
"provides":{"tools":["echo"],"skills":["note"],"mcpServers":["local"],"knowledge":["regs"],"parsers":["pdf"],"agents":["writer"],"channels":["feishu"],"pages":["home"]}
}`)
write("tools/echo.json", `{"name":"v2_echo","description":"e","parameters":{"type":"object"},"http":{"method":"GET","url":"https://api.example.com/x"}}`)
write("skills/note.md", "# note\n")
write("mcp/local.json", `{"name":"local","command":"echo"}`)
write("knowledge/regs.json", `{"id":"regs","title":"法规"}`)
write("parsers/pdf.json", `{"id":"pdf","title":"PDF","mime":"application/pdf"}`)
write("agents/writer.json", `{"id":"writer","title":"写作"}`)
write("channels/feishu.json", `{"id":"feishu","kind":"feishu","title":"飞书"}`)
write("pages/home.json", `{"id":"home","title":"首页","module":"./page.mjs"}`)
if err := host.LoadDir()["v2"]; err != nil {
t.Fatal(err)
}
ld := host.List()[0]
if ld.Skills["note"] == "" || len(ld.MCP) != 1 || len(ld.Pages) != 1 || len(ld.Channels) != 1 {
t.Fatalf("assets: %+v", ld)
}
if _, err := os.Stat(filepath.Join(skills, "plugin-v2", "note", "SKILL.md")); err != nil {
t.Fatal(err)
}
if got := host.ListFor("other", ""); len(got) != 0 {
t.Fatalf("scope leak: %d", len(got))
}
if got := host.ListFor("acme", ""); len(got) != 1 {
t.Fatalf("scope miss: %d", len(got))
}
if err := host.st.SetPluginEnabled("acme", "v2", false); err != nil {
t.Fatal(err)
}
if got := host.ListFor("acme", ""); len(got) != 0 {
t.Fatal("grant should hide plugin")
}
}
func TestExperimentalNetworkDeny(t *testing.T) {
host, _, dir := newHost(t)
root := filepath.Join(dir, "open")
os.MkdirAll(filepath.Join(root, "tools"), 0o755)
os.WriteFile(filepath.Join(root, "plugin.json"), []byte(`{"id":"open","name":"O","version":"0.1.0","apiVersion":"`+core.PluginAPIVersionExperimental+`","provides":{"tools":["e"]}}`), 0o644)
os.WriteFile(filepath.Join(root, "tools", "e.json"), []byte(`{"name":"open_e","description":"e","parameters":{"type":"object"},"http":{"method":"GET","url":"https://example.com"}}`), 0o644)
if err := host.LoadDir()["open"]; err == nil {
t.Fatal("v2 without network allowlist must fail closed")
}
}
func TestSidecarTouch(t *testing.T) {
host, _, dir := newHost(t)
host.SetHostAPI("http://127.0.0.1:1", "tok")
root := filepath.Join(dir, "side")
os.MkdirAll(root, 0o755)
mark := filepath.Join(t.TempDir(), "up")
os.WriteFile(filepath.Join(root, "plugin.json"), []byte(`{"id":"side","name":"S","version":"0.1.0","apiVersion":"1.0","backend":{"mode":"sidecar","command":"/bin/touch","args":["`+mark+`"],"schemaVersion":1},"provides":{}}`), 0o644)
if err := host.LoadDir()["side"]; err != nil {
t.Fatal(err)
}
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
if _, err := os.Stat(mark); err == nil {
return
}
time.Sleep(20 * time.Millisecond)
}
t.Fatal("sidecar did not run")
}