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

491 lines
17 KiB
Go

package db
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// listSortedIDs returns the session IDs in the order ListSessions returns them.
func listSortedIDs(t *testing.T, d *DB, f SessionFilter) []string {
t.Helper()
page, err := d.ListSessions(context.Background(), f)
require.NoError(t, err, "ListSessions")
ids := make([]string, len(page.Sessions))
for i, s := range page.Sessions {
ids[i] = s.ID
}
return ids
}
// TestListSessions_SortAscDesc covers each sort key in both directions using an
// explicit absolute Descending so the default-direction logic is exercised
// separately (TestListSessions_SortDefaultDirection).
func TestListSessions_SortAscDesc(t *testing.T) {
cases := []struct {
name string
orderBy string
setup func(t *testing.T, d *DB, project string)
// wantAsc is the order with Descending=false; desc is its reverse.
wantAsc []string
}{
{
name: "recent",
orderBy: "recent",
setup: func(t *testing.T, d *DB, project string) {
insertSession(t, d, "r-old", project, func(s *Session) { s.EndedAt = Ptr("2024-01-01T00:00:00Z") })
insertSession(t, d, "r-mid", project, func(s *Session) { s.EndedAt = Ptr("2024-02-01T00:00:00Z") })
insertSession(t, d, "r-new", project, func(s *Session) { s.EndedAt = Ptr("2024-03-01T00:00:00Z") })
},
wantAsc: []string{"r-old", "r-mid", "r-new"},
},
{
name: "started",
orderBy: "started",
setup: func(t *testing.T, d *DB, project string) {
insertSession(t, d, "s-old", project, func(s *Session) { s.StartedAt = Ptr("2024-01-01T00:00:00Z") })
insertSession(t, d, "s-mid", project, func(s *Session) { s.StartedAt = Ptr("2024-02-01T00:00:00Z") })
insertSession(t, d, "s-new", project, func(s *Session) { s.StartedAt = Ptr("2024-03-01T00:00:00Z") })
},
wantAsc: []string{"s-old", "s-mid", "s-new"},
},
{
name: "messages",
orderBy: "messages",
setup: func(t *testing.T, d *DB, project string) {
insertSession(t, d, "m1", project, func(s *Session) { s.MessageCount = 1 })
insertSession(t, d, "m5", project, func(s *Session) { s.MessageCount = 5 })
insertSession(t, d, "m9", project, func(s *Session) { s.MessageCount = 9 })
},
wantAsc: []string{"m1", "m5", "m9"},
},
{
name: "user-messages",
orderBy: "user-messages",
setup: func(t *testing.T, d *DB, project string) {
insertSession(t, d, "u1", project, func(s *Session) { s.UserMessageCount = 1 })
insertSession(t, d, "u4", project, func(s *Session) { s.UserMessageCount = 4 })
insertSession(t, d, "u8", project, func(s *Session) { s.UserMessageCount = 8 })
},
wantAsc: []string{"u1", "u4", "u8"},
},
{
name: "output-tokens",
orderBy: "output-tokens",
setup: func(t *testing.T, d *DB, project string) {
insertSession(t, d, "o100", project, func(s *Session) { s.TotalOutputTokens = 100 })
insertSession(t, d, "o500", project, func(s *Session) { s.TotalOutputTokens = 500 })
insertSession(t, d, "o900", project, func(s *Session) { s.TotalOutputTokens = 900 })
},
wantAsc: []string{"o100", "o500", "o900"},
},
{
name: "peak-context",
orderBy: "peak-context",
setup: func(t *testing.T, d *DB, project string) {
insertSession(t, d, "pc1", project, func(s *Session) { s.PeakContextTokens = 1000 })
insertSession(t, d, "pc2", project, func(s *Session) { s.PeakContextTokens = 2000 })
insertSession(t, d, "pc3", project, func(s *Session) { s.PeakContextTokens = 3000 })
},
wantAsc: []string{"pc1", "pc2", "pc3"},
},
{
name: "failures",
orderBy: "failures",
setup: func(t *testing.T, d *DB, project string) {
insertSession(t, d, "f0", project)
insertSession(t, d, "f3", project)
updateSignals(t, d, "f3", SessionSignalUpdate{ToolFailureSignalCount: 3})
insertSession(t, d, "f7", project)
updateSignals(t, d, "f7", SessionSignalUpdate{ToolFailureSignalCount: 7})
},
wantAsc: []string{"f0", "f3", "f7"},
},
{
name: "retries",
orderBy: "retries",
setup: func(t *testing.T, d *DB, project string) {
insertSession(t, d, "rt0", project)
insertSession(t, d, "rt2", project)
updateSignals(t, d, "rt2", SessionSignalUpdate{ToolRetryCount: 2})
insertSession(t, d, "rt6", project)
updateSignals(t, d, "rt6", SessionSignalUpdate{ToolRetryCount: 6})
},
wantAsc: []string{"rt0", "rt2", "rt6"},
},
{
name: "edit-churn",
orderBy: "edit-churn",
setup: func(t *testing.T, d *DB, project string) {
insertSession(t, d, "ec0", project)
insertSession(t, d, "ec4", project)
updateSignals(t, d, "ec4", SessionSignalUpdate{EditChurnCount: 4})
insertSession(t, d, "ec8", project)
updateSignals(t, d, "ec8", SessionSignalUpdate{EditChurnCount: 8})
},
wantAsc: []string{"ec0", "ec4", "ec8"},
},
{
name: "compactions",
orderBy: "compactions",
setup: func(t *testing.T, d *DB, project string) {
insertSession(t, d, "c0", project)
insertSession(t, d, "c1", project)
updateSignals(t, d, "c1", SessionSignalUpdate{CompactionCount: 1})
insertSession(t, d, "c5", project)
updateSignals(t, d, "c5", SessionSignalUpdate{CompactionCount: 5})
},
wantAsc: []string{"c0", "c1", "c5"},
},
{
name: "context-pressure",
orderBy: "context-pressure",
setup: func(t *testing.T, d *DB, project string) {
insertSession(t, d, "cp2", project)
updateSignals(t, d, "cp2", SessionSignalUpdate{ContextPressureMax: Ptr(0.2)})
insertSession(t, d, "cp5", project)
updateSignals(t, d, "cp5", SessionSignalUpdate{ContextPressureMax: Ptr(0.5)})
insertSession(t, d, "cp9", project)
updateSignals(t, d, "cp9", SessionSignalUpdate{ContextPressureMax: Ptr(0.9)})
},
wantAsc: []string{"cp2", "cp5", "cp9"},
},
{
name: "health",
orderBy: "health",
setup: func(t *testing.T, d *DB, project string) {
insertSession(t, d, "h20", project)
updateSignals(t, d, "h20", SessionSignalUpdate{HealthScore: Ptr(20)})
insertSession(t, d, "h60", project)
updateSignals(t, d, "h60", SessionSignalUpdate{HealthScore: Ptr(60)})
insertSession(t, d, "h95", project)
updateSignals(t, d, "h95", SessionSignalUpdate{HealthScore: Ptr(95)})
},
wantAsc: []string{"h20", "h60", "h95"},
},
{
name: "secrets",
orderBy: "secrets",
setup: func(t *testing.T, d *DB, project string) {
insertSession(t, d, "sec0", project)
insertSession(t, d, "sec2", project)
require.NoError(t, d.ReplaceSessionSecretFindings("sec2", nil, 2, "v1"))
insertSession(t, d, "sec5", project)
require.NoError(t, d.ReplaceSessionSecretFindings("sec5", nil, 5, "v1"))
},
wantAsc: []string{"sec0", "sec2", "sec5"},
},
{
name: "id",
orderBy: "id",
setup: func(t *testing.T, d *DB, project string) {
insertSession(t, d, "id-a", project)
insertSession(t, d, "id-b", project)
insertSession(t, d, "id-c", project)
},
wantAsc: []string{"id-a", "id-b", "id-c"},
},
}
d := testDB(t)
caseProjects := make([]string, len(cases))
for i, tc := range cases {
project := fmt.Sprintf("sort-case-%02d", i)
caseProjects[i] = project
tc.setup(t, d, project)
}
for i, tc := range cases {
project := caseProjects[i]
t.Run(tc.name, func(t *testing.T) {
gotAsc := listSortedIDs(t, d, filterWith(func(f *SessionFilter) {
f.Project = project
f.OrderBy = tc.orderBy
f.Descending = Ptr(false)
}))
assert.Equal(t, tc.wantAsc, gotAsc, "ascending order")
wantDesc := reversed(tc.wantAsc)
gotDesc := listSortedIDs(t, d, filterWith(func(f *SessionFilter) {
f.Project = project
f.OrderBy = tc.orderBy
f.Descending = Ptr(true)
}))
assert.Equal(t, wantDesc, gotDesc, "descending order")
})
}
}
func reversed(in []string) []string {
out := make([]string, len(in))
for i, v := range in {
out[len(in)-1-i] = v
}
return out
}
// TestListSessions_SortDefaultDirection verifies OrderBy with a nil Descending
// uses the sort key's canonical default: recent is descending (most recent
// first, the historical behavior), every other key is ascending.
func TestListSessions_SortDefaultDirection(t *testing.T) {
d := testDB(t)
insertSession(t, d, "a", "p", func(s *Session) {
s.EndedAt = Ptr("2024-01-01T00:00:00Z")
s.MessageCount = 1
})
insertSession(t, d, "b", "p", func(s *Session) {
s.EndedAt = Ptr("2024-02-01T00:00:00Z")
s.MessageCount = 9
})
// recent default: newest first.
assert.Equal(t, []string{"b", "a"},
listSortedIDs(t, d, filterWith(func(f *SessionFilter) { f.OrderBy = "recent" })))
// empty OrderBy behaves like recent.
assert.Equal(t, []string{"b", "a"},
listSortedIDs(t, d, filterWith(func(f *SessionFilter) {})))
// messages default: ascending (fewest first).
assert.Equal(t, []string{"a", "b"},
listSortedIDs(t, d, filterWith(func(f *SessionFilter) { f.OrderBy = "messages" })))
}
// TestListSessions_SortPaginationWalk paginates through a non-default sort and
// asserts the full ordered set is returned exactly once.
func TestListSessions_SortPaginationWalk(t *testing.T) {
d := testDB(t)
const n = 7
for i := 1; i <= n; i++ {
insertSession(t, d, fmt.Sprintf("p%02d", i), "p", func(s *Session) {
s.MessageCount = i * 3
})
}
for _, desc := range []bool{false, true} {
t.Run(fmt.Sprintf("desc=%v", desc), func(t *testing.T) {
var got []string
seen := map[string]bool{}
cursor := ""
for pages := 0; ; pages++ {
require.LessOrEqual(t, pages, n+1, "pagination did not terminate")
page, err := d.ListSessions(context.Background(), SessionFilter{
Limit: 2,
OrderBy: "messages",
Descending: Ptr(desc),
Cursor: cursor,
})
require.NoError(t, err, "ListSessions page")
for _, s := range page.Sessions {
require.False(t, seen[s.ID], "duplicate %s", s.ID)
seen[s.ID] = true
got = append(got, s.ID)
}
if page.NextCursor == "" {
break
}
cursor = page.NextCursor
}
want := make([]string, n)
for i := 1; i <= n; i++ {
want[i-1] = fmt.Sprintf("p%02d", i)
}
if desc {
want = reversed(want)
}
assert.Equal(t, want, got, "walked order")
})
}
}
// TestListSessions_SortNullsLast verifies nullable sorts (health) place NULL
// rows last in both directions and that keyset pagination crosses the NULL
// boundary without dropping or duplicating rows.
func TestListSessions_SortNullsLast(t *testing.T) {
d := testDB(t)
insertSession(t, d, "h10", "p")
updateSignals(t, d, "h10", SessionSignalUpdate{HealthScore: Ptr(10)})
insertSession(t, d, "h90", "p")
updateSignals(t, d, "h90", SessionSignalUpdate{HealthScore: Ptr(90)})
insertSession(t, d, "hnull-a", "p") // health_score NULL
insertSession(t, d, "hnull-b", "p") // health_score NULL
// Ascending: non-null ascending, NULLs last (id ascending among NULLs).
assert.Equal(t, []string{"h10", "h90", "hnull-a", "hnull-b"},
listSortedIDs(t, d, filterWith(func(f *SessionFilter) {
f.OrderBy = "health"
f.Descending = Ptr(false)
})))
// Descending: non-null descending, NULLs still last (id descending among NULLs).
assert.Equal(t, []string{"h90", "h10", "hnull-b", "hnull-a"},
listSortedIDs(t, d, filterWith(func(f *SessionFilter) {
f.OrderBy = "health"
f.Descending = Ptr(true)
})))
// Paginate one row at a time to exercise the sentinel keyset across the
// null boundary.
var got []string
cursor := ""
for {
page, err := d.ListSessions(context.Background(), SessionFilter{
Limit: 1, OrderBy: "health", Descending: Ptr(false), Cursor: cursor,
})
require.NoError(t, err)
for _, s := range page.Sessions {
got = append(got, s.ID)
}
if page.NextCursor == "" {
break
}
cursor = page.NextCursor
}
assert.Equal(t, []string{"h10", "h90", "hnull-a", "hnull-b"}, got, "paginated null walk")
}
// TestListSessions_CursorSortMismatch rejects a cursor reused under a different
// sort or direction, while accepting it under the same ordering.
func TestListSessions_CursorSortMismatch(t *testing.T) {
d := testDB(t)
for i := 1; i <= 4; i++ {
insertSession(t, d, fmt.Sprintf("x%d", i), "p", func(s *Session) {
s.MessageCount = i
})
}
page, err := d.ListSessions(context.Background(), SessionFilter{
Limit: 2, OrderBy: "messages", Descending: Ptr(false),
})
require.NoError(t, err)
require.NotEmpty(t, page.NextCursor, "expected a next cursor")
cursor := page.NextCursor
// Same sort + direction: accepted.
_, err = d.ListSessions(context.Background(), SessionFilter{
Limit: 2, OrderBy: "messages", Descending: Ptr(false), Cursor: cursor,
})
require.NoError(t, err, "same sort should accept cursor")
// Different sort key: rejected.
_, err = d.ListSessions(context.Background(), SessionFilter{
Limit: 2, OrderBy: "failures", Descending: Ptr(false), Cursor: cursor,
})
require.ErrorIs(t, err, ErrInvalidCursor, "cross-sort cursor")
// Same sort, flipped direction: rejected.
_, err = d.ListSessions(context.Background(), SessionFilter{
Limit: 2, OrderBy: "messages", Descending: Ptr(true), Cursor: cursor,
})
require.ErrorIs(t, err, ErrInvalidCursor, "flipped-direction cursor")
}
// TestListSessions_LegacyCursorRecent confirms a cursor minted in the old shape
// (only EndedAt/ID/Total, no Sort/Desc/Value) still paginates the recent order.
// This exercises the cur.Sort == "" backward-compatibility path directly, rather
// than a cursor produced by the current implementation.
func TestListSessions_LegacyCursorRecent(t *testing.T) {
d := testDB(t)
for i := 1; i <= 4; i++ {
insertSession(t, d, fmt.Sprintf("rc%d", i), "p", func(s *Session) {
s.EndedAt = Ptr(fmt.Sprintf("2024-0%d-01T00:00:00Z", i))
})
}
page1, err := d.ListSessions(context.Background(), SessionFilter{Limit: 2})
require.NoError(t, err)
require.Equal(t, []string{"rc4", "rc3"}, idsOf(page1.Sessions))
// Mint a legacy-shaped cursor by hand: no Sort/Desc/Value fields, just the
// activity timestamp + id + total a pre-sort build would have produced.
legacy := d.EncodeCursor(SessionCursor{
EndedAt: "2024-03-01T00:00:00Z",
ID: "rc3",
Total: page1.Total,
})
cur, err := d.DecodeCursor(legacy)
require.NoError(t, err)
require.Empty(t, cur.Sort, "legacy cursor must carry no sort key")
page2, err := d.ListSessions(context.Background(), SessionFilter{
Limit: 2, Cursor: legacy,
})
require.NoError(t, err)
assert.Equal(t, []string{"rc2", "rc1"}, idsOf(page2.Sessions))
}
func idsOf(sessions []Session) []string {
ids := make([]string, len(sessions))
for i, s := range sessions {
ids[i] = s.ID
}
return ids
}
// TestListSessions_SecretsSortVersionGated checks that the secrets sort matches
// the displayed (version-gated) count, and that gating flows through the keyset
// cursor on a paginated request.
func TestListSessions_SecretsSortVersionGated(t *testing.T) {
d := testDB(t)
insertSession(t, d, "sec-cur", "p")
require.NoError(t, d.ReplaceSessionSecretFindings("sec-cur", nil, 5, "v1"))
insertSession(t, d, "sec-stale", "p")
require.NoError(t, d.ReplaceSessionSecretFindings("sec-stale", nil, 9, "old"))
insertSession(t, d, "sec-none", "p")
// With active version v1: the stale count (9) gates to 0, so the
// current-version session (5) ranks first descending.
gated := listSortedIDs(t, d, filterWith(func(f *SessionFilter) {
f.OrderBy = "secrets"
f.Descending = Ptr(true)
f.SecretsRulesVersions = []string{"v1"}
}))
require.Len(t, gated, 3)
assert.Equal(t, "sec-cur", gated[0], "current-version session ranks first")
// Without active versions: raw counts, so the stale 9 ranks first.
raw := listSortedIDs(t, d, filterWith(func(f *SessionFilter) {
f.OrderBy = "secrets"
f.Descending = Ptr(true)
}))
assert.Equal(t, "sec-stale", raw[0], "raw counts rank stale 9 first")
// Paginated, gated: the cursor must carry the gated value so the
// current-version session still leads and pages do not duplicate.
var got []string
seen := map[string]bool{}
cursor := ""
for {
page, err := d.ListSessions(context.Background(), SessionFilter{
Limit: 1,
OrderBy: "secrets",
Descending: Ptr(true),
SecretsRulesVersions: []string{"v1"},
Cursor: cursor,
})
require.NoError(t, err)
for _, s := range page.Sessions {
require.False(t, seen[s.ID], "duplicate %s", s.ID)
seen[s.ID] = true
got = append(got, s.ID)
}
if page.NextCursor == "" {
break
}
cursor = page.NextCursor
}
require.Len(t, got, 3)
assert.Equal(t, "sec-cur", got[0], "paginated gated order leads with current-version session")
}
func TestValidSortKeyAndDefaultDirection(t *testing.T) {
assert.True(t, ValidSortKey(""), "empty key is valid (default)")
assert.True(t, ValidSortKey("failures"))
assert.False(t, ValidSortKey("bogus"))
assert.True(t, SortDefaultDescending("recent"), "recent defaults descending")
assert.True(t, SortDefaultDescending(""), "empty key defaults to recent (descending)")
assert.False(t, SortDefaultDescending("messages"), "non-recent keys default ascending")
}