Files
Agent-System-Backend/internal/plugins/host_v2_test.go
T
weichen f1302ed5b3 Enable canonical versioned Go module consumption
Why: Allow consumers to download an immutable module revision without nested source checkouts or local path replacements.

Evidence: SELF package tests and the prior canonical-path rejection are recorded in the Dev dependency-workspace task card; remote consumption is replayed against the committed candidate.

Impact: Change the module identity and its Go imports while preserving API behavior. Existing historical submodule revisions remain buildable.

Refs: Versioned dependencies U1/U3; feature/dependency-workspace
2026-10-13 15:47:16 +00:00

129 lines
4.8 KiB
Go

package plugins
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
core "git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git"
)
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", "---\nname: note\ndescription: 笔记技能\n---\n\n# 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")
}
// N2 WP4:不合规插件技能整批拒载,错误带插件上下文与字段定位。
func TestV2InvalidSkillRejected(t *testing.T) {
host, _, dir := newHost(t)
root := filepath.Join(dir, "badskill")
if err := os.MkdirAll(filepath.Join(root, "skills"), 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":"badskill","name":"Bad","version":"0.1.0","apiVersion":"`+core.PluginAPIVersionExperimental+`",
"backend":{"schemaVersion":1},
"provides":{"skills":["note"]}
}`)
// 缺 name(开放规范必填)。
write("skills/note.md", "---\ndescription: 没有名字\n---\n正文\n")
err := host.LoadDir()["badskill"]
if err == nil {
t.Fatal("不合规技能必须拒载")
}
if !strings.Contains(err.Error(), "plugins: badskill: skill note:") ||
!strings.Contains(err.Error(), "name") {
t.Fatalf("错误须带插件上下文与字段定位:%v", err)
}
}