package engine import ( "context" "encoding/json" "testing" "git.flytoex.net/yuanwei/flyto-agent/pkg/security" "git.flytoex.net/yuanwei/flyto-agent/pkg/tools" ) // L1191 测试: AuditObserver.operationFromTool 的三条路径 // // 1. Registry 有工具, Metadata.AuditOperation 非空 → 直接返回声明值 // 2. Registry 有工具, 但未声明 AuditOperation → fallback 启发式 // 3. Registry 为 nil 或工具未注册 → fallback 启发式 // // 设计意图: 消费者加新工具声明 AuditOperation 即可生效, 不声明时零回归. // stubAuditTool 是一个最小 Tool + MetadataProvider 实现, 仅用于 L1191 测试. // 不测执行路径, 只测 Name/Metadata 两条被 Registry 查询的路径. type stubAuditTool struct { name string meta tools.Metadata } func (t *stubAuditTool) Name() string { return t.name } func (t *stubAuditTool) Description(_ context.Context) string { return "stub" } func (t *stubAuditTool) InputSchema() json.RawMessage { return json.RawMessage(`{}`) } func (t *stubAuditTool) Execute(_ context.Context, _ json.RawMessage, _ tools.ProgressFunc) (*tools.Result, error) { return nil, nil } func (t *stubAuditTool) Metadata() tools.Metadata { return t.meta } // captureSinkL1191 捕获 Write 调用, 用于断言生成的 AuditEntry 内容. type captureSinkL1191 struct { entries []security.AuditEntry } func (c *captureSinkL1191) Write(e security.AuditEntry) error { c.entries = append(c.entries, e) return nil } func (c *captureSinkL1191) Close() error { return nil } func TestAuditObserver_L1191_RegistryDeclaredOperation(t *testing.T) { // Registry 命中路径: 工具声明 AuditOperation="send", 覆盖 fallback 启发式 // (fallback 会把未知工具名判成 "invoke"). sink := &captureSinkL1191{} obs := NewAuditObserver(sink, "sess-L1191-declared") registry := tools.NewRegistry() if err := registry.Register(&stubAuditTool{ name: "CustomSend", // 故意不命中 fallback switch 的任何 case meta: tools.Metadata{AuditOperation: "send"}, }); err != nil { t.Fatalf("Register: %v", err) } obs.SetToolRegistry(registry) obs.Event("operation_recorded", map[string]any{ "tool": "CustomSend", "status": "success", }) if len(sink.entries) != 1 { t.Fatalf("expected 1 audit entry, got %d", len(sink.entries)) } if got := sink.entries[0].Operation; got != "send" { t.Errorf("Operation = %q, want %q (from Metadata.AuditOperation)", got, "send") } } func TestAuditObserver_L1191_RegistryMissingFallsBack(t *testing.T) { // Registry 有, 但工具未声明 AuditOperation, 走 fallback 启发式. sink := &captureSinkL1191{} obs := NewAuditObserver(sink, "sess-L1191-missing") registry := tools.NewRegistry() if err := registry.Register(&stubAuditTool{ name: "Write", // 命中 fallback switch 的 "Write" case meta: tools.Metadata{}, // 故意不声明 AuditOperation }); err != nil { t.Fatalf("Register: %v", err) } obs.SetToolRegistry(registry) obs.Event("operation_recorded", map[string]any{ "tool": "Write", "status": "success", }) if len(sink.entries) != 1 { t.Fatalf("expected 1 audit entry, got %d", len(sink.entries)) } if got := sink.entries[0].Operation; got != "write" { t.Errorf("Operation = %q, want %q (from fallback heuristic)", got, "write") } } func TestAuditObserver_L1191_NilRegistryFallsBack(t *testing.T) { // Registry 未设置 (nil), AuditObserver 退化到 fallback, 等价于重构前行为. sink := &captureSinkL1191{} obs := NewAuditObserver(sink, "sess-L1191-nil") // 故意不调 SetToolRegistry obs.Event("operation_recorded", map[string]any{ "tool": "Bash", "status": "success", }) if len(sink.entries) != 1 { t.Fatalf("expected 1 audit entry, got %d", len(sink.entries)) } if got := sink.entries[0].Operation; got != "execute" { t.Errorf("Operation = %q, want %q (nil registry fallback)", got, "execute") } } func TestAuditObserver_L1191_UnknownToolInRegistryFallsBack(t *testing.T) { // Registry 存在, 但请求的工具名不在注册表里 (consumer 直接 emit 未注册工具事件), // 走 fallback 路径, 不 panic. sink := &captureSinkL1191{} obs := NewAuditObserver(sink, "sess-L1191-unknown") registry := tools.NewRegistry() // 只注册 OtherTool, 但事件里发送 "SomeOtherName" if err := registry.Register(&stubAuditTool{ name: "OtherTool", meta: tools.Metadata{AuditOperation: "write"}, }); err != nil { t.Fatalf("Register: %v", err) } obs.SetToolRegistry(registry) obs.Event("operation_recorded", map[string]any{ "tool": "SomeOtherName", // 未注册 "status": "success", }) if len(sink.entries) != 1 { t.Fatalf("expected 1 audit entry, got %d", len(sink.entries)) } // fallback 的 default case 返回 "invoke" if got := sink.entries[0].Operation; got != "invoke" { t.Errorf("Operation = %q, want %q (unknown tool fallback default)", got, "invoke") } }