Files
wehub-resource-sync f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
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
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

489 lines
15 KiB
Go

//go:build pgtest
package postgres
import (
"context"
"slices"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.kenn.io/agentsview/internal/db"
)
func TestGetActiveProjectLabelsIncludesRelationshipSessions(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, relationship_type)
VALUES
('active-project-subagent', 'm', 'child-only', 'claude', 'subagent'),
('active-project-fork', 'm', 'fork-only', 'claude', 'fork'),
('active-project-deleted', 'm', 'deleted-only', 'claude', 'root');
UPDATE sessions
SET deleted_at = NOW()
WHERE id = 'active-project-deleted'
`)
require.NoError(t, err)
labels, err := store.GetActiveProjectLabels(context.Background())
require.NoError(t, err)
assert.Equal(t, []string{
"child-only", "fork-only", "test-project",
}, labels)
}
func TestListSessionsDateFilterIncludesOverlappingSessions(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, started_at, ended_at,
message_count, user_message_count)
VALUES
('date-overlap-before', 'm', 'date-overlap', 'claude',
'2024-06-15T08:00:00Z'::timestamptz,
'2024-06-15T09:00:00Z'::timestamptz, 2, 1),
('date-overlap-spanning', 'm', 'date-overlap', 'claude',
'2024-06-15T23:00:00Z'::timestamptz,
'2024-06-16T10:00:00Z'::timestamptz, 2, 1),
('date-overlap-open', 'm', 'date-overlap', 'claude',
'2024-06-15T22:00:00Z'::timestamptz,
NULL, 2, 1),
('date-overlap-after', 'm', 'date-overlap', 'claude',
'2024-06-17T08:00:00Z'::timestamptz,
'2024-06-17T09:00:00Z'::timestamptz, 2, 1),
('date-overlap-child', 'm', 'date-overlap', 'claude',
'2024-06-17T08:00:00Z'::timestamptz,
'2024-06-17T09:00:00Z'::timestamptz, 1, 1);
UPDATE sessions
SET parent_session_id = 'date-overlap-spanning',
relationship_type = 'subagent'
WHERE id = 'date-overlap-child';
INSERT INTO messages
(session_id, ordinal, role, content, timestamp, content_length)
VALUES
('date-overlap-open', 1, 'user', 'x',
'2024-06-16T11:00:00Z'::timestamptz, 1)
`)
require.NoError(t, err, "seeding date-overlap sessions")
page, err := store.ListSessions(context.Background(), db.SessionFilter{
Project: "date-overlap",
Date: "2024-06-16",
Limit: 50,
})
require.NoError(t, err, "ListSessions")
ids := make([]string, len(page.Sessions))
for i, session := range page.Sessions {
ids[i] = session.ID
}
slices.Sort(ids)
assert.Equal(t, []string{
"date-overlap-open",
"date-overlap-spanning",
}, ids)
index, err := store.GetSidebarSessionIndex(context.Background(), db.SessionFilter{
Project: "date-overlap",
Date: "2024-06-16",
})
require.NoError(t, err, "GetSidebarSessionIndex")
ids = ids[:0]
for _, session := range index.Sessions {
ids = append(ids, session.ID)
}
slices.Sort(ids)
assert.Equal(t, []string{
"date-overlap-child",
"date-overlap-open",
"date-overlap-spanning",
}, ids)
}
// TestListSessions_HasSecret verifies that the HasSecret filter
// returns only sessions where secret_leak_count > 0.
func TestListSessions_HasSecret(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
pg := store.DB()
// Seed a session with leaks and one without.
_, err = pg.Exec(`
INSERT INTO sessions
(id, machine, project, agent, first_message,
started_at, ended_at, message_count,
user_message_count, secret_leak_count)
VALUES
('has-secret-leaky', 'test-machine', 'test-project',
'claude-code', 'secret session',
'2026-03-12T09:00:00Z'::timestamptz,
'2026-03-12T09:30:00Z'::timestamptz,
2, 1, 3),
('has-secret-clean', 'test-machine', 'test-project',
'claude-code', 'clean session',
'2026-03-12T08:00:00Z'::timestamptz,
'2026-03-12T08:30:00Z'::timestamptz,
2, 1, 0)
`)
require.NoError(t, err, "inserting test sessions")
ctx := context.Background()
page, err := store.ListSessions(ctx, db.SessionFilter{
HasSecret: true,
Limit: 50,
})
require.NoError(t, err, "ListSessions")
// Only the leaky session should appear.
for _, s := range page.Sessions {
assert.NotEqual(t, "has-secret-clean", s.ID,
"clean session (secret_leak_count=0) included in HasSecret results")
}
var found *db.Session
for i := range page.Sessions {
if page.Sessions[i].ID == "has-secret-leaky" {
found = &page.Sessions[i]
break
}
}
require.NotNil(t, found, "leaky session not found in HasSecret results")
assert.Equal(t, 3, found.SecretLeakCount)
_, err = pg.Exec(`
UPDATE sessions
SET secrets_rules_version = 'v-current'
WHERE id = 'has-secret-leaky';
INSERT INTO sessions
(id, machine, project, agent, first_message,
started_at, ended_at, message_count,
user_message_count, secret_leak_count, secrets_rules_version)
VALUES
('has-secret-stale', 'test-machine', 'test-project',
'claude-code', 'stale secret session',
'2026-03-12T07:00:00Z'::timestamptz,
'2026-03-12T07:30:00Z'::timestamptz,
2, 1, 2, 'old-rules')
`)
require.NoError(t, err, "seeding stale secret session")
current, err := store.ListSessions(ctx, db.SessionFilter{
HasSecret: true,
SecretsRulesVersions: []string{"v-current"},
Limit: 50,
})
require.NoError(t, err, "ListSessions current rules")
for _, s := range current.Sessions {
require.NotEqual(t, "has-secret-stale", s.ID,
"stale secret session included in versioned HasSecret results")
}
}
// TestListSessions_Sort verifies a non-default sort and its keyset cursor render
// correctly on PostgreSQL (numbered placeholders, ::bigint cast). Scoped to a
// unique project so the schema's seed session does not interfere.
func TestListSessions_Sort(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
('sort-a', 'm', 'sort-test', 'claude', 'a',
'2026-03-01T00:00:00Z'::timestamptz, '2026-03-01T00:10:00Z'::timestamptz, 3, 1),
('sort-b', 'm', 'sort-test', 'claude', 'b',
'2026-03-02T00:00:00Z'::timestamptz, '2026-03-02T00:10:00Z'::timestamptz, 9, 1),
('sort-c', 'm', 'sort-test', 'claude', 'c',
'2026-03-03T00:00:00Z'::timestamptz, '2026-03-03T00:10:00Z'::timestamptz, 6, 1)
`)
require.NoError(t, err, "seeding sort sessions")
ctx := context.Background()
ids := func(sessions []db.Session) []string {
out := make([]string, len(sessions))
for i, s := range sessions {
out[i] = s.ID
}
return out
}
// messages ascending (the default for non-recent sorts).
asc, err := store.ListSessions(ctx, db.SessionFilter{
Project: "sort-test", OrderBy: "messages", Limit: 10,
})
require.NoError(t, err, "ListSessions sort asc")
require.Equal(t, []string{"sort-a", "sort-c", "sort-b"}, ids(asc.Sessions))
// Paginated walk one row at a time exercises the ::bigint keyset cursor.
var walked []string
cursor := ""
for {
page, err := store.ListSessions(ctx, db.SessionFilter{
Project: "sort-test", OrderBy: "messages", Limit: 1, Cursor: cursor,
})
require.NoError(t, err, "ListSessions sort page")
walked = append(walked, ids(page.Sessions)...)
if page.NextCursor == "" {
break
}
cursor = page.NextCursor
}
require.Equal(t, []string{"sort-a", "sort-c", "sort-b"}, walked)
}
// TestListSessions_SortMultiKey verifies a mixed-direction multi-key sort on
// PostgreSQL, where the lexicographic OR-expansion places ::bigint and
// ::timestamptz casts on the equality and comparison clauses. The paginated walk
// must match the full listing.
func TestListSessions_SortMultiKey(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, message_count, user_message_count)
VALUES
('mk-a', 'm', 'mk', 'claude', 'a', '2024-03-01T00:00:00Z'::timestamptz, 1, 2),
('mk-b', 'm', 'mk', 'claude', 'b', '2024-01-01T00:00:00Z'::timestamptz, 1, 2),
('mk-c', 'm', 'mk', 'claude', 'c', '2024-02-01T00:00:00Z'::timestamptz, 2, 2),
('mk-d', 'm', 'mk', 'claude', 'd', '2024-05-01T00:00:00Z'::timestamptz, 2, 2),
('mk-e', 'm', 'mk', 'claude', 'e', '2024-03-01T00:00:00Z'::timestamptz, 1, 2)
`)
require.NoError(t, err, "seeding multi-key sessions")
ctx := context.Background()
ids := func(sessions []db.Session) []string {
out := make([]string, len(sessions))
for i, s := range sessions {
out[i] = s.ID
}
return out
}
asc, desc := false, true
sortKeys := []db.SortKey{
{Key: "messages", Descending: &asc},
{Key: "started", Descending: &desc},
}
full, err := store.ListSessions(ctx, db.SessionFilter{
Project: "mk", Sort: sortKeys, Limit: 100,
})
require.NoError(t, err, "ListSessions multi-key")
want := ids(full.Sessions)
require.Equal(t, []string{"mk-e", "mk-a", "mk-b", "mk-d", "mk-c"}, want)
var walked []string
seen := map[string]bool{}
cursor := ""
for {
page, err := store.ListSessions(ctx, db.SessionFilter{
Project: "mk", Sort: sortKeys, Limit: 1, Cursor: cursor,
})
require.NoError(t, err, "ListSessions multi-key page")
for _, s := range page.Sessions {
require.False(t, seen[s.ID], "duplicate %s", s.ID)
seen[s.ID] = true
walked = append(walked, s.ID)
}
if page.NextCursor == "" {
break
}
cursor = page.NextCursor
}
require.Equal(t, want, walked, "paginated walk matches full listing")
}
// TestListSessions_SortSecretsVersioned verifies the version-gated secrets sort
// renders on PostgreSQL, where the gating CASE places numbered placeholders
// inside both ORDER BY and the keyset cursor predicate.
func TestListSessions_SortSecretsVersioned(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,
secret_leak_count, secrets_rules_version)
VALUES
('sec-cur', 'm', 'sec-test', 'claude', 'a',
'2026-03-01T00:00:00Z'::timestamptz, '2026-03-01T00:10:00Z'::timestamptz, 2, 1, 5, 'v1'),
('sec-stale', 'm', 'sec-test', 'claude', 'b',
'2026-03-02T00:00:00Z'::timestamptz, '2026-03-02T00:10:00Z'::timestamptz, 2, 1, 9, 'old'),
('sec-none', 'm', 'sec-test', 'claude', 'c',
'2026-03-03T00:00:00Z'::timestamptz, '2026-03-03T00:10:00Z'::timestamptz, 2, 1, 0, '')
`)
require.NoError(t, err, "seeding secret sessions")
ctx := context.Background()
ids := func(sessions []db.Session) []string {
out := make([]string, len(sessions))
for i, s := range sessions {
out[i] = s.ID
}
return out
}
// With v1 active, the stale 9 gates to 0, so the current-version 5 leads.
gated, err := store.ListSessions(ctx, db.SessionFilter{
Project: "sec-test",
OrderBy: "secrets",
Descending: new(true),
SecretsRulesVersions: []string{"v1"},
Limit: 10,
})
require.NoError(t, err, "ListSessions secrets gated")
require.Equal(t, "sec-cur", ids(gated.Sessions)[0])
// Paginate one at a time so the gating CASE also renders in the cursor
// predicate; the current-version session must still lead with no dupes.
var walked []string
seen := map[string]bool{}
cursor := ""
for {
page, err := store.ListSessions(ctx, db.SessionFilter{
Project: "sec-test",
OrderBy: "secrets",
Descending: new(true),
SecretsRulesVersions: []string{"v1"},
Limit: 1,
Cursor: cursor,
})
require.NoError(t, err, "ListSessions secrets page")
for _, s := range page.Sessions {
require.False(t, seen[s.ID], "duplicate %s", s.ID)
seen[s.ID] = true
walked = append(walked, s.ID)
}
if page.NextCursor == "" {
break
}
cursor = page.NextCursor
}
require.Len(t, walked, 3)
require.Equal(t, "sec-cur", walked[0])
}
// TestListSessions_SortNullsLast verifies a nullable sort (health) places NULL
// rows last on PostgreSQL and paginates across the sentinel boundary.
func TestListSessions_SortNullsLast(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, health_score)
VALUES
('h20', 'm', 'null-test', 'claude', 'a',
'2026-03-01T00:00:00Z'::timestamptz, '2026-03-01T00:10:00Z'::timestamptz, 2, 1, 20),
('h80', 'm', 'null-test', 'claude', 'b',
'2026-03-02T00:00:00Z'::timestamptz, '2026-03-02T00:10:00Z'::timestamptz, 2, 1, 80),
('hnull', 'm', 'null-test', 'claude', 'c',
'2026-03-03T00:00:00Z'::timestamptz, '2026-03-03T00:10:00Z'::timestamptz, 2, 1, NULL)
`)
require.NoError(t, err, "seeding health sessions")
ctx := context.Background()
var walked []string
cursor := ""
for {
page, err := store.ListSessions(ctx, db.SessionFilter{
Project: "null-test", OrderBy: "health", Limit: 1, Cursor: cursor,
})
require.NoError(t, err, "ListSessions health page")
for _, s := range page.Sessions {
walked = append(walked, s.ID)
}
if page.NextCursor == "" {
break
}
cursor = page.NextCursor
}
// Ascending with NULLs last, paginated across the sentinel boundary.
require.Equal(t, []string{"h20", "h80", "hnull"}, walked)
}
func TestFindSessionIDsByPartialLiteralCaseSensitivePG(t *testing.T) {
pgURL := testPGURL(t)
const schema = "agentsview_partial_lookup_test"
pg, err := Open(pgURL, schema, true)
require.NoError(t, err, "Open")
defer pg.Close()
ctx := context.Background()
_, err = pg.Exec(`DROP SCHEMA IF EXISTS ` + schema + ` CASCADE`)
require.NoError(t, err, "drop schema")
require.NoError(t, EnsureSchema(ctx, pg, schema), "EnsureSchema")
local := testDB(t)
for _, id := range []string{"abc_def", "abcXdef", "abc%def", "ABCdef"} {
require.NoError(t, local.UpsertSession(db.Session{
ID: id, Project: "proj", Machine: "local",
Agent: "claude", MessageCount: 1,
}), "upsert %q", id)
}
syncer := &Sync{
pg: pg,
local: local,
machine: "test-machine",
schema: schema,
schemaDone: true,
}
_, err = syncer.Push(ctx, true, nil)
require.NoError(t, err, "Push")
store := &Store{pg: pg}
got, err := store.FindSessionIDsByPartial(ctx, "c_d", 10)
require.NoError(t, err, "underscore lookup")
assert.Equal(t, []string{"abc_def"}, got)
got, err = store.FindSessionIDsByPartial(ctx, "c%d", 10)
require.NoError(t, err, "percent lookup")
assert.Equal(t, []string{"abc%def"}, got)
got, err = store.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")
}