package engine // subagent_silentbug_test.go — validates the silent-bug fix claimed in // commit bf5c7a7 (2026-04-20, "事件回流 + 生命周期"). That commit asserted — // but did not empirically verify — that before the refactor, RunSync's // type switch could never match *TextEvent because sa.runLoop was // emitting *SubAgentEvent wrappers onto sa's own channel. After the // switch to bare events on the local channel, RunSync should receive // *TextEvent directly and accumulate resultTexts. // // This test drives sa.RunSync through a scripted provider that emits a // real *flyto.TextEvent (the block_stop complete-text event, NOT just // TextDeltaEvent) followed by UsageEvent{StopReason:"end_turn"}. That // event script is the minimum shape runLoop needs to collect one // assistant turn and exit cleanly without requiring tool execution. // // Passing this test converts the commit-B claim from "inference" to // "validated". Failing means the silent bug still exists (or was // misdiagnosed) and requires another fix pass. // // subagent_silentbug_test.go — 验证 commit bf5c7a7 (2026-04-20) 声称 // 修复但未实际跑端到端的 silent bug. 该 commit 推断: 重构前 sa.runLoop // 在本地 channel 发 *SubAgentEvent wrapper, RunSync 的 type switch // 永远匹配不到 *TextEvent. 重构后本地发 bare 事件, RunSync 应能直接 // 收到 *TextEvent 并累积 resultTexts. // // 本测试通过 scripted provider 发出真 *flyto.TextEvent (block_stop // 完整文本块, 不只是 TextDeltaEvent) + UsageEvent{StopReason: // "end_turn"}, 这是 runLoop 收完一个 assistant 轮次并干净退出所需的 // 最小事件形状, 不触发工具执行. // // 测试通过 = commit B 的声明从"推断"升为"已验证". 测试失败 = silent // bug 仍存在 (或原诊断有误) 需再次修复. import ( "context" "strings" "testing" "time" "git.flytoex.net/yuanwei/flyto-agent/pkg/flyto" "git.flytoex.net/yuanwei/flyto-agent/pkg/tools" ) // scriptedProvider emits a preset sequence of flyto events then closes // the stream. Non-blocking; runs the script in a goroutine and respects // ctx cancellation between events. Used only by silent-bug tests in // this file — evolve/llm_adapter_flyto_test.go has its own fakeProvider // (different package, not shared). // // scriptedProvider 按预设脚本发 events 然后关闭 stream. 不阻塞; // goroutine 内跑脚本, 事件间尊重 ctx 取消. 仅本文件 silent-bug 测试 // 使用 — evolve 包有自己的 fakeProvider (跨包不共享). type scriptedProvider struct { events []flyto.Event } func (p *scriptedProvider) Name() string { return "scripted" } func (p *scriptedProvider) Stream(ctx context.Context, _ *flyto.Request) (<-chan flyto.Event, error) { ch := make(chan flyto.Event, len(p.events)) go func() { defer close(ch) for _, e := range p.events { select { case <-ctx.Done(): return case ch <- e: } } }() return ch, nil } func (p *scriptedProvider) Models(_ context.Context) ([]flyto.ModelInfo, error) { return nil, nil } // TestSubAgent_RunSync_PropagatesTextEvent is the silent-bug validation // gate. Drives sa.RunSync against a scripted provider that emits one // complete text block ("hello") and an end_turn usage event. The bug // claim is that RunSync's resultTexts accumulator sees zero TextEvents // when runLoop wraps bare events. If the fix is real, result should // contain "hello". // // TestSubAgent_RunSync_PropagatesTextEvent 是 silent-bug 验证闸. 通过 // scripted provider 发一个完整文本块 ("hello") + end_turn usage 事件 // 驱动 sa.RunSync. bug 声明 = runLoop 包装成 wrapper 时 RunSync 的 // resultTexts 累积看不到 TextEvents. 修复真生效则 result 应含 "hello". func TestSubAgent_RunSync_PropagatesTextEvent(t *testing.T) { cwd := t.TempDir() cfg := testConfig() cfg.Cwd = cwd cfg.Provider = &scriptedProvider{ events: []flyto.Event{ &flyto.TextEvent{Text: "hello"}, &flyto.UsageEvent{StopReason: "end_turn", InputTokens: 1, OutputTokens: 1}, }, } parent := &Engine{ cfg: cfg, tools: tools.NewRegistry(), observer: &NoopObserver{}, } sa := SpawnSubAgent(parent, &SubAgentConfig{ Description: "silent-bug-validation", Model: cfg.Model, }) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() result, err := sa.RunSync(ctx, "hi") if err != nil { t.Fatalf("RunSync error: %v", err) } if !strings.Contains(result, "hello") { t.Fatalf("RunSync result = %q, want to contain \"hello\"", result) } }