返回 DeepSeek-Reasonix
response_format_test.go
根目录 / internal / control / response_format_test.go
1 package control
2
3 import (
4 "context"
5 "strings"
6 "sync"
7 "testing"
8 "time"
9
10 "reasonix/internal/agent"
11 "reasonix/internal/event"
12 )
13
14 func TestIsNonTurnHTTPInput(t *testing.T) {
15 for _, tc := range []struct {
16 input string
17 want bool
18 }{
19 {"", true}, // empty
20 {" ", true}, // blank
21 {"# note text", true}, // memory quick-add (# + space)
22 {"/remember MiMo", true}, // remember command note
23 {"/compact", true}, // slash command
24 {"/model qwen3", true}, // management verb
25 {"/new", true}, // slash command
26 {"!ls", true}, // shell commands rejected by submitHTTP (403) before any turn
27 {"hello", false}, // ordinary turn
28 {"explain this code", false},
29 } {
30 if got := isNonTurnHTTPInput(tc.input); got != tc.want {
31 t.Errorf("isNonTurnHTTPInput(%q) = %v, want %v", tc.input, got, tc.want)
32 }
33 }
34 }
35
36 type observedTurnFormat struct {
37 input string
38 format string
39 }
40
41 type formatRecordingRunner struct {
42 observed chan<- observedTurnFormat
43 }
44
45 func (r formatRecordingRunner) Run(ctx context.Context, input string) error {
46 format := ""
47 if responseFormat := agent.ResponseFormatFromRequest(ctx); responseFormat != nil {
48 format = responseFormat.Type
49 }
50 r.observed <- observedTurnFormat{input: input, format: format}
51 return nil
52 }
53
54 type formatTurnDoneGate struct {
55 mu sync.Mutex
56 turns int
57 firstEntered chan struct{}
58 releaseFirst chan struct{}
59 allDone chan struct{}
60 }
61
62 func (g *formatTurnDoneGate) Emit(e event.Event) {
63 if e.Kind != event.TurnDone {
64 return
65 }
66 g.mu.Lock()
67 g.turns++
68 turn := g.turns
69 g.mu.Unlock()
70
71 if turn == 1 {
72 close(g.firstEntered)
73 <-g.releaseFirst
74 }
75 if turn == 2 {
76 close(g.allDone)
77 }
78 }
79
80 func receiveObservedTurnFormat(t *testing.T, observed <-chan observedTurnFormat) observedTurnFormat {
81 t.Helper()
82 select {
83 case got := <-observed:
84 return got
85 case <-time.After(5 * time.Second):
86 t.Fatal("timed out waiting for submitted turn")
87 return observedTurnFormat{}
88 }
89 }
90
91 func waitForFormatTestSignal(t *testing.T, signal <-chan struct{}, message string) {
92 t.Helper()
93 select {
94 case <-signal:
95 case <-time.After(5 * time.Second):
96 t.Fatal(message)
97 }
98 }
99
100 // TestSubmitHTTPFormatBindsToTurn holds the first turn's finishing window open,
101 // submits a second turn with a different format, and proves the parked closure
102 // preserves each accepted turn's format. This deterministically exercises the
103 // interleaving that a controller-global one-shot slot could cross-wire.
104 func TestSubmitHTTPFormatBindsToTurn(t *testing.T) {
105 observed := make(chan observedTurnFormat, 2)
106 gate := &formatTurnDoneGate{
107 firstEntered: make(chan struct{}),
108 releaseFirst: make(chan struct{}),
109 allDone: make(chan struct{}),
110 }
111 c := New(Options{Runner: formatRecordingRunner{observed: observed}, Sink: gate})
112
113 c.SubmitHTTPFormat("first turn", "format-a")
114 first := receiveObservedTurnFormat(t, observed)
115 waitForFormatTestSignal(t, gate.firstEntered, "first turn did not enter the finishing window")
116
117 c.SubmitHTTPFormat("second turn", "format-b")
118 close(gate.releaseFirst)
119 second := receiveObservedTurnFormat(t, observed)
120 waitForFormatTestSignal(t, gate.allDone, "second turn did not finish")
121
122 if !strings.Contains(first.input, "first turn") || first.format != "format-a" {
123 t.Fatalf("first turn = %+v, want first input with format-a", first)
124 }
125 if !strings.Contains(second.input, "second turn") || second.format != "format-b" {
126 t.Fatalf("second turn = %+v, want second input with format-b", second)
127 }
128 }
129
130 // TestWithTurnFormatInjectsFormatIntoContext:format 绑定 turn 的实际效果
131 // ——withTurnFormat 注入后 agent 请求路径能读到(不是全局槽)。
132 func TestWithTurnFormatInjectsFormatIntoContext(t *testing.T) {
133 c := New(Options{})
134 ctx := context.Background()
135 if got := agent.ResponseFormatFromRequest(c.withTurnFormat(ctx, "")); got != nil {
136 t.Fatalf("empty format must be no-op, got %+v", got)
137 }
138 if got := agent.ResponseFormatFromRequest(c.withTurnFormat(ctx, "json_object")); got == nil || got.Type != "json_object" {
139 t.Fatalf("turn format must reach agent request, got %+v", got)
140 }
141 }
142
143 // TestRefTurnFormatBound:@reference turn 同样绑定 format(统一架构——
144 // format 是每个被接纳 turn 的属性,非 runGoalLoop 特例)。
145 func TestRefTurnFormatBound(t *testing.T) {
146 c := New(Options{})
147 ctx := context.Background()
148 // runRefTurnWithFormat 注入后 agent 请求路径读到 json_object
149 if got := agent.ResponseFormatFromRequest(c.withTurnFormat(ctx, "json_object")); got == nil || got.Type != "json_object" {
150 t.Fatalf("ref-turn format must bind to ctx, got %+v", got)
151 }
152 // isRefTurnInput 识别 @引用 turn(format 经 wrapper 绑定,不再丢弃)
153 // ref-turn 输入识别(SlashCodeCommentLine 不依赖文件系统)
154 for _, input := range []string{"// comment line", "//src/main.go:12"} {
155 if !SlashCodeCommentLine(input) {
156 t.Errorf("SlashCodeCommentLine(%q) = false, want true", input)
157 }
158 }
159 }
160
160 lines GO