// session_send_opts_test.go - Session.Send per-Send RunOption 透传 + history 覆盖 invariant. package engine import ( "context" "testing" "git.flytoex.net/yuanwei/flyto-agent/pkg/query" ) // fakeSessionEngineRef 是最小 EngineRef 实现,仅记录 Run 被调用时收到的 opts. // // ELEVATED: 为什么用 fake 而不是真 Engine. // 原方案: new 一个真 Engine 跑 Send -- 否决: Engine 有 40+ 字段, // 构造成本高,且会把 LLM 客户端/工具注册/配置等无关子系统拖进测试. // 新方案: 定一个只实现 Run (捕获 opts) 的 fake,其他 EngineRef 方法 // 返回零值 -- Send 的热路径只触碰 Run,其他方法根本不被调用, // 返回 nil 完全安全 (不调用就不会 panic). type fakeSessionEngineRef struct { lastPrompt string lastOpts []RunOption } func (f *fakeSessionEngineRef) Run(ctx context.Context, prompt string, opts ...RunOption) <-chan Event { f.lastPrompt = prompt f.lastOpts = opts // 立即关闭的 channel: trackEvents goroutine 能瞬间完成 drain ch := make(chan Event) close(ch) return ch } func (f *fakeSessionEngineRef) Session(id string) *Session { return nil } func (f *fakeSessionEngineRef) Observer() EventObserver { return nil } func (f *fakeSessionEngineRef) Activity() *ActivityTracker { return nil } func (f *fakeSessionEngineRef) Context() context.Context { return context.Background() } func (f *fakeSessionEngineRef) Cwd() string { return "" } func (f *fakeSessionEngineRef) ForkSubAgent(*SubAgentConfig) *SubAgent { return nil } // TestSessionSend_PassesRunOption_WithModel 验证 caller 传的 WithModel 能透传到 engine.Run. func TestSessionSend_PassesRunOption_WithModel(t *testing.T) { fake := &fakeSessionEngineRef{} s := newSession("test-send-model", fake) ch := s.Send(context.Background(), "hello", WithModel("claude-sonnet-4-6")) for range ch { // drain } if len(fake.lastOpts) == 0 { t.Fatal("Send did not pass any RunOption to engine.Run") } // 把捕获的 opts 应用到新 runConfig,验证 model 字段已被设置 cfg := &runConfig{} for _, opt := range fake.lastOpts { opt(cfg) } if cfg.model != "claude-sonnet-4-6" { t.Errorf("WithModel not propagated: got %q, want %q", cfg.model, "claude-sonnet-4-6") } } // TestSessionSend_HistoryOverridesCallerMessages 验证 Session 的历史快照 // 会强制覆盖 caller 传入的 WithMessages -- 这是锁死 Session "自动追踪历史" // 核心语义的安全兜底,不能被 caller 绕过. func TestSessionSend_HistoryOverridesCallerMessages(t *testing.T) { fake := &fakeSessionEngineRef{} s := newSession("test-send-history", fake) // 预置 Session 历史 s.messages = []query.Message{ { Role: query.RoleUser, Content: []query.Content{ {Type: query.ContentText, Text: "real history"}, }, }, } // caller 尝试用 WithMessages 注入"攻击"消息,企图抹掉真实历史 attacker := []query.Message{ { Role: query.RoleUser, Content: []query.Content{ {Type: query.ContentText, Text: "attacker replaced history"}, }, }, } ch := s.Send(context.Background(), "new prompt", WithMessages(attacker)) for range ch { // drain } cfg := &runConfig{} for _, opt := range fake.lastOpts { opt(cfg) } if len(cfg.messages) != 1 { t.Fatalf("expected 1 history message (Session override wins), got %d", len(cfg.messages)) } if got := cfg.messages[0].Content[0].Text; got != "real history" { t.Errorf("Session history should override caller WithMessages: got %q, want %q", got, "real history") } } // TestSessionSend_NoOpts_BackwardCompat 验证旧的 0-option 调用路径仍然工作 // (daemon.go / platform server.go 这两个现有 caller 不用改一行). func TestSessionSend_NoOpts_BackwardCompat(t *testing.T) { fake := &fakeSessionEngineRef{} s := newSession("test-send-compat", fake) ch := s.Send(context.Background(), "plain prompt") for range ch { // drain } // 应该只有 Session 自己 append 的 WithMessages,没有其他 opt if len(fake.lastOpts) != 1 { t.Errorf("expected exactly 1 opt (Session's WithMessages), got %d", len(fake.lastOpts)) } if fake.lastPrompt != "plain prompt" { t.Errorf("prompt not forwarded: got %q", fake.lastPrompt) } }