Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 129 additions & 25 deletions pkg/agentdrain/miner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package agentdrain

import (
"fmt"
"strings"
"sync"
"testing"

Expand Down Expand Up @@ -148,36 +149,139 @@ func TestMasking(t *testing.T) {
}

func TestFlattenEvent(t *testing.T) {
evt := AgentEvent{
Stage: "tool_call",
Fields: map[string]string{
"tool": "search",
"query": "foo",
"session_id": "abc123",
"latency_ms": "42",
tests := []struct {
name string
evt AgentEvent
exclude []string
expected string
excludedField string
checkStagePrefix bool
checkSortedOrder bool
}{
{
name: "normal event excludes field and keeps sorted output",
evt: AgentEvent{
Stage: "tool_call",
Fields: map[string]string{
"tool": "search",
"query": "foo",
"session_id": "abc123",
"latency_ms": "42",
},
},
exclude: []string{"session_id"},
expected: "stage=tool_call latency_ms=42 query=foo tool=search",
excludedField: "session_id",
checkStagePrefix: true,
checkSortedOrder: true,
},
{
name: "empty stage omits stage token",
evt: AgentEvent{
Fields: map[string]string{
"z": "last",
"a": "first",
},
},
expected: "a=first z=last",
},
{
name: "all fields excluded keeps only stage",
evt: AgentEvent{
Stage: "plan",
Fields: map[string]string{
"action": "start",
"step": "1",
},
},
exclude: []string{"action", "step"},
expected: "stage=plan",
},
{
name: "empty event returns empty string",
evt: AgentEvent{},
expected: "",
},
}
exclude := []string{"session_id"}
result := FlattenEvent(evt, exclude)

assert.NotContains(t, result, "session_id", "excluded field should not appear in flattened output")
assert.True(t, len(result) > 0 &&
indexIn(result, "latency_ms=") < indexIn(result, "query=") &&
indexIn(result, "query=") < indexIn(result, "tool="),
"keys should be sorted alphabetically in flattened output: %q", result)
assert.True(t, len(result) >= len("stage=tool_call") &&
result[:len("stage=tool_call")] == "stage=tool_call",
"stage should appear first in flattened output: %q", result)

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := FlattenEvent(tt.evt, tt.exclude)
assert.Equal(t, tt.expected, got, "FlattenEvent output mismatch for case %q", tt.name)
if tt.excludedField != "" {
assert.NotContains(t, got, tt.excludedField, "excluded field should not appear in flattened output")
}
if tt.checkStagePrefix {
assert.True(t, strings.HasPrefix(got, "stage="+tt.evt.Stage), "stage should appear first in flattened output: %q", got)
}
if tt.checkSortedOrder {
latencyIndex := strings.Index(got, "latency_ms=")
queryIndex := strings.Index(got, "query=")
toolIndex := strings.Index(got, "tool=")
assert.True(t, latencyIndex < queryIndex && queryIndex < toolIndex, "keys should be sorted alphabetically in flattened output: %q", got)
}
Comment on lines +209 to +222
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In TestFlattenEvent, assert.Equal is used for the primary correctness check, but the test then continues to run additional assertions (excluded field, stage prefix, sorted order). If the equality check fails, the follow-on assertions can produce extra noise or misleading failures. Consider switching the equality to require.Equal (or guarding subsequent checks with if assert.Equal(...) { ... }) so ordering/prefix assertions only run when the baseline output matches expectations; also consider asserting strings.Index results are >= 0 before comparing ordering to make failures clearer if tokens are missing.

Copilot uses AI. Check for mistakes.
})
}
}

// indexIn returns the byte offset of substr in s, or -1 if not found.
func indexIn(s, substr string) int {
for i := range len(s) - len(substr) + 1 {
if s[i:i+len(substr)] == substr {
return i
}
func TestTokenize(t *testing.T) {
tests := []struct {
name string
line string
expected []string
}{
{
name: "empty string",
line: "",
expected: []string{},
},
{
name: "extra whitespace",
line: " stage=plan\t action=start \n id=123 ",
expected: []string{"stage=plan", "action=start", "id=123"},
},
{
name: "single token",
line: "stage=finish",
expected: []string{"stage=finish"},
},
{
name: "key value pairs",
line: "tool=bash status=ok",
expected: []string{"tool=bash", "status=ok"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Tokenize(tt.line)
assert.Equal(t, tt.expected, got, "Tokenize(%q) should split into expected tokens", tt.line)
})
}
return -1
}

func TestTrainEmptyLine(t *testing.T) {
m, err := NewMiner(DefaultConfig())
require.NoError(t, err, "NewMiner should succeed for empty-line training test")

result, err := m.Train(" \t\n ")
assert.Nil(t, result, "Train should return nil result for whitespace-only input")
require.Error(t, err, "Train should return an error for whitespace-only input")
assert.Contains(t, err.Error(), "empty line after masking", "Train error should explain empty line after masking")
}

func TestNewMaskerInvalidPattern(t *testing.T) {
masker, err := NewMasker([]MaskRule{
{
Name: "invalid",
Pattern: "(",
Replacement: "<BAD>",
},
})

assert.Nil(t, masker, "NewMasker should return nil masker for invalid regex pattern")
require.Error(t, err, "NewMasker should fail when a regex pattern is invalid")
assert.Contains(t, err.Error(), `mask rule "invalid"`, "NewMasker error should identify the failing rule")
}

func TestConcurrency(t *testing.T) {
Expand Down
Loading