From c7696920e02b8e68a52748f892eeb8936236665b Mon Sep 17 00:00:00 2001 From: Snider Date: Tue, 17 Mar 2026 08:03:31 +0000 Subject: [PATCH] fix(dx): fix coreerr.E() signatures, add SPDX headers and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix two coreerr.E() calls missing the required third (error) argument, add SPDX-Licence-Identifier headers to all source files, and add tests for PruneSessions, IsExpired, and FetchSession (coverage 88.1% → 92.1%). Co-Authored-By: Virgil --- html.go | 1 + parser.go | 3 +- parser_test.go | 114 +++++++++++++++++++++++++++++++++++++++++++++++++ search.go | 1 + video.go | 3 +- 5 files changed, 120 insertions(+), 2 deletions(-) diff --git a/html.go b/html.go index 7113659..db36ac8 100644 --- a/html.go +++ b/html.go @@ -1,3 +1,4 @@ +// SPDX-Licence-Identifier: EUPL-1.2 package session import ( diff --git a/parser.go b/parser.go index 1390d92..dc877e4 100644 --- a/parser.go +++ b/parser.go @@ -1,3 +1,4 @@ +// SPDX-Licence-Identifier: EUPL-1.2 package session import ( @@ -238,7 +239,7 @@ func (s *Session) IsExpired(maxAge time.Duration) bool { // It ensures the ID does not contain path traversal characters. func FetchSession(projectsDir, id string) (*Session, *ParseStats, error) { if strings.Contains(id, "..") || strings.ContainsAny(id, `/\`) { - return nil, nil, coreerr.E("FetchSession", "invalid session id") + return nil, nil, coreerr.E("FetchSession", "invalid session id", nil) } path := filepath.Join(projectsDir, id+".jsonl") diff --git a/parser_test.go b/parser_test.go index 2f5c68d..7ec5326 100644 --- a/parser_test.go +++ b/parser_test.go @@ -1330,3 +1330,117 @@ func TestListSessions_TruncatedFile_Good(t *testing.T) { // End time should reflect the last valid timestamp. assert.True(t, sessions[0].EndTime.After(sessions[0].StartTime)) } + +// --- PruneSessions tests --- + +func TestPruneSessions_DeletesOld_Good(t *testing.T) { + dir := t.TempDir() + writeJSONL(t, dir, "old-session.jsonl", userTextEntry(ts(0), "old")) + writeJSONL(t, dir, "new-session.jsonl", userTextEntry(ts(0), "new")) + + // Touch old-session to make it appear old (1 hour ago). + oldPath := filepath.Join(dir, "old-session.jsonl") + past := time.Now().Add(-2 * time.Hour) + require.NoError(t, os.Chtimes(oldPath, past, past)) + + deleted, err := PruneSessions(dir, 1*time.Hour) + require.NoError(t, err) + assert.Equal(t, 1, deleted) + + // Only new-session should remain. + sessions, err := ListSessions(dir) + require.NoError(t, err) + require.Len(t, sessions, 1) + assert.Equal(t, "new-session", sessions[0].ID) +} + +func TestPruneSessions_NoneExpired_Good(t *testing.T) { + dir := t.TempDir() + writeJSONL(t, dir, "fresh.jsonl", userTextEntry(ts(0), "fresh")) + + deleted, err := PruneSessions(dir, 24*time.Hour) + require.NoError(t, err) + assert.Equal(t, 0, deleted) + + sessions, err := ListSessions(dir) + require.NoError(t, err) + require.Len(t, sessions, 1) +} + +func TestPruneSessions_EmptyDir_Good(t *testing.T) { + dir := t.TempDir() + + deleted, err := PruneSessions(dir, 1*time.Hour) + require.NoError(t, err) + assert.Equal(t, 0, deleted) +} + +// --- IsExpired tests --- + +func TestIsExpired_Expired_Good(t *testing.T) { + s := &Session{ + EndTime: time.Now().Add(-2 * time.Hour), + } + assert.True(t, s.IsExpired(1*time.Hour)) +} + +func TestIsExpired_NotExpired_Good(t *testing.T) { + s := &Session{ + EndTime: time.Now().Add(-30 * time.Minute), + } + assert.False(t, s.IsExpired(1*time.Hour)) +} + +func TestIsExpired_ZeroEndTime_Bad(t *testing.T) { + s := &Session{} + assert.False(t, s.IsExpired(1*time.Hour)) +} + +// --- FetchSession tests --- + +func TestFetchSession_ValidID_Good(t *testing.T) { + dir := t.TempDir() + writeJSONL(t, dir, "abc123.jsonl", + userTextEntry(ts(0), "Hello"), + assistantTextEntry(ts(1), "Hi"), + ) + + sess, stats, err := FetchSession(dir, "abc123") + require.NoError(t, err) + require.NotNil(t, sess) + require.NotNil(t, stats) + assert.Equal(t, "abc123", sess.ID) + assert.Len(t, sess.Events, 2) +} + +func TestFetchSession_PathTraversal_Bad(t *testing.T) { + dir := t.TempDir() + + _, _, err := FetchSession(dir, "../etc/passwd") + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid session id") +} + +func TestFetchSession_BackslashTraversal_Bad(t *testing.T) { + dir := t.TempDir() + + _, _, err := FetchSession(dir, `..\\windows\\system32`) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid session id") +} + +func TestFetchSession_SlashInID_Bad(t *testing.T) { + dir := t.TempDir() + + _, _, err := FetchSession(dir, "sub/dir") + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid session id") +} + +func TestFetchSession_NotFound_Bad(t *testing.T) { + dir := t.TempDir() + + _, _, err := FetchSession(dir, "nonexistent") + require.Error(t, err) + assert.Contains(t, err.Error(), "open transcript") +} diff --git a/search.go b/search.go index 403428a..232446c 100644 --- a/search.go +++ b/search.go @@ -1,3 +1,4 @@ +// SPDX-Licence-Identifier: EUPL-1.2 package session import ( diff --git a/video.go b/video.go index 9b525b8..24839d8 100644 --- a/video.go +++ b/video.go @@ -1,3 +1,4 @@ +// SPDX-Licence-Identifier: EUPL-1.2 package session import ( @@ -12,7 +13,7 @@ import ( // RenderMP4 generates an MP4 video from session events using VHS (charmbracelet). func RenderMP4(sess *Session, outputPath string) error { if _, err := exec.LookPath("vhs"); err != nil { - return coreerr.E("RenderMP4", "vhs not installed (go install github.com/charmbracelet/vhs@latest)") + return coreerr.E("RenderMP4", "vhs not installed (go install github.com/charmbracelet/vhs@latest)", nil) } tape := generateTape(sess, outputPath) -- 2.45.3