Files
Agent-Intent/intent/registry_test.go
weichen b58595fb38 Implement the intent core
Details (original message):
Implement the intent core, conformance suite, catalog alignment, examples, and the intentctl diagnostics CLI.
2026-09-29 15:35:18 +00:00

257 lines
8.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package intent
import (
"context"
"errors"
"testing"
"testing/fstest"
)
func demoGuardSpec() IntentSpec {
return IntentSpec{
ID: "guard.config_change",
Domain: "guard",
Version: 1,
Priority: 1000,
Matcher: MatchSpec{IncludesAny: []string{"系统配置", "网关", "改密码"}},
Risk: RiskHigh,
Handler: "refuse.and_guide",
}
}
func newTestBuilder() *RegistryBuilder {
return NewRegistryBuilder(Options{
ProtectedDomains: []string{"guard"},
GuardDomains: []string{"guard"},
})
}
func TestBuilderRejectsDuplicateID(t *testing.T) {
b := newTestBuilder()
if err := b.Register(demoGuardSpec()); err != nil {
t.Fatalf("register: %v", err)
}
err := b.Register(demoGuardSpec())
var e *Error
if !errors.As(err, &e) || e.Code != InvalidSpec {
t.Fatalf("expected intent.invalid_spec for duplicate id, got %v", err)
}
}
func TestBuilderRejectsInvalidSpec(t *testing.T) {
b := newTestBuilder()
bad := demoGuardSpec()
bad.Domain = "Guard"
err := b.Register(bad)
var e *Error
if !errors.As(err, &e) || e.Code != InvalidSpec {
t.Fatalf("expected intent.invalid_spec, got %v", err)
}
}
func TestBuildBaselineOnly(t *testing.T) {
b := newTestBuilder()
if err := b.Register(demoGuardSpec()); err != nil {
t.Fatal(err)
}
reg, err := b.Build()
if err != nil {
t.Fatal(err)
}
snap := reg.Snapshot()
if snap.Revision() != 1 {
t.Fatalf("expected rev 1, got %d", snap.Revision())
}
if got := snap.SpecIDs(); len(got) != 1 || got[0] != "guard.config_change" {
t.Fatalf("spec ids: %v", got)
}
if src, ok := snap.Source("guard.config_change"); !ok || src != "builtin" {
t.Fatalf("source: %q %v", src, ok)
}
// 无配置来源的 Registry 不能 Reload。
if _, err := reg.Reload(context.Background()); err == nil {
t.Fatal("expected reload error without source")
} else {
var e *Error
if !errors.As(err, &e) || e.Code != LoadFailed {
t.Fatalf("expected intent.load_failed, got %v", err)
}
}
}
func TestBuildFSMergeSemantics(t *testing.T) {
fsys := fstest.MapFS{"specs/overlay.json": &fstest.MapFile{Data: []byte(`{
"intents": [
{"id": "demo.run.added", "domain": "demo", "version": 1, "matcher": {"includes_any": ["新增"]}},
{"id": "demo.base.existing", "domain": "demo", "version": 2, "matcher": {"includes_any": ["覆盖后"]}}
],
"disable": ["demo.base.other"]
}`)}}
b := newTestBuilder()
if err := b.Register(
demoGuardSpec(),
IntentSpec{ID: "demo.base.existing", Domain: "demo", Version: 1, Matcher: MatchSpec{IncludesAny: []string{"覆盖前"}}},
IntentSpec{ID: "demo.base.other", Domain: "demo", Version: 1, Matcher: MatchSpec{IncludesAny: []string{"其它"}}},
); err != nil {
t.Fatal(err)
}
reg, err := b.BuildFS(context.Background(), fsys, "specs")
if err != nil {
t.Fatalf("build: %v", err)
}
snap := reg.Snapshot()
ids := snap.SpecIDs()
want := []string{"guard.config_change", "demo.base.existing", "demo.run.added"}
if len(ids) != len(want) {
t.Fatalf("spec ids: %v", ids)
}
for i := range want {
if ids[i] != want[i] {
t.Fatalf("spec ids order: %v", ids)
}
}
// 覆盖保持原位(合并序不变),来源指向 overlay。
if spec, ok := snap.SpecByID("demo.base.existing"); !ok || spec.Version != 2 {
t.Fatalf("override not applied: %+v %v", spec, ok)
}
if src, _ := snap.Source("demo.base.existing"); src != "specs/overlay.json#intents[1]" {
t.Fatalf("override source: %q", src)
}
// disable 生效并保留来源。
if _, ok := snap.SpecByID("demo.base.other"); ok {
t.Fatal("disabled spec still enabled")
}
if d := snap.DisabledIDs(); len(d) != 1 || d[0] != "demo.base.other" {
t.Fatalf("disabled ids: %v", d)
}
}
func TestBuildFSProtectedDomain(t *testing.T) {
for name, doc := range map[string]string{
"disable_protected": `{"disable": ["guard.config_change"]}`,
"override_protected": `{"intents": [{"id": "guard.config_change", "domain": "guard", "version": 2, "matcher": {"includes_any": ["x"]}}]}`,
} {
t.Run(name, func(t *testing.T) {
fsys := fstest.MapFS{"specs/o.json": &fstest.MapFile{Data: []byte(doc)}}
b := newTestBuilder()
if err := b.Register(demoGuardSpec()); err != nil {
t.Fatal(err)
}
_, err := b.BuildFS(context.Background(), fsys, "specs")
var e *Error
if !errors.As(err, &e) || e.Code != SpecProtected {
t.Fatalf("expected intent.spec_protected, got %v", err)
}
if e.Fields["id"] != "guard.config_change" {
t.Fatalf("fields: %v", e.Fields)
}
})
}
}
func TestBuildFSOverrideMustIncreaseVersion(t *testing.T) {
fsys := fstest.MapFS{"specs/o.json": &fstest.MapFile{Data: []byte(
`{"intents": [{"id": "demo.base.existing", "domain": "demo", "version": 1, "matcher": {"includes_any": ["same"]}}]}`)}}
b := newTestBuilder()
if err := b.Register(IntentSpec{ID: "demo.base.existing", Domain: "demo", Version: 1, Matcher: MatchSpec{IncludesAny: []string{"orig"}}}); err != nil {
t.Fatal(err)
}
_, err := b.BuildFS(context.Background(), fsys, "specs")
var e *Error
if !errors.As(err, &e) || e.Code != InvalidSpec {
t.Fatalf("expected intent.invalid_spec, got %v", err)
}
if e.Fields["source"] != "specs/o.json#intents[0]" {
t.Fatalf("fields: %v", e.Fields)
}
}
func TestBuildFSUnknownDisableTarget(t *testing.T) {
fsys := fstest.MapFS{"specs/o.json": &fstest.MapFile{Data: []byte(`{"disable": ["demo.nope.absent"]}`)}}
b := newTestBuilder()
_, err := b.BuildFS(context.Background(), fsys, "specs")
var e *Error
if !errors.As(err, &e) || e.Code != InvalidSpec {
t.Fatalf("expected intent.invalid_spec, got %v", err)
}
}
func TestSnapshotConflicts(t *testing.T) {
b := NewRegistryBuilder(Options{})
if err := b.Register(
IntentSpec{ID: "demo.greet.hello", Domain: "demo", Version: 1, Matcher: MatchSpec{IncludesAny: []string{"你好"}}},
IntentSpec{ID: "demo.greet.hi", Domain: "demo", Version: 1, Matcher: MatchSpec{IncludesAny: []string{"你好"}}},
IntentSpec{ID: "demo.other.x", Domain: "demo", Version: 1, Matcher: MatchSpec{IncludesAny: []string{"你好"}}},
IntentSpec{ID: "demo.solo.y", Domain: "demo", Version: 1, Matcher: MatchSpec{IncludesAny: []string{"独占"}}},
); err != nil {
t.Fatal(err)
}
reg, err := b.Build()
if err != nil {
t.Fatal(err)
}
conflicts := reg.Snapshot().Conflicts()
// 你好 由三个 spec 共享 → C(3,2)=3 条报告;独占词条不报。
if len(conflicts) != 3 {
t.Fatalf("expected 3 conflicts, got %d: %+v", len(conflicts), conflicts)
}
for _, c := range conflicts {
if c.Kind != "shared_term" || c.Term != "你好" {
t.Fatalf("conflict: %+v", c)
}
}
}
func TestReloadSwapsAndKeepsOldOnFailure(t *testing.T) {
fsys := fstest.MapFS{"specs/a.json": &fstest.MapFile{Data: []byte(
`{"intents": [{"id": "demo.x.one", "domain": "demo", "version": 1, "matcher": {"includes_any": ["a"]}}]}`)}}
b := newTestBuilder()
reg, err := b.BuildFS(context.Background(), fsys, "specs")
if err != nil {
t.Fatal(err)
}
// 修改配置(MapFS 可变)后 Reload:revision 递增、内容更新。
fsys["specs/a.json"].Data = []byte(
`{"intents": [{"id": "demo.x.two", "domain": "demo", "version": 1, "matcher": {"includes_any": ["b"]}}]}`)
ns, err := reg.Reload(context.Background())
if err != nil {
t.Fatalf("reload: %v", err)
}
if ns.Revision() != 2 || reg.Snapshot().Revision() != 2 {
t.Fatalf("revision after reload: %d", ns.Revision())
}
if _, ok := reg.Snapshot().SpecByID("demo.x.two"); !ok {
t.Fatal("reloaded spec missing")
}
// 写坏配置再 Reload:失败且旧快照保持生效。
fsys["specs/a.json"].Data = []byte(`{broken`)
if _, err := reg.Reload(context.Background()); err == nil {
t.Fatal("expected reload failure on broken config")
}
snap := reg.Snapshot()
if snap.Revision() != 2 {
t.Fatalf("revision changed on failed reload: %d", snap.Revision())
}
if _, ok := snap.SpecByID("demo.x.two"); !ok {
t.Fatal("old snapshot content lost after failed reload")
}
}
func TestBuildFSMaxSpecs(t *testing.T) {
fsys := fstest.MapFS{"specs/a.json": &fstest.MapFile{Data: []byte(
`{"intents": [
{"id": "demo.x.one", "domain": "demo", "version": 1, "matcher": {"includes_any": ["a"]}},
{"id": "demo.x.two", "domain": "demo", "version": 1, "matcher": {"includes_any": ["b"]}}
]}`)}}
b := NewRegistryBuilder(Options{MaxSpecs: 1})
_, err := b.BuildFS(context.Background(), fsys, "specs")
var e *Error
if !errors.As(err, &e) || e.Code != InvalidSpec {
t.Fatalf("expected intent.invalid_spec for over-limit, got %v", err)
}
}