go-session/search_test.go
Claude 7771e64e07
test(phase0): add comprehensive test suite — 51 tests, 90.9% coverage
Parser, HTML renderer, video/tape generator, and search function tests
with table-driven subtests and inline JSONL fixtures. Adds testify for
assertions. go vet clean.

Co-Authored-By: Charon <developers@lethean.io>
2026-02-20 00:42:11 +00:00

173 lines
7.6 KiB
Go

package session
import (
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSearch_FindsMatchingToolEvents(t *testing.T) {
dir := t.TempDir()
lines := []string{
`{"type":"assistant","timestamp":"` + ts(baseTime, 0) + `","sessionId":"s1","message":{"role":"assistant","content":[{"type":"tool_use","id":"tu_1","name":"Bash","input":{"command":"go test ./..."}}]}}`,
`{"type":"user","timestamp":"` + ts(baseTime, time.Second) + `","sessionId":"s1","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"tu_1","content":"PASS"}]}}`,
`{"type":"assistant","timestamp":"` + ts(baseTime, 2*time.Second) + `","sessionId":"s1","message":{"role":"assistant","content":[{"type":"tool_use","id":"tu_2","name":"Grep","input":{"pattern":"TODO","path":"/src"}}]}}`,
`{"type":"user","timestamp":"` + ts(baseTime, 3*time.Second) + `","sessionId":"s1","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"tu_2","content":"main.go:42: // TODO fix this"}]}}`,
}
writeTempJSONL(t, dir, "session.jsonl", lines...)
t.Run("match_input", func(t *testing.T) {
results, err := Search(dir, "go test")
require.NoError(t, err)
require.Len(t, results, 1)
assert.Equal(t, "session", results[0].SessionID)
assert.Equal(t, "Bash", results[0].Tool)
assert.Contains(t, results[0].Match, "go test")
})
t.Run("match_output", func(t *testing.T) {
results, err := Search(dir, "TODO fix")
require.NoError(t, err)
require.Len(t, results, 1)
assert.Equal(t, "Grep", results[0].Tool)
})
t.Run("case_insensitive", func(t *testing.T) {
results, err := Search(dir, "PASS")
require.NoError(t, err)
require.Len(t, results, 1)
})
t.Run("no_match", func(t *testing.T) {
results, err := Search(dir, "nonexistent query")
require.NoError(t, err)
assert.Empty(t, results)
})
}
func TestSearch_SkipsNonToolEvents(t *testing.T) {
dir := t.TempDir()
lines := []string{
`{"type":"user","timestamp":"` + ts(baseTime, 0) + `","sessionId":"s2","message":{"role":"user","content":[{"type":"text","text":"find all TODO items"}]}}`,
`{"type":"assistant","timestamp":"` + ts(baseTime, time.Second) + `","sessionId":"s2","message":{"role":"assistant","content":[{"type":"text","text":"I will search for TODO items."}]}}`,
}
writeTempJSONL(t, dir, "notool.jsonl", lines...)
// The text "TODO" exists in user and assistant messages but Search only
// looks at tool_use events.
results, err := Search(dir, "TODO")
require.NoError(t, err)
assert.Empty(t, results)
}
func TestSearch_EmptyDirectory(t *testing.T) {
dir := t.TempDir()
results, err := Search(dir, "anything")
require.NoError(t, err)
assert.Empty(t, results)
}
func TestSearch_MultipleSessions(t *testing.T) {
dir := t.TempDir()
// Session A has a matching tool call.
linesA := []string{
`{"type":"assistant","timestamp":"` + ts(baseTime, 0) + `","sessionId":"a","message":{"role":"assistant","content":[{"type":"tool_use","id":"tu_a","name":"Read","input":{"file_path":"/tmp/config.yaml"}}]}}`,
`{"type":"user","timestamp":"` + ts(baseTime, time.Second) + `","sessionId":"a","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"tu_a","content":"port: 8080"}]}}`,
}
// Session B has a different matching tool call.
linesB := []string{
`{"type":"assistant","timestamp":"` + ts(baseTime, 0) + `","sessionId":"b","message":{"role":"assistant","content":[{"type":"tool_use","id":"tu_b","name":"Bash","input":{"command":"cat /tmp/config.yaml"}}]}}`,
`{"type":"user","timestamp":"` + ts(baseTime, time.Second) + `","sessionId":"b","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"tu_b","content":"port: 8080"}]}}`,
}
writeTempJSONL(t, dir, "a.jsonl", linesA...)
writeTempJSONL(t, dir, "b.jsonl", linesB...)
results, err := Search(dir, "config.yaml")
require.NoError(t, err)
assert.Len(t, results, 2, "should find matches in both sessions")
}
func TestSearch_IgnoresNonJSONLFiles(t *testing.T) {
dir := t.TempDir()
// Write a .txt file containing the search term.
require.NoError(t, os.WriteFile(filepath.Join(dir, "notes.txt"), []byte("config.yaml"), 0644))
results, err := Search(dir, "config.yaml")
require.NoError(t, err)
assert.Empty(t, results)
}
func TestSearch_MalformedSessionSkipped(t *testing.T) {
dir := t.TempDir()
// A completely invalid JSONL file.
writeTempJSONL(t, dir, "broken.jsonl", `{not valid json at all`)
// A valid session with a match.
lines := []string{
`{"type":"assistant","timestamp":"` + ts(baseTime, 0) + `","sessionId":"ok","message":{"role":"assistant","content":[{"type":"tool_use","id":"tu_ok","name":"Bash","input":{"command":"echo found"}}]}}`,
`{"type":"user","timestamp":"` + ts(baseTime, time.Second) + `","sessionId":"ok","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"tu_ok","content":"found"}]}}`,
}
writeTempJSONL(t, dir, "valid.jsonl", lines...)
results, err := Search(dir, "found")
require.NoError(t, err)
assert.Len(t, results, 1, "should find match in valid session, ignoring broken one")
}
func TestSearch_MatchUsesInputAsContext(t *testing.T) {
dir := t.TempDir()
lines := []string{
`{"type":"assistant","timestamp":"` + ts(baseTime, 0) + `","sessionId":"ctx","message":{"role":"assistant","content":[{"type":"tool_use","id":"tu_ctx","name":"Bash","input":{"command":"deploy production"}}]}}`,
`{"type":"user","timestamp":"` + ts(baseTime, time.Second) + `","sessionId":"ctx","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"tu_ctx","content":"deployed successfully"}]}}`,
}
writeTempJSONL(t, dir, "ctx.jsonl", lines...)
// Search matches on output but Match field should show input.
results, err := Search(dir, "deployed")
require.NoError(t, err)
require.Len(t, results, 1)
assert.Contains(t, results[0].Match, "deploy production",
"Match context should be the tool input, not output")
}
func TestSearch_MatchFallsBackToOutput(t *testing.T) {
// When a tool_use event has empty input but matching output, Match should
// show the (truncated) output. We simulate this by creating a session
// where the tool input resolves to empty.
dir := t.TempDir()
lines := []string{
`{"type":"assistant","timestamp":"` + ts(baseTime, 0) + `","sessionId":"fb","message":{"role":"assistant","content":[{"type":"tool_use","id":"tu_fb","name":"Bash","input":{}}]}}`,
`{"type":"user","timestamp":"` + ts(baseTime, time.Second) + `","sessionId":"fb","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"tu_fb","content":"fallback output text"}]}}`,
}
writeTempJSONL(t, dir, "fallback.jsonl", lines...)
results, err := Search(dir, "fallback output")
require.NoError(t, err)
require.Len(t, results, 1)
assert.Contains(t, results[0].Match, "fallback output")
}
func TestSearchResult_Fields(t *testing.T) {
// Verify that SearchResult fields are populated correctly.
dir := t.TempDir()
lines := []string{
`{"type":"assistant","timestamp":"` + ts(baseTime, 0) + `","sessionId":"fields","message":{"role":"assistant","content":[{"type":"tool_use","id":"tu_f","name":"Grep","input":{"pattern":"error","path":"/var/log"}}]}}`,
`{"type":"user","timestamp":"` + ts(baseTime, time.Second) + `","sessionId":"fields","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"tu_f","content":"found 3 errors"}]}}`,
}
writeTempJSONL(t, dir, "fields.jsonl", lines...)
results, err := Search(dir, "error")
require.NoError(t, err)
require.Len(t, results, 1)
r := results[0]
assert.Equal(t, "fields", r.SessionID)
assert.False(t, r.Timestamp.IsZero())
assert.Equal(t, "Grep", r.Tool)
assert.NotEmpty(t, r.Match)
}