test: add comprehensive Phase 0 test suite — 67 tests, 90.9% coverage
Add parser_test.go (22 tests), search_test.go (9 tests), html_test.go (6 tests),
video_test.go (12 tests), and bench_test.go (4 benchmarks) covering all Phase 0
TODO items:
- ParseTranscript: minimal JSONL, all 7 tool types, errors, truncated/malformed
input, large sessions (1100+ events), nested array/map results, mixed content
- ListSessions: empty dir, single/multi sorted, non-JSONL ignored, modtime fallback
- extractToolInput: all tool types plus nil, invalid JSON, unknown tool fallback
- extractResultContent: string, array, map, and other types
- Search: empty dir, no matches, multi-match, case insensitive, output matching
- RenderHTML: basic, empty, errors, XSS escaping, label types, invalid path
- generateTape/extractCommand: all event types, empty/failed commands, truncation
- Benchmarks: 2.2MB and 11MB ParseTranscript, ListSessions, Search with b.Loop()
go vet ./... clean, go test -race ./... clean.
Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 05:00:38 +00:00
// SPDX-Licence-Identifier: EUPL-1.2
package session
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
)
// BenchmarkParseTranscript benchmarks parsing a ~1MB+ JSONL file.
func BenchmarkParseTranscript ( b * testing . B ) {
dir := b . TempDir ( )
path := generateBenchJSONL ( b , dir , 5000 ) // ~1MB+ of JSONL
b . ResetTimer ( )
b . ReportAllocs ( )
for b . Loop ( ) {
feat(parser): Phase 1+2 — parse stats, truncation detection, session analytics
Phase 1: ParseTranscript now returns (*Session, *ParseStats, error).
ParseStats tracks TotalLines, SkippedLines, OrphanedToolCalls, and
Warnings (line numbers + previews for bad JSON, orphaned tool IDs,
truncated final line detection). All call sites updated.
Phase 2: New analytics.go with Analyse() and FormatAnalytics().
SessionAnalytics computes Duration, ActiveTime, ToolCounts, ErrorCounts,
SuccessRate, AvgLatency, MaxLatency, and token estimation.
Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 07:40:52 +00:00
sess , _ , err := ParseTranscript ( path )
test: add comprehensive Phase 0 test suite — 67 tests, 90.9% coverage
Add parser_test.go (22 tests), search_test.go (9 tests), html_test.go (6 tests),
video_test.go (12 tests), and bench_test.go (4 benchmarks) covering all Phase 0
TODO items:
- ParseTranscript: minimal JSONL, all 7 tool types, errors, truncated/malformed
input, large sessions (1100+ events), nested array/map results, mixed content
- ListSessions: empty dir, single/multi sorted, non-JSONL ignored, modtime fallback
- extractToolInput: all tool types plus nil, invalid JSON, unknown tool fallback
- extractResultContent: string, array, map, and other types
- Search: empty dir, no matches, multi-match, case insensitive, output matching
- RenderHTML: basic, empty, errors, XSS escaping, label types, invalid path
- generateTape/extractCommand: all event types, empty/failed commands, truncation
- Benchmarks: 2.2MB and 11MB ParseTranscript, ListSessions, Search with b.Loop()
go vet ./... clean, go test -race ./... clean.
Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 05:00:38 +00:00
if err != nil {
b . Fatal ( err )
}
if len ( sess . Events ) == 0 {
b . Fatal ( "expected events" )
}
}
}
// BenchmarkParseTranscript_Large benchmarks a larger ~5MB file.
func BenchmarkParseTranscript_Large ( b * testing . B ) {
dir := b . TempDir ( )
path := generateBenchJSONL ( b , dir , 25000 ) // ~5MB
b . ResetTimer ( )
b . ReportAllocs ( )
for b . Loop ( ) {
feat(parser): Phase 1+2 — parse stats, truncation detection, session analytics
Phase 1: ParseTranscript now returns (*Session, *ParseStats, error).
ParseStats tracks TotalLines, SkippedLines, OrphanedToolCalls, and
Warnings (line numbers + previews for bad JSON, orphaned tool IDs,
truncated final line detection). All call sites updated.
Phase 2: New analytics.go with Analyse() and FormatAnalytics().
SessionAnalytics computes Duration, ActiveTime, ToolCounts, ErrorCounts,
SuccessRate, AvgLatency, MaxLatency, and token estimation.
Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 07:40:52 +00:00
_ , _ , err := ParseTranscript ( path )
test: add comprehensive Phase 0 test suite — 67 tests, 90.9% coverage
Add parser_test.go (22 tests), search_test.go (9 tests), html_test.go (6 tests),
video_test.go (12 tests), and bench_test.go (4 benchmarks) covering all Phase 0
TODO items:
- ParseTranscript: minimal JSONL, all 7 tool types, errors, truncated/malformed
input, large sessions (1100+ events), nested array/map results, mixed content
- ListSessions: empty dir, single/multi sorted, non-JSONL ignored, modtime fallback
- extractToolInput: all tool types plus nil, invalid JSON, unknown tool fallback
- extractResultContent: string, array, map, and other types
- Search: empty dir, no matches, multi-match, case insensitive, output matching
- RenderHTML: basic, empty, errors, XSS escaping, label types, invalid path
- generateTape/extractCommand: all event types, empty/failed commands, truncation
- Benchmarks: 2.2MB and 11MB ParseTranscript, ListSessions, Search with b.Loop()
go vet ./... clean, go test -race ./... clean.
Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 05:00:38 +00:00
if err != nil {
b . Fatal ( err )
}
}
}
// BenchmarkListSessions benchmarks listing sessions in a directory.
func BenchmarkListSessions ( b * testing . B ) {
dir := b . TempDir ( )
// Create 20 session files
2026-02-22 21:00:17 +00:00
for range 20 {
test: add comprehensive Phase 0 test suite — 67 tests, 90.9% coverage
Add parser_test.go (22 tests), search_test.go (9 tests), html_test.go (6 tests),
video_test.go (12 tests), and bench_test.go (4 benchmarks) covering all Phase 0
TODO items:
- ParseTranscript: minimal JSONL, all 7 tool types, errors, truncated/malformed
input, large sessions (1100+ events), nested array/map results, mixed content
- ListSessions: empty dir, single/multi sorted, non-JSONL ignored, modtime fallback
- extractToolInput: all tool types plus nil, invalid JSON, unknown tool fallback
- extractResultContent: string, array, map, and other types
- Search: empty dir, no matches, multi-match, case insensitive, output matching
- RenderHTML: basic, empty, errors, XSS escaping, label types, invalid path
- generateTape/extractCommand: all event types, empty/failed commands, truncation
- Benchmarks: 2.2MB and 11MB ParseTranscript, ListSessions, Search with b.Loop()
go vet ./... clean, go test -race ./... clean.
Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 05:00:38 +00:00
generateBenchJSONL ( b , dir , 100 )
}
b . ResetTimer ( )
b . ReportAllocs ( )
for b . Loop ( ) {
sessions , err := ListSessions ( dir )
if err != nil {
b . Fatal ( err )
}
if len ( sessions ) == 0 {
b . Fatal ( "expected sessions" )
}
}
}
// BenchmarkSearch benchmarks searching across multiple sessions.
func BenchmarkSearch ( b * testing . B ) {
dir := b . TempDir ( )
// Create 10 session files with varied content
2026-02-22 21:00:17 +00:00
for range 10 {
test: add comprehensive Phase 0 test suite — 67 tests, 90.9% coverage
Add parser_test.go (22 tests), search_test.go (9 tests), html_test.go (6 tests),
video_test.go (12 tests), and bench_test.go (4 benchmarks) covering all Phase 0
TODO items:
- ParseTranscript: minimal JSONL, all 7 tool types, errors, truncated/malformed
input, large sessions (1100+ events), nested array/map results, mixed content
- ListSessions: empty dir, single/multi sorted, non-JSONL ignored, modtime fallback
- extractToolInput: all tool types plus nil, invalid JSON, unknown tool fallback
- extractResultContent: string, array, map, and other types
- Search: empty dir, no matches, multi-match, case insensitive, output matching
- RenderHTML: basic, empty, errors, XSS escaping, label types, invalid path
- generateTape/extractCommand: all event types, empty/failed commands, truncation
- Benchmarks: 2.2MB and 11MB ParseTranscript, ListSessions, Search with b.Loop()
go vet ./... clean, go test -race ./... clean.
Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 05:00:38 +00:00
generateBenchJSONL ( b , dir , 500 )
}
b . ResetTimer ( )
b . ReportAllocs ( )
for b . Loop ( ) {
_ , err := Search ( dir , "echo" )
if err != nil {
b . Fatal ( err )
}
}
}
// generateBenchJSONL creates a synthetic JSONL file with the given number of tool pairs.
// Returns the file path.
func generateBenchJSONL ( b testing . TB , dir string , numTools int ) string {
b . Helper ( )
var sb strings . Builder
baseTS := "2026-02-20T10:00:00Z"
// Opening user message
sb . WriteString ( fmt . Sprintf ( ` { "type":"user","timestamp":"%s","sessionId":"bench","message": { "role":"user","content":[ { "type":"text","text":"Start benchmark session"}]}} ` , baseTS ) )
sb . WriteByte ( '\n' )
2026-02-22 21:00:17 +00:00
for i := range numTools {
test: add comprehensive Phase 0 test suite — 67 tests, 90.9% coverage
Add parser_test.go (22 tests), search_test.go (9 tests), html_test.go (6 tests),
video_test.go (12 tests), and bench_test.go (4 benchmarks) covering all Phase 0
TODO items:
- ParseTranscript: minimal JSONL, all 7 tool types, errors, truncated/malformed
input, large sessions (1100+ events), nested array/map results, mixed content
- ListSessions: empty dir, single/multi sorted, non-JSONL ignored, modtime fallback
- extractToolInput: all tool types plus nil, invalid JSON, unknown tool fallback
- extractResultContent: string, array, map, and other types
- Search: empty dir, no matches, multi-match, case insensitive, output matching
- RenderHTML: basic, empty, errors, XSS escaping, label types, invalid path
- generateTape/extractCommand: all event types, empty/failed commands, truncation
- Benchmarks: 2.2MB and 11MB ParseTranscript, ListSessions, Search with b.Loop()
go vet ./... clean, go test -race ./... clean.
Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 05:00:38 +00:00
toolID := fmt . Sprintf ( "tool-%d" , i )
offset := i * 2
// Alternate between different tool types for realistic distribution
var toolUse , toolResult string
switch i % 5 {
case 0 : // Bash
toolUse = fmt . Sprintf ( ` { "type":"assistant","timestamp":"2026-02-20T10:%02d:%02dZ","sessionId":"bench","message": { "role":"assistant","content":[ { "type":"tool_use","name":"Bash","id":"%s","input": { "command":"echo iteration %d","description":"echo test"}}]}} ` ,
offset / 60 , offset % 60 , toolID , i )
toolResult = fmt . Sprintf ( ` { "type":"user","timestamp":"2026-02-20T10:%02d:%02dZ","sessionId":"bench","message": { "role":"user","content":[ { "type":"tool_result","tool_use_id":"%s","content":"iteration %d output line one\niteration %d output line two","is_error":false}]}} ` ,
( offset + 1 ) / 60 , ( offset + 1 ) % 60 , toolID , i , i )
case 1 : // Read
toolUse = fmt . Sprintf ( ` { "type":"assistant","timestamp":"2026-02-20T10:%02d:%02dZ","sessionId":"bench","message": { "role":"assistant","content":[ { "type":"tool_use","name":"Read","id":"%s","input": { "file_path":"/tmp/bench/file-%d.go"}}]}} ` ,
offset / 60 , offset % 60 , toolID , i )
toolResult = fmt . Sprintf ( ` { "type":"user","timestamp":"2026-02-20T10:%02d:%02dZ","sessionId":"bench","message": { "role":"user","content":[ { "type":"tool_result","tool_use_id":"%s","content":"package main\n\nfunc main() { \n\tfmt.Println(%d)\n}","is_error":false}]}} ` ,
( offset + 1 ) / 60 , ( offset + 1 ) % 60 , toolID , i )
case 2 : // Edit
toolUse = fmt . Sprintf ( ` { "type":"assistant","timestamp":"2026-02-20T10:%02d:%02dZ","sessionId":"bench","message": { "role":"assistant","content":[ { "type":"tool_use","name":"Edit","id":"%s","input": { "file_path":"/tmp/bench/file-%d.go","old_string":"old","new_string":"new"}}]}} ` ,
offset / 60 , offset % 60 , toolID , i )
toolResult = fmt . Sprintf ( ` { "type":"user","timestamp":"2026-02-20T10:%02d:%02dZ","sessionId":"bench","message": { "role":"user","content":[ { "type":"tool_result","tool_use_id":"%s","content":"ok","is_error":false}]}} ` ,
( offset + 1 ) / 60 , ( offset + 1 ) % 60 , toolID )
case 3 : // Grep
toolUse = fmt . Sprintf ( ` { "type":"assistant","timestamp":"2026-02-20T10:%02d:%02dZ","sessionId":"bench","message": { "role":"assistant","content":[ { "type":"tool_use","name":"Grep","id":"%s","input": { "pattern":"TODO","path":"/tmp/bench"}}]}} ` ,
offset / 60 , offset % 60 , toolID )
toolResult = fmt . Sprintf ( ` { "type":"user","timestamp":"2026-02-20T10:%02d:%02dZ","sessionId":"bench","message": { "role":"user","content":[ { "type":"tool_result","tool_use_id":"%s","content":"/tmp/bench/file.go:10: // TODO fix this","is_error":false}]}} ` ,
( offset + 1 ) / 60 , ( offset + 1 ) % 60 , toolID )
case 4 : // Glob
toolUse = fmt . Sprintf ( ` { "type":"assistant","timestamp":"2026-02-20T10:%02d:%02dZ","sessionId":"bench","message": { "role":"assistant","content":[ { "type":"tool_use","name":"Glob","id":"%s","input": { "pattern":"**/*.go"}}]}} ` ,
offset / 60 , offset % 60 , toolID )
toolResult = fmt . Sprintf ( ` { "type":"user","timestamp":"2026-02-20T10:%02d:%02dZ","sessionId":"bench","message": { "role":"user","content":[ { "type":"tool_result","tool_use_id":"%s","content":"/tmp/a.go\n/tmp/b.go\n/tmp/c.go","is_error":false}]}} ` ,
( offset + 1 ) / 60 , ( offset + 1 ) % 60 , toolID )
}
sb . WriteString ( toolUse )
sb . WriteByte ( '\n' )
sb . WriteString ( toolResult )
sb . WriteByte ( '\n' )
}
// Closing assistant message
sb . WriteString ( fmt . Sprintf ( ` { "type":"assistant","timestamp":"2026-02-20T12:00:00Z","sessionId":"bench","message": { "role":"assistant","content":[ { "type":"text","text":"Benchmark session complete."}]}}%s ` , "\n" ) )
name := fmt . Sprintf ( "bench-%d.jsonl" , numTools )
path := filepath . Join ( dir , name )
if err := os . WriteFile ( path , [ ] byte ( sb . String ( ) ) , 0644 ) ; err != nil {
b . Fatal ( err )
}
info , _ := os . Stat ( path )
b . Logf ( "Generated %s: %d bytes, %d tool pairs" , name , info . Size ( ) , numTools )
return path
}