返回 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 approveWorkspace(t, dir)
72
73 sink := &coalesceRecordSink{}
74 ctrl, err := Build(context.Background(), Options{Sink: sink})
75 if err != nil {
76 t.Fatalf("Build: %v", err)
77 }
78 defer ctrl.Close()
79 if err := ctrl.Run(context.Background(), "stream"); err != nil {
80 t.Fatalf("Run: %v", err)
81 }
82
83 sink.mu.Lock()
84 events := sink.events
85 joined := strings.Join(sink.texts, "")
86 sink.mu.Unlock()
87
88 var want strings.Builder
89 for i := range deltas {
90 fmt.Fprintf(&want, "w%d ", i)
91 }
92 if joined != want.String() {
93 t.Fatalf("concatenated deltas = %q, want %q", joined, want.String())
94 }
95 if events >= deltas/2 {
96 t.Fatalf("frontend sink saw %d text events for %d provider chunks — agent stream is not coalesced", events, deltas)
97 }
98 }
99
99 lines GO