kenn-io--agentsview
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
636 行
20 KiB
Go
636 行
20 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestDeleteSession_LargeSessionFTSDelete(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping perf test in -short mode")
|
|
}
|
|
t.Parallel()
|
|
d := openLargeSessionFixtureDB(t, true)
|
|
requireFTS(t, d)
|
|
|
|
start := time.Now()
|
|
require.NoError(t, d.DeleteSession(largeSessionFixtureID), "DeleteSession")
|
|
elapsed := time.Since(start)
|
|
require.LessOrEqual(t, elapsed, largeSessionPerfCeiling,
|
|
"DeleteSession took %s, want < 10s (per-row FTS trigger regression?)",
|
|
elapsed.Round(time.Millisecond))
|
|
|
|
requireSessionGone(t, d, largeSessionFixtureID)
|
|
assertNoFTSLeak(t, d, largeSessionFixtureToken)
|
|
requireMessagesDeleteTriggerRestored(t, d)
|
|
|
|
var neighborPins int
|
|
err := d.getReader().QueryRow(
|
|
"SELECT count(*) FROM pinned_messages WHERE session_id LIKE ?",
|
|
largeSessionNeighborPrefix+"-%",
|
|
).Scan(&neighborPins)
|
|
require.NoError(t, err, "neighbor pins count")
|
|
assert.Equal(t, crossSessionNeighborCount, neighborPins,
|
|
"neighbor pins count")
|
|
}
|
|
|
|
func TestFindSessionIDsByPartial(t *testing.T) {
|
|
d := testDB(t)
|
|
insertSession(t, d, "abcdef-1111-2222", "proj")
|
|
insertSession(t, d, "abcdef-3333-4444", "proj")
|
|
insertSession(t, d, "fedcba-5555", "proj")
|
|
|
|
ctx := context.Background()
|
|
|
|
got, err := d.FindSessionIDsByPartial(ctx, "abcdef", 5)
|
|
require.NoError(t, err, "FindSessionIDsByPartial")
|
|
assert.Len(t, got, 2, "abcdef matches")
|
|
|
|
got, err = d.FindSessionIDsByPartial(ctx, "fedcba", 5)
|
|
require.NoError(t, err, "FindSessionIDsByPartial")
|
|
assert.Equal(t, []string{"fedcba-5555"}, got, "fedcba matches")
|
|
|
|
got, err = d.FindSessionIDsByPartial(ctx, "nope", 5)
|
|
require.NoError(t, err, "FindSessionIDsByPartial")
|
|
assert.Empty(t, got, "nope matches")
|
|
|
|
got, err = d.FindSessionIDsByPartial(ctx, "", 5)
|
|
require.NoError(t, err, "FindSessionIDsByPartial")
|
|
assert.Nil(t, got, "empty input")
|
|
}
|
|
|
|
func TestFindSessionIDsByPartialLiteralCaseSensitive(t *testing.T) {
|
|
d := testDB(t)
|
|
insertSession(t, d, "abc_def", "proj")
|
|
insertSession(t, d, "abcXdef", "proj")
|
|
insertSession(t, d, "abc%def", "proj")
|
|
insertSession(t, d, "ABCdef", "proj")
|
|
|
|
ctx := context.Background()
|
|
|
|
got, err := d.FindSessionIDsByPartial(ctx, "c_d", 10)
|
|
require.NoError(t, err, "underscore lookup")
|
|
assert.Equal(t, []string{"abc_def"}, got)
|
|
|
|
got, err = d.FindSessionIDsByPartial(ctx, "c%d", 10)
|
|
require.NoError(t, err, "percent lookup")
|
|
assert.Equal(t, []string{"abc%def"}, got)
|
|
|
|
got, err = d.FindSessionIDsByPartial(ctx, "abc", 10)
|
|
require.NoError(t, err, "case-sensitive lookup")
|
|
assert.ElementsMatch(t, []string{"abc_def", "abcXdef", "abc%def"}, got)
|
|
assert.NotContains(t, got, "ABCdef")
|
|
}
|
|
|
|
func TestListSessions_OutcomeFilter(t *testing.T) {
|
|
d := testDB(t)
|
|
|
|
// Insert sessions then set signals with different outcomes.
|
|
for _, tc := range []struct {
|
|
id string
|
|
outcome string
|
|
}{
|
|
{"out-1", "completed"},
|
|
{"out-2", "abandoned"},
|
|
{"out-3", "errored"},
|
|
{"out-4", "completed"},
|
|
} {
|
|
insertSession(t, d, tc.id, "proj", func(s *Session) {
|
|
s.StartedAt = new("2024-06-01T10:00:00Z")
|
|
s.EndedAt = new("2024-06-01T11:00:00Z")
|
|
s.MessageCount = 5
|
|
s.UserMessageCount = 3
|
|
})
|
|
err := d.UpdateSessionSignals(tc.id, SessionSignalUpdate{
|
|
Outcome: tc.outcome,
|
|
})
|
|
require.NoError(t, err, "UpdateSessionSignals %s", tc.id)
|
|
}
|
|
|
|
// Single outcome.
|
|
requireSessions(t, d, filterWith(func(f *SessionFilter) {
|
|
f.Outcome = []string{"abandoned"}
|
|
}), []string{"out-2"})
|
|
|
|
// Multiple outcomes.
|
|
requireSessions(t, d, filterWith(func(f *SessionFilter) {
|
|
f.Outcome = []string{"completed", "errored"}
|
|
}), []string{"out-1", "out-3", "out-4"})
|
|
}
|
|
|
|
func TestListSessions_HealthGradeFilter(t *testing.T) {
|
|
d := testDB(t)
|
|
|
|
for _, tc := range []struct {
|
|
id string
|
|
grade string
|
|
score int
|
|
}{
|
|
{"hg-1", "A", 95},
|
|
{"hg-2", "C", 60},
|
|
{"hg-3", "F", 20},
|
|
{"hg-4", "A", 90},
|
|
} {
|
|
insertSession(t, d, tc.id, "proj", func(s *Session) {
|
|
s.StartedAt = new("2024-06-01T10:00:00Z")
|
|
s.EndedAt = new("2024-06-01T11:00:00Z")
|
|
s.MessageCount = 5
|
|
s.UserMessageCount = 3
|
|
})
|
|
err := d.UpdateSessionSignals(tc.id, SessionSignalUpdate{
|
|
HealthGrade: new(tc.grade),
|
|
HealthScore: new(tc.score),
|
|
})
|
|
require.NoError(t, err, "UpdateSessionSignals %s", tc.id)
|
|
}
|
|
|
|
requireSessions(t, d, filterWith(func(f *SessionFilter) {
|
|
f.HealthGrade = []string{"A"}
|
|
}), []string{"hg-1", "hg-4"})
|
|
|
|
requireSessions(t, d, filterWith(func(f *SessionFilter) {
|
|
f.HealthGrade = []string{"C", "F"}
|
|
}), []string{"hg-2", "hg-3"})
|
|
}
|
|
|
|
func TestListSessions_MinToolFailuresFilter(t *testing.T) {
|
|
d := testDB(t)
|
|
|
|
for _, tc := range []struct {
|
|
id string
|
|
failures int
|
|
}{
|
|
{"tf-1", 0},
|
|
{"tf-2", 3},
|
|
{"tf-3", 7},
|
|
} {
|
|
insertSession(t, d, tc.id, "proj", func(s *Session) {
|
|
s.StartedAt = new("2024-06-01T10:00:00Z")
|
|
s.EndedAt = new("2024-06-01T11:00:00Z")
|
|
s.MessageCount = 5
|
|
s.UserMessageCount = 3
|
|
})
|
|
err := d.UpdateSessionSignals(tc.id, SessionSignalUpdate{
|
|
ToolFailureSignalCount: tc.failures,
|
|
})
|
|
require.NoError(t, err, "UpdateSessionSignals %s", tc.id)
|
|
}
|
|
|
|
requireSessions(t, d, filterWith(func(f *SessionFilter) {
|
|
f.MinToolFailures = new(3)
|
|
}), []string{"tf-2", "tf-3"})
|
|
|
|
requireSessions(t, d, filterWith(func(f *SessionFilter) {
|
|
f.MinToolFailures = new(5)
|
|
}), []string{"tf-3"})
|
|
|
|
// Zero threshold returns all.
|
|
requireSessions(t, d, filterWith(func(f *SessionFilter) {
|
|
f.MinToolFailures = new(0)
|
|
}), []string{"tf-1", "tf-2", "tf-3"})
|
|
}
|
|
|
|
func TestUpsertSession_DisplayNameUpdateBehavior(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
|
|
sessionName := "My Chat Title"
|
|
err := d.UpsertSession(Session{
|
|
ID: "claude-ai:dn-test",
|
|
Project: "claude.ai",
|
|
Machine: "local",
|
|
Agent: "claude-ai",
|
|
SessionName: &sessionName,
|
|
MessageCount: 1,
|
|
})
|
|
require.NoError(t, err, "UpsertSession insert")
|
|
|
|
// Verify session_name is visible via COALESCE in GetSession.
|
|
s, err := d.GetSession(ctx, "claude-ai:dn-test")
|
|
require.NoError(t, err, "GetSession after insert")
|
|
require.NotNil(t, s, "GetSession returned nil after insert")
|
|
require.NotNil(t, s.DisplayName, "DisplayName is nil after insert")
|
|
assert.Equal(t, "My Chat Title", *s.DisplayName, "DisplayName")
|
|
|
|
// Re-upsert with a different session_name: should overwrite (agent names
|
|
// are always refreshed on re-parse; only display_name is user-protected).
|
|
newName := "Updated Title"
|
|
err = d.UpsertSession(Session{
|
|
ID: "claude-ai:dn-test",
|
|
Project: "claude.ai",
|
|
Machine: "local",
|
|
Agent: "claude-ai",
|
|
SessionName: &newName,
|
|
MessageCount: 2,
|
|
})
|
|
require.NoError(t, err, "UpsertSession update")
|
|
|
|
// session_name should be updated on re-upsert.
|
|
s, err = d.GetSession(ctx, "claude-ai:dn-test")
|
|
require.NoError(t, err, "GetSession after re-upsert")
|
|
require.NotNil(t, s, "GetSession returned nil after re-upsert")
|
|
require.NotNil(t, s.DisplayName, "DisplayName is nil after re-upsert")
|
|
assert.Equal(t, "Updated Title", *s.DisplayName,
|
|
"session_name should be updated on re-upsert")
|
|
// Other fields should also update.
|
|
assert.Equal(t, 2, s.MessageCount, "MessageCount")
|
|
}
|
|
|
|
// TestUpsertSessionDoesNotAdvanceDataVersion guards the
|
|
// invariant that data_version is never touched by
|
|
// UpsertSession -- it must only advance via
|
|
// SetSessionDataVersion after a successful message rewrite,
|
|
// so a transient write failure cannot leave a session row
|
|
// stamped at the current parser version with stale
|
|
// messages.
|
|
func TestUpsertSessionDoesNotAdvanceDataVersion(t *testing.T) {
|
|
d := testDB(t)
|
|
|
|
// New session: data_version stays 0 even when the
|
|
// caller passes a non-zero value on the struct.
|
|
require.NoError(t, d.UpsertSession(Session{
|
|
ID: "dv-1",
|
|
Project: "p",
|
|
Machine: "m",
|
|
Agent: "claude",
|
|
MessageCount: 1,
|
|
DataVersion: CurrentDataVersion(),
|
|
}), "UpsertSession (insert)")
|
|
assert.Equal(t, 0, d.GetSessionDataVersion("dv-1"),
|
|
"after insert, data_version")
|
|
|
|
// Stamp a current value to simulate a successful write.
|
|
require.NoError(t, d.SetSessionDataVersion(
|
|
"dv-1", CurrentDataVersion(),
|
|
), "SetSessionDataVersion")
|
|
assert.Equal(t, CurrentDataVersion(), d.GetSessionDataVersion("dv-1"),
|
|
"after Set, data_version")
|
|
|
|
// Re-upserting (e.g. as part of an incremental sync)
|
|
// must NOT clobber the stamped version with the
|
|
// struct's value (here 0), and must NOT replace it
|
|
// with a future "current" value before the rewrite
|
|
// succeeds.
|
|
require.NoError(t, d.UpsertSession(Session{
|
|
ID: "dv-1",
|
|
Project: "p",
|
|
Machine: "m",
|
|
Agent: "claude",
|
|
MessageCount: 5,
|
|
DataVersion: 0,
|
|
}), "UpsertSession (update)")
|
|
assert.Equal(t, CurrentDataVersion(), d.GetSessionDataVersion("dv-1"),
|
|
"after re-upsert, data_version (must be preserved across UpsertSession)")
|
|
}
|
|
|
|
func TestSessionTranscriptFidelityRoundTrips(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
s := Session{
|
|
ID: "antigravity-cli:fidelity-rt",
|
|
Agent: "antigravity-cli",
|
|
TranscriptFidelity: "summary",
|
|
}
|
|
require.NoError(t, d.UpsertSession(s))
|
|
|
|
got, err := d.GetSession(ctx, s.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, got)
|
|
assert.Equal(t, "summary", got.TranscriptFidelity)
|
|
}
|
|
|
|
func TestUpsertSessionTerminationStatus(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
|
|
clean := "clean"
|
|
pending := "tool_call_pending"
|
|
|
|
tests := []struct {
|
|
name string
|
|
val *string
|
|
}{
|
|
{name: "null", val: nil},
|
|
{name: "clean", val: &clean},
|
|
{name: "tool_call_pending", val: &pending},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
id := "session_" + tc.name
|
|
s := Session{
|
|
ID: id,
|
|
Project: "p",
|
|
Machine: "local",
|
|
Agent: "claude",
|
|
MessageCount: 1,
|
|
UserMessageCount: 1,
|
|
TerminationStatus: tc.val,
|
|
}
|
|
require.NoError(t, d.UpsertSession(s), "upsert")
|
|
|
|
got, err := d.GetSession(ctx, id)
|
|
require.NoError(t, err, "get")
|
|
require.NotNil(t, got, "session not found")
|
|
|
|
if tc.val == nil {
|
|
assert.Nil(t, got.TerminationStatus, "nil mismatch")
|
|
} else {
|
|
require.NotNil(t, got.TerminationStatus, "nil mismatch")
|
|
assert.Equal(t, *tc.val, *got.TerminationStatus, "value mismatch")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestListSessionsTerminationFilter(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
|
|
clean := "clean"
|
|
pending := "tool_call_pending"
|
|
truncated := "truncated"
|
|
|
|
now := time.Now().UTC()
|
|
mkTS := func(d time.Duration) string {
|
|
return now.Add(-d).Format("2006-01-02T15:04:05.000Z")
|
|
}
|
|
|
|
insertAt := func(id string, age time.Duration, term *string) {
|
|
ts := mkTS(age)
|
|
s := Session{
|
|
ID: id,
|
|
Project: "p",
|
|
Machine: "local",
|
|
Agent: "claude",
|
|
StartedAt: &ts,
|
|
EndedAt: &ts,
|
|
MessageCount: 1,
|
|
UserMessageCount: 2,
|
|
TerminationStatus: term,
|
|
}
|
|
require.NoError(t, d.UpsertSession(s), "upsert %s", id)
|
|
}
|
|
|
|
// Active (< 10 min idle): regardless of termination_status,
|
|
// these are surfaced by ?termination=active.
|
|
insertAt("active-clean", 1*time.Minute, &clean)
|
|
insertAt("active-pending", 2*time.Minute, &pending)
|
|
|
|
// Stale (10–60 min idle): surfaced by ?termination=stale.
|
|
insertAt("stale-clean", 30*time.Minute, &clean)
|
|
insertAt("stale-pending", 40*time.Minute, &pending)
|
|
|
|
// Idle > 60 min: surfaced by ?termination=unclean only when
|
|
// termination_status flags an issue.
|
|
insertAt("old-clean", 2*time.Hour, &clean)
|
|
insertAt("old-pending", 2*time.Hour, &pending)
|
|
insertAt("old-truncated", 3*time.Hour, &truncated)
|
|
insertAt("old-null", 2*time.Hour, nil)
|
|
|
|
collect := func(f SessionFilter) []string {
|
|
page, err := d.ListSessions(ctx, f)
|
|
require.NoError(t, err, "list")
|
|
ids := make([]string, len(page.Sessions))
|
|
for i, s := range page.Sessions {
|
|
ids[i] = s.ID
|
|
}
|
|
return ids
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
termination string
|
|
wantIDs []string
|
|
}{
|
|
{
|
|
name: "all (default)",
|
|
termination: "",
|
|
wantIDs: []string{
|
|
"active-clean", "active-pending",
|
|
"stale-clean", "stale-pending",
|
|
"old-clean", "old-pending",
|
|
"old-truncated", "old-null",
|
|
},
|
|
},
|
|
{
|
|
name: "active",
|
|
termination: "active",
|
|
wantIDs: []string{"active-clean", "active-pending"},
|
|
},
|
|
{
|
|
// Yellow only fires for parser-flagged sessions —
|
|
// stale-clean stays quiet, no false positive for
|
|
// sessions that ended normally.
|
|
name: "stale",
|
|
termination: "stale",
|
|
wantIDs: []string{"stale-pending"},
|
|
},
|
|
{
|
|
name: "unclean",
|
|
termination: "unclean",
|
|
wantIDs: []string{"old-pending", "old-truncated"},
|
|
},
|
|
{
|
|
// Multi-select: comma-separated values OR together,
|
|
// so "stale,unclean" surfaces every parser-flagged
|
|
// session past the active window.
|
|
name: "stale or unclean",
|
|
termination: "stale,unclean",
|
|
wantIDs: []string{"stale-pending", "old-pending", "old-truncated"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := collect(SessionFilter{Termination: tc.termination})
|
|
assertStringSetsEqual(t, got, tc.wantIDs)
|
|
})
|
|
}
|
|
}
|
|
|
|
// assertStringSetsEqual checks that two slices contain the same
|
|
// elements regardless of order.
|
|
func assertStringSetsEqual(t *testing.T, got, want []string) {
|
|
t.Helper()
|
|
assert.ElementsMatch(t, want, got)
|
|
}
|
|
|
|
// getSessionRow reads display_name and session_name directly from the
|
|
// sessions table without going through scanSessionRow.
|
|
func getSessionRow(t *testing.T, d *DB, id string) Session {
|
|
t.Helper()
|
|
var s Session
|
|
s.ID = id
|
|
requireNoError(t, d.getWriter().QueryRow(
|
|
"SELECT display_name, session_name FROM sessions WHERE id = ?", id).
|
|
Scan(&s.DisplayName, &s.SessionName), "get session row")
|
|
return s
|
|
}
|
|
|
|
func TestUpsertSessionNameOwnership(t *testing.T) {
|
|
d := testDB(t)
|
|
|
|
// Agent name lands on a fresh row via session_name.
|
|
requireNoError(t, d.UpsertSession(Session{
|
|
ID: "s1", Project: "p", Machine: "local", Agent: "claude",
|
|
SessionName: Ptr("agent-one"),
|
|
}), "insert agent name")
|
|
got := getSessionRow(t, d, "s1")
|
|
require.NotNil(t, got.SessionName)
|
|
assert.Equal(t, "agent-one", *got.SessionName)
|
|
assert.Nil(t, got.DisplayName, "display_name not set by upsert")
|
|
|
|
// A newer agent name overwrites session_name.
|
|
requireNoError(t, d.UpsertSession(Session{
|
|
ID: "s1", Project: "p", Machine: "local", Agent: "claude",
|
|
SessionName: Ptr("agent-two"),
|
|
}), "update agent name")
|
|
got = getSessionRow(t, d, "s1")
|
|
assert.Equal(t, "agent-two", *got.SessionName, "session_name updated on re-upsert")
|
|
assert.Nil(t, got.DisplayName, "display_name still not set by upsert")
|
|
|
|
// A manual rename sets display_name.
|
|
requireNoError(t, d.RenameSession("s1", Ptr("user-name")), "rename")
|
|
|
|
// A subsequent agent name must NOT overwrite the user's display_name.
|
|
requireNoError(t, d.UpsertSession(Session{
|
|
ID: "s1", Project: "p", Machine: "local", Agent: "claude",
|
|
SessionName: Ptr("agent-three"),
|
|
}), "agent after user")
|
|
got = getSessionRow(t, d, "s1")
|
|
assert.Equal(t, "user-name", *got.DisplayName, "user display_name must survive re-parse")
|
|
assert.Equal(t, "agent-three", *got.SessionName, "session_name always updated")
|
|
}
|
|
|
|
// TestGetSessionFullPopulatesSessionName verifies that GetSessionFull
|
|
// keeps the raw session_name on the Go struct while DisplayName carries
|
|
// the visible name (user rename, else session_name), matching the PG and
|
|
// DuckDB GetSessionFull implementations. session_name is a backend-only
|
|
// field: it is NOT serialised in JSON responses (json:"-" on
|
|
// Session.SessionName). Push paths that need display_name unmerged read
|
|
// via ListSessionsModifiedBetween, not GetSessionFull.
|
|
func TestGetSessionFullPopulatesSessionName(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
|
|
// Insert a session with an agent-provided session_name.
|
|
requireNoError(t, d.UpsertSession(Session{
|
|
ID: "s-ns",
|
|
Project: "p",
|
|
Machine: "local",
|
|
Agent: "claude",
|
|
SessionName: Ptr("Agent Title"),
|
|
MessageCount: 1,
|
|
}), "upsert agent-named session")
|
|
|
|
// GetSessionFull populates Session.SessionName from the DB for internal use.
|
|
s, err := d.GetSessionFull(ctx, "s-ns")
|
|
require.NoError(t, err, "GetSessionFull")
|
|
require.NotNil(t, s, "session not found")
|
|
require.NotNil(t, s.SessionName, "SessionName is nil after GetSessionFull")
|
|
assert.Equal(t, "Agent Title", *s.SessionName, "SessionName round-trips")
|
|
// No user rename yet: DisplayName falls back to session_name.
|
|
require.NotNil(t, s.DisplayName, "DisplayName coalesces to session_name")
|
|
assert.Equal(t, "Agent Title", *s.DisplayName, "visible name before rename")
|
|
|
|
// After a manual rename, display_name is set; session_name is unchanged.
|
|
requireNoError(t, d.RenameSession("s-ns", Ptr("User Title")), "rename")
|
|
s, err = d.GetSessionFull(ctx, "s-ns")
|
|
require.NoError(t, err, "GetSessionFull after rename")
|
|
require.NotNil(t, s, "session not found after rename")
|
|
require.NotNil(t, s.DisplayName, "DisplayName should be set after rename")
|
|
assert.Equal(t, "User Title", *s.DisplayName, "display_name after rename")
|
|
require.NotNil(t, s.SessionName, "SessionName should still be set after rename")
|
|
assert.Equal(t, "Agent Title", *s.SessionName, "SessionName unchanged after rename")
|
|
}
|
|
|
|
func TestGetSessionName(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
|
|
require.NoError(t, d.UpsertSession(Session{
|
|
ID: "null-name", Project: "p", Machine: "local", Agent: "codex",
|
|
}), "upsert session with null name")
|
|
require.NoError(t, d.UpsertSession(Session{
|
|
ID: "stored-name", Project: "p", Machine: "local", Agent: "codex",
|
|
SessionName: Ptr("Agent Title"),
|
|
}), "upsert session with stored name")
|
|
|
|
tests := []struct {
|
|
name string
|
|
id string
|
|
wantName string
|
|
wantFound bool
|
|
}{
|
|
{name: "missing", id: "missing", wantFound: false},
|
|
{name: "null", id: "null-name", wantFound: true},
|
|
{name: "stored", id: "stored-name", wantName: "Agent Title", wantFound: true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
name, found, err := d.GetSessionName(ctx, tt.id)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.wantName, name)
|
|
assert.Equal(t, tt.wantFound, found)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRenameSessionSetsAndClears(t *testing.T) {
|
|
d := testDB(t)
|
|
requireNoError(t, d.UpsertSession(Session{
|
|
ID: "s1", Project: "p", Machine: "local", Agent: "claude",
|
|
SessionName: Ptr("Agent Name"),
|
|
}), "upsert")
|
|
|
|
requireNoError(t, d.RenameSession("s1", Ptr("User Name")), "rename")
|
|
got := getSessionRow(t, d, "s1")
|
|
require.NotNil(t, got.DisplayName, "display_name set after rename")
|
|
assert.Equal(t, "User Name", *got.DisplayName)
|
|
// session_name is unchanged by RenameSession.
|
|
require.NotNil(t, got.SessionName, "session_name not cleared by rename")
|
|
assert.Equal(t, "Agent Name", *got.SessionName)
|
|
|
|
requireNoError(t, d.RenameSession("s1", nil), "clear")
|
|
got = getSessionRow(t, d, "s1")
|
|
assert.Nil(t, got.DisplayName, "display_name cleared")
|
|
// session_name persists after clearing the user rename.
|
|
require.NotNil(t, got.SessionName, "session_name persists")
|
|
assert.Equal(t, "Agent Name", *got.SessionName)
|
|
}
|
|
|
|
func TestSessionNameCOALESCEInGetSession(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
|
|
// Session with only session_name — GetSession should return it via COALESCE.
|
|
requireNoError(t, d.UpsertSession(Session{
|
|
ID: "s1", Project: "p", Machine: "local", Agent: "claude",
|
|
SessionName: Ptr("Agent Title"), MessageCount: 1,
|
|
}), "upsert with session_name")
|
|
s, err := d.GetSession(ctx, "s1")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, s.DisplayName)
|
|
assert.Equal(t, "Agent Title", *s.DisplayName, "COALESCE returns session_name when no user rename")
|
|
|
|
// User renames — display_name wins.
|
|
requireNoError(t, d.RenameSession("s1", Ptr("User Title")), "rename")
|
|
s, err = d.GetSession(ctx, "s1")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, s.DisplayName)
|
|
assert.Equal(t, "User Title", *s.DisplayName, "display_name wins over session_name")
|
|
|
|
// Clear rename — session_name visible again.
|
|
requireNoError(t, d.RenameSession("s1", nil), "clear rename")
|
|
s, err = d.GetSession(ctx, "s1")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, s.DisplayName)
|
|
assert.Equal(t, "Agent Title", *s.DisplayName, "session_name restored after clearing rename")
|
|
}
|