@@ -150,3 +150,197 @@ func TestChatLogsOutput(t *testing.T) {
150150 t .Logf ("chat response: %q" , resp .Message .Content )
151151 t .Logf ("usage: input=%d output=%d total=%d" , resp .Usage .InputTokens , resp .Usage .OutputTokens , resp .Usage .TotalTokens ())
152152}
153+
154+ func TestFindPartialStop (t * testing.T ) {
155+ tests := []struct {
156+ name string
157+ text string
158+ stops []string
159+ expected int
160+ }{
161+ {
162+ name : "no partial match" ,
163+ text : "hello world" ,
164+ stops : []string {"</s>" , "<|end|>" },
165+ expected : - 1 ,
166+ },
167+ {
168+ name : "full stop not detected as partial" ,
169+ text : "hello</s>" ,
170+ stops : []string {"</s>" },
171+ expected : 5 , // partial match starts at position 5
172+ },
173+ {
174+ name : "partial match single char" ,
175+ text : "hello<" ,
176+ stops : []string {"</s>" },
177+ expected : 5 ,
178+ },
179+ {
180+ name : "partial match two chars" ,
181+ text : "hello</" ,
182+ stops : []string {"</s>" },
183+ expected : 5 ,
184+ },
185+ {
186+ name : "partial match three chars" ,
187+ text : "hello</s" ,
188+ stops : []string {"</s>" },
189+ expected : 5 ,
190+ },
191+ {
192+ name : "partial match with pipe" ,
193+ text : "test<|" ,
194+ stops : []string {"<|end|>" },
195+ expected : 4 ,
196+ },
197+ {
198+ name : "partial match longer prefix" ,
199+ text : "test<|end" ,
200+ stops : []string {"<|end|>" },
201+ expected : 4 ,
202+ },
203+ {
204+ name : "partial match almost complete" ,
205+ text : "test<|end|" ,
206+ stops : []string {"<|end|>" },
207+ expected : 4 ,
208+ },
209+ {
210+ name : "multiple stops first matches" ,
211+ text : "hello<" ,
212+ stops : []string {"</s>" , "<|end|>" },
213+ expected : 5 ,
214+ },
215+ {
216+ name : "empty text" ,
217+ text : "" ,
218+ stops : []string {"</s>" },
219+ expected : - 1 ,
220+ },
221+ {
222+ name : "empty stops" ,
223+ text : "hello" ,
224+ stops : []string {},
225+ expected : - 1 ,
226+ },
227+ {
228+ name : "text is just partial" ,
229+ text : "</" ,
230+ stops : []string {"</s>" },
231+ expected : 0 ,
232+ },
233+ {
234+ name : "no match when char not in stop" ,
235+ text : "hellox" ,
236+ stops : []string {"</s>" },
237+ expected : - 1 ,
238+ },
239+ }
240+
241+ for _ , tt := range tests {
242+ t .Run (tt .name , func (t * testing.T ) {
243+ result := findPartialStop (tt .text , tt .stops )
244+ if result != tt .expected {
245+ t .Errorf ("findPartialStop(%q, %v) = %d, want %d" , tt .text , tt .stops , result , tt .expected )
246+ }
247+ })
248+ }
249+ }
250+
251+ func TestStopMarkerFilter (t * testing.T ) {
252+ tests := []struct {
253+ name string
254+ stops []string
255+ tokens []string
256+ expectedChunks []string
257+ expectedStop bool
258+ }{
259+ {
260+ name : "no stop sequences" ,
261+ stops : []string {"</s>" },
262+ tokens : []string {"hello" , " " , "world" },
263+ expectedChunks : []string {"hello" , " " , "world" },
264+ expectedStop : false ,
265+ },
266+ {
267+ name : "full stop in single token" ,
268+ stops : []string {"</s>" },
269+ tokens : []string {"hello" , "</s>" , "extra" },
270+ expectedChunks : []string {"hello" , "" },
271+ expectedStop : true ,
272+ },
273+ {
274+ name : "stop split across tokens" ,
275+ stops : []string {"</s>" },
276+ tokens : []string {"hello<" , "/s>more" },
277+ expectedChunks : []string {"hello" , "" },
278+ expectedStop : true ,
279+ },
280+ {
281+ name : "partial withholding then release" ,
282+ stops : []string {"</s>" },
283+ tokens : []string {"hello<" , "notastop" },
284+ expectedChunks : []string {"hello" , "<notastop" },
285+ expectedStop : false ,
286+ },
287+ {
288+ name : "partial withholding with longer sequence" ,
289+ stops : []string {"<|end|>" },
290+ tokens : []string {"test<|" , "end|>done" },
291+ expectedChunks : []string {"test" , "" },
292+ expectedStop : true ,
293+ },
294+ {
295+ name : "buffer accumulates partial" ,
296+ stops : []string {"</s>" },
297+ tokens : []string {"a<" , "/" , "s>" },
298+ expectedChunks : []string {"a" , "" , "" },
299+ expectedStop : true ,
300+ },
301+ {
302+ name : "stop at very beginning" ,
303+ stops : []string {"</s>" },
304+ tokens : []string {"</s>rest" },
305+ expectedChunks : []string {"" },
306+ expectedStop : true ,
307+ },
308+ }
309+
310+ for _ , tt := range tests {
311+ t .Run (tt .name , func (t * testing.T ) {
312+ filter := newStopMarkerFilter (tt .stops )
313+ var chunks []string
314+
315+ for _ , token := range tt .tokens {
316+ chunk , stopped := filter .Process (token )
317+ chunks = append (chunks , chunk )
318+ if stopped {
319+ break
320+ }
321+ }
322+
323+ // If not stopped, flush remaining buffer
324+ if ! filter .Stopped () {
325+ if tail := filter .Flush (); tail != "" {
326+ chunks [len (chunks )- 1 ] += tail
327+ }
328+ }
329+
330+ if len (chunks ) != len (tt .expectedChunks ) {
331+ t .Errorf ("got %d chunks %v, want %d chunks %v" , len (chunks ), chunks , len (tt .expectedChunks ), tt .expectedChunks )
332+ return
333+ }
334+
335+ for i , chunk := range chunks {
336+ if chunk != tt .expectedChunks [i ] {
337+ t .Errorf ("chunk[%d] = %q, want %q" , i , chunk , tt .expectedChunks [i ])
338+ }
339+ }
340+
341+ if filter .Stopped () != tt .expectedStop {
342+ t .Errorf ("Stopped() = %v, want %v" , filter .Stopped (), tt .expectedStop )
343+ }
344+ })
345+ }
346+ }
0 commit comments