Files
Agent-Native/adapters/intentgate/intentgate_test.go
T
weichenandCursor 17f2a4ecfa Close the native P1 gaps the audit listed for this batch
Why: token budget let the current tool batch run, compaction could split a tool pair or race an append, writes were not crash-atomic, and several MCP and session edges only existed as comments.
Evidence: GOWORK=off go test -count=1 -race ./runtime/ ./runtime/llm/ ./runtime/session/ ./runtime/tools/ ./runtime/mcp/ ./adapters/native/ ./adapters/intentgate/ passed, including the TestR2* cases for budget, replacement, cancel, compaction, SSE, UTF-8, schema, MCP retry, session terminate, poison reload, and registry reload.
Impact: over-budget tool calls are skipped and paired; Stop.Replacement is appended to the session; the intent gate can follow Registry.Reload.
Refs: N5, N6, N7, N8, N9, N11, N12, N15, N16, N17, N18, N19, N21, I9
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 13:05:58 +00:00

198 lines
6.6 KiB
Go

package intentgate
import (
"context"
"errors"
"reflect"
"testing"
"testing/fstest"
"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")
}
}
func TestR2RegistryReloadIsVisibleWithoutRewrap(t *testing.T) {
fsys := fstest.MapFS{"intents.json": &fstest.MapFile{Data: []byte(`{"intents":[{"id":"guard.block","domain":"guard","version":1,"matcher":{"includes_any":["旧词"]}}]}`)}}
reg, err := intent.NewRegistryBuilder(intent.Options{GuardDomains: []string{"guard"}}).BuildFS(context.Background(), fsys, ".")
if err != nil {
t.Fatal(err)
}
next := &stubBackend{name: "native"}
var hits int
be := WrapRegistry(next, newRec(), reg, func(ctx context.Context, req exec.Request, res intent.Result) (*exec.Result, error, bool) {
hits++
return &exec.Result{Success: false, ErrorReason: "blocked"}, nil, true
})
if _, err := be.Execute(context.Background(), exec.Request{Prompt: "请处理新词"}); err != nil {
t.Fatal(err)
}
if hits != 0 || next.calls != 1 {
t.Fatalf("before reload hits=%d calls=%d", hits, next.calls)
}
fsys["intents.json"].Data = []byte(`{"intents":[{"id":"guard.block","domain":"guard","version":2,"matcher":{"includes_any":["新词"]}}]}`)
if _, err := reg.Reload(context.Background()); err != nil {
t.Fatal(err)
}
if _, err := be.Execute(context.Background(), exec.Request{Prompt: "请处理新词"}); err != nil {
t.Fatal(err)
}
if hits != 1 || next.calls != 1 {
t.Fatalf("after reload hits=%d calls=%d", hits, next.calls)
}
}