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>
98 lines
3.7 KiB
Go
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")
|
|
}
|