返回 DeepSeek-Reasonix
coalesce_wiring_test.go
根目录 / internal / boot / coalesce_wiring_test.go
1 package boot
2
3 import (
4 "context"
5 "fmt"
6 "strings"
7 "sync"
8 "testing"
9
10 "reasonix/internal/event"
11 "reasonix/internal/provider"
12 )
13
14 type coalesceWiringProvider struct{ deltas int }
15
16 func (p *coalesceWiringProvider) Name() string { return "boot-coalesce-test" }
17
18 func (p *coalesceWiringProvider) Stream(_ context.Context, req provider.Request) (<-chan provider.Chunk, error) {
19 chunks := make([]provider.Chunk, 0, p.deltas+1)
20 for i := range p.deltas {
21 chunks = append(chunks, provider.Chunk{Type: provider.ChunkText, Text: fmt.Sprintf("w%d ", i)})
22 }
23 chunks = append(chunks, provider.Chunk{Type: provider.ChunkDone})
24 ch := make(chan provider.Chunk, len(chunks))
25 for _, chunk := range chunks {
26 ch <- chunk
27 }
28 close(ch)
29 return ch, nil
30 }
31
32 type coalesceRecordSink struct {
33 mu sync.Mutex
34 texts []string
35 events int
36 }
37
38 func (s *coalesceRecordSink) Emit(e event.Event) {
39 s.mu.Lock()
40 defer s.mu.Unlock()
41 if e.Kind == event.Text {
42 s.events++
43 s.texts = append(s.texts, e.Text)
44 }
45 }
46
47 // TestBuildCoalescesAgentStreamDeltas pins the wiring, not the coalescer: the
48 // executor emits into the shared boot sink directly, so coalescing must wrap
49 // that sink — wrapping only the controller's reference leaves the per-chunk
50 // stream untouched (the regression this test exists for).
51 func TestBuildCoalescesAgentStreamDeltas(t *testing.T) {
52 isolateConfigHome(t)
53 dir := robustTempDir(t)
54 t.Chdir(dir)
55
56 const deltas = 40
57 provider.Register("boot-coalesce-test", func(provider.Config) (provider.Provider, error) {
58 return &coalesceWiringProvider{deltas: deltas}, nil
59 })
60 writeFile(t, dir, "reasonix.toml", `
61 default_model = "test-model"
62
63 [agent]
64 system_prompt = "BASE"
65
66 [[providers]]
67 name = "test-model"
68 kind = "boot-coalesce-test"
69 model = "x"
70 `)
71
72 sink := &coalesceRecordSink{}
73 ctrl, err := Build(context.Background(), Options{Sink: sink})
74 if err != nil {
75 t.Fatalf("Build: %v", err)
76 }
77 defer ctrl.Close()
78 if err := ctrl.Run(context.Background(), "stream"); err != nil {
79 t.Fatalf("Run: %v", err)
80 }
81
82 sink.mu.Lock()
83 events := sink.events
84 joined := strings.Join(sink.texts, "")
85 sink.mu.Unlock()
86
87 var want strings.Builder
88 for i := range deltas {
89 fmt.Fprintf(&want, "w%d ", i)
90 }
91 if joined != want.String() {
92 t.Fatalf("concatenated deltas = %q, want %q", joined, want.String())
93 }
94 if events >= deltas/2 {
95 t.Fatalf("frontend sink saw %d text events for %d provider chunks — agent stream is not coalesced", events, deltas)
96 }
97 }
98
98 lines GO