package engine import ( "encoding/json" "testing" "git.flytoex.net/yuanwei/flyto-agent/pkg/flyto" "git.flytoex.net/yuanwei/flyto-agent/pkg/tools" ) // TestInvokeCheckpointHandler_Nil 验证 nil handler 返回 false(deny-safe). func TestInvokeCheckpointHandler_Nil(t *testing.T) { e := &Engine{} evt := CheckpointEvent{ToolName: "send_email"} if e.invokeCheckpointHandler(nil, evt) { t.Error("nil handler should deny (return false)") } } // TestInvokeCheckpointHandler_Allow 验证 handler 返回 true 时放行. func TestInvokeCheckpointHandler_Allow(t *testing.T) { e := &Engine{observer: &NoopObserver{}} handler := CheckpointHandlerFn(func(evt CheckpointEvent) bool { return true }) evt := CheckpointEvent{ToolName: "deploy"} if !e.invokeCheckpointHandler(handler, evt) { t.Error("handler returning true should allow") } } // TestInvokeCheckpointHandler_Deny 验证 handler 返回 false 时拒绝. func TestInvokeCheckpointHandler_Deny(t *testing.T) { e := &Engine{observer: &NoopObserver{}} handler := CheckpointHandlerFn(func(evt CheckpointEvent) bool { return false }) evt := CheckpointEvent{ToolName: "drop_table"} if e.invokeCheckpointHandler(handler, evt) { t.Error("handler returning false should deny") } } // TestInvokeCheckpointHandler_PanicRecovery 验证 handler panic 时 deny-safe. func TestInvokeCheckpointHandler_PanicRecovery(t *testing.T) { e := &Engine{observer: &NoopObserver{}} handler := CheckpointHandlerFn(func(evt CheckpointEvent) bool { panic("handler exploded") }) evt := CheckpointEvent{ToolName: "rm_rf"} // panic 不应传播,应返回 false if e.invokeCheckpointHandler(handler, evt) { t.Error("panicking handler should deny (deny-safe)") } } // TestCheckpointEvent_EventType 验证 CheckpointEvent 实现 Event 接口. func TestCheckpointEvent_EventType(t *testing.T) { var _ Event = &CheckpointEvent{} evt := &CheckpointEvent{ToolName: "test_tool"} if evt.EventType() != "checkpoint" { t.Errorf("unexpected eventType: %q", evt.EventType()) } } // TestWithCheckpointHandler_NilFn 验证 nil fn 不覆盖原有 handler(静默忽略). func TestWithCheckpointHandler_NilFn(t *testing.T) { cfg := &runConfig{} // 先设置一个合法 handler existing := CheckpointHandlerFn(func(evt CheckpointEvent) bool { return true }) WithCheckpointHandler(existing)(cfg) // 再以 nil 调用,不应覆盖 WithCheckpointHandler(nil)(cfg) if cfg.checkpointHandler == nil { t.Error("WithCheckpointHandler(nil) should not clear existing handler") } } // TestWithSecret_Accumulate 验证多次 WithSecret 累积而非覆盖. func TestWithSecret_Accumulate(t *testing.T) { cfg := &runConfig{} WithSecret("A", "aaaaaaaaaa")(cfg) WithSecret("B", "bbbbbbbbbb")(cfg) if len(cfg.extraSecrets) != 2 { t.Errorf("expected 2 extraSecrets, got %d", len(cfg.extraSecrets)) } } // --- CheckpointSuggestedEvent 测试(INF-7 P1)--- // TestCheckpointSuggestedEvent_EventType 验证 CheckpointSuggestedEvent 实现 Event 接口. func TestCheckpointSuggestedEvent_EventType(t *testing.T) { var _ flyto.Event = &CheckpointSuggestedEvent{} evt := &CheckpointSuggestedEvent{ToolName: "Bash", RiskPattern: "rm -rf"} if evt.EventType() != "checkpoint_suggested" { t.Errorf("unexpected eventType: %q", evt.EventType()) } } // newTestEngine 创建最小化 Engine 用于单元测试(不依赖外部资源). func newTestEngine() *Engine { return &Engine{observer: &NoopObserver{}} } // makeToolCall 构造测试用 ToolCall,input 由 map 转 json.RawMessage. func makeToolCall(name string, input map[string]any) tools.ToolCall { raw, _ := json.Marshal(input) return tools.ToolCall{ ID: "tc-test-1", Name: name, Input: raw, } } // TestEmitCheckpointSuggested_BashDangerous 验证高风险 Bash 命令触发建议事件. func TestEmitCheckpointSuggested_BashDangerous(t *testing.T) { e := newTestEngine() ch := make(chan flyto.Event, 8) dangerousCmds := []string{ "rm -rf /tmp/work", "sudo systemctl restart nginx", "git push --force origin main", "psql -c 'DROP TABLE users'", } for _, cmd := range dangerousCmds { tc := makeToolCall("Bash", map[string]any{"command": cmd}) e.emitCheckpointSuggested(ch, tc) } if len(ch) != len(dangerousCmds) { t.Errorf("expected %d events, got %d", len(dangerousCmds), len(ch)) } close(ch) for evt := range ch { cse, ok := evt.(*CheckpointSuggestedEvent) if !ok { t.Fatalf("expected *CheckpointSuggestedEvent, got %T", evt) } if cse.ToolName != "Bash" { t.Errorf("ToolName: %q, want Bash", cse.ToolName) } if cse.RiskReason == "" { t.Error("RiskReason should not be empty") } if cse.RiskPattern == "" { t.Error("RiskPattern should not be empty") } } } // TestEmitCheckpointSuggested_BashSafe 验证安全 Bash 命令不触发建议事件. func TestEmitCheckpointSuggested_BashSafe(t *testing.T) { e := newTestEngine() ch := make(chan flyto.Event, 8) safeCmds := []string{ "ls -la", "echo hello", "git status", "npm install", } for _, cmd := range safeCmds { tc := makeToolCall("Bash", map[string]any{"command": cmd}) e.emitCheckpointSuggested(ch, tc) } if len(ch) != 0 { t.Errorf("safe commands should not emit events, got %d events", len(ch)) } } // TestEmitCheckpointSuggested_FileEditProtectedPath 验证受保护路径的 FileEdit 触发建议. func TestEmitCheckpointSuggested_FileEditProtectedPath(t *testing.T) { e := newTestEngine() ch := make(chan flyto.Event, 4) tc := makeToolCall("FileEdit", map[string]any{ "file_path": "/home/user/.bashrc", "old_string": "# comment", "new_string": "alias ls='rm -rf'", }) e.emitCheckpointSuggested(ch, tc) if len(ch) != 1 { t.Fatalf("expected 1 event for .bashrc edit, got %d", len(ch)) } cse := (<-ch).(*CheckpointSuggestedEvent) if cse.ToolName != "FileEdit" { t.Errorf("ToolName: %q, want FileEdit", cse.ToolName) } } // TestEmitCheckpointSuggested_UnknownTool 验证未知工具不触发建议(无误报). func TestEmitCheckpointSuggested_UnknownTool(t *testing.T) { e := newTestEngine() ch := make(chan flyto.Event, 4) tc := makeToolCall("WebSearch", map[string]any{"query": "golang tutorial"}) e.emitCheckpointSuggested(ch, tc) if len(ch) != 0 { t.Errorf("unknown tool should not emit events, got %d events", len(ch)) } } // TestEmitCheckpointSuggested_EmptyBashInput 验证 Bash 工具无 command 字段时不 panic. func TestEmitCheckpointSuggested_EmptyBashInput(t *testing.T) { e := newTestEngine() ch := make(chan flyto.Event, 4) // 空 input(无 command 字段) tc := makeToolCall("Bash", map[string]any{}) defer func() { if r := recover(); r != nil { t.Errorf("should not panic on empty input: %v", r) } }() e.emitCheckpointSuggested(ch, tc) // 无 command → 无事件 if len(ch) != 0 { t.Errorf("empty command should not emit events") } }