@@ -4,6 +4,7 @@ package agentdrain
44
55import (
66 "fmt"
7+ "strings"
78 "sync"
89 "testing"
910
@@ -148,36 +149,139 @@ func TestMasking(t *testing.T) {
148149}
149150
150151func TestFlattenEvent (t * testing.T ) {
151- evt := AgentEvent {
152- Stage : "tool_call" ,
153- Fields : map [string ]string {
154- "tool" : "search" ,
155- "query" : "foo" ,
156- "session_id" : "abc123" ,
157- "latency_ms" : "42" ,
152+ tests := []struct {
153+ name string
154+ evt AgentEvent
155+ exclude []string
156+ expected string
157+ excludedField string
158+ checkStagePrefix bool
159+ checkSortedOrder bool
160+ }{
161+ {
162+ name : "normal event excludes field and keeps sorted output" ,
163+ evt : AgentEvent {
164+ Stage : "tool_call" ,
165+ Fields : map [string ]string {
166+ "tool" : "search" ,
167+ "query" : "foo" ,
168+ "session_id" : "abc123" ,
169+ "latency_ms" : "42" ,
170+ },
171+ },
172+ exclude : []string {"session_id" },
173+ expected : "stage=tool_call latency_ms=42 query=foo tool=search" ,
174+ excludedField : "session_id" ,
175+ checkStagePrefix : true ,
176+ checkSortedOrder : true ,
177+ },
178+ {
179+ name : "empty stage omits stage token" ,
180+ evt : AgentEvent {
181+ Fields : map [string ]string {
182+ "z" : "last" ,
183+ "a" : "first" ,
184+ },
185+ },
186+ expected : "a=first z=last" ,
187+ },
188+ {
189+ name : "all fields excluded keeps only stage" ,
190+ evt : AgentEvent {
191+ Stage : "plan" ,
192+ Fields : map [string ]string {
193+ "action" : "start" ,
194+ "step" : "1" ,
195+ },
196+ },
197+ exclude : []string {"action" , "step" },
198+ expected : "stage=plan" ,
199+ },
200+ {
201+ name : "empty event returns empty string" ,
202+ evt : AgentEvent {},
203+ expected : "" ,
158204 },
159205 }
160- exclude := []string {"session_id" }
161- result := FlattenEvent (evt , exclude )
162-
163- assert .NotContains (t , result , "session_id" , "excluded field should not appear in flattened output" )
164- assert .True (t , len (result ) > 0 &&
165- indexIn (result , "latency_ms=" ) < indexIn (result , "query=" ) &&
166- indexIn (result , "query=" ) < indexIn (result , "tool=" ),
167- "keys should be sorted alphabetically in flattened output: %q" , result )
168- assert .True (t , len (result ) >= len ("stage=tool_call" ) &&
169- result [:len ("stage=tool_call" )] == "stage=tool_call" ,
170- "stage should appear first in flattened output: %q" , result )
206+
207+ for _ , tt := range tests {
208+ t .Run (tt .name , func (t * testing.T ) {
209+ got := FlattenEvent (tt .evt , tt .exclude )
210+ assert .Equal (t , tt .expected , got , "FlattenEvent output mismatch for case %q" , tt .name )
211+ if tt .excludedField != "" {
212+ assert .NotContains (t , got , tt .excludedField , "excluded field should not appear in flattened output" )
213+ }
214+ if tt .checkStagePrefix {
215+ assert .True (t , strings .HasPrefix (got , "stage=" + tt .evt .Stage ), "stage should appear first in flattened output: %q" , got )
216+ }
217+ if tt .checkSortedOrder {
218+ latencyIndex := strings .Index (got , "latency_ms=" )
219+ queryIndex := strings .Index (got , "query=" )
220+ toolIndex := strings .Index (got , "tool=" )
221+ assert .True (t , latencyIndex < queryIndex && queryIndex < toolIndex , "keys should be sorted alphabetically in flattened output: %q" , got )
222+ }
223+ })
224+ }
171225}
172226
173- // indexIn returns the byte offset of substr in s, or -1 if not found.
174- func indexIn (s , substr string ) int {
175- for i := range len (s ) - len (substr ) + 1 {
176- if s [i :i + len (substr )] == substr {
177- return i
178- }
227+ func TestTokenize (t * testing.T ) {
228+ tests := []struct {
229+ name string
230+ line string
231+ expected []string
232+ }{
233+ {
234+ name : "empty string" ,
235+ line : "" ,
236+ expected : []string {},
237+ },
238+ {
239+ name : "extra whitespace" ,
240+ line : " stage=plan\t action=start \n id=123 " ,
241+ expected : []string {"stage=plan" , "action=start" , "id=123" },
242+ },
243+ {
244+ name : "single token" ,
245+ line : "stage=finish" ,
246+ expected : []string {"stage=finish" },
247+ },
248+ {
249+ name : "key value pairs" ,
250+ line : "tool=bash status=ok" ,
251+ expected : []string {"tool=bash" , "status=ok" },
252+ },
253+ }
254+
255+ for _ , tt := range tests {
256+ t .Run (tt .name , func (t * testing.T ) {
257+ got := Tokenize (tt .line )
258+ assert .Equal (t , tt .expected , got , "Tokenize(%q) should split into expected tokens" , tt .line )
259+ })
179260 }
180- return - 1
261+ }
262+
263+ func TestTrainEmptyLine (t * testing.T ) {
264+ m , err := NewMiner (DefaultConfig ())
265+ require .NoError (t , err , "NewMiner should succeed for empty-line training test" )
266+
267+ result , err := m .Train (" \t \n " )
268+ assert .Nil (t , result , "Train should return nil result for whitespace-only input" )
269+ require .Error (t , err , "Train should return an error for whitespace-only input" )
270+ assert .Contains (t , err .Error (), "empty line after masking" , "Train error should explain empty line after masking" )
271+ }
272+
273+ func TestNewMaskerInvalidPattern (t * testing.T ) {
274+ masker , err := NewMasker ([]MaskRule {
275+ {
276+ Name : "invalid" ,
277+ Pattern : "(" ,
278+ Replacement : "<BAD>" ,
279+ },
280+ })
281+
282+ assert .Nil (t , masker , "NewMasker should return nil masker for invalid regex pattern" )
283+ require .Error (t , err , "NewMasker should fail when a regex pattern is invalid" )
284+ assert .Contains (t , err .Error (), `mask rule "invalid"` , "NewMasker error should identify the failing rule" )
181285}
182286
183287func TestConcurrency (t * testing.T ) {
0 commit comments