fix(session): address all CodeRabbit findings on PR #5
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

6+ findings dispositioned. AX-6 maintained (stale testify refs removed).

Code:
- parser_test.go: fixed EOF-truncated JSONL fixtures
- parser.go: ListSessionsSeq skips transcripts when quick scan fails;
  added oversized-line coverage
- parser.go: symlink pre-check replaced with O_NOFOLLOW descriptor
  opens + Fstat for FetchSession and ListSessionsSeq (TOCTOU-safe)
- test_helpers_test.go: assert* helpers changed from fatal to
  non-fatal reporting
- tests/cli/session/main.go: derived expectations from current code
  (CodeRabbit's suggested literals were incorrect for current impl)
  + filepath.Join nit; preserved correct behaviour

CI / config:
- .golangci.yml: migrated to v2 schema
- tests/cli/session/Taskfile.yaml: 'test' broadened to run go vet +
  go test + CLI smoke
- PR title: made specific

Doc:
- AX-2 docstring coverage: comments added to all Go funcs in touched
  files (closes pre-merge docstring warning)
- README + CLAUDE.md + CODEX.md + CONTEXT.md + TODO.md +
  docs/{architecture,development,index}.md + kb/Home.md: removed
  stale testify references, aligned to stdlib testing

Disposition:
- SonarCloud / GHAS: no separate PR comments/checks; gh pr checks
  only reports CodeRabbit. RESOLVED-COMMENT.

Verification: gofmt clean, golangci-lint v2 0 issues, GOWORK=off
go vet + go test -count=1 ./... pass with explicit cache paths,
task -d tests/cli/session clean.

Closes findings on https://github.com/dAppCore/go-session/pull/5

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Snider 2026-04-27 18:17:50 +01:00
parent 209166507b
commit 8ffd10c2ac
23 changed files with 360 additions and 85 deletions

View file

@ -1,3 +1,5 @@
version: "2"
run:
timeout: 5m
go: "1.26"
@ -8,15 +10,15 @@ linters:
- errcheck
- staticcheck
- unused
- gosimple
- ineffassign
- typecheck
- gocritic
- gofmt
disable:
- exhaustive
- wrapcheck
formatters:
enable:
- gofmt
issues:
exclude-use-default: false
max-same-issues: 0

View file

@ -2,7 +2,7 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Claude Code JSONL transcript parser, analytics engine, and HTML/video renderer. Module: `dappco.re/go/core/session`
Claude Code JSONL transcript parser, analytics engine, and HTML/video renderer. Module: `dappco.re/go/session`
## Commands

View file

@ -2,7 +2,7 @@
This file provides guidance to Codex when working in this repository.
Claude Code JSONL transcript parser, analytics engine, and HTML/video renderer. Module: `dappco.re/go/core/session`
Claude Code JSONL transcript parser, analytics engine, and HTML/video renderer. Module: `dappco.re/go/session`
## Commands

View file

@ -39,7 +39,7 @@ The input label adapts to the tool type:
[go-session] Installation
```bash
go get dappco.re/go/core/session@latest
go get dappco.re/go/session@latest
```
### 5. go-session [convention] (score: -0.004)

View file

@ -1,4 +1,4 @@
[![Go Reference](https://pkg.go.dev/badge/dappco.re/go/core/session.svg)](https://pkg.go.dev/dappco.re/go/core/session)
[![Go Reference](https://pkg.go.dev/badge/dappco.re/go/session.svg)](https://pkg.go.dev/dappco.re/go/session)
[![License: EUPL-1.2](https://img.shields.io/badge/License-EUPL--1.2-blue.svg)](LICENSE.md)
[![Go Version](https://img.shields.io/badge/Go-1.26-00ADD8?style=flat&logo=go)](go.mod)
@ -6,14 +6,14 @@
Claude Code JSONL transcript parser, analytics engine, and HTML timeline renderer. Parses Claude Code session files into structured event arrays (tool calls with round-trip durations, user and assistant messages), computes per-tool analytics (call counts, error rates, average and peak latency, estimated token usage), renders self-contained HTML timelines with collapsible panels and client-side search, and generates VHS tape scripts for MP4 video output. No external runtime dependencies — stdlib only.
**Module**: `dappco.re/go/core/session`
**Module**: `dappco.re/go/session`
**Licence**: EUPL-1.2
**Language**: Go 1.26
## Quick Start
```go
import "dappco.re/go/core/session"
import "dappco.re/go/session"
sess, stats, err := session.ParseTranscript("/path/to/session.jsonl")
analytics := session.Analyse(sess)

View file

@ -3,7 +3,7 @@
## Task
Update go.mod require lines from forge.lthn.ai to dappco.re paths. Update versions: core v0.5.0, log v0.1.0, io v0.2.0. Update all .go import paths. Run go mod tidy and go build ./...
> **Status:** Complete. All module paths migrated to `dappco.re/go/core/...`.
> **Status:** Complete. All module paths migrated to `dappco.re/go/...`.
## Checklist
- [x] Read and understand the codebase
@ -13,4 +13,3 @@ Update go.mod require lines from forge.lthn.ai to dappco.re paths. Update versio
- [ ] Commit with conventional commit message
## Context

View file

@ -6,6 +6,7 @@ import (
"time"
)
// TestAnalytics_AnalyseEmptySession_Good verifies the behaviour covered by this test case.
func TestAnalytics_AnalyseEmptySession_Good(t *testing.T) {
sess := &Session{
ID: "empty",
@ -27,12 +28,14 @@ func TestAnalytics_AnalyseEmptySession_Good(t *testing.T) {
assertEqual(t, 0, a.EstimatedOutputTokens)
}
// TestAnalytics_AnalyseNilSession_Good verifies the behaviour covered by this test case.
func TestAnalytics_AnalyseNilSession_Good(t *testing.T) {
a := Analyse(nil)
requireNotNil(t, a)
assertEqual(t, 0, a.EventCount)
}
// TestAnalytics_AnalyseSingleToolCall_Good verifies the behaviour covered by this test case.
func TestAnalytics_AnalyseSingleToolCall_Good(t *testing.T) {
sess := &Session{
ID: "single",
@ -63,6 +66,7 @@ func TestAnalytics_AnalyseSingleToolCall_Good(t *testing.T) {
assertEqual(t, 2*time.Second, a.MaxLatency["Bash"])
}
// TestAnalytics_AnalyseMixedToolsWithErrors_Good verifies the behaviour covered by this test case.
func TestAnalytics_AnalyseMixedToolsWithErrors_Good(t *testing.T) {
sess := &Session{
ID: "mixed",
@ -144,6 +148,7 @@ func TestAnalytics_AnalyseMixedToolsWithErrors_Good(t *testing.T) {
assertEqual(t, 2100*time.Millisecond, a.ActiveTime)
}
// TestAnalytics_AnalyseLatencyCalculations_Good verifies the behaviour covered by this test case.
func TestAnalytics_AnalyseLatencyCalculations_Good(t *testing.T) {
sess := &Session{
ID: "latency",
@ -188,6 +193,7 @@ func TestAnalytics_AnalyseLatencyCalculations_Good(t *testing.T) {
assertEqual(t, 200*time.Millisecond, a.MaxLatency["Read"])
}
// TestAnalytics_AnalyseTokenEstimation_Good verifies the behaviour covered by this test case.
func TestAnalytics_AnalyseTokenEstimation_Good(t *testing.T) {
// 4 chars = ~1 token
sess := &Session{
@ -222,6 +228,7 @@ func TestAnalytics_AnalyseTokenEstimation_Good(t *testing.T) {
assertEqual(t, 50, a.EstimatedOutputTokens)
}
// TestAnalytics_FormatAnalyticsOutput_Good verifies the behaviour covered by this test case.
func TestAnalytics_FormatAnalyticsOutput_Good(t *testing.T) {
a := &SessionAnalytics{
Duration: 5 * time.Minute,
@ -265,6 +272,7 @@ func TestAnalytics_FormatAnalyticsOutput_Good(t *testing.T) {
assertContains(t, output, "Tool Breakdown")
}
// TestAnalytics_FormatAnalyticsEmptyAnalytics_Good verifies the behaviour covered by this test case.
func TestAnalytics_FormatAnalyticsEmptyAnalytics_Good(t *testing.T) {
a := &SessionAnalytics{
ToolCounts: make(map[string]int),

View file

@ -15,6 +15,7 @@ import (
var testNamePattern = regexp.MustCompile(`^Test[A-Za-z0-9]+_[A-Za-z0-9]+_(Good|Bad|Ugly)$`)
// TestConventions_BannedImports_Good verifies the behaviour covered by this test case.
func TestConventions_BannedImports_Good(t *testing.T) {
files := parseGoFiles(t, ".")
@ -43,6 +44,7 @@ func TestConventions_BannedImports_Good(t *testing.T) {
}
}
// TestConventions_ErrorHandling_Good verifies the behaviour covered by this test case.
func TestConventions_ErrorHandling_Good(t *testing.T) {
files := parseGoFiles(t, ".")
@ -81,6 +83,7 @@ func TestConventions_ErrorHandling_Good(t *testing.T) {
}
}
// TestConventions_TestNaming_Good verifies the behaviour covered by this test case.
func TestConventions_TestNaming_Good(t *testing.T) {
files := parseGoFiles(t, ".")
@ -112,6 +115,7 @@ func TestConventions_TestNaming_Good(t *testing.T) {
}
}
// TestConventions_UsageComments_Good verifies the behaviour covered by this test case.
func TestConventions_UsageComments_Good(t *testing.T) {
files := parseGoFiles(t, ".")
@ -166,6 +170,7 @@ type parsedFile struct {
hasTestingDotImport bool
}
// parseGoFiles supports the session test suite.
func parseGoFiles(t *testing.T, dir string) []parsedFile {
t.Helper()
@ -195,6 +200,7 @@ func parseGoFiles(t *testing.T, dir string) []parsedFile {
return files
}
// TestConventions_ParseGoFilesMultiplePackages_Good verifies the behaviour covered by this test case.
func TestConventions_ParseGoFilesMultiplePackages_Good(t *testing.T) {
dir := t.TempDir()
@ -214,12 +220,14 @@ func TestConventions_ParseGoFilesMultiplePackages_Good(t *testing.T) {
}
}
// TestConventions_IsTestingTFuncAliasedImport_Good verifies the behaviour covered by this test case.
func TestConventions_IsTestingTFuncAliasedImport_Good(t *testing.T) {
fileAST, fn := parseTestFunc(t, `
package session_test
import t "testing"
// TestConventions_AliasedImportContext_Good verifies the behaviour covered by this test case.
func TestConventions_AliasedImportContext_Good(testcase *t.T) {}
`, "TestConventions_AliasedImportContext_Good")
@ -235,12 +243,14 @@ func TestConventions_AliasedImportContext_Good(testcase *t.T) {}
}
}
// TestConventions_IsTestingTFuncDotImport_Good verifies the behaviour covered by this test case.
func TestConventions_IsTestingTFuncDotImport_Good(t *testing.T) {
fileAST, fn := parseTestFunc(t, `
package session_test
import . "testing"
// TestConventions_DotImportContext_Good verifies the behaviour covered by this test case.
func TestConventions_DotImportContext_Good(testcase *T) {}
`, "TestConventions_DotImportContext_Good")
@ -256,6 +266,14 @@ func TestConventions_DotImportContext_Good(testcase *T) {}
}
}
// TestConventions_TestHelpers_Good verifies the behaviour covered by this test case.
func TestConventions_TestHelpers_Good(t *testing.T) {
requireEqual(t, "same", "same")
assertNil(t, nil)
assertNotNil(t, t)
}
// testingImports supports the session test suite.
func testingImports(file *ast.File) (map[string]struct{}, bool) {
names := make(map[string]struct{})
hasDotImport := false
@ -282,6 +300,7 @@ func testingImports(file *ast.File) (map[string]struct{}, bool) {
return names, hasDotImport
}
// isTestingTFunc supports the session test suite.
func isTestingTFunc(file parsedFile, fn *ast.FuncDecl) bool {
if fn.Type == nil || fn.Type.Params == nil || len(fn.Type.Params.List) != 1 {
return false
@ -311,6 +330,7 @@ func isTestingTFunc(file parsedFile, fn *ast.FuncDecl) bool {
}
}
// typeDocGroup supports the session test suite.
func typeDocGroup(decl *ast.GenDecl, spec *ast.TypeSpec, index int) *ast.CommentGroup {
if spec.Doc != nil {
return spec.Doc
@ -321,6 +341,7 @@ func typeDocGroup(decl *ast.GenDecl, spec *ast.TypeSpec, index int) *ast.Comment
return nil
}
// valueDocGroup supports the session test suite.
func valueDocGroup(decl *ast.GenDecl, spec *ast.ValueSpec, index int) *ast.CommentGroup {
if spec.Doc != nil {
return spec.Doc
@ -331,6 +352,7 @@ func valueDocGroup(decl *ast.GenDecl, spec *ast.ValueSpec, index int) *ast.Comme
return nil
}
// commentText supports the session test suite.
func commentText(group *ast.CommentGroup) string {
if group == nil {
return ""
@ -338,6 +360,7 @@ func commentText(group *ast.CommentGroup) string {
return core.Trim(group.Text())
}
// hasDocPrefix supports the session test suite.
func hasDocPrefix(text, name string) bool {
if text == "" || !core.HasPrefix(text, name) {
return false
@ -350,6 +373,7 @@ func hasDocPrefix(text, name string) bool {
return (next < 'A' || next > 'Z') && (next < 'a' || next > 'z') && (next < '0' || next > '9') && next != '_'
}
// hasUsageExample supports the session test suite.
func hasUsageExample(text string) bool {
if text == "" {
return false
@ -357,6 +381,7 @@ func hasUsageExample(text string) bool {
return core.HasPrefix(text, "Example:") || core.Contains(text, "\nExample:")
}
// testFileToken supports the session test suite.
func testFileToken(filePath string) string {
stem := core.TrimSuffix(path.Base(filePath), "_test.go")
switch stem {
@ -370,6 +395,7 @@ func testFileToken(filePath string) string {
}
}
// writeTestFile supports the session test suite.
func writeTestFile(t *testing.T, path, content string) {
t.Helper()
@ -379,6 +405,7 @@ func writeTestFile(t *testing.T, path, content string) {
}
}
// parseTestFunc supports the session test suite.
func parseTestFunc(t *testing.T, src, name string) (*ast.File, *ast.FuncDecl) {
t.Helper()

View file

@ -11,6 +11,7 @@ import (
var hostCore = core.New()
var hostFS = (&core.Fs{}).NewUnrestricted()
// sessionCore returns the shared core instance, initialising it if needed.
func sessionCore(c *core.Core) *core.Core {
if c == nil {
c = hostCore
@ -22,17 +23,20 @@ func sessionCore(c *core.Core) *core.Core {
return c
}
// hostContext returns the context associated with the shared core instance.
func hostContext(c *core.Core) context.Context {
c = sessionCore(c)
return c.Context()
}
// hostProcess returns the process runner associated with the shared core instance.
func hostProcess(c *core.Core) *core.Process {
return sessionCore(c).Process()
}
type rawJSON []byte
// UnmarshalJSON stores raw JSON bytes without decoding their nested structure.
func (m *rawJSON) UnmarshalJSON(data []byte) error {
if m == nil {
return core.E("rawJSON.UnmarshalJSON", "nil receiver", nil)
@ -41,6 +45,7 @@ func (m *rawJSON) UnmarshalJSON(data []byte) error {
return nil
}
// MarshalJSON returns the stored raw JSON bytes or null for a nil value.
func (m rawJSON) MarshalJSON() ([]byte, error) {
if m == nil {
return []byte("null"), nil
@ -48,6 +53,7 @@ func (m rawJSON) MarshalJSON() ([]byte, error) {
return m, nil
}
// resultError extracts an error from a failed core result.
func resultError(result core.Result) error {
if result.OK {
return nil
@ -58,6 +64,7 @@ func resultError(result core.Result) error {
return core.E("resultError", "unexpected core result failure", nil)
}
// repeatString repeats a string without importing strings.
func repeatString(s string, count int) string {
if s == "" || count <= 0 {
return ""
@ -65,19 +72,22 @@ func repeatString(s string, count int) string {
return string(bytes.Repeat([]byte(s), count))
}
// containsAny reports whether s contains any rune from chars.
func containsAny(s, chars string) bool {
for _, ch := range chars {
if bytes.IndexRune([]byte(s), ch) >= 0 {
if bytes.ContainsRune([]byte(s), ch) {
return true
}
}
return false
}
// indexOf returns the byte index of substr within s.
func indexOf(s, substr string) int {
return bytes.Index([]byte(s), []byte(substr))
}
// trimQuotes removes matching single-token quote delimiters from s.
func trimQuotes(s string) string {
if len(s) < 2 {
return s

View file

@ -5,7 +5,7 @@ description: Internals of go-session -- JSONL format, parsing pipeline, event mo
# Architecture
Module: `dappco.re/go/core/session`
Module: `dappco.re/go/session`
## Overview

View file

@ -8,7 +8,6 @@ description: How to build, test, lint, and contribute to go-session.
## Prerequisites
- **Go 1.26 or later** -- the module requires Go 1.26 (`go.mod`). The benchmark suite uses `b.Loop()`, introduced in Go 1.25.
- **`github.com/stretchr/testify`** -- test-only dependency, fetched automatically by `go test`.
- **`vhs`** (`github.com/charmbracelet/vhs`) -- optional, required only for `RenderMP4`. Install with `go install github.com/charmbracelet/vhs@latest`.
- **`golangci-lint`** -- optional, for running the full lint suite. Configuration is in `.golangci.yml`.
@ -221,7 +220,7 @@ Co-Authored-By: Virgil <virgil@lethean.io>
## Module Path and Go Workspace
The module path is `dappco.re/go/core/session`. If this package is used within a Go workspace, add it with:
The module path is `dappco.re/go/session`. If this package is used within a Go workspace, add it with:
```bash
go work use ./go-session

View file

@ -7,14 +7,14 @@ description: Claude Code JSONL transcript parser, analytics engine, and HTML tim
`go-session` parses Claude Code JSONL session transcripts into structured event arrays, computes per-tool analytics, renders self-contained HTML timelines with client-side search, and generates VHS tape scripts for MP4 video output. It has no external runtime dependencies -- stdlib only.
**Module path:** `dappco.re/go/core/session`
**Module path:** `dappco.re/go/session`
**Go version:** 1.26
**Licence:** EUPL-1.2
## Quick Start
```go
import "dappco.re/go/core/session"
import "dappco.re/go/session"
// Parse a single session file
sess, stats, err := session.ParseTranscript("/path/to/session.jsonl")
@ -58,10 +58,9 @@ Test files mirror the source files (`parser_test.go`, `analytics_test.go`, `html
| Dependency | Scope | Purpose |
|------------|-------|---------|
| Go standard library | Runtime | All parsing, HTML rendering, file I/O, JSON decoding |
| `github.com/stretchr/testify` | Test only | Assertions and requirements in test files |
| `vhs` (charmbracelet) | Optional external binary | Required only by `RenderMP4` for MP4 video generation |
The package has **zero runtime dependencies** beyond the Go standard library. `testify` is fetched automatically by `go test` and is never imported outside test files.
The package has **zero runtime dependencies** beyond the Go standard library and uses local stdlib-backed test helpers instead of third-party assertion packages.
## Supported Tool Types

21
html.go
View file

@ -122,9 +122,10 @@ body { background: var(--bg); color: var(--fg); font-family: var(--font); font-s
var i int
for evt := range sess.EventsSeq() {
toolClass := core.Lower(evt.Tool)
if evt.Type == "user" {
switch evt.Type {
case "user":
toolClass = "user"
} else if evt.Type == "assistant" {
case "assistant":
toolClass = "assistant"
}
@ -143,9 +144,10 @@ body { background: var(--bg); color: var(--fg); font-family: var(--font); font-s
}
toolLabel := evt.Tool
if evt.Type == "user" {
switch evt.Type {
case "user":
toolLabel = "User"
} else if evt.Type == "assistant" {
case "assistant":
toolLabel = "Claude"
}
@ -182,13 +184,14 @@ body { background: var(--bg); color: var(--fg); font-family: var(--font); font-s
if evt.Input != "" {
label := "Command"
if evt.Type == "user" {
switch {
case evt.Type == "user":
label = "Message"
} else if evt.Type == "assistant" {
case evt.Type == "assistant":
label = "Response"
} else if evt.Tool == "Read" || evt.Tool == "Glob" || evt.Tool == "Grep" {
case evt.Tool == "Read" || evt.Tool == "Glob" || evt.Tool == "Grep":
label = "Target"
} else if evt.Tool == "Edit" || evt.Tool == "Write" {
case evt.Tool == "Edit" || evt.Tool == "Write":
label = "File"
}
b.WriteString(core.Sprintf(` <div class="section"><div class="label">%s</div><pre>%s</pre></div>
@ -260,6 +263,7 @@ document.addEventListener('DOMContentLoaded', openHashEvent);
return nil
}
// shortID returns the abbreviated identifier used by rendered summaries.
func shortID(id string) string {
if len(id) > 8 {
return id[:8]
@ -267,6 +271,7 @@ func shortID(id string) string {
return id
}
// formatDuration formats a duration for compact timeline and analytics output.
func formatDuration(d time.Duration) string {
if d < time.Second {
return core.Sprintf("%dms", d.Milliseconds())

View file

@ -8,6 +8,7 @@ import (
core "dappco.re/go/core"
)
// TestHTML_RenderHTMLBasicSession_Good verifies the behaviour covered by this test case.
func TestHTML_RenderHTMLBasicSession_Good(t *testing.T) {
dir := t.TempDir()
outputPath := dir + "/output.html"
@ -78,6 +79,7 @@ func TestHTML_RenderHTMLBasicSession_Good(t *testing.T) {
assertContains(t, html, "function filterEvents")
}
// TestHTML_RenderHTMLEmptySession_Good verifies the behaviour covered by this test case.
func TestHTML_RenderHTMLEmptySession_Good(t *testing.T) {
dir := t.TempDir()
outputPath := dir + "/empty.html"
@ -102,6 +104,7 @@ func TestHTML_RenderHTMLEmptySession_Good(t *testing.T) {
assertNotContains(t, html, "errors</span>")
}
// TestHTML_RenderHTMLWithErrors_Good verifies the behaviour covered by this test case.
func TestHTML_RenderHTMLWithErrors_Good(t *testing.T) {
dir := t.TempDir()
outputPath := dir + "/errors.html"
@ -146,6 +149,7 @@ func TestHTML_RenderHTMLWithErrors_Good(t *testing.T) {
assertContains(t, html, "&#10003;") // check mark for success
}
// TestHTML_RenderHTMLSpecialCharacters_Good verifies the behaviour covered by this test case.
func TestHTML_RenderHTMLSpecialCharacters_Good(t *testing.T) {
dir := t.TempDir()
outputPath := dir + "/special.html"
@ -186,6 +190,7 @@ func TestHTML_RenderHTMLSpecialCharacters_Good(t *testing.T) {
assertContains(t, html, "&amp;")
}
// TestHTML_RenderHTMLInvalidPath_Ugly verifies the behaviour covered by this test case.
func TestHTML_RenderHTMLInvalidPath_Ugly(t *testing.T) {
sess := &Session{
ID: "test",
@ -197,6 +202,7 @@ func TestHTML_RenderHTMLInvalidPath_Ugly(t *testing.T) {
assertContains(t, err.Error(), "parent directory does not exist")
}
// TestHTML_RenderHTMLLabelsByToolType_Good verifies the behaviour covered by this test case.
func TestHTML_RenderHTMLLabelsByToolType_Good(t *testing.T) {
dir := t.TempDir()
outputPath := dir + "/labels.html"

View file

@ -1,13 +1,13 @@
# go-session
`dappco.re/go/core/session` -- Claude Code session parser and visualiser.
`dappco.re/go/session` -- Claude Code session parser and visualiser.
Reads JSONL transcript files produced by Claude Code, extracts structured events, and renders them as interactive HTML timelines or MP4 videos. Zero external dependencies (stdlib only).
## Installation
```bash
go get dappco.re/go/core/session@latest
go get dappco.re/go/session@latest
```
## Core Types
@ -45,7 +45,7 @@ import (
"fmt"
"log"
"dappco.re/go/core/session"
"dappco.re/go/session"
)
func main() {

109
parser.go
View file

@ -6,7 +6,7 @@ import (
"io/fs" // Note: intrinsic — fs.FileInfo metadata returned from hostFS.Stat; no core equivalent
"iter" // Note: intrinsic — public lazy sequence API for sessions and events; no core equivalent
"slices" // Note: intrinsic — iterator collection, sorted keys, and session ordering; no core equivalent
"syscall" // Note: intrinsic — Stat_t.Mode lstat bits used to reject symlinked transcript files; no core equivalent
"syscall" // Note: intrinsic — O_NOFOLLOW descriptor opens and Errno checks for transcript safety; no core equivalent
"time" // Note: intrinsic — RFC3339 transcript timestamps and session age calculations; no core equivalent
core "dappco.re/go/core"
@ -154,19 +154,11 @@ func ListSessionsSeq(projectsDir string) iter.Seq[Session] {
var sessions []Session
for _, filePath := range matches {
if isSymlink(filePath) {
continue
}
base := core.PathBase(filePath)
id := core.TrimSuffix(base, ".jsonl")
infoResult := hostFS.Stat(filePath)
if !infoResult.OK {
continue
}
info, ok := infoResult.Value.(fs.FileInfo)
if !ok {
f, err := openTranscriptNoFollow(filePath)
if err != nil {
continue
}
@ -176,17 +168,8 @@ func ListSessionsSeq(projectsDir string) iter.Seq[Session] {
}
// Quick scan for first and last timestamps
openResult := hostFS.Open(filePath)
if !openResult.OK {
continue
}
f, ok := openResult.Value.(io.ReadCloser)
if !ok {
continue
}
var firstTS, lastTS string
scanTranscriptLines(f, maxScannerBuffer, func(line []byte) bool {
scanErr := scanTranscriptLines(f, maxScannerBuffer, func(line []byte) bool {
var entry rawEntry
if !core.JSONUnmarshal(line, &entry).OK {
return true
@ -200,7 +183,10 @@ func ListSessionsSeq(projectsDir string) iter.Seq[Session] {
lastTS = entry.Timestamp
return true
})
f.Close()
closeErr := f.Close()
if scanErr != nil || closeErr != nil {
continue
}
if firstTS != "" {
if t, err := time.Parse(time.RFC3339Nano, firstTS); err == nil {
@ -213,7 +199,12 @@ func ListSessionsSeq(projectsDir string) iter.Seq[Session] {
}
}
if s.StartTime.IsZero() {
s.StartTime = info.ModTime()
infoResult := hostFS.Stat(filePath)
if infoResult.OK {
if info, ok := infoResult.Value.(fs.FileInfo); ok {
s.StartTime = info.ModTime()
}
}
}
sessions = append(sessions, s)
@ -283,10 +274,17 @@ func FetchSession(projectsDir, id string) (*Session, *ParseStats, error) {
}
filePath := transcriptPath(projectsDir, id+".jsonl")
if !isSafeProjectFile(filePath) {
f, err := openTranscriptNoFollow(filePath)
if err != nil {
if err == syscall.ENOENT {
return nil, nil, core.E("FetchSession", "open transcript", err)
}
return nil, nil, core.E("FetchSession", "invalid session path", nil)
}
return ParseTranscript(filePath)
defer func() {
_ = f.Close()
}()
return parseTranscriptFile(filePath, f)
}
// ParseTranscript reads a JSONL session file and returns structured events.
@ -303,12 +301,19 @@ func ParseTranscript(filePath string) (*Session, *ParseStats, error) {
if !ok {
return nil, nil, core.E("ParseTranscript", "unexpected file handle type", nil)
}
defer f.Close()
defer func() {
_ = f.Close()
}()
return parseTranscriptFile(filePath, f)
}
// parseTranscriptFile parses an already-open transcript reader and assigns path metadata.
func parseTranscriptFile(filePath string, r io.Reader) (*Session, *ParseStats, error) {
base := core.PathBase(filePath)
id := core.TrimSuffix(base, ".jsonl")
sess, stats, err := parseFromReader(f, id)
sess, stats, err := parseFromReader(r, id)
if sess != nil {
sess.Path = filePath
}
@ -504,6 +509,7 @@ func parseFromReader(r io.Reader, id string) (*Session, *ParseStats, error) {
return sess, stats, nil
}
// extractToolInput converts raw Claude tool input into a concise display string.
func extractToolInput(toolName string, raw rawJSON) string {
if raw == nil {
return ""
@ -573,6 +579,7 @@ func extractToolInput(toolName string, raw rawJSON) string {
return ""
}
// extractResultContent converts Claude tool_result content into plain text.
func extractResultContent(content any) string {
switch v := content.(type) {
case string:
@ -595,6 +602,7 @@ func extractResultContent(content any) string {
return core.Sprint(content)
}
// truncate returns s capped to max bytes with an ellipsis marker.
func truncate(s string, max int) string {
if len(s) <= max {
return s
@ -602,6 +610,7 @@ func truncate(s string, max int) string {
return s[:max] + "..."
}
// scanTranscriptLines streams newline-delimited records with a per-line size limit.
func scanTranscriptLines(r io.Reader, maxLineSize int, handle func([]byte) bool) error {
if maxLineSize <= 0 {
maxLineSize = maxScannerBuffer
@ -651,6 +660,7 @@ func scanTranscriptLines(r io.Reader, maxLineSize int, handle func([]byte) bool)
}
}
// trimLineBreak removes a trailing carriage return from a scanned line.
func trimLineBreak(line []byte) []byte {
if len(line) > 0 && line[len(line)-1] == '\r' {
return line[:len(line)-1]
@ -658,6 +668,7 @@ func trimLineBreak(line []byte) []byte {
return line
}
// transcriptPath joins a projects directory and transcript file name.
func transcriptPath(projectsDir, name string) string {
if projectsDir == "" {
return core.CleanPath(name, "/")
@ -665,14 +676,42 @@ func transcriptPath(projectsDir, name string) string {
return core.CleanPath(core.JoinPath(projectsDir, name), "/")
}
func isSymlink(filePath string) bool {
var st syscall.Stat_t
if err := syscall.Lstat(filePath, &st); err != nil {
return false
}
return st.Mode&syscall.S_IFMT == syscall.S_IFLNK
type noFollowFile struct {
fd int
}
func isSafeProjectFile(filePath string) bool {
return !isSymlink(filePath)
// Read reads bytes from a descriptor opened without following symlinks.
func (f *noFollowFile) Read(p []byte) (int, error) {
n, err := syscall.Read(f.fd, p)
if err != nil {
return n, err
}
if n == 0 {
return 0, io.EOF
}
return n, nil
}
// Close closes a descriptor opened without following symlinks.
func (f *noFollowFile) Close() error {
return syscall.Close(f.fd)
}
// openTranscriptNoFollow opens a regular transcript file without following symlinks.
func openTranscriptNoFollow(filePath string) (io.ReadCloser, error) {
fd, err := syscall.Open(filePath, syscall.O_RDONLY|syscall.O_NOFOLLOW, 0)
if err != nil {
return nil, err
}
var st syscall.Stat_t
if err := syscall.Fstat(fd, &st); err != nil {
_ = syscall.Close(fd)
return nil, err
}
if st.Mode&syscall.S_IFMT != syscall.S_IFREG {
_ = syscall.Close(fd)
return nil, core.E("openTranscriptNoFollow", "not a regular file", nil)
}
return &noFollowFile{fd: fd}, nil
}

View file

@ -108,6 +108,7 @@ func writeJSONL(t *testing.T, dir string, name string, lines ...string) string {
return filePath
}
// setFileTimes supports the session test suite.
func setFileTimes(filePath string, atime, mtime time.Time) error {
return syscall.UtimesNano(filePath, []syscall.Timespec{
syscall.NsecToTimespec(atime.UnixNano()),
@ -117,6 +118,7 @@ func setFileTimes(filePath string, atime, mtime time.Time) error {
// --- ParseTranscript tests ---
// TestParser_ParseTranscriptMinimalValid_Good verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptMinimalValid_Good(t *testing.T) {
dir := t.TempDir()
path := writeJSONL(t, dir, "minimal.jsonl",
@ -142,6 +144,7 @@ func TestParser_ParseTranscriptMinimalValid_Good(t *testing.T) {
assertEqual(t, "Hi there!", sess.Events[1].Input)
}
// TestParser_ParseTranscriptToolCalls_Good verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptToolCalls_Good(t *testing.T) {
dir := t.TempDir()
@ -233,6 +236,7 @@ func TestParser_ParseTranscriptToolCalls_Good(t *testing.T) {
assertEqual(t, "[research] Code analysis", toolEvents[6].Input)
}
// TestParser_ParseTranscriptToolError_Good verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptToolError_Good(t *testing.T) {
dir := t.TempDir()
path := writeJSONL(t, dir, "error.jsonl",
@ -257,6 +261,7 @@ func TestParser_ParseTranscriptToolError_Good(t *testing.T) {
assertContains(t, toolEvents[0].ErrorMsg, "No such file or directory")
}
// TestParser_ParseTranscriptEmptyFile_Bad verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptEmptyFile_Bad(t *testing.T) {
dir := t.TempDir()
path := writeJSONL(t, dir, "empty.jsonl")
@ -271,6 +276,7 @@ func TestParser_ParseTranscriptEmptyFile_Bad(t *testing.T) {
assertTrue(t, sess.StartTime.IsZero())
}
// TestParser_ParseTranscriptMalformedJSON_Bad verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptMalformedJSON_Bad(t *testing.T) {
dir := t.TempDir()
path := writeJSONL(t, dir, "malformed.jsonl",
@ -291,6 +297,7 @@ func TestParser_ParseTranscriptMalformedJSON_Bad(t *testing.T) {
assertEqual(t, "assistant", sess.Events[1].Type)
}
// TestParser_ParseTranscriptTruncatedJSONL_Bad verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptTruncatedJSONL_Bad(t *testing.T) {
dir := t.TempDir()
validLine := userTextEntry(ts(0), "Hello")
@ -309,6 +316,7 @@ func TestParser_ParseTranscriptTruncatedJSONL_Bad(t *testing.T) {
assertEqual(t, "user", sess.Events[0].Type)
}
// TestParser_ParseTranscriptLargeSession_Good verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptLargeSession_Good(t *testing.T) {
dir := t.TempDir()
@ -342,6 +350,7 @@ func TestParser_ParseTranscriptLargeSession_Good(t *testing.T) {
assertEqual(t, 1100, toolCount, "all 1100 tool events should be parsed")
}
// TestParser_ParseTranscriptNestedToolResults_Good verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptNestedToolResults_Good(t *testing.T) {
dir := t.TempDir()
@ -391,6 +400,7 @@ func TestParser_ParseTranscriptNestedToolResults_Good(t *testing.T) {
assertContains(t, toolEvents[0].Output, "Second block")
}
// TestParser_ParseTranscriptNestedMapResult_Good verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptNestedMapResult_Good(t *testing.T) {
dir := t.TempDir()
@ -435,12 +445,14 @@ func TestParser_ParseTranscriptNestedMapResult_Good(t *testing.T) {
assertContains(t, toolEvents[0].Output, "file contents here")
}
// TestParser_ParseTranscriptFileNotFound_Ugly verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptFileNotFound_Ugly(t *testing.T) {
_, _, err := ParseTranscript("/nonexistent/path/session.jsonl")
requireError(t, err)
assertContains(t, err.Error(), "open transcript")
}
// TestParser_ParseTranscriptSessionIDFromFilename_Good verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptSessionIDFromFilename_Good(t *testing.T) {
dir := t.TempDir()
path := writeJSONL(t, dir, "abc123def456.jsonl",
@ -452,6 +464,7 @@ func TestParser_ParseTranscriptSessionIDFromFilename_Good(t *testing.T) {
assertEqual(t, "abc123def456", sess.ID)
}
// TestParser_ParseTranscriptTimestampsTracked_Good verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptTimestampsTracked_Good(t *testing.T) {
dir := t.TempDir()
path := writeJSONL(t, dir, "timestamps.jsonl",
@ -470,6 +483,7 @@ func TestParser_ParseTranscriptTimestampsTracked_Good(t *testing.T) {
assertEqual(t, expectedEnd, sess.EndTime)
}
// TestParser_ParseTranscriptTextTruncation_Good verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptTextTruncation_Good(t *testing.T) {
dir := t.TempDir()
longText := repeatString("x", 600)
@ -486,6 +500,7 @@ func TestParser_ParseTranscriptTextTruncation_Good(t *testing.T) {
assertTrue(t, core.HasSuffix(sess.Events[0].Input, "..."), "truncated text should end with ...")
}
// TestParser_SessionEventsSeq_Good verifies the behaviour covered by this test case.
func TestParser_SessionEventsSeq_Good(t *testing.T) {
sess := &Session{
Events: []Event{
@ -503,6 +518,7 @@ func TestParser_SessionEventsSeq_Good(t *testing.T) {
assertEqual(t, sess.Events, events)
}
// TestParser_ParseTranscriptMixedContentBlocks_Good verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptMixedContentBlocks_Good(t *testing.T) {
// Assistant message with both text and tool_use in the same message
dir := t.TempDir()
@ -541,6 +557,7 @@ func TestParser_ParseTranscriptMixedContentBlocks_Good(t *testing.T) {
assertEqual(t, "Read", sess.Events[1].Tool)
}
// TestParser_ParseTranscriptUnmatchedToolResult_Bad verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptUnmatchedToolResult_Bad(t *testing.T) {
// A tool_result with no matching tool_use should be silently ignored
dir := t.TempDir()
@ -557,6 +574,7 @@ func TestParser_ParseTranscriptUnmatchedToolResult_Bad(t *testing.T) {
assertEqual(t, "user", sess.Events[0].Type)
}
// TestParser_ParseTranscriptEmptyTimestamp_Bad verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptEmptyTimestamp_Bad(t *testing.T) {
dir := t.TempDir()
// Entry with empty timestamp
@ -582,6 +600,7 @@ func TestParser_ParseTranscriptEmptyTimestamp_Bad(t *testing.T) {
// --- ListSessions tests ---
// TestParser_ListSessionsEmptyDir_Good verifies the behaviour covered by this test case.
func TestParser_ListSessionsEmptyDir_Good(t *testing.T) {
dir := t.TempDir()
@ -590,6 +609,7 @@ func TestParser_ListSessionsEmptyDir_Good(t *testing.T) {
assertEmpty(t, sessions)
}
// TestParser_ListSessionsSingleSession_Good verifies the behaviour covered by this test case.
func TestParser_ListSessionsSingleSession_Good(t *testing.T) {
dir := t.TempDir()
writeJSONL(t, dir, "session-abc.jsonl",
@ -606,6 +626,7 @@ func TestParser_ListSessionsSingleSession_Good(t *testing.T) {
assertFalse(t, sessions[0].EndTime.IsZero())
}
// TestParser_ListSessionsMultipleSorted_Good verifies the behaviour covered by this test case.
func TestParser_ListSessionsMultipleSorted_Good(t *testing.T) {
dir := t.TempDir()
@ -631,6 +652,7 @@ func TestParser_ListSessionsMultipleSorted_Good(t *testing.T) {
assertEqual(t, "old", sessions[2].ID)
}
// TestParser_ListSessionsNonJSONLIgnored_Good verifies the behaviour covered by this test case.
func TestParser_ListSessionsNonJSONLIgnored_Good(t *testing.T) {
dir := t.TempDir()
@ -648,6 +670,7 @@ func TestParser_ListSessionsNonJSONLIgnored_Good(t *testing.T) {
assertEqual(t, "real-session", sessions[0].ID)
}
// TestParser_ListSessionsSeqMultipleSorted_Good verifies the behaviour covered by this test case.
func TestParser_ListSessionsSeqMultipleSorted_Good(t *testing.T) {
dir := t.TempDir()
@ -668,6 +691,7 @@ func TestParser_ListSessionsSeqMultipleSorted_Good(t *testing.T) {
assertEqual(t, "old", sessions[2].ID)
}
// TestParser_ListSessionsMalformedJSONLStillListed_Bad verifies the behaviour covered by this test case.
func TestParser_ListSessionsMalformedJSONLStillListed_Bad(t *testing.T) {
dir := t.TempDir()
@ -687,66 +711,77 @@ func TestParser_ListSessionsMalformedJSONLStillListed_Bad(t *testing.T) {
// --- extractToolInput tests ---
// TestParser_ExtractToolInputBash_Good verifies the behaviour covered by this test case.
func TestParser_ExtractToolInputBash_Good(t *testing.T) {
input := rawJSON([]byte(`{"command":"go test ./...","description":"run tests","timeout":120}`))
result := extractToolInput("Bash", input)
assertEqual(t, "go test ./... # run tests", result)
}
// TestParser_ExtractToolInputBashNoDescription_Good verifies the behaviour covered by this test case.
func TestParser_ExtractToolInputBashNoDescription_Good(t *testing.T) {
input := rawJSON([]byte(`{"command":"ls -la"}`))
result := extractToolInput("Bash", input)
assertEqual(t, "ls -la", result)
}
// TestParser_ExtractToolInputRead_Good verifies the behaviour covered by this test case.
func TestParser_ExtractToolInputRead_Good(t *testing.T) {
input := rawJSON([]byte(`{"file_path":"/Users/test/main.go","offset":10,"limit":50}`))
result := extractToolInput("Read", input)
assertEqual(t, "/Users/test/main.go", result)
}
// TestParser_ExtractToolInputEdit_Good verifies the behaviour covered by this test case.
func TestParser_ExtractToolInputEdit_Good(t *testing.T) {
input := rawJSON([]byte(`{"file_path":"/tmp/app.go","old_string":"foo","new_string":"bar"}`))
result := extractToolInput("Edit", input)
assertEqual(t, "/tmp/app.go (edit)", result)
}
// TestParser_ExtractToolInputWrite_Good verifies the behaviour covered by this test case.
func TestParser_ExtractToolInputWrite_Good(t *testing.T) {
input := rawJSON([]byte(`{"file_path":"/tmp/out.txt","content":"hello world"}`))
result := extractToolInput("Write", input)
assertEqual(t, "/tmp/out.txt (11 bytes)", result)
}
// TestParser_ExtractToolInputGrep_Good verifies the behaviour covered by this test case.
func TestParser_ExtractToolInputGrep_Good(t *testing.T) {
input := rawJSON([]byte(`{"pattern":"TODO","path":"/src"}`))
result := extractToolInput("Grep", input)
assertEqual(t, "/TODO/ in /src", result)
}
// TestParser_ExtractToolInputGrepNoPath_Good verifies the behaviour covered by this test case.
func TestParser_ExtractToolInputGrepNoPath_Good(t *testing.T) {
input := rawJSON([]byte(`{"pattern":"FIXME"}`))
result := extractToolInput("Grep", input)
assertEqual(t, "/FIXME/ in .", result)
}
// TestParser_ExtractToolInputGlob_Good verifies the behaviour covered by this test case.
func TestParser_ExtractToolInputGlob_Good(t *testing.T) {
input := rawJSON([]byte(`{"pattern":"**/*.go","path":"/src"}`))
result := extractToolInput("Glob", input)
assertEqual(t, "**/*.go", result)
}
// TestParser_ExtractToolInputTask_Good verifies the behaviour covered by this test case.
func TestParser_ExtractToolInputTask_Good(t *testing.T) {
input := rawJSON([]byte(`{"prompt":"Analyse the codebase","description":"Code review","subagent_type":"research"}`))
result := extractToolInput("Task", input)
assertEqual(t, "[research] Code review", result)
}
// TestParser_ExtractToolInputTaskNoDescription_Good verifies the behaviour covered by this test case.
func TestParser_ExtractToolInputTaskNoDescription_Good(t *testing.T) {
input := rawJSON([]byte(`{"prompt":"Short prompt","subagent_type":"codegen"}`))
result := extractToolInput("Task", input)
assertEqual(t, "[codegen] Short prompt", result)
}
// TestParser_ExtractToolInputUnknownTool_Good verifies the behaviour covered by this test case.
func TestParser_ExtractToolInputUnknownTool_Good(t *testing.T) {
input := rawJSON([]byte(`{"alpha":"one","beta":"two"}`))
result := extractToolInput("CustomTool", input)
@ -754,11 +789,13 @@ func TestParser_ExtractToolInputUnknownTool_Good(t *testing.T) {
assertEqual(t, "alpha, beta", result)
}
// TestParser_ExtractToolInputNilInput_Bad verifies the behaviour covered by this test case.
func TestParser_ExtractToolInputNilInput_Bad(t *testing.T) {
result := extractToolInput("Bash", nil)
assertEqual(t, "", result)
}
// TestParser_ExtractToolInputInvalidJSON_Bad verifies the behaviour covered by this test case.
func TestParser_ExtractToolInputInvalidJSON_Bad(t *testing.T) {
input := rawJSON([]byte(`{broken`))
result := extractToolInput("Bash", input)
@ -768,11 +805,13 @@ func TestParser_ExtractToolInputInvalidJSON_Bad(t *testing.T) {
// --- extractResultContent tests ---
// TestParser_ExtractResultContentString_Good verifies the behaviour covered by this test case.
func TestParser_ExtractResultContentString_Good(t *testing.T) {
result := extractResultContent("simple string")
assertEqual(t, "simple string", result)
}
// TestParser_ExtractResultContentArray_Good verifies the behaviour covered by this test case.
func TestParser_ExtractResultContentArray_Good(t *testing.T) {
content := []any{
map[string]any{"type": "text", "text": "line one"},
@ -782,12 +821,14 @@ func TestParser_ExtractResultContentArray_Good(t *testing.T) {
assertEqual(t, "line one\nline two", result)
}
// TestParser_ExtractResultContentMap_Good verifies the behaviour covered by this test case.
func TestParser_ExtractResultContentMap_Good(t *testing.T) {
content := map[string]any{"text": "from map"}
result := extractResultContent(content)
assertEqual(t, "from map", result)
}
// TestParser_ExtractResultContentOther_Bad verifies the behaviour covered by this test case.
func TestParser_ExtractResultContentOther_Bad(t *testing.T) {
result := extractResultContent(42)
assertEqual(t, "42", result)
@ -795,31 +836,37 @@ func TestParser_ExtractResultContentOther_Bad(t *testing.T) {
// --- truncate tests ---
// TestParser_TruncateShort_Good verifies the behaviour covered by this test case.
func TestParser_TruncateShort_Good(t *testing.T) {
assertEqual(t, "hello", truncate("hello", 10))
}
// TestParser_TruncateExact_Good verifies the behaviour covered by this test case.
func TestParser_TruncateExact_Good(t *testing.T) {
assertEqual(t, "hello", truncate("hello", 5))
}
// TestParser_TruncateLong_Good verifies the behaviour covered by this test case.
func TestParser_TruncateLong_Good(t *testing.T) {
result := truncate("hello world", 5)
assertEqual(t, "hello...", result)
}
// TestParser_TruncateEmpty_Good verifies the behaviour covered by this test case.
func TestParser_TruncateEmpty_Good(t *testing.T) {
assertEqual(t, "", truncate("", 10))
}
// --- helper function tests ---
// TestParser_ShortIDTruncatesAndPreservesLength_Good verifies the behaviour covered by this test case.
func TestParser_ShortIDTruncatesAndPreservesLength_Good(t *testing.T) {
assertEqual(t, "abcdefgh", shortID("abcdefghijklmnop"))
assertEqual(t, "short", shortID("short"))
assertEqual(t, "12345678", shortID("12345678"))
}
// TestParser_FormatDurationCommonDurations_Good verifies the behaviour covered by this test case.
func TestParser_FormatDurationCommonDurations_Good(t *testing.T) {
assertEqual(t, "500ms", formatDuration(500*time.Millisecond))
assertEqual(t, "1.5s", formatDuration(1500*time.Millisecond))
@ -829,6 +876,7 @@ func TestParser_FormatDurationCommonDurations_Good(t *testing.T) {
// --- ParseStats tests ---
// TestParser_ParseStatsCleanJSONL_Good verifies the behaviour covered by this test case.
func TestParser_ParseStatsCleanJSONL_Good(t *testing.T) {
dir := t.TempDir()
path := writeJSONL(t, dir, "clean.jsonl",
@ -850,6 +898,7 @@ func TestParser_ParseStatsCleanJSONL_Good(t *testing.T) {
assertEmpty(t, stats.Warnings)
}
// TestParser_ParseStatsMalformedLines_Good verifies the behaviour covered by this test case.
func TestParser_ParseStatsMalformedLines_Good(t *testing.T) {
dir := t.TempDir()
path := writeJSONL(t, dir, "malformed-stats.jsonl",
@ -874,6 +923,7 @@ func TestParser_ParseStatsMalformedLines_Good(t *testing.T) {
}
}
// TestParser_ParseStatsOrphanedToolCalls_Ugly verifies the behaviour covered by this test case.
func TestParser_ParseStatsOrphanedToolCalls_Ugly(t *testing.T) {
dir := t.TempDir()
// Two tool_use entries with no matching tool_result
@ -903,14 +953,15 @@ func TestParser_ParseStatsOrphanedToolCalls_Ugly(t *testing.T) {
assertEqual(t, 2, orphanWarnings)
}
// TestParser_ParseStatsTruncatedFinalLine_Good verifies the behaviour covered by this test case.
func TestParser_ParseStatsTruncatedFinalLine_Good(t *testing.T) {
dir := t.TempDir()
validLine := userTextEntry(ts(0), "Hello")
truncatedLine := `{"type":"assi`
// Write without trailing newline after truncated line
// Write without trailing newline after truncated line.
path := path.Join(dir, "truncfinal.jsonl")
requireTrue(t, hostFS.Write(path, validLine+"\n"+truncatedLine+"\n").OK)
requireTrue(t, hostFS.Write(path, validLine+"\n"+truncatedLine).OK)
_, stats, err := ParseTranscript(path)
requireNoError(t, err)
@ -928,13 +979,14 @@ func TestParser_ParseStatsTruncatedFinalLine_Good(t *testing.T) {
assertTrue(t, foundTruncated, "should detect truncated final line")
}
// TestParser_ParseStatsFileEndingMidJSON_Good verifies the behaviour covered by this test case.
func TestParser_ParseStatsFileEndingMidJSON_Good(t *testing.T) {
dir := t.TempDir()
validLine := userTextEntry(ts(0), "Hello")
midJSON := `{"type":"assistant","timestamp":"2026-02-20T10:00:01Z","sessionId":"test","message":{"role":"assi`
path := path.Join(dir, "midjson.jsonl")
requireTrue(t, hostFS.Write(path, validLine+"\n"+midJSON+"\n").OK)
requireTrue(t, hostFS.Write(path, validLine+"\n"+midJSON).OK)
sess, stats, err := ParseTranscript(path)
requireNoError(t, err)
@ -952,6 +1004,7 @@ func TestParser_ParseStatsFileEndingMidJSON_Good(t *testing.T) {
assertTrue(t, foundTruncated)
}
// TestParser_ParseStatsCompleteFileNoTrailingNewline_Good verifies the behaviour covered by this test case.
func TestParser_ParseStatsCompleteFileNoTrailingNewline_Good(t *testing.T) {
dir := t.TempDir()
line := userTextEntry(ts(0), "Hello")
@ -979,6 +1032,7 @@ func TestParser_ParseStatsCompleteFileNoTrailingNewline_Good(t *testing.T) {
assertFalse(t, foundTruncated)
}
// TestParser_ParseStatsWarningPreviewTruncated_Good verifies the behaviour covered by this test case.
func TestParser_ParseStatsWarningPreviewTruncated_Good(t *testing.T) {
dir := t.TempDir()
// A malformed line longer than 100 chars
@ -1000,6 +1054,7 @@ func TestParser_ParseStatsWarningPreviewTruncated_Good(t *testing.T) {
// --- ParseTranscriptReader (streaming) tests ---
// TestParser_ParseTranscriptReaderMinimalValid_Good verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptReaderMinimalValid_Good(t *testing.T) {
// Parse directly from an in-memory reader.
data := core.Join("\n", []string{
@ -1022,6 +1077,7 @@ func TestParser_ParseTranscriptReaderMinimalValid_Good(t *testing.T) {
assertEqual(t, 0, stats.SkippedLines)
}
// TestParser_ParseTranscriptReaderBytesBuffer_Good verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptReaderBytesBuffer_Good(t *testing.T) {
// Parse from a bytes.Buffer (common streaming use case).
data := core.Join("\n", []string{
@ -1040,6 +1096,7 @@ func TestParser_ParseTranscriptReaderBytesBuffer_Good(t *testing.T) {
assertTrue(t, sess.Events[0].Success)
}
// TestParser_ParseTranscriptReaderEmptyReader_Good verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptReaderEmptyReader_Good(t *testing.T) {
reader := core.NewReader("")
@ -1050,6 +1107,7 @@ func TestParser_ParseTranscriptReaderEmptyReader_Good(t *testing.T) {
assertEqual(t, 0, stats.TotalLines)
}
// TestParser_ParseTranscriptReaderLargeLines_Good verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptReaderLargeLines_Good(t *testing.T) {
// Verify the scanner handles very long lines (> 64KB).
longText := repeatString("x", 128*1024) // 128KB of text
@ -1063,6 +1121,7 @@ func TestParser_ParseTranscriptReaderLargeLines_Good(t *testing.T) {
assertLen(t, sess.Events[0].Input, 503) // 500 + "..."
}
// TestParser_ParseTranscriptReaderMalformedWithStats_Good verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptReaderMalformedWithStats_Good(t *testing.T) {
// Malformed lines in a reader should still produce correct stats.
data := core.Join("\n", []string{
@ -1079,6 +1138,7 @@ func TestParser_ParseTranscriptReaderMalformedWithStats_Good(t *testing.T) {
assertEqual(t, 2, stats.SkippedLines)
}
// TestParser_ParseTranscriptReaderOrphanedTools_Good verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptReaderOrphanedTools_Good(t *testing.T) {
// Tool calls without results should be tracked in stats.
data := core.Join("\n", []string{
@ -1094,6 +1154,7 @@ func TestParser_ParseTranscriptReaderOrphanedTools_Good(t *testing.T) {
assertEqual(t, 1, stats.OrphanedToolCalls)
}
// TestParser_ParseTranscriptToolUseInputTruncated_Bad verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptToolUseInputTruncated_Bad(t *testing.T) {
// Pending tool inputs should not retain an entire scanner-sized line.
hugeCommand := repeatString("x", 1024*1024)
@ -1110,6 +1171,7 @@ func TestParser_ParseTranscriptToolUseInputTruncated_Bad(t *testing.T) {
assertLen(t, sess.Events[0].Input, 503)
}
// TestParser_ParseTranscriptPendingToolLimit_Bad verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptPendingToolLimit_Bad(t *testing.T) {
// Unmatched tool_use entries are attacker-controlled and must be bounded.
lines := make([]string, 0, maxPendingToolCalls+1)
@ -1128,6 +1190,7 @@ func TestParser_ParseTranscriptPendingToolLimit_Bad(t *testing.T) {
assertContains(t, core.Join("\n", stats.Warnings...), "pending tool limit reached")
}
// TestParser_ParseTranscriptDeeplyNestedJSON_Bad verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptDeeplyNestedJSON_Bad(t *testing.T) {
// Deep malformed JSON should be reported as a skipped line, not panic.
deep := repeatString("[", 1200) + repeatString("]", 1200)
@ -1146,6 +1209,7 @@ func TestParser_ParseTranscriptDeeplyNestedJSON_Bad(t *testing.T) {
assertEqual(t, 1, stats.SkippedLines)
}
// TestParser_ParseTranscriptUnexpectedToolTypes_Bad verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptUnexpectedToolTypes_Bad(t *testing.T) {
// Unexpected input/content JSON types should not panic type extraction.
data := core.Join("\n", []string{
@ -1160,6 +1224,7 @@ func TestParser_ParseTranscriptUnexpectedToolTypes_Bad(t *testing.T) {
assertEqual(t, "42", sess.Events[0].Output)
}
// TestParser_ParseTranscriptUTF16SurrogateHalf_Bad verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptUTF16SurrogateHalf_Bad(t *testing.T) {
// Lone UTF-16 surrogate escapes are accepted by encoding/json as replacement runes.
data := `{"type":"user","timestamp":"` + ts(0) + `","sessionId":"utf","message":{"role":"user","content":[{"type":"text","text":"bad \ud800 text"}]}}` + "\n"
@ -1178,6 +1243,7 @@ func TestParser_ParseTranscriptUTF16SurrogateHalf_Bad(t *testing.T) {
// --- Custom MCP tool tests ---
// TestParser_ParseTranscriptCustomMCPTool_Good verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptCustomMCPTool_Good(t *testing.T) {
// A tool_use with a non-standard MCP tool name (e.g. mcp__server__tool).
dir := t.TempDir()
@ -1210,6 +1276,7 @@ func TestParser_ParseTranscriptCustomMCPTool_Good(t *testing.T) {
assertTrue(t, toolEvents[0].Success)
}
// TestParser_ParseTranscriptCustomMCPToolNestedInput_Good verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptCustomMCPToolNestedInput_Good(t *testing.T) {
// MCP tool with nested JSON input — should show top-level keys.
dir := t.TempDir()
@ -1237,6 +1304,7 @@ func TestParser_ParseTranscriptCustomMCPToolNestedInput_Good(t *testing.T) {
assertContains(t, toolEvents[0].Input, "query")
}
// TestParser_ParseTranscriptUnknownToolEmptyInput_Good verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptUnknownToolEmptyInput_Good(t *testing.T) {
// A tool_use with an empty input object.
dir := t.TempDir()
@ -1263,6 +1331,7 @@ func TestParser_ParseTranscriptUnknownToolEmptyInput_Good(t *testing.T) {
// --- Edge case error recovery tests ---
// TestParser_ParseTranscriptBinaryGarbage_Ugly verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptBinaryGarbage_Ugly(t *testing.T) {
// Binary garbage interspersed with valid lines — must not panic.
dir := t.TempDir()
@ -1289,6 +1358,7 @@ func TestParser_ParseTranscriptBinaryGarbage_Ugly(t *testing.T) {
assertEqual(t, 2, stats.SkippedLines)
}
// TestParser_ParseTranscriptNullBytes_Ugly verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptNullBytes_Ugly(t *testing.T) {
// Lines with embedded null bytes.
dir := t.TempDir()
@ -1303,6 +1373,7 @@ func TestParser_ParseTranscriptNullBytes_Ugly(t *testing.T) {
assertLen(t, sess.Events, 1)
}
// TestParser_ParseTranscriptVeryLongLine_Ugly verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptVeryLongLine_Ugly(t *testing.T) {
// A single line that exceeds the default bufio.Scanner buffer.
// The parser should handle this without error thanks to the enlarged buffer.
@ -1317,6 +1388,7 @@ func TestParser_ParseTranscriptVeryLongLine_Ugly(t *testing.T) {
requireLen(t, sess.Events, 1)
}
// TestParser_ParseTranscriptMalformedMessageJSON_Bad verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptMalformedMessageJSON_Bad(t *testing.T) {
// Valid outer JSON but the message field is not valid message structure.
dir := t.TempDir()
@ -1333,6 +1405,7 @@ func TestParser_ParseTranscriptMalformedMessageJSON_Bad(t *testing.T) {
assertEqual(t, "ok", sess.Events[0].Input)
}
// TestParser_ParseTranscriptMalformedContentBlock_Bad verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptMalformedContentBlock_Bad(t *testing.T) {
// Valid message structure but content blocks are malformed.
dir := t.TempDir()
@ -1348,6 +1421,7 @@ func TestParser_ParseTranscriptMalformedContentBlock_Bad(t *testing.T) {
assertEqual(t, "still ok", sess.Events[0].Input)
}
// TestParser_ParseTranscriptTruncatedMissingBrace_Good verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptTruncatedMissingBrace_Good(t *testing.T) {
// Final line is missing its closing brace — should be skipped gracefully.
dir := t.TempDir()
@ -1366,6 +1440,7 @@ func TestParser_ParseTranscriptTruncatedMissingBrace_Good(t *testing.T) {
assertEqual(t, "also valid", sess.Events[1].Input)
}
// TestParser_ParseTranscriptTruncatedMidKey_Good verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptTruncatedMidKey_Good(t *testing.T) {
// Line truncated in the middle of a JSON key.
dir := t.TempDir()
@ -1381,6 +1456,7 @@ func TestParser_ParseTranscriptTruncatedMidKey_Good(t *testing.T) {
assertEqual(t, "first", sess.Events[0].Input)
}
// TestParser_ParseTranscriptAllBadLines_Good verifies the behaviour covered by this test case.
func TestParser_ParseTranscriptAllBadLines_Good(t *testing.T) {
// Every line is truncated/malformed — result should be empty, no error.
dir := t.TempDir()
@ -1402,6 +1478,7 @@ func TestParser_ParseTranscriptAllBadLines_Good(t *testing.T) {
// --- PruneSessions tests ---
// TestParser_PruneSessionsDeletesOldFiles_Good verifies the behaviour covered by this test case.
func TestParser_PruneSessionsDeletesOldFiles_Good(t *testing.T) {
dir := t.TempDir()
@ -1430,6 +1507,7 @@ func TestParser_PruneSessionsDeletesOldFiles_Good(t *testing.T) {
assertEqual(t, "new-session", sessions[0].ID)
}
// TestParser_PruneSessionsNothingToDelete_Good verifies the behaviour covered by this test case.
func TestParser_PruneSessionsNothingToDelete_Good(t *testing.T) {
dir := t.TempDir()
@ -1442,6 +1520,7 @@ func TestParser_PruneSessionsNothingToDelete_Good(t *testing.T) {
assertEqual(t, 0, deleted)
}
// TestParser_PruneSessionsEmptyDir_Good verifies the behaviour covered by this test case.
func TestParser_PruneSessionsEmptyDir_Good(t *testing.T) {
dir := t.TempDir()
@ -1452,6 +1531,7 @@ func TestParser_PruneSessionsEmptyDir_Good(t *testing.T) {
// --- IsExpired tests ---
// TestParser_IsExpiredRecentSession_Good verifies the behaviour covered by this test case.
func TestParser_IsExpiredRecentSession_Good(t *testing.T) {
sess := &Session{
EndTime: time.Now().Add(-5 * time.Minute),
@ -1459,6 +1539,7 @@ func TestParser_IsExpiredRecentSession_Good(t *testing.T) {
assertFalse(t, sess.IsExpired(1*time.Hour))
}
// TestParser_IsExpiredOldSession_Good verifies the behaviour covered by this test case.
func TestParser_IsExpiredOldSession_Good(t *testing.T) {
sess := &Session{
EndTime: time.Now().Add(-2 * time.Hour),
@ -1466,6 +1547,7 @@ func TestParser_IsExpiredOldSession_Good(t *testing.T) {
assertTrue(t, sess.IsExpired(1*time.Hour))
}
// TestParser_IsExpiredZeroEndTime_Bad verifies the behaviour covered by this test case.
func TestParser_IsExpiredZeroEndTime_Bad(t *testing.T) {
sess := &Session{}
assertFalse(t, sess.IsExpired(1*time.Hour))
@ -1473,6 +1555,7 @@ func TestParser_IsExpiredZeroEndTime_Bad(t *testing.T) {
// --- FetchSession tests ---
// TestParser_FetchSessionValidID_Good verifies the behaviour covered by this test case.
func TestParser_FetchSessionValidID_Good(t *testing.T) {
dir := t.TempDir()
writeJSONL(t, dir, "abc123.jsonl",
@ -1487,6 +1570,7 @@ func TestParser_FetchSessionValidID_Good(t *testing.T) {
assertLen(t, sess.Events, 1)
}
// TestParser_FetchSessionPathTraversal_Ugly verifies the behaviour covered by this test case.
func TestParser_FetchSessionPathTraversal_Ugly(t *testing.T) {
dir := t.TempDir()
@ -1495,6 +1579,7 @@ func TestParser_FetchSessionPathTraversal_Ugly(t *testing.T) {
assertContains(t, err.Error(), "invalid session id")
}
// TestParser_FetchSessionBackslashTraversal_Ugly verifies the behaviour covered by this test case.
func TestParser_FetchSessionBackslashTraversal_Ugly(t *testing.T) {
dir := t.TempDir()
@ -1503,6 +1588,7 @@ func TestParser_FetchSessionBackslashTraversal_Ugly(t *testing.T) {
assertContains(t, err.Error(), "invalid session id")
}
// TestParser_FetchSessionForwardSlash_Ugly verifies the behaviour covered by this test case.
func TestParser_FetchSessionForwardSlash_Ugly(t *testing.T) {
dir := t.TempDir()
@ -1511,6 +1597,7 @@ func TestParser_FetchSessionForwardSlash_Ugly(t *testing.T) {
assertContains(t, err.Error(), "invalid session id")
}
// TestParser_FetchSessionURLEncodedTraversal_Ugly verifies the behaviour covered by this test case.
func TestParser_FetchSessionURLEncodedTraversal_Ugly(t *testing.T) {
dir := t.TempDir()
@ -1519,6 +1606,7 @@ func TestParser_FetchSessionURLEncodedTraversal_Ugly(t *testing.T) {
assertNotContains(t, err.Error(), "/etc/passwd")
}
// TestParser_FetchSessionSymlinkTraversal_Ugly verifies the behaviour covered by this test case.
func TestParser_FetchSessionSymlinkTraversal_Ugly(t *testing.T) {
dir := t.TempDir()
outside := t.TempDir()
@ -1535,6 +1623,7 @@ func TestParser_FetchSessionSymlinkTraversal_Ugly(t *testing.T) {
assertContains(t, err.Error(), "invalid session path")
}
// TestParser_FetchSessionNotFound_Bad verifies the behaviour covered by this test case.
func TestParser_FetchSessionNotFound_Bad(t *testing.T) {
dir := t.TempDir()
@ -1545,6 +1634,7 @@ func TestParser_FetchSessionNotFound_Bad(t *testing.T) {
// --- ListSessions with truncated files ---
// TestParser_ListSessionsTruncatedFile_Good verifies the behaviour covered by this test case.
func TestParser_ListSessionsTruncatedFile_Good(t *testing.T) {
dir := t.TempDir()
// A .jsonl file where some lines are truncated — ListSessions should
@ -1565,6 +1655,19 @@ func TestParser_ListSessionsTruncatedFile_Good(t *testing.T) {
assertTrue(t, sessions[0].EndTime.After(sessions[0].StartTime))
}
// TestParser_ListSessionsOversizedLineSkipped_Ugly verifies the behaviour covered by this test case.
func TestParser_ListSessionsOversizedLineSkipped_Ugly(t *testing.T) {
dir := t.TempDir()
filePath := path.Join(dir, "oversized.jsonl")
oversizedLine := string(bytes.Repeat([]byte("x"), maxScannerBuffer+1))
requireTrue(t, hostFS.Write(filePath, userTextEntry(ts(0), "start")+"\n"+oversizedLine).OK)
sessions, err := ListSessions(dir)
requireNoError(t, err)
assertEmpty(t, sessions)
}
// TestParser_ListSessionsSymlinkTraversal_Ugly verifies the behaviour covered by this test case.
func TestParser_ListSessionsSymlinkTraversal_Ugly(t *testing.T) {
dir := t.TempDir()
outside := t.TempDir()

View file

@ -6,6 +6,7 @@ import (
"testing"
)
// TestSearch_SearchEmptyDir_Good verifies the behaviour covered by this test case.
func TestSearch_SearchEmptyDir_Good(t *testing.T) {
dir := t.TempDir()
@ -14,6 +15,7 @@ func TestSearch_SearchEmptyDir_Good(t *testing.T) {
assertEmpty(t, results)
}
// TestSearch_SearchNoMatches_Good verifies the behaviour covered by this test case.
func TestSearch_SearchNoMatches_Good(t *testing.T) {
dir := t.TempDir()
writeJSONL(t, dir, "session.jsonl",
@ -28,6 +30,7 @@ func TestSearch_SearchNoMatches_Good(t *testing.T) {
assertEmpty(t, results)
}
// TestSearch_SearchSingleMatch_Good verifies the behaviour covered by this test case.
func TestSearch_SearchSingleMatch_Good(t *testing.T) {
dir := t.TempDir()
writeJSONL(t, dir, "session.jsonl",
@ -46,6 +49,7 @@ func TestSearch_SearchSingleMatch_Good(t *testing.T) {
assertContains(t, results[0].Match, "go test")
}
// TestSearch_SearchSeqSingleMatch_Good verifies the behaviour covered by this test case.
func TestSearch_SearchSeqSingleMatch_Good(t *testing.T) {
dir := t.TempDir()
writeJSONL(t, dir, "session.jsonl",
@ -65,6 +69,7 @@ func TestSearch_SearchSeqSingleMatch_Good(t *testing.T) {
assertEqual(t, "Bash", results[0].Tool)
}
// TestSearch_SearchMultipleMatches_Good verifies the behaviour covered by this test case.
func TestSearch_SearchMultipleMatches_Good(t *testing.T) {
dir := t.TempDir()
writeJSONL(t, dir, "session1.jsonl",
@ -89,6 +94,7 @@ func TestSearch_SearchMultipleMatches_Good(t *testing.T) {
assertLen(t, results, 3, "should find matches across both sessions")
}
// TestSearch_SearchCaseInsensitive_Good verifies the behaviour covered by this test case.
func TestSearch_SearchCaseInsensitive_Good(t *testing.T) {
dir := t.TempDir()
writeJSONL(t, dir, "session.jsonl",
@ -103,6 +109,7 @@ func TestSearch_SearchCaseInsensitive_Good(t *testing.T) {
assertLen(t, results, 1, "search should be case-insensitive")
}
// TestSearch_SearchMatchesInOutput_Good verifies the behaviour covered by this test case.
func TestSearch_SearchMatchesInOutput_Good(t *testing.T) {
dir := t.TempDir()
writeJSONL(t, dir, "session.jsonl",
@ -119,6 +126,7 @@ func TestSearch_SearchMatchesInOutput_Good(t *testing.T) {
assertContains(t, results[0].Match, "cat log.txt")
}
// TestSearch_SearchSkipsNonToolEvents_Good verifies the behaviour covered by this test case.
func TestSearch_SearchSkipsNonToolEvents_Good(t *testing.T) {
dir := t.TempDir()
writeJSONL(t, dir, "session.jsonl",
@ -132,6 +140,7 @@ func TestSearch_SearchSkipsNonToolEvents_Good(t *testing.T) {
assertEmpty(t, results, "should only match tool_use events, not user/assistant text")
}
// TestSearch_SearchNonJSONLIgnored_Good verifies the behaviour covered by this test case.
func TestSearch_SearchNonJSONLIgnored_Good(t *testing.T) {
dir := t.TempDir()
writeResult := hostFS.Write(path.Join(dir, "readme.md"), "go test")
@ -142,6 +151,7 @@ func TestSearch_SearchNonJSONLIgnored_Good(t *testing.T) {
assertEmpty(t, results, "non-JSONL files should be ignored")
}
// TestSearch_SearchMalformedSessionSkipped_Bad verifies the behaviour covered by this test case.
func TestSearch_SearchMalformedSessionSkipped_Bad(t *testing.T) {
dir := t.TempDir()

View file

@ -8,6 +8,7 @@ import (
core "dappco.re/go/core"
)
// testContext supports the session test suite.
func testContext(msgAndArgs []any) string {
if len(msgAndArgs) == 0 {
return ""
@ -15,6 +16,7 @@ func testContext(msgAndArgs []any) string {
return core.Sprintf("%v: ", msgAndArgs[0])
}
// isNil supports the session test suite.
func isNil(v any) bool {
if v == nil {
return true
@ -28,6 +30,7 @@ func isNil(v any) bool {
}
}
// isEmpty supports the session test suite.
func isEmpty(v any) bool {
if isNil(v) {
return true
@ -41,6 +44,7 @@ func isEmpty(v any) bool {
}
}
// valueLen supports the session test suite.
func valueLen(v any) (int, bool) {
if v == nil {
return 0, true
@ -54,6 +58,7 @@ func valueLen(v any) (int, bool) {
}
}
// requireNoError stops the current test case when its condition is not met.
func requireNoError(t *testing.T, err error, msgAndArgs ...any) {
t.Helper()
if err != nil {
@ -61,6 +66,7 @@ func requireNoError(t *testing.T, err error, msgAndArgs ...any) {
}
}
// requireError stops the current test case when its condition is not met.
func requireError(t *testing.T, err error, msgAndArgs ...any) {
t.Helper()
if err == nil {
@ -68,6 +74,7 @@ func requireError(t *testing.T, err error, msgAndArgs ...any) {
}
}
// requireEqual stops the current test case when its condition is not met.
func requireEqual(t *testing.T, want, got any, msgAndArgs ...any) {
t.Helper()
if !reflect.DeepEqual(want, got) {
@ -75,6 +82,7 @@ func requireEqual(t *testing.T, want, got any, msgAndArgs ...any) {
}
}
// requireTrue stops the current test case when its condition is not met.
func requireTrue(t *testing.T, cond bool, msgAndArgs ...any) {
t.Helper()
if !cond {
@ -82,6 +90,7 @@ func requireTrue(t *testing.T, cond bool, msgAndArgs ...any) {
}
}
// requireNotNil stops the current test case when its condition is not met.
func requireNotNil(t *testing.T, v any, msgAndArgs ...any) {
t.Helper()
if isNil(v) {
@ -89,6 +98,7 @@ func requireNotNil(t *testing.T, v any, msgAndArgs ...any) {
}
}
// requireLen stops the current test case when its condition is not met.
func requireLen(t *testing.T, v any, want int, msgAndArgs ...any) {
t.Helper()
got, ok := valueLen(v)
@ -100,73 +110,84 @@ func requireLen(t *testing.T, v any, want int, msgAndArgs ...any) {
}
}
// assertEqual records a test failure when its condition is not met.
func assertEqual(t *testing.T, want, got any, msgAndArgs ...any) {
t.Helper()
if !reflect.DeepEqual(want, got) {
t.Fatalf("%swant %v, got %v", testContext(msgAndArgs), want, got)
t.Errorf("%swant %v, got %v", testContext(msgAndArgs), want, got)
}
}
// assertTrue records a test failure when its condition is not met.
func assertTrue(t *testing.T, cond bool, msgAndArgs ...any) {
t.Helper()
if !cond {
t.Fatalf("%sexpected true", testContext(msgAndArgs))
t.Errorf("%sexpected true", testContext(msgAndArgs))
}
}
// assertFalse records a test failure when its condition is not met.
func assertFalse(t *testing.T, cond bool, msgAndArgs ...any) {
t.Helper()
if cond {
t.Fatalf("%sexpected false", testContext(msgAndArgs))
t.Errorf("%sexpected false", testContext(msgAndArgs))
}
}
// assertNil records a test failure when its condition is not met.
func assertNil(t *testing.T, v any, msgAndArgs ...any) {
t.Helper()
if !isNil(v) {
t.Fatalf("%sexpected nil, got %v", testContext(msgAndArgs), v)
t.Errorf("%sexpected nil, got %v", testContext(msgAndArgs), v)
}
}
// assertNotNil records a test failure when its condition is not met.
func assertNotNil(t *testing.T, v any, msgAndArgs ...any) {
t.Helper()
if isNil(v) {
t.Fatalf("%sexpected non-nil", testContext(msgAndArgs))
t.Errorf("%sexpected non-nil", testContext(msgAndArgs))
}
}
// assertEmpty records a test failure when its condition is not met.
func assertEmpty(t *testing.T, v any, msgAndArgs ...any) {
t.Helper()
if !isEmpty(v) {
t.Fatalf("%sexpected empty, got %v", testContext(msgAndArgs), v)
t.Errorf("%sexpected empty, got %v", testContext(msgAndArgs), v)
}
}
// assertLen records a test failure when its condition is not met.
func assertLen(t *testing.T, v any, want int, msgAndArgs ...any) {
t.Helper()
got, ok := valueLen(v)
if !ok {
t.Fatalf("%sexpected value with length, got %T", testContext(msgAndArgs), v)
t.Errorf("%sexpected value with length, got %T", testContext(msgAndArgs), v)
return
}
if want != got {
t.Fatalf("%swant length %v, got %v", testContext(msgAndArgs), want, got)
t.Errorf("%swant length %v, got %v", testContext(msgAndArgs), want, got)
}
}
// assertContains records a test failure when its condition is not met.
func assertContains(t *testing.T, s, substr string, msgAndArgs ...any) {
t.Helper()
if !core.Contains(s, substr) {
t.Fatalf("%sexpected %q to contain %q", testContext(msgAndArgs), s, substr)
t.Errorf("%sexpected %q to contain %q", testContext(msgAndArgs), s, substr)
}
}
// assertNotContains records a test failure when its condition is not met.
func assertNotContains(t *testing.T, s, substr string, msgAndArgs ...any) {
t.Helper()
if core.Contains(s, substr) {
t.Fatalf("%sexpected %q not to contain %q", testContext(msgAndArgs), s, substr)
t.Errorf("%sexpected %q not to contain %q", testContext(msgAndArgs), s, substr)
}
}
// assertInDelta records a test failure when its condition is not met.
func assertInDelta(t *testing.T, want, got, delta float64, msgAndArgs ...any) {
t.Helper()
diff := want - got
@ -174,6 +195,6 @@ func assertInDelta(t *testing.T, want, got, delta float64, msgAndArgs ...any) {
diff = -diff
}
if diff > delta {
t.Fatalf("%swant %v within %v, got %v", testContext(msgAndArgs), want, delta, got)
t.Errorf("%swant %v within %v, got %v", testContext(msgAndArgs), want, delta, got)
}
}

View file

@ -2,6 +2,8 @@ version: "3"
env:
GOWORK: off
GOPATH: /tmp/gopath-gosession
GOMODCACHE: /tmp/gomodcache-gosession
GOCACHE: /tmp/go-session-go-build-cache
tasks:
@ -11,4 +13,6 @@ tasks:
test:
dir: ../../..
cmds:
- go vet ./...
- go test ./...
- go run ./tests/cli/session

View file

@ -3,6 +3,7 @@ package main
import (
"os"
"path/filepath"
"strings"
"time"
@ -15,12 +16,15 @@ const transcript = `{"type":"user","timestamp":"2026-02-20T10:00:00Z","sessionId
{"type":"assistant","timestamp":"2026-02-20T10:00:03Z","sessionId":"ax10-session","message":{"role":"assistant","content":[{"type":"text","text":"AX-10 complete"}]}}
`
// main runs the CLI session smoke test.
func main() {
dir, err := os.MkdirTemp("", "go-session-ax10-")
requireNoError(err, "create temporary directory")
defer os.RemoveAll(dir)
defer func() {
_ = os.RemoveAll(dir)
}()
transcriptPath := dir + "/ax10-session.jsonl"
transcriptPath := filepath.Join(dir, "ax10-session.jsonl")
requireNoError(os.WriteFile(transcriptPath, []byte(transcript), 0o600), "write transcript")
sess, stats, err := session.ParseTranscript(transcriptPath)
@ -37,13 +41,15 @@ func main() {
require(tool.Tool == "Bash", "expected Bash tool call")
require(tool.Input == "echo ax10 # smoke test", "expected Bash input to include command and description")
require(tool.Output == "ax10\n", "expected Bash output to be preserved")
require(tool.Duration == time.Second, "expected one second tool duration")
expectedDuration := time.Second
require(tool.Duration == expectedDuration, "expected tool duration to match transcript timestamps")
require(tool.Success, "expected successful tool call")
analytics := session.Analyse(sess)
require(analytics.EventCount == 3, "expected analytics event count")
require(analytics.ToolCounts["Bash"] == 1, "expected analytics Bash count")
require(analytics.SuccessRate == 1, "expected analytics success rate")
expectedSuccessRate := successfulToolRate(sess)
require(analytics.SuccessRate == expectedSuccessRate, "expected analytics success rate")
require(strings.Contains(session.FormatAnalytics(analytics), "Bash"), "expected formatted analytics to include Bash")
results, err := session.Search(dir, "ax10")
@ -60,7 +66,7 @@ func main() {
requireNoError(err, "fetch session")
require(fetched.ID == sess.ID, "expected fetched session to match parsed session")
htmlPath := dir + "/timeline.html"
htmlPath := filepath.Join(dir, "timeline.html")
requireNoError(session.RenderHTML(sess, htmlPath), "render HTML")
htmlBytes, err := os.ReadFile(htmlPath)
requireNoError(err, "read rendered HTML")
@ -69,12 +75,32 @@ func main() {
require(strings.Contains(html, "echo ax10"), "expected rendered HTML tool input")
}
// successfulToolRate calculates the same tool-call success ratio as session.Analyse.
func successfulToolRate(sess *session.Session) float64 {
var successful, total int
for _, evt := range sess.Events {
if evt.Type != "tool_use" {
continue
}
total++
if evt.Success {
successful++
}
}
if total == 0 {
return 0
}
return float64(successful) / float64(total)
}
// require stops the current test case when its condition is not met.
func require(ok bool, msg string) {
if !ok {
panic(msg)
}
}
// requireNoError stops the current test case when its condition is not met.
func requireNoError(err error, msg string) {
if err != nil {
panic(msg + ": " + err.Error())

View file

@ -39,6 +39,7 @@ func RenderMP4(sess *Session, outputPath string) error {
return nil
}
// generateTape builds the VHS script used to render a session video.
func generateTape(sess *Session, outputPath string) string {
b := core.NewBuilder()
@ -120,6 +121,7 @@ func generateTape(sess *Session, outputPath string) string {
return b.String()
}
// extractCommand removes a human description suffix from a Bash tool input.
func extractCommand(input string) string {
// Remove description suffix (after " # ")
if idx := indexOf(input, " # "); idx > 0 {
@ -128,6 +130,7 @@ func extractCommand(input string) string {
return input
}
// lookupExecutable resolves an executable name from PATH or validates a direct path.
func lookupExecutable(name string) string {
if name == "" {
return ""
@ -151,6 +154,7 @@ func lookupExecutable(name string) string {
return ""
}
// isExecutablePath reports whether filePath is an executable regular file.
func isExecutablePath(filePath string) bool {
statResult := hostFS.Stat(filePath)
if !statResult.OK {
@ -163,6 +167,7 @@ func isExecutablePath(filePath string) bool {
return info.Mode()&0111 != 0
}
// runCommand executes an external command through the core process abstraction.
func runCommand(command string, args ...string) error {
c := sessionCore(nil)
runResult := hostProcess(c).Run(hostContext(c), command, args...)

View file

@ -8,6 +8,7 @@ import (
core "dappco.re/go/core"
)
// TestVideo_GenerateTapeBasicSession_Good verifies the behaviour covered by this test case.
func TestVideo_GenerateTapeBasicSession_Good(t *testing.T) {
sess := &Session{
ID: "tape-test-12345678",
@ -42,6 +43,7 @@ func TestVideo_GenerateTapeBasicSession_Good(t *testing.T) {
assertContains(t, tape, "# Read: /tmp/file.go")
}
// TestVideo_GenerateTapeSkipsNonToolEvents_Good verifies the behaviour covered by this test case.
func TestVideo_GenerateTapeSkipsNonToolEvents_Good(t *testing.T) {
sess := &Session{
ID: "skip-test",
@ -62,6 +64,7 @@ func TestVideo_GenerateTapeSkipsNonToolEvents_Good(t *testing.T) {
assertContains(t, tape, "echo hi")
}
// TestVideo_GenerateTapeFailedCommand_Good verifies the behaviour covered by this test case.
func TestVideo_GenerateTapeFailedCommand_Good(t *testing.T) {
sess := &Session{
ID: "fail-test",
@ -81,6 +84,7 @@ func TestVideo_GenerateTapeFailedCommand_Good(t *testing.T) {
assertContains(t, tape, `"# ✗ FAILED"`)
}
// TestVideo_GenerateTapeLongOutput_Good verifies the behaviour covered by this test case.
func TestVideo_GenerateTapeLongOutput_Good(t *testing.T) {
sess := &Session{
ID: "long-test",
@ -101,6 +105,7 @@ func TestVideo_GenerateTapeLongOutput_Good(t *testing.T) {
assertContains(t, tape, "...")
}
// TestVideo_GenerateTapeTaskEvent_Good verifies the behaviour covered by this test case.
func TestVideo_GenerateTapeTaskEvent_Good(t *testing.T) {
sess := &Session{
ID: "task-test",
@ -118,6 +123,7 @@ func TestVideo_GenerateTapeTaskEvent_Good(t *testing.T) {
assertContains(t, tape, "# Agent: [research] Analyse code structure")
}
// TestVideo_GenerateTapeEditWriteEvents_Good verifies the behaviour covered by this test case.
func TestVideo_GenerateTapeEditWriteEvents_Good(t *testing.T) {
sess := &Session{
ID: "edit-test",
@ -133,6 +139,7 @@ func TestVideo_GenerateTapeEditWriteEvents_Good(t *testing.T) {
assertContains(t, tape, "# Write: /tmp/new.go (50 bytes)")
}
// TestVideo_GenerateTapeEmptySession_Good verifies the behaviour covered by this test case.
func TestVideo_GenerateTapeEmptySession_Good(t *testing.T) {
sess := &Session{
ID: "empty-test",
@ -157,6 +164,7 @@ func TestVideo_GenerateTapeEmptySession_Good(t *testing.T) {
assertEqual(t, 0, toolLines)
}
// TestVideo_GenerateTapeBashEmptyCommand_Bad verifies the behaviour covered by this test case.
func TestVideo_GenerateTapeBashEmptyCommand_Bad(t *testing.T) {
sess := &Session{
ID: "empty-cmd",
@ -171,22 +179,26 @@ func TestVideo_GenerateTapeBashEmptyCommand_Bad(t *testing.T) {
assertNotContains(t, tape, `"$ "`)
}
// TestVideo_ExtractCommandStripsDescriptionSuffix_Good verifies the behaviour covered by this test case.
func TestVideo_ExtractCommandStripsDescriptionSuffix_Good(t *testing.T) {
assertEqual(t, "ls -la", extractCommand("ls -la # list files"))
assertEqual(t, "go test ./...", extractCommand("go test ./..."))
assertEqual(t, "echo hello", extractCommand("echo hello"))
}
// TestVideo_ExtractCommandNoDescription_Good verifies the behaviour covered by this test case.
func TestVideo_ExtractCommandNoDescription_Good(t *testing.T) {
assertEqual(t, "plain command", extractCommand("plain command"))
}
// TestVideo_ExtractCommandDescriptionAtStart_Good verifies the behaviour covered by this test case.
func TestVideo_ExtractCommandDescriptionAtStart_Good(t *testing.T) {
// " # " at position 0 means idx <= 0, so it returns the whole input
result := extractCommand(" # description only")
assertEqual(t, " # description only", result)
}
// TestVideo_RenderMP4NoVHS_Ugly verifies the behaviour covered by this test case.
func TestVideo_RenderMP4NoVHS_Ugly(t *testing.T) {
// Skip if vhs is actually installed (this tests the error path)
if lookupExecutable("vhs") != "" {