Details (original message): Implement the intent core, conformance suite, catalog alignment, examples, and the intentctl diagnostics CLI.
257 lines
8.3 KiB
Go
257 lines
8.3 KiB
Go
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)
|
||
}
|
||
}
|