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

636 lines
20 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (1060 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")
}