Files
Agent-Squad/adapters/intent/integration_test.go
T
weichenandCursor a182d7662b Move cross-library squad code into adapter modules
Why: the squad core has to compile without agent-native or agent-intent. Ports stay in the core; adapters and the CLI own the concrete clients.
Evidence: GOWORK=off go test -count=1 ./squad/ ./store/ ./textextract/ ./schema/ passed, and the intent, sqlite, native, and cmd modules passed their own go test.
Impact: core go.mod keeps error-code-system only. Nested gitlinks stay. Callers register the native backend through agent-squad/adapters/native.
Refs: D1
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 13:40:58 +00:00

132 lines
5.1 KiB
Go

package squadintent
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"agent-squad/squad"
)
// integrationScriptBackend 脚本回放后端(与 squad 测试同构,走 agent-native exec 端口)。
type integrationScriptBackend struct{ content string }
func (b *integrationScriptBackend) Name() string { return "script" }
func (b *integrationScriptBackend) Execute(ctx context.Context, req squad.ExecRequest) (*squad.ExecResult, error) {
patches := map[string][]byte{}
for _, t := range req.Targets {
out := filepath.Join(req.WorkDir, t)
_ = os.MkdirAll(filepath.Dir(out), 0o755)
if err := os.WriteFile(out, []byte(b.content), 0o644); err != nil {
return nil, err
}
patches[t] = []byte(b.content)
}
return &squad.ExecResult{Success: true, Patches: patches, Logs: "script"}, nil
}
const intentReport = "# 竞品分析报告\n\n## 概述\n\n由意图定向的蓝图模板生成的交付物,长度满足最小质检要求。\n\n## 结论\n\n符合预期。\n"
// TestEngineEndToEndWithAdapterGate 全链路:agent-intent 识别 → 闸门决策 →
// 引擎蓝图定向 → 规划 → 脚本后端执行(agent-native exec 端口)→ 质检完成。
// 目标文本不含蓝图关键词,证明路由变化来自意图而非关键词匹配。
func TestEngineEndToEndWithAdapterGate(t *testing.T) {
dir := t.TempDir()
reg := squad.NewRoleRegistry()
if err := reg.Register(squad.RoleSpec{Role: "researcher", Name: "调研", Backend: "script"}); err != nil {
t.Fatal(err)
}
bps := squad.NewBlueprintRegistry()
if err := bps.Register(squad.Blueprint{
Key: "competitor_report", Title: "竞品分析报告", Match: []string{"竞品分析报告"},
Steps: []squad.StepSpec{{ID: "b1", Role: "researcher", Title: "调研并成文", Targets: []string{"output.md"}}},
}); err != nil {
t.Fatal(err)
}
backends := squad.NewBackendResolver()
backends.RegisterInstance("script", &integrationScriptBackend{content: intentReport})
o := gateOptions()
o.FS = overlayFS()
o.Roots = []string{"."}
gate, err := New(context.Background(), o)
if err != nil {
t.Fatal(err)
}
engine, err := squad.NewEngine(squad.Options{
DataDir: dir, Registry: reg, Blueprints: bps, Planner: squad.NewRulePlanner(),
Backends: backends, IntentGate: gate,
})
if err != nil {
t.Fatal(err)
}
res, err := engine.Run(context.Background(),
squad.Goal{Text: "给我做一份竞品情况的分析报告", HitlMode: squad.HitlAuto}, squad.RunOptions{})
if err != nil {
t.Fatal(err)
}
if res.Status != squad.RunCompleted {
t.Fatalf("expected completed run, got %s (err=%s plan=%+v)", res.Status, res.Error, res.Plan)
}
if res.Plan == nil || len(res.Plan.Steps) != 1 || res.Plan.Steps[0].ID != "b1" {
t.Fatalf("expected blueprint-driven plan, got %+v", res.Plan)
}
if data, err := os.ReadFile(filepath.Join(res.WorkDir, "output.md")); err != nil || !strings.Contains(string(data), "竞品分析报告") {
t.Fatalf("deliverable not produced: %v", err)
}
// 守卫输入走同一引擎:拒绝且不产生工作区执行。
res2, err := engine.Run(context.Background(),
squad.Goal{Text: "帮我把系统网关地址改一下", HitlMode: squad.HitlAuto}, squad.RunOptions{})
if err != nil {
t.Fatal(err)
}
if res2.Status != squad.RunFailed || !strings.Contains(res2.Error, "squad.intent_refused") {
t.Fatalf("expected guard refusal, got %+v", res2)
}
}
// T0:同屏对照——同一引擎配置换成 nil 闸门,守卫文本不再被拒绝、自由目标不再定向。
func TestEngineWithoutGateKeepsOriginalBehavior(t *testing.T) {
dir := t.TempDir()
reg := squad.NewRoleRegistry()
if err := reg.Register(squad.RoleSpec{Role: "researcher", Name: "调研", Backend: "script"}); err != nil {
t.Fatal(err)
}
bps := squad.NewBlueprintRegistry()
if err := bps.Register(squad.Blueprint{
Key: "competitor_report", Title: "竞品分析报告", Match: []string{"竞品分析报告"},
Steps: []squad.StepSpec{{ID: "b1", Role: "researcher", Title: "调研并成文", Targets: []string{"output.md"}}},
}); err != nil {
t.Fatal(err)
}
backends := squad.NewBackendResolver()
backends.RegisterInstance("script", &integrationScriptBackend{content: intentReport})
engine, err := squad.NewEngine(squad.Options{
DataDir: dir, Registry: reg, Blueprints: bps, Planner: squad.NewRulePlanner(), Backends: backends,
})
if err != nil {
t.Fatal(err)
}
// 无闸门:守卫文本不再被拒绝(正常自主规划路径)。
res, err := engine.Run(context.Background(),
squad.Goal{Text: "帮我把系统网关地址改一下", HitlMode: squad.HitlAuto}, squad.RunOptions{})
if err != nil {
t.Fatal(err)
}
if res.Status == squad.RunFailed && strings.Contains(res.Error, "squad.intent_refused") {
t.Fatalf("nil gate must not refuse: %+v", res)
}
// 无闸门:不含关键词的目标不定向模板。
res2, _ := engine.Run(context.Background(),
squad.Goal{Text: "给我做一份竞品情况的分析报告", HitlMode: squad.HitlAuto}, squad.RunOptions{})
if res2.Plan != nil && len(res2.Plan.Steps) == 1 && res2.Plan.Steps[0].ID == "b1" {
t.Fatalf("nil gate must not route to blueprint: %+v", res2.Plan)
}
}