package intentgate import ( "context" "errors" "reflect" "testing" "agent-intent/intent" "agent-native/exec" ) type stubBackend struct { name string calls int } func (s *stubBackend) Name() string { return s.name } func (s *stubBackend) Execute(ctx context.Context, req exec.Request) (*exec.Result, error) { s.calls++ return &exec.Result{Success: true, Logs: "stub:" + req.Prompt}, nil } func emptySnapshot(t *testing.T) *intent.Snapshot { t.Helper() reg, err := intent.NewRegistryBuilder(intent.Options{}).Build() if err != nil { t.Fatal(err) } return reg.Snapshot() } // 带一个守卫 spec 与一个可路由 spec 的快照。 func loadedSnapshot(t *testing.T) *intent.Snapshot { t.Helper() b := intent.NewRegistryBuilder(intent.Options{ GuardDomains: []string{"guard"}, ProtectedDomains: []string{"guard"}, }) if err := b.Register(intent.IntentSpec{ ID: "guard.config_change", Domain: "guard", Version: 1, Matcher: intent.MatchSpec{IncludesAny: []string{"系统配置", "网关"}}, Risk: intent.RiskHigh, Handler: "refuse.and_guide", Metadata: map[string]string{ "guidance": "系统配置请在管理界面修改", }, }); err != nil { t.Fatal(err) } reg, err := b.Build() if err != nil { t.Fatal(err) } return reg.Snapshot() } func newRec() intent.Recognizer { return intent.NewRecognizer(intent.Options{}, nil, nil) } func TestZeroSpecSnapshotPassthroughIdentical(t *testing.T) { // T0 行为解耦:零意图快照 + Wrap 后,与裸后端行为逐字节一致。 bare := &stubBackend{name: "native"} gatedBe := Wrap(&stubBackend{name: "native"}, newRec(), emptySnapshot(t), func(ctx context.Context, req exec.Request, res intent.Result) (*exec.Result, error, bool) { t.Fatal("handler must not fire on empty snapshot") return nil, nil, false }) req := exec.Request{Prompt: "帮我改一下系统网关地址"} want, werr := bare.Execute(context.Background(), req) got, gerr := gatedBe.Execute(context.Background(), req) if werr != nil || gerr != nil { t.Fatalf("execute errors: %v %v", werr, gerr) } if !reflect.DeepEqual(want, got) { t.Fatalf("results differ: %+v vs %+v", want, got) } if gatedBe.(*gated).next.(*stubBackend).calls != 1 { t.Fatal("next backend not invoked exactly once") } } func TestGuardHitInterceptsWithoutModel(t *testing.T) { next := &stubBackend{name: "native"} be := Wrap(next, newRec(), loadedSnapshot(t), func(ctx context.Context, req exec.Request, res intent.Result) (*exec.Result, error, bool) { if res.Branch != intent.BranchGuardHit || res.Top == nil || res.Top.ID != "guard.config_change" { t.Fatalf("unexpected result: %+v", res) } // 宿主解释守卫意图:拒绝 + 引导(合成结果语义由宿主定义)。 return &exec.Result{Success: false, ErrorReason: "guard refused: " + res.Top.Metadata["guidance"]}, nil, true }) out, err := be.Execute(context.Background(), exec.Request{Prompt: "帮我改一下系统网关地址"}) if err != nil { t.Fatal(err) } if out.Success || out.ErrorReason == "" { t.Fatalf("expected guard refusal, got %+v", out) } if next.calls != 0 { t.Fatalf("model backend must not be called on guard hit, got %d calls", next.calls) } } func TestFallbackPassesThroughToBackend(t *testing.T) { next := &stubBackend{name: "native"} be := Wrap(next, newRec(), loadedSnapshot(t), func(ctx context.Context, req exec.Request, res intent.Result) (*exec.Result, error, bool) { t.Fatal("handler must not fire on fallback") return nil, nil, false }) out, err := be.Execute(context.Background(), exec.Request{Prompt: "写一份周报"}) if err != nil || out == nil || !out.Success { t.Fatalf("fallback must reach backend: %+v %v", out, err) } if next.calls != 1 { t.Fatalf("backend calls = %d", next.calls) } } func TestHandlerDeclineFallsBackToBackend(t *testing.T) { next := &stubBackend{name: "native"} be := Wrap(next, newRec(), loadedSnapshot(t), func(ctx context.Context, req exec.Request, res intent.Result) (*exec.Result, error, bool) { return nil, nil, false // 宿主放弃处理 }) out, err := be.Execute(context.Background(), exec.Request{Prompt: "帮我改一下系统网关地址"}) if err != nil || out == nil || !out.Success { t.Fatalf("declined handling must pass through: %+v %v", out, err) } if next.calls != 1 { t.Fatalf("backend calls = %d", next.calls) } } func TestNilComponentsReturnNext(t *testing.T) { next := &stubBackend{name: "native"} if got := Wrap(next, nil, nil, nil); got != exec.Backend(next) { t.Fatal("nil components must return next unchanged") } } func TestRecognizeErrorFailsClosed(t *testing.T) { next := &stubBackend{name: "native"} b := intent.NewRegistryBuilder(intent.Options{MaxInputLen: 8}) reg, err := b.Build() if err != nil { t.Fatal(err) } be := Wrap(next, newRec(), reg.Snapshot(), func(ctx context.Context, req exec.Request, res intent.Result) (*exec.Result, error, bool) { t.Fatal("handler must not fire on recognize error") return nil, nil, false }) _, err = be.Execute(context.Background(), exec.Request{Prompt: "这一段输入明显超过八个字节"}) if err == nil { t.Fatal("expected fail-closed error") } var ie *intent.Error if !errors.As(err, &ie) { t.Fatalf("expected *intent.Error, got %T", err) } if next.calls != 0 { t.Fatal("backend must not run on recognize error") } }