package squadintent import ( "context" "strings" "testing" "testing/fstest" "agent-intent/intent" "agent-squad/squad" ) // catalogFS:带槽位(label/question/enum)的可调用能力 + 守卫。 func catalogFS() fstest.MapFS { return fstest.MapFS{"intents.json": &fstest.MapFile{Data: []byte(`{ "intents": [ {"id": "guard.config_change", "domain": "guard", "version": 1, "matcher": {"includes_any": ["系统配置"]}}, {"id": "demo.run.status_query", "domain": "demo", "version": 1, "description": "查询运行进度", "matcher": {"includes_any": ["进度"]}, "handler": "taskrun.query_status", "slots": [ {"key": "run_id", "required": true, "pattern": "(run-\\d+)", "label": "运行编号", "question": "要查询哪个运行?请提供 run 编号"} ]} ] }`)}} } func newCatalogGate(t *testing.T) *Gate { t.Helper() g, err := New(context.Background(), Options{ IntentOptions: intent.Options{GuardDomains: []string{"guard"}, ProtectedDomains: []string{"guard"}}, FS: catalogFS(), Roots: []string{"."}, }) if err != nil { t.Fatal(err) } return g } func TestGateCatalogMapsSpecsAndSlots(t *testing.T) { g := newCatalogGate(t) c := g.Catalog() if c == nil || len(c.Specs) != 1 { t.Fatalf("catalog specs: %+v", c) } view, ok := c.Get("demo.run.status_query") if !ok || view.Handler != "taskrun.query_status" || len(view.RequiredSlots) != 1 || view.RequiredSlots[0] != "run_id" { t.Fatalf("view: %+v", view) } if len(view.Slots) != 1 || view.Slots[0].Question == "" || view.Slots[0].Label != "运行编号" { t.Fatalf("slot view: %+v", view.Slots) } if _, ok := c.Get("guard.config_change"); ok { t.Fatal("guard spec must not appear in catalog") } if !strings.Contains(c.Markdown, "提问建议:要查询哪个运行") { t.Fatalf("markdown missing question: %s", c.Markdown) } } func TestRunnerDispatchesToHandlerAndFailsClosed(t *testing.T) { g := newCatalogGate(t) var got squad.IntentStep runner := g.Runner(map[string]IntentHandler{ "taskrun.query_status": func(ctx context.Context, step squad.IntentStep) (string, error) { got = step return "# 进度\n\nrun-42 已完成,正文满足最小长度要求用于质检演示。\n", nil }, }) res, err := runner.Run(context.Background(), squad.IntentStep{ IntentID: "demo.run.status_query", Slots: map[string]string{"run_id": "run-42"}, Targets: []string{"output.md"}, }) if err != nil || res == nil || !res.Success { t.Fatalf("run: %+v %v", res, err) } if got.Slots["run_id"] != "run-42" || string(res.Patches["output.md"]) == "" { t.Fatalf("handler dispatch: %+v", got) } // 未知意图与未注册 handler:fail-closed(无 handler 的 runner 拒绝执行)。 if _, err := runner.Run(context.Background(), squad.IntentStep{IntentID: "demo.nope"}); err == nil { t.Fatal("expected unknown intent error") } bare := g.Runner(nil) if _, err := bare.Run(context.Background(), squad.IntentStep{IntentID: "demo.run.status_query", Slots: map[string]string{"run_id": "run-1"}}); err == nil { t.Fatal("expected unregistered handler error") } else if !strings.Contains(err.Error(), "fail-closed") { t.Fatalf("error: %v", err) } } func TestEchoRunnerMaterializesCall(t *testing.T) { g := newCatalogGate(t) res, err := g.EchoRunner().Run(context.Background(), squad.IntentStep{ IntentID: "demo.run.status_query", Slots: map[string]string{"run_id": "run-7"}, Targets: []string{"output.md"}, Title: "查询进度", }) if err != nil || !res.Success { t.Fatalf("echo run: %+v %v", res, err) } text := string(res.Patches["output.md"]) for _, want := range []string{"EchoRunner", "demo.run.status_query", "run-7", "未真实执行"} { if !strings.Contains(text, want) { t.Fatalf("echo text missing %q: %s", want, text) } } } func TestNilGateCatalogAndRunner(t *testing.T) { var g *Gate if g.Catalog() != nil { t.Fatal("nil gate catalog must be nil") } if _, err := g.EchoRunner().Run(context.Background(), squad.IntentStep{IntentID: "x"}); err == nil { t.Fatal("nil gate runner must fail-closed") } } func TestR2GateReloadSeesNewIntent(t *testing.T) { fsys := fstest.MapFS{"intents.json": &fstest.MapFile{Data: []byte(`{"intents":[{"id":"demo.old","domain":"demo","version":1,"matcher":{"includes_any":["旧"]}}]}`)}} g, err := New(context.Background(), Options{ IntentOptions: intent.Options{}, FS: fsys, Roots: []string{"."}, }) if err != nil { t.Fatal(err) } if _, ok := g.Snapshot().SpecByID("demo.new"); ok { t.Fatal("new spec exists before reload") } fsys["intents.json"].Data = []byte(`{"intents":[{"id":"demo.old","domain":"demo","version":1,"matcher":{"includes_any":["旧"]}},{"id":"demo.new","domain":"demo","version":1,"matcher":{"includes_any":["新"]}}]}`) if _, err := g.Reload(context.Background()); err != nil { t.Fatal(err) } if _, ok := g.Snapshot().SpecByID("demo.new"); !ok { t.Fatal("reload did not publish demo.new") } }