Files
Agent-Squad/squad/intent_gate_test.go
weichen f3d000f6a6 Add the optional intent gate port with the squadintent adapter, CLI wiring, and pin cascade
Details (original message):
Add the optional intent gate port with the squadintent adapter, CLI wiring, and pin cascade.
2026-09-29 16:05:33 +00:00

141 lines
5.3 KiB
Go

package squad
import (
"context"
"strings"
"testing"
)
// stubGate 引擎级测试用闸门桩:squad 包测试不得 import agent-intent(结构解耦纪律)。
type stubGate struct {
dec *IntentDecision
err error
calls int
lastText string
}
func (g *stubGate) Decide(ctx context.Context, goal Goal, opts RunOptions) (*IntentDecision, error) {
g.calls++
g.lastText = goal.Text
return g.dec, g.err
}
func gateTestBlueprint(key string, match []string, stepID string) Blueprint {
return Blueprint{
Key: key, Title: key, Match: match,
Steps: []StepSpec{{ID: stepID, Role: "researcher", Title: "按模板执行", Targets: []string{"output.md"}}},
}
}
func newGateTestEngine(t *testing.T, gate IntentGate, bps ...Blueprint) *SquadEngine {
t.Helper()
engine, _ := newTestEngine(t, NewRulePlanner(),
map[string]map[string]string{"01_researcher": {"output.md": goodReport}}, nil)
for _, bp := range bps {
if err := engine.Blueprints().Register(bp); err != nil {
t.Fatal(err)
}
}
engine.opts.IntentGate = gate // 测试直改;产品侧经 Options 组装
return engine
}
func runGoal(t *testing.T, e *SquadEngine, text, taskKey string) *RunResult {
t.Helper()
res, err := e.Run(context.Background(), Goal{Text: text, HitlMode: HitlAuto}, RunOptions{TaskKey: taskKey})
if err != nil {
t.Fatalf("run: %v", err)
}
return res
}
// T0:闸门存在但恒不干预时,运行结果与无闸门引擎一致(等价回归)。
func TestIntentGateNilDecisionKeepsOriginalPath(t *testing.T) {
bp := gateTestBlueprint("kw_report", []string{"调研报告"}, "b1")
plain := newGateTestEngine(t, nil, bp)
gated := newGateTestEngine(t, &stubGate{}, bp)
for _, text := range []string{"帮我做一份调研报告", "随便写点什么"} {
a, b := runGoal(t, plain, text, ""), runGoal(t, gated, text, "")
if a.Status != b.Status {
t.Fatalf("status differs for %q: %s vs %s", text, a.Status, b.Status)
}
if (a.Plan == nil) != (b.Plan == nil) || len(a.Plan.Steps) != len(b.Plan.Steps) {
t.Fatalf("plan shape differs for %q: %+v vs %+v", text, a.Plan, b.Plan)
}
}
}
// 意图定向:目标文本不含蓝图关键词,无闸门走自主规划;有闸门定向到模板步骤。
func TestIntentGateBlueprintRouting(t *testing.T) {
bp := gateTestBlueprint("competitor_report", []string{"竞品分析报告"}, "b_intent")
plain := newGateTestEngine(t, nil, bp)
gated := newGateTestEngine(t, &stubGate{dec: &IntentDecision{
BlueprintKey: "competitor_report", IntentID: "demo.report.competitor",
}}, bp)
text := "给我做一份竞品情况的分析报告" // 不含关键词"竞品分析报告"
plainRes := runGoal(t, plain, text, "")
if plainRes.Plan == nil || len(plainRes.Plan.Steps) == 0 || plainRes.Plan.Steps[0].ID == "b_intent" {
t.Fatalf("without gate expected autonomous plan, got %+v", plainRes.Plan)
}
gatedRes := runGoal(t, gated, text, "")
if gatedRes.Plan == nil || len(gatedRes.Plan.Steps) != 1 || gatedRes.Plan.Steps[0].ID != "b_intent" {
t.Fatalf("with gate expected blueprint plan, got %+v", gatedRes.Plan)
}
}
// 守卫拒绝:稳定码 squad.intent_refused,运行标记失败。
func TestIntentGateRejectFailsWithStableCode(t *testing.T) {
gated := newGateTestEngine(t, &stubGate{dec: &IntentDecision{
Reject: &IntentRejection{Reason: "命中守卫意图 guard.config_change", Guidance: "请到管理界面修改"},
IntentID: "guard.config_change",
}})
res := runGoal(t, gated, "帮我把系统网关地址改了", "")
if res.Status != RunFailed {
t.Fatalf("expected failed run, got %s", res.Status)
}
if !strings.Contains(res.Error, "squad.intent_refused") {
t.Fatalf("error missing stable code: %s", res.Error)
}
}
// 意图定向到不存在的蓝图:fail-closed(同未知 backend 先例)。
func TestIntentGateUnknownBlueprintFailsClosed(t *testing.T) {
gated := newGateTestEngine(t, &stubGate{dec: &IntentDecision{BlueprintKey: "missing_bp", IntentID: "demo.x"}})
res := runGoal(t, gated, "随便什么目标", "")
if res.Status != RunFailed || !strings.Contains(res.Error, "squad.blueprint_not_found") {
t.Fatalf("expected fail-closed blueprint error, got %+v", res)
}
}
// 闸门系统错误:fail-closed 成失败运行,不静默放行。
func TestIntentGateErrorFailsClosed(t *testing.T) {
gated := newGateTestEngine(t, &stubGate{err: errf("recognizer down")})
res := runGoal(t, gated, "随便什么目标", "")
if res.Status != RunFailed || !strings.Contains(res.Error, "recognizer down") {
t.Fatalf("expected failed run, got %+v", res)
}
}
// 显式 TaskKey 优先:闸门不被调用。
func TestIntentGateExplicitTaskKeyBypassesGate(t *testing.T) {
bpA := gateTestBlueprint("competitor_report", nil, "b_intent")
bpB := gateTestBlueprint("explicit_bp", nil, "b_explicit")
gate := &stubGate{dec: &IntentDecision{BlueprintKey: "competitor_report"}}
gated := newGateTestEngine(t, gate, bpA, bpB)
res := runGoal(t, gated, "任意目标", "explicit_bp")
if gate.calls != 0 {
t.Fatalf("gate must not fire on explicit task key, calls=%d", gate.calls)
}
if res.Plan == nil || len(res.Plan.Steps) != 1 || res.Plan.Steps[0].ID != "b_explicit" {
t.Fatalf("expected explicit blueprint, got %+v", res.Plan)
}
}
func errf(s string) error { return &gateErr{msg: s} }
type gateErr struct{ msg string }
func (e *gateErr) Error() string { return e.msg }