Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

1549 lines
43 KiB
Go

//go:build pgtest
package postgres
import (
"context"
"database/sql"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.kenn.io/agentsview/internal/db"
)
const testSchema = "agentsview_store_test"
// ensureStoreSchema creates the test schema and seed data.
func ensureStoreSchema(t *testing.T, pgURL string) {
t.Helper()
pg, err := Open(pgURL, testSchema, true)
require.NoError(t, err, "connecting to pg")
defer pg.Close()
_, err = pg.Exec(`
DROP SCHEMA IF EXISTS ` + testSchema + ` CASCADE;
`)
require.NoError(t, err, "dropping schema")
ctx := context.Background()
require.NoError(t, EnsureSchema(ctx, pg, testSchema), "creating schema")
_, err = pg.Exec(`
INSERT INTO sessions
(id, machine, project, agent, first_message,
started_at, ended_at, message_count,
user_message_count)
VALUES
('store-test-001', 'test-machine',
'test-project', 'claude-code',
'hello world',
'2026-03-12T10:00:00Z'::timestamptz,
'2026-03-12T10:30:00Z'::timestamptz,
2, 1)
`)
require.NoError(t, err, "inserting test session")
_, err = pg.Exec(`
INSERT INTO messages
(session_id, ordinal, role, content,
timestamp, content_length)
VALUES
('store-test-001', 0, 'user',
'hello world',
'2026-03-12T10:00:00Z'::timestamptz, 11),
('store-test-001', 1, 'assistant',
'hi there',
'2026-03-12T10:00:01Z'::timestamptz, 8)
`)
require.NoError(t, err, "inserting test messages")
}
func ensureAnalyticsTokenStoreSchema(
t *testing.T, pgURL string,
) {
t.Helper()
pg, err := Open(pgURL, testSchema, true)
require.NoError(t, err, "connecting to pg")
defer pg.Close()
_, err = pg.Exec(`
DROP SCHEMA IF EXISTS ` + testSchema + ` CASCADE;
`)
require.NoError(t, err, "dropping schema")
ctx := context.Background()
require.NoError(t, EnsureSchema(ctx, pg, testSchema), "creating schema")
_, err = pg.Exec(`
INSERT INTO sessions (
id, machine, project, agent, first_message,
started_at, ended_at, message_count,
user_message_count, total_output_tokens,
has_total_output_tokens
) VALUES
('pg-token-001', 'test-machine', 'proj-a', 'claude',
'largest token session',
'2026-03-12T10:00:00Z'::timestamptz,
'2026-03-12T10:30:00Z'::timestamptz,
12, 6, 900, TRUE),
('pg-token-002', 'test-machine', 'proj-a', 'codex',
'second token session',
'2026-03-12T12:00:00Z'::timestamptz,
'2026-03-12T12:15:00Z'::timestamptz,
8, 4, 600, TRUE),
('pg-token-003', 'test-machine', 'proj-b', 'claude',
'third token session',
'2026-03-13T09:00:00Z'::timestamptz,
'2026-03-13T09:10:00Z'::timestamptz,
5, 3, 300, TRUE),
('pg-token-missing', 'test-machine', 'proj-c', 'claude',
'missing token coverage',
'2026-03-13T11:00:00Z'::timestamptz,
'2026-03-13T11:20:00Z'::timestamptz,
9, 5, 0, FALSE)
`)
require.NoError(t, err, "inserting analytics token sessions")
}
func sessionIDs(sessions []db.Session) []string {
ids := make([]string, 0, len(sessions))
for _, s := range sessions {
ids = append(ids, s.ID)
}
return ids
}
func TestNewStore(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
assert.True(t, store.ReadOnly())
assert.True(t, store.HasFTS())
}
func TestDetectInsightGenerationAvailability(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
require.NoError(t, store.DetectInsightGenerationAvailability(
context.Background(),
), "DetectInsightGenerationAvailability")
assert.True(t, store.InsightGenerationAvailable())
}
func TestProbeInsightGenerationAvailabilityTx_ReadOnly(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
pg, err := Open(pgURL, testSchema, true)
require.NoError(t, err, "Open")
defer pg.Close()
tx, err := pg.BeginTx(
context.Background(),
&sql.TxOptions{ReadOnly: true},
)
require.NoError(t, err, "BeginTx")
defer func() { _ = tx.Rollback() }()
available, err := probeInsightGenerationAvailabilityTx(
context.Background(), tx,
)
require.NoError(t, err, "probeInsightGenerationAvailabilityTx")
assert.False(t, available)
}
func TestStoreListSessions(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
ctx := context.Background()
page, err := store.ListSessions(
ctx, db.SessionFilter{Limit: 10},
)
require.NoError(t, err, "ListSessions")
assert.NotZero(t, page.Total, "expected at least 1 session")
t.Logf("sessions: %d, total: %d",
len(page.Sessions), page.Total)
}
func TestStoreListSessions_MachineMultiSelect(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
_, err = store.DB().Exec(`
INSERT INTO sessions
(id, machine, project, agent, first_message,
started_at, ended_at, message_count,
user_message_count)
VALUES
('store-test-002', 'machine-b',
'test-project', 'codex',
'hello machine b',
'2026-03-12T11:00:00Z'::timestamptz,
'2026-03-12T11:30:00Z'::timestamptz,
2, 1),
('store-test-003', 'machine-c',
'test-project', 'gemini',
'hello machine c',
'2026-03-12T12:00:00Z'::timestamptz,
'2026-03-12T12:30:00Z'::timestamptz,
2, 1)
`)
require.NoError(t, err, "inserting extra sessions")
ctx := context.Background()
page, err := store.ListSessions(
ctx,
db.SessionFilter{
Machine: "test-machine,machine-c",
Limit: 10,
},
)
require.NoError(t, err, "ListSessions")
require.Equal(t, 2, page.Total)
got := []string{
page.Sessions[0].Machine,
page.Sessions[1].Machine,
}
assert.Contains(t, got, "test-machine")
assert.Contains(t, got, "machine-c")
}
func ensureSidebarIndexStoreSchema(
t *testing.T, pgURL string,
) *Store {
t.Helper()
ensureStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
_, err = store.DB().Exec(`DELETE FROM messages`)
require.NoError(t, err, "clearing seed messages")
_, err = store.DB().Exec(`DELETE FROM sessions`)
require.NoError(t, err, "clearing seed sessions")
return store
}
func insertSidebarIndexSession(
t *testing.T,
store *Store,
id string,
opts ...func(*sidebarIndexSessionSeed),
) {
t.Helper()
row := sidebarIndexSessionSeed{
id: id,
machine: "test-machine",
project: "sidebar-project",
agent: "claude",
firstMessage: "sidebar session",
startedAt: "2026-03-12T10:00:00Z",
endedAt: "2026-03-12T10:30:00Z",
messageCount: 3,
userMessageCount: 2,
}
for _, opt := range opts {
opt(&row)
}
_, err := store.DB().Exec(`
INSERT INTO sessions (
id, machine, project, agent, first_message,
display_name, started_at, ended_at, message_count,
user_message_count, parent_session_id,
relationship_type, is_automated, deleted_at
) VALUES (
$1, $2, $3, $4, $5,
$6, $7::timestamptz, $8::timestamptz, $9,
$10, $11, $12, $13, $14::timestamptz
)
`, row.id, row.machine, row.project, row.agent,
row.firstMessage, row.displayName, row.startedAt,
row.endedAt, row.messageCount, row.userMessageCount,
row.parentSessionID, row.relationshipType,
row.isAutomated, row.deletedAt)
require.NoError(t, err, "inserting sidebar index session %s", id)
}
type sidebarIndexSessionSeed struct {
id string
machine string
project string
agent string
firstMessage string
displayName *string
startedAt string
endedAt string
messageCount int
userMessageCount int
parentSessionID *string
relationshipType string
isAutomated bool
deletedAt *string
}
func sidebarIndexRowsByID(
sessions []db.SidebarSessionIndexRow,
) map[string]db.SidebarSessionIndexRow {
rows := make(map[string]db.SidebarSessionIndexRow, len(sessions))
for _, s := range sessions {
rows[s.ID] = s
}
return rows
}
func requireSidebarIndexIDs(
t *testing.T,
sessions []db.SidebarSessionIndexRow,
wantIDs []string,
) {
t.Helper()
rows := sidebarIndexRowsByID(sessions)
require.Len(t, rows, len(wantIDs), "session count; rows=%v", rows)
for _, id := range wantIDs {
_, ok := rows[id]
require.True(t, ok, "session %q missing from rows=%v", id, rows)
}
}
func sidebarIndexIDs(
sessions []db.SidebarSessionIndexRow,
) []string {
ids := make([]string, 0, len(sessions))
for _, s := range sessions {
ids = append(ids, s.ID)
}
return ids
}
func TestStoreGetSidebarSessionIndexComputesIsTeammate(
t *testing.T,
) {
pgURL := testPGURL(t)
store := ensureSidebarIndexStoreSchema(t, pgURL)
defer store.Close()
insertSidebarIndexSession(t, store, "teammate", func(
s *sidebarIndexSessionSeed,
) {
s.firstMessage = `<teammate-message from="reviewer">hi`
})
insertSidebarIndexSession(t, store, "normal")
index, err := store.GetSidebarSessionIndex(
context.Background(), db.SessionFilter{},
)
require.NoError(t, err, "GetSidebarSessionIndex")
rows := sidebarIndexRowsByID(index.Sessions)
assert.True(t, rows["teammate"].IsTeammate, "teammate IsTeammate")
assert.False(t, rows["normal"].IsTeammate, "normal IsTeammate")
}
func TestStoreGetSidebarSessionIndexReturnsDisplayName(
t *testing.T,
) {
pgURL := testPGURL(t)
store := ensureSidebarIndexStoreSchema(t, pgURL)
defer store.Close()
displayName := "Named sidebar session"
insertSidebarIndexSession(t, store, "named", func(
s *sidebarIndexSessionSeed,
) {
s.displayName = &displayName
})
index, err := store.GetSidebarSessionIndex(
context.Background(), db.SessionFilter{},
)
require.NoError(t, err, "GetSidebarSessionIndex")
require.Len(t, index.Sessions, 1)
got := index.Sessions[0].DisplayName
require.NotNil(t, got)
assert.Equal(t, displayName, *got)
}
func TestStoreGetSidebarSessionIndexExcludeAutomated(
t *testing.T,
) {
pgURL := testPGURL(t)
store := ensureSidebarIndexStoreSchema(t, pgURL)
defer store.Close()
insertSidebarIndexSession(t, store, "normal")
insertSidebarIndexSession(t, store, "review", func(
s *sidebarIndexSessionSeed,
) {
s.firstMessage = "You are a code reviewer. Review the code."
s.userMessageCount = 1
s.isAutomated = true
})
index, err := store.GetSidebarSessionIndex(
context.Background(),
db.SessionFilter{ExcludeAutomated: true},
)
require.NoError(t, err, "GetSidebarSessionIndex exclude automated")
requireSidebarIndexIDs(t, index.Sessions, []string{"normal"})
assert.Equal(t, 1, index.Total)
index, err = store.GetSidebarSessionIndex(
context.Background(),
db.SessionFilter{ExcludeAutomated: false},
)
require.NoError(t, err, "GetSidebarSessionIndex include automated")
requireSidebarIndexIDs(
t, index.Sessions, []string{"normal", "review"},
)
}
func TestStoreGetSidebarSessionIndexExcludeOneShotKeepsAutomated(
t *testing.T,
) {
pgURL := testPGURL(t)
store := ensureSidebarIndexStoreSchema(t, pgURL)
defer store.Close()
insertSidebarIndexSession(t, store, "multi", func(
s *sidebarIndexSessionSeed,
) {
s.userMessageCount = 5
})
insertSidebarIndexSession(t, store, "oneshot", func(
s *sidebarIndexSessionSeed,
) {
s.userMessageCount = 1
})
insertSidebarIndexSession(t, store, "review", func(
s *sidebarIndexSessionSeed,
) {
s.firstMessage = "You are a code reviewer. Review the code."
s.userMessageCount = 1
s.isAutomated = true
})
index, err := store.GetSidebarSessionIndex(
context.Background(),
db.SessionFilter{
ExcludeOneShot: true,
ExcludeAutomated: false,
},
)
require.NoError(t, err, "GetSidebarSessionIndex")
requireSidebarIndexIDs(t, index.Sessions, []string{"multi", "review"})
}
func TestStoreGetSidebarSessionIndexIncludesChildrenForMatchingRoot(
t *testing.T,
) {
pgURL := testPGURL(t)
store := ensureSidebarIndexStoreSchema(t, pgURL)
defer store.Close()
rootID := "root"
subID := "sub"
insertSidebarIndexSession(t, store, rootID, func(
s *sidebarIndexSessionSeed,
) {
s.agent = "claude"
s.userMessageCount = 5
})
insertSidebarIndexSession(t, store, subID, func(
s *sidebarIndexSessionSeed,
) {
s.agent = "codex"
s.parentSessionID = &rootID
s.relationshipType = "subagent"
})
insertSidebarIndexSession(t, store, "fork", func(
s *sidebarIndexSessionSeed,
) {
s.agent = "codex"
s.parentSessionID = &subID
s.relationshipType = "fork"
})
insertSidebarIndexSession(t, store, "other", func(
s *sidebarIndexSessionSeed,
) {
s.agent = "codex"
s.userMessageCount = 5
})
index, err := store.GetSidebarSessionIndex(
context.Background(), db.SessionFilter{Agent: "claude"},
)
require.NoError(t, err, "GetSidebarSessionIndex")
requireSidebarIndexIDs(
t, index.Sessions, []string{"root", "sub", "fork"},
)
}
func TestStoreGetSidebarSessionIndexPagedExcludesAutomatedDescendants(
t *testing.T,
) {
pgURL := testPGURL(t)
store := ensureSidebarIndexStoreSchema(t, pgURL)
defer store.Close()
rootID := "root"
insertSidebarIndexSession(t, store, rootID, func(
s *sidebarIndexSessionSeed,
) {
s.endedAt = "2024-01-03T00:00:00Z"
s.userMessageCount = 5
})
insertSidebarIndexSession(t, store, "human-child", func(
s *sidebarIndexSessionSeed,
) {
s.endedAt = "2024-01-02T00:00:00Z"
s.parentSessionID = &rootID
s.relationshipType = "subagent"
s.userMessageCount = 1
})
insertSidebarIndexSession(t, store, "automated-child", func(
s *sidebarIndexSessionSeed,
) {
s.firstMessage = "You are a code reviewer. Review the code."
s.endedAt = "2024-01-01T00:00:00Z"
s.parentSessionID = &rootID
s.relationshipType = "subagent"
s.userMessageCount = 1
s.isAutomated = true
})
index, err := store.GetSidebarSessionIndex(
context.Background(),
db.SessionFilter{ExcludeAutomated: true, Limit: 1},
)
require.NoError(t, err, "GetSidebarSessionIndex exclude automated")
requireSidebarIndexIDs(t, index.Sessions, []string{"root", "human-child"})
index, err = store.GetSidebarSessionIndex(
context.Background(),
db.SessionFilter{ExcludeAutomated: false, Limit: 1},
)
require.NoError(t, err, "GetSidebarSessionIndex include automated")
requireSidebarIndexIDs(
t, index.Sessions, []string{"root", "human-child", "automated-child"},
)
}
func TestStoreGetSidebarSessionIndexPaginatesContinuationsAsDescendants(
t *testing.T,
) {
pgURL := testPGURL(t)
store := ensureSidebarIndexStoreSchema(t, pgURL)
defer store.Close()
rootEnd := "2024-01-01T00:00:00Z"
continuationEnd := "2024-01-10T00:00:00Z"
otherEnd := "2024-01-05T00:00:00Z"
rootID := "root"
insertSidebarIndexSession(t, store, rootID, func(
s *sidebarIndexSessionSeed,
) {
s.endedAt = rootEnd
})
insertSidebarIndexSession(t, store, "continuation", func(
s *sidebarIndexSessionSeed,
) {
s.parentSessionID = &rootID
s.relationshipType = "continuation"
s.endedAt = continuationEnd
})
insertSidebarIndexSession(t, store, "other", func(
s *sidebarIndexSessionSeed,
) {
s.endedAt = otherEnd
})
ctx := context.Background()
first, err := store.GetSidebarSessionIndex(
ctx, db.SessionFilter{Limit: 1},
)
require.NoError(t, err, "first page")
assert.Equal(t, 2, first.Total)
assert.NotEmpty(t, first.NextCursor)
assert.ElementsMatch(t,
[]string{"root", "continuation"},
sidebarIndexIDs(first.Sessions),
)
second, err := store.GetSidebarSessionIndex(
ctx, db.SessionFilter{Limit: 1, Cursor: first.NextCursor},
)
require.NoError(t, err, "second page")
assert.Equal(t, 2, second.Total)
assert.Empty(t, second.NextCursor)
assert.Equal(t, []string{"other"}, sidebarIndexIDs(second.Sessions))
}
func TestStoreGetSidebarSessionIndexPaginatesByDescendantFreshness(
t *testing.T,
) {
pgURL := testPGURL(t)
store := ensureSidebarIndexStoreSchema(t, pgURL)
defer store.Close()
rootEnd := "2024-01-01T00:00:00Z"
childEnd := "2024-01-10T00:00:00Z"
otherEnd := "2024-01-05T00:00:00Z"
rootID := "root"
insertSidebarIndexSession(t, store, rootID, func(
s *sidebarIndexSessionSeed,
) {
s.endedAt = rootEnd
})
insertSidebarIndexSession(t, store, "child", func(
s *sidebarIndexSessionSeed,
) {
s.parentSessionID = &rootID
s.relationshipType = "subagent"
s.endedAt = childEnd
})
insertSidebarIndexSession(t, store, "other", func(
s *sidebarIndexSessionSeed,
) {
s.endedAt = otherEnd
})
ctx := context.Background()
first, err := store.GetSidebarSessionIndex(
ctx, db.SessionFilter{Limit: 1},
)
require.NoError(t, err, "first page")
assert.Equal(t, 2, first.Total)
assert.NotEmpty(t, first.NextCursor)
assert.ElementsMatch(t,
[]string{"root", "child"},
sidebarIndexIDs(first.Sessions),
)
second, err := store.GetSidebarSessionIndex(
ctx, db.SessionFilter{Limit: 1, Cursor: first.NextCursor},
)
require.NoError(t, err, "second page")
assert.Equal(t, 2, second.Total)
assert.Empty(t, second.NextCursor)
assert.Equal(t, []string{"other"}, sidebarIndexIDs(second.Sessions))
}
func TestStoreGetSidebarSessionIndexStarredIncludesStarredDescendantRoot(
t *testing.T,
) {
pgURL := testPGURL(t)
store := ensureSidebarIndexStoreSchema(t, pgURL)
defer store.Close()
insertSidebarIndexSession(t, store, "unstarred-newer", func(
s *sidebarIndexSessionSeed,
) {
s.endedAt = "2024-01-20T00:00:00Z"
})
rootID := "root"
insertSidebarIndexSession(t, store, rootID, func(
s *sidebarIndexSessionSeed,
) {
s.endedAt = "2024-01-01T00:00:00Z"
})
insertSidebarIndexSession(t, store, "starred-child", func(
s *sidebarIndexSessionSeed,
) {
s.parentSessionID = &rootID
s.relationshipType = "subagent"
s.endedAt = "2024-01-10T00:00:00Z"
})
ok, err := store.StarSession("starred-child")
require.NoError(t, err, "StarSession")
require.True(t, ok, "starred-child should exist")
index, err := store.GetSidebarSessionIndex(
context.Background(), db.SessionFilter{
Starred: true,
Limit: 1,
},
)
require.NoError(t, err, "GetSidebarSessionIndex")
assert.Empty(t, index.NextCursor)
assert.Equal(t, 1, index.Total)
assert.ElementsMatch(t,
[]string{"root", "starred-child"},
sidebarIndexIDs(index.Sessions),
)
}
func TestStoreGetSidebarSessionIndexPaginatesOrphanRoots(t *testing.T) {
pgURL := testPGURL(t)
store := ensureSidebarIndexStoreSchema(t, pgURL)
defer store.Close()
insertSidebarIndexSession(t, store, "root", func(s *sidebarIndexSessionSeed) {
s.endedAt = "2024-01-20T00:00:00Z"
s.userMessageCount = 2
})
insertSidebarIndexSession(t, store, "orphan-sub", func(s *sidebarIndexSessionSeed) {
s.endedAt = "2024-01-19T00:00:00Z"
s.parentSessionID = strPtr("missing-parent")
s.relationshipType = "subagent"
})
insertSidebarIndexSession(t, store, "orphan-fork", func(s *sidebarIndexSessionSeed) {
s.endedAt = "2024-01-18T00:00:00Z"
s.parentSessionID = strPtr("orphan-sub")
s.relationshipType = "fork"
})
insertSidebarIndexSession(t, store, "continuation-orphan", func(s *sidebarIndexSessionSeed) {
s.endedAt = "2024-01-17T00:00:00Z"
s.parentSessionID = strPtr("missing-continuation-parent")
s.relationshipType = "continuation"
})
first, err := store.GetSidebarSessionIndex(
context.Background(), db.SessionFilter{Limit: 2},
)
require.NoError(t, err, "first page")
assert.Equal(t, 3, first.Total)
assert.NotEmpty(t, first.NextCursor)
assert.ElementsMatch(t,
[]string{"root", "orphan-sub", "orphan-fork"},
sidebarIndexIDs(first.Sessions),
)
second, err := store.GetSidebarSessionIndex(
context.Background(), db.SessionFilter{Limit: 2, Cursor: first.NextCursor},
)
require.NoError(t, err, "second page")
assert.Equal(t, 3, second.Total)
assert.Empty(t, second.NextCursor)
assert.Equal(t, []string{"continuation-orphan"}, sidebarIndexIDs(second.Sessions))
}
func TestStoreGetSidebarSessionIndexDoesNotPromoteSoftDeletedParentChildren(t *testing.T) {
pgURL := testPGURL(t)
store := ensureSidebarIndexStoreSchema(t, pgURL)
defer store.Close()
rootID := "soft-deleted-root"
insertSidebarIndexSession(t, store, rootID, func(s *sidebarIndexSessionSeed) {
s.endedAt = "2024-01-20T00:00:00Z"
s.deletedAt = strPtr("2024-01-21T00:00:00Z")
})
insertSidebarIndexSession(t, store, "child-of-deleted-parent", func(s *sidebarIndexSessionSeed) {
s.endedAt = "2024-01-19T00:00:00Z"
s.parentSessionID = &rootID
s.relationshipType = "subagent"
})
insertSidebarIndexSession(t, store, "other", func(s *sidebarIndexSessionSeed) {
s.endedAt = "2024-01-18T00:00:00Z"
})
index, err := store.GetSidebarSessionIndex(
context.Background(), db.SessionFilter{Limit: 10},
)
require.NoError(t, err, "GetSidebarSessionIndex")
assert.ElementsMatch(t,
[]string{"other"},
sidebarIndexIDs(index.Sessions),
)
}
func TestStoreGetSession(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
ctx := context.Background()
sess, err := store.GetSession(ctx, "store-test-001")
require.NoError(t, err, "GetSession")
require.NotNil(t, sess, "expected session, got nil")
assert.Equal(t, "test-project", sess.Project)
}
func TestStoreGetMessages(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
ctx := context.Background()
msgs, err := store.GetMessages(
ctx, "store-test-001", 0, 100, true,
)
require.NoError(t, err, "GetMessages")
assert.Len(t, msgs, 2)
}
func TestStoreGetStats(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
ctx := context.Background()
stats, err := store.GetStats(ctx, false, false)
require.NoError(t, err, "GetStats")
assert.NotZero(t, stats.SessionCount, "expected at least 1 session in stats")
t.Logf("stats: %+v", stats)
}
func TestStoreSearch(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
ctx := context.Background()
page, err := store.Search(ctx, db.SearchFilter{
Query: "hello",
Limit: 5,
})
require.NoError(t, err, "Search")
assert.NotEmpty(t, page.Results, "expected at least 1 search result")
t.Logf("search results: %d", len(page.Results))
}
func TestStoreAnalyticsSummary(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
ctx := context.Background()
summary, err := store.GetAnalyticsSummary(
ctx, db.AnalyticsFilter{
From: "2026-01-01",
To: "2026-12-31",
},
)
require.NoError(t, err, "GetAnalyticsSummary")
assert.NotZero(t, summary.TotalSessions, "expected at least 1 session in summary")
t.Logf("summary: %+v", summary)
}
func seedActivitySession(
t *testing.T, store *Store, sid string, msgs []struct {
ordinal int
role string
content string
ts string
system bool
},
) {
t.Helper()
pg := store.DB()
// PG doesn't allow multi-statement prepared queries,
// so run each statement separately.
_, err := pg.Exec(
`DELETE FROM messages WHERE session_id = $1`, sid,
)
require.NoError(t, err, "deleting messages")
_, err = pg.Exec(
`DELETE FROM sessions WHERE id = $1`, sid,
)
require.NoError(t, err, "deleting session")
_, err = pg.Exec(`
INSERT INTO sessions
(id, machine, project, agent, first_message,
started_at, ended_at, message_count,
user_message_count)
VALUES
($1, 'test-machine', 'test-project',
'claude', 'activity test',
'2026-03-26T10:00:00Z'::timestamptz,
'2026-03-26T11:00:00Z'::timestamptz,
$2, 0)
`, sid, len(msgs))
require.NoError(t, err, "inserting session")
for _, m := range msgs {
var tsVal interface{} = nil
if m.ts != "" {
tsVal = m.ts
}
_, err := pg.Exec(`
INSERT INTO messages
(session_id, ordinal, role, content,
timestamp, content_length, is_system)
VALUES ($1, $2, $3, $4,
$5::timestamptz, $6, $7)
`, sid, m.ordinal, m.role, m.content,
tsVal, len(m.content), m.system)
require.NoError(t, err, "inserting message ord=%d", m.ordinal)
}
}
func TestStoreGetSessionActivity(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
sid := "store-test-activity"
seedActivitySession(t, store, sid, []struct {
ordinal int
role string
content string
ts string
system bool
}{
{0, "user", "hello", "2026-03-26T10:00:00Z", false},
{1, "assistant", "hi", "2026-03-26T10:00:30Z", false},
{2, "user", "next", "2026-03-26T10:01:30Z", false},
{3, "assistant", "resp", "2026-03-26T10:02:00Z", false},
{4, "user", "back", "2026-03-26T10:28:00Z", false},
{5, "assistant", "wb", "2026-03-26T10:29:00Z", false},
// System message — excluded from buckets.
{6, "user", "This session is being continued from a previous conversation.", "2026-03-26T10:29:30Z", true},
})
ctx := context.Background()
resp, err := store.GetSessionActivity(ctx, sid)
require.NoError(t, err, "GetSessionActivity")
assert.Equal(t, int64(60), resp.IntervalSeconds)
assert.Equal(t, 7, resp.TotalMessages)
assert.GreaterOrEqual(t, len(resp.Buckets), 28, "bucket count")
first := resp.Buckets[0]
assert.Equal(t, 1, first.UserCount)
assert.Equal(t, 1, first.AssistantCount)
require.NotNil(t, first.FirstOrdinal)
assert.Equal(t, 0, *first.FirstOrdinal)
mid := resp.Buckets[15]
assert.Equal(t, 0, mid.UserCount)
assert.Equal(t, 0, mid.AssistantCount)
assert.Nil(t, mid.FirstOrdinal)
}
func TestStoreGetSessionActivity_NoMessages(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
sid := "store-test-activity-empty"
seedActivitySession(t, store, sid, nil)
resp, err := store.GetSessionActivity(
context.Background(), sid,
)
require.NoError(t, err, "GetSessionActivity")
assert.Empty(t, resp.Buckets)
}
func TestStoreGetSessionActivity_NullTimestamps(
t *testing.T,
) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
sid := "store-test-activity-nullts"
seedActivitySession(t, store, sid, []struct {
ordinal int
role string
content string
ts string
system bool
}{
{0, "user", "hi", "", false},
{1, "assistant", "hello", "", false},
})
resp, err := store.GetSessionActivity(
context.Background(), sid,
)
require.NoError(t, err, "GetSessionActivity")
assert.Empty(t, resp.Buckets)
assert.Equal(t, 2, resp.TotalMessages)
}
func TestStoreGetSessionActivity_SingleMessage(
t *testing.T,
) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
sid := "store-test-activity-single"
seedActivitySession(t, store, sid, []struct {
ordinal int
role string
content string
ts string
system bool
}{
{0, "user", "hi", "2026-03-26T10:00:00Z", false},
})
resp, err := store.GetSessionActivity(
context.Background(), sid,
)
require.NoError(t, err, "GetSessionActivity")
require.Len(t, resp.Buckets, 1)
assert.Equal(t, 1, resp.Buckets[0].UserCount)
}
func TestStoreGetSessionActivity_PrefixInjectedExcluded(
t *testing.T,
) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
sid := "store-test-activity-prefix"
seedActivitySession(t, store, sid, []struct {
ordinal int
role string
content string
ts string
system bool
}{
{0, "user", "hello", "2026-03-26T10:00:00Z", false},
{1, "assistant", "hi", "2026-03-26T10:00:30Z", false},
// Prefix-detected injected message: is_system=false but
// content starts with a system prefix.
{2, "user", "This session is being continued from a previous conversation.", "2026-03-26T10:01:00Z", false},
})
ctx := context.Background()
resp, err := store.GetSessionActivity(ctx, sid)
require.NoError(t, err, "GetSessionActivity")
// The prefix-detected message should be excluded from
// buckets but still count toward TotalMessages.
assert.Equal(t, 3, resp.TotalMessages)
// Only ordinals 0 and 1 should appear in buckets.
totalBucketed := 0
for _, b := range resp.Buckets {
totalBucketed += b.UserCount + b.AssistantCount
}
assert.Equal(t, 2, totalBucketed)
// The excluded message at 10:01:00 must not extend the
// timestamp range. With only 10:00:00-10:00:30 visible,
// a single bucket should cover the entire span.
assert.Len(t, resp.Buckets, 1)
}
func TestStoreGetSessionActivity_FractionalTimestamps(
t *testing.T,
) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
sid := "store-test-frac-ts"
seedActivitySession(t, store, sid, []struct {
ordinal int
role string
content string
ts string
system bool
}{
{0, "user", "a", "2026-03-26T10:00:00.900Z", false},
{1, "assistant", "b", "2026-03-26T10:00:59.100Z", false},
{2, "user", "c", "2026-03-26T10:01:01.000Z", false},
})
ctx := context.Background()
resp, err := store.GetSessionActivity(ctx, sid)
require.NoError(t, err, "GetSessionActivity")
require.Equal(t, int64(60), resp.IntervalSeconds)
require.GreaterOrEqual(t, len(resp.Buckets), 2)
// First bucket should have both sub-second messages.
first := resp.Buckets[0]
assert.Equal(t, 1, first.UserCount)
assert.Equal(t, 1, first.AssistantCount)
// Second bucket should have the third message.
second := resp.Buckets[1]
assert.Equal(t, 1, second.UserCount)
}
func TestStoreAnalyticsSummaryOutputTokenCoverage(
t *testing.T,
) {
pgURL := testPGURL(t)
ensureAnalyticsTokenStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
summary, err := store.GetAnalyticsSummary(
context.Background(),
db.AnalyticsFilter{
From: "2026-03-12",
To: "2026-03-13",
},
)
require.NoError(t, err, "GetAnalyticsSummary")
assert.Equal(t, 1800, summary.TotalOutputTokens)
assert.Equal(t, 3, summary.TokenReportingSessions)
}
func TestStoreAnalyticsHeatmapOutputTokens(t *testing.T) {
pgURL := testPGURL(t)
ensureAnalyticsTokenStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
heatmap, err := store.GetAnalyticsHeatmap(
context.Background(),
db.AnalyticsFilter{
From: "2026-03-12",
To: "2026-03-13",
},
"output_tokens",
)
require.NoError(t, err, "GetAnalyticsHeatmap")
assert.Equal(t, "output_tokens", heatmap.Metric)
require.Len(t, heatmap.Entries, 2)
assert.Equal(t, "2026-03-12", heatmap.Entries[0].Date)
assert.Equal(t, 1500, heatmap.Entries[0].Value)
assert.Equal(t, "2026-03-13", heatmap.Entries[1].Date)
assert.Equal(t, 300, heatmap.Entries[1].Value)
}
func TestStoreAnalyticsTopSessionsOutputTokens(
t *testing.T,
) {
pgURL := testPGURL(t)
ensureAnalyticsTokenStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
top, err := store.GetAnalyticsTopSessions(
context.Background(),
db.AnalyticsFilter{
From: "2026-03-12",
To: "2026-03-13",
},
"output_tokens",
)
require.NoError(t, err, "GetAnalyticsTopSessions")
assert.Equal(t, "output_tokens", top.Metric)
require.Len(t, top.Sessions, 3)
assert.Equal(t, "pg-token-001", top.Sessions[0].ID)
assert.Equal(t, 900, top.Sessions[0].OutputTokens)
for _, session := range top.Sessions {
assert.NotEqual(t, "pg-token-missing", session.ID,
"session without token coverage was included: %+v", session)
}
}
func TestStoreAnalyticsTopSessionsDisplayName(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
_, err = store.DB().Exec(`
INSERT INTO sessions (
id, machine, project, agent, first_message,
display_name, session_name,
started_at, ended_at, message_count,
user_message_count
) VALUES
('pg-session-name', 'test-machine', 'test-project',
'claude', 'raw first user message', NULL,
'Agent generated title',
'2026-03-12T11:00:00Z'::timestamptz,
'2026-03-12T11:30:00Z'::timestamptz,
10, 2),
('pg-custom-name', 'test-machine', 'test-project',
'claude', 'raw first user message',
'User renamed title', 'Generated title hidden by rename',
'2026-03-12T12:00:00Z'::timestamptz,
'2026-03-12T12:30:00Z'::timestamptz,
9, 2)
`)
require.NoError(t, err, "inserting top session names")
top, err := store.GetAnalyticsTopSessions(
context.Background(),
db.AnalyticsFilter{
From: "2026-03-12",
To: "2026-03-12",
},
"messages",
)
require.NoError(t, err, "GetAnalyticsTopSessions")
byID := map[string]db.TopSession{}
for _, session := range top.Sessions {
byID[session.ID] = session
}
named, ok := byID["pg-session-name"]
require.True(t, ok, "pg-session-name missing from top sessions")
require.NotNil(t, named.DisplayName,
"session_name should be exposed as display_name")
assert.Equal(t, "Agent generated title", *named.DisplayName)
custom, ok := byID["pg-custom-name"]
require.True(t, ok, "pg-custom-name missing from top sessions")
require.NotNil(t, custom.DisplayName,
"custom display_name should be exposed")
assert.Equal(t, "User renamed title", *custom.DisplayName)
}
func TestStoreAnalyticsTopSessionsMessagesAllowRunningSessions(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
_, err = store.DB().Exec(`
INSERT INTO sessions (
id, machine, project, agent, first_message,
started_at, ended_at, message_count,
user_message_count
) VALUES
('pg-running-session', 'test-machine', 'test-project',
'claude', 'still running',
'2026-03-12T13:00:00Z'::timestamptz,
NULL,
12, 3),
('pg-finished-session', 'test-machine', 'test-project',
'claude', 'finished',
'2026-03-12T11:00:00Z'::timestamptz,
'2026-03-12T11:30:00Z'::timestamptz,
10, 2)
`)
require.NoError(t, err, "inserting top sessions")
top, err := store.GetAnalyticsTopSessions(
context.Background(),
db.AnalyticsFilter{
From: "2026-03-12",
To: "2026-03-12",
},
"messages",
)
require.NoError(t, err, "GetAnalyticsTopSessions")
byID := map[string]db.TopSession{}
for _, session := range top.Sessions {
byID[session.ID] = session
}
running, ok := byID["pg-running-session"]
require.True(t, ok, "running session missing from top sessions")
assert.Equal(t, 0.0, running.DurationMin)
}
func TestStoreAnalyticsTopSessionsDurationUsesClampedActiveDuration(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
// Unique date so only these two sessions match the filter,
// regardless of what other tests leave behind in the schema.
_, err = store.DB().Exec(`
INSERT INTO sessions (
id, machine, project, agent, first_message,
started_at, ended_at, message_count, user_message_count
) VALUES
('pg-clamp-wall', 'test-machine', 'clamp-parity',
'claude', 'wall start',
'2027-07-15T09:00:00Z'::timestamptz,
'2027-07-15T11:00:00Z'::timestamptz,
3, 2),
('pg-clamp-active', 'test-machine', 'clamp-parity',
'claude', 'active start',
'2027-07-15T09:30:00Z'::timestamptz,
'2027-07-15T09:50:00Z'::timestamptz,
3, 2)
`)
require.NoError(t, err, "inserting sessions")
_, err = store.DB().Exec(`
INSERT INTO messages
(session_id, ordinal, role, content, timestamp) VALUES
('pg-clamp-wall', 0, 'user', 'noop',
'2027-07-15T09:00:00Z'::timestamptz),
('pg-clamp-wall', 1, 'assistant', 'idle wait',
'2027-07-15T10:59:00Z'::timestamptz),
('pg-clamp-wall', 2, 'user', 'done',
'2027-07-15T11:00:00Z'::timestamptz),
('pg-clamp-active', 0, 'user', 'start',
'2027-07-15T09:30:00Z'::timestamptz),
('pg-clamp-active', 1, 'assistant', 'tooling',
'2027-07-15T09:35:00Z'::timestamptz),
('pg-clamp-active', 2, 'user', 'finish',
'2027-07-15T09:50:00Z'::timestamptz)
`)
require.NoError(t, err, "inserting messages")
top, err := store.GetAnalyticsTopSessions(
context.Background(),
db.AnalyticsFilter{From: "2027-07-15", To: "2027-07-15"},
"duration",
)
require.NoError(t, err, "GetAnalyticsTopSessions")
require.Len(t, top.Sessions, 2)
// Active duration ranks ahead of wall: the engaged 20-min session
// (5 min gap + a 15 min gap capped at the 5 min idle cap = 10)
// beats the mostly-idle 2-hour session (119 min capped to 5 + a
// 1 min gap = 6). Generation gaps count even with no tool calls.
assert.Equal(t, "pg-clamp-active", top.Sessions[0].ID)
assert.Equal(t, 20.0, top.Sessions[0].DurationMin)
assert.Equal(t, 10.0, top.Sessions[0].ActiveDurationMin)
assert.Equal(t, "pg-clamp-wall", top.Sessions[1].ID)
assert.Equal(t, 120.0, top.Sessions[1].DurationMin)
assert.Equal(t, 6.0, top.Sessions[1].ActiveDurationMin)
}
func TestStoreAnalyticsTopSessionsDurationExcludesReversedTimestamps(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
// A reversed session (ended_at < started_at) still accumulates
// positive message-gap active duration, so without an eligibility
// guard it would rank into the duration list ordered by active
// duration. PostgreSQL must reject it, matching SQLite and DuckDB.
// (Empty-string timestamps are not representable in timestamptz
// columns, so only the reversed case applies on this backend.)
_, err = store.DB().Exec(`
INSERT INTO sessions (
id, machine, project, agent, first_message,
started_at, ended_at, message_count, user_message_count
) VALUES
('pg-elig-valid', 'test-machine', 'elig-parity',
'claude', 'valid start',
'2027-08-20T09:00:00Z'::timestamptz,
'2027-08-20T09:30:00Z'::timestamptz,
2, 1),
('pg-elig-reversed', 'test-machine', 'elig-parity',
'claude', 'reversed start',
'2027-08-20T10:00:00Z'::timestamptz,
'2027-08-20T09:00:00Z'::timestamptz,
2, 1)
`)
require.NoError(t, err, "inserting sessions")
_, err = store.DB().Exec(`
INSERT INTO messages
(session_id, ordinal, role, content, timestamp) VALUES
('pg-elig-valid', 0, 'user', 'start',
'2027-08-20T09:00:00Z'::timestamptz),
('pg-elig-valid', 1, 'assistant', 'work',
'2027-08-20T09:03:00Z'::timestamptz),
('pg-elig-reversed', 0, 'user', 'start',
'2027-08-20T09:00:00Z'::timestamptz),
('pg-elig-reversed', 1, 'assistant', 'work',
'2027-08-20T09:04:00Z'::timestamptz)
`)
require.NoError(t, err, "inserting messages")
top, err := store.GetAnalyticsTopSessions(
context.Background(),
db.AnalyticsFilter{From: "2027-08-20", To: "2027-08-20"},
"duration",
)
require.NoError(t, err, "GetAnalyticsTopSessions")
ids := []string{}
for _, session := range top.Sessions {
ids = append(ids, session.ID)
}
assert.Equal(t, []string{"pg-elig-valid"}, ids,
"reversed duration row must be excluded")
}
func TestStoreWriteSurfaceSplitByCapability(t *testing.T) {
pgURL := testPGURL(t)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
assert.True(t, store.ReadOnly())
ctx := context.Background()
project := "store-capability"
sessionID := "store-capability-001"
trashedID := "store-capability-002"
emptyTrashID := "store-capability-003"
batchTrashID := "store-capability-004"
insightID, err := store.InsertInsight(db.Insight{
Type: "dashboard",
DateFrom: "2026-03-12",
DateTo: "2026-03-12",
Project: &project,
Agent: "claude",
Content: "insight content",
CacheKey: "capability-cache",
})
require.NoError(t, err, "InsertInsight")
require.NotZero(t, insightID)
insight, err := store.GetInsight(ctx, insightID)
require.NoError(t, err, "GetInsight")
require.NotNil(t, insight)
assert.Equal(t, project, *insight.Project)
cached, err := store.GetCachedInsight(ctx, "capability-cache")
require.NoError(t, err, "GetCachedInsight")
require.NotNil(t, cached)
assert.Equal(t, insightID, cached.ID)
listed, err := store.ListInsights(ctx, db.InsightFilter{
Type: "dashboard",
})
require.NoError(t, err, "ListInsights")
require.NotEmpty(t, listed)
assert.Equal(t, insightID, listed[0].ID)
require.NoError(t, store.DeleteInsight(insightID), "DeleteInsight")
insight, err = store.GetInsight(ctx, insightID)
require.NoError(t, err, "GetInsight after delete")
assert.Nil(t, insight)
_, err = store.DB().Exec(`
INSERT INTO sessions (
id, machine, project, agent, first_message,
display_name, started_at, ended_at, message_count,
user_message_count
) VALUES
($1, 'machine', $2, 'claude', 'hello',
NULL, '2026-03-12T10:00:00Z'::timestamptz,
'2026-03-12T10:30:00Z'::timestamptz, 2, 1),
($3, 'machine', $2, 'claude', 'trash me',
NULL, '2026-03-12T11:00:00Z'::timestamptz,
'2026-03-12T11:30:00Z'::timestamptz, 2, 1),
($4, 'machine', $2, 'claude', 'empty trash me',
NULL, '2026-03-12T12:00:00Z'::timestamptz,
'2026-03-12T12:30:00Z'::timestamptz, 2, 1),
($5, 'machine', $2, 'claude', 'batch trash me',
NULL, '2026-03-12T13:00:00Z'::timestamptz,
'2026-03-12T13:30:00Z'::timestamptz, 2, 1)
`, sessionID, project, trashedID, emptyTrashID, batchTrashID)
require.NoError(t, err, "inserting session rows")
renamed := "Capability renamed session"
require.NoError(t, store.RenameSession(sessionID, &renamed),
"RenameSession")
sess, err := store.GetSession(ctx, sessionID)
require.NoError(t, err, "GetSession after rename")
require.NotNil(t, sess)
require.NotNil(t, sess.DisplayName)
assert.Equal(t, renamed, *sess.DisplayName)
require.NoError(t, store.SoftDeleteSession(sessionID),
"SoftDeleteSession")
sess, err = store.GetSession(ctx, sessionID)
require.NoError(t, err, "GetSession after soft delete")
assert.Nil(t, sess)
trashed, err := store.ListTrashedSessions(ctx)
require.NoError(t, err, "ListTrashedSessions")
assert.Contains(t, sessionIDs(trashed), sessionID)
restored, err := store.RestoreSession(sessionID)
require.NoError(t, err, "RestoreSession")
assert.EqualValues(t, 1, restored)
require.NoError(t, store.SoftDeleteSession(trashedID),
"SoftDeleteSession trashedID")
deleted, err := store.DeleteSessionIfTrashed(trashedID)
require.NoError(t, err, "DeleteSessionIfTrashed")
assert.EqualValues(t, 1, deleted)
sess, err = store.GetSessionFull(ctx, trashedID)
require.NoError(t, err, "GetSessionFull after permanent delete")
assert.Nil(t, sess)
deletedCount, err := store.SoftDeleteSessions([]string{
emptyTrashID, batchTrashID,
})
require.NoError(t, err, "SoftDeleteSessions")
assert.Equal(t, 2, deletedCount)
count, err := store.EmptyTrash()
require.NoError(t, err, "EmptyTrash")
assert.Equal(t, 2, count)
trashed, err = store.ListTrashedSessions(ctx)
require.NoError(t, err, "ListTrashedSessions after empty trash")
assert.NotContains(t, sessionIDs(trashed), emptyTrashID)
assert.NotContains(t, sessionIDs(trashed), batchTrashID)
assert.Equal(t, db.ErrReadOnly, store.UpsertSession(db.Session{}))
assert.Equal(t, db.ErrReadOnly,
store.ReplaceSessionMessages("x", nil))
_, err = store.WriteSessionBatchAtomic(nil)
assert.ErrorIs(t, err, db.ErrReadOnly)
_, err = store.RecordRecallQueryEvent(ctx, db.RecallQueryEvent{
Surface: db.RecallQuerySurfaceQuery,
})
assert.ErrorIs(t, err, db.ErrReadOnly)
}