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

909 lines
29 KiB
Go

//go:build pgtest
package postgres
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.kenn.io/agentsview/internal/db"
)
func TestStoreSearchILIKE(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: 10,
})
require.NoError(t, err, "Search")
assert.NotEmpty(t, page.Results, "expected at least 1 search result")
for _, r := range page.Results {
assert.NotEmpty(t, r.Agent, "Agent field is empty")
assert.NotEmpty(t, r.SessionEndedAt, "SessionEndedAt is empty")
}
}
func TestPGSearchDeduplication(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
// store-test-001 has 2 messages; searching "hello" only matches ordinal 0.
// With session grouping, should return exactly 1 result.
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: 10,
})
require.NoError(t, err, "Search")
assert.Len(t, page.Results, 1, "deduplicated to session")
}
func TestPGSearchRecencySort(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
// Open a write connection to insert additional test data.
pg, err := Open(pgURL, testSchema, false)
require.NoError(t, err, "Open")
defer pg.Close()
// Insert a newer session that also matches "hello".
_, err = pg.Exec(`
INSERT INTO sessions
(id, machine, project, agent, first_message,
started_at, ended_at, message_count,
user_message_count)
VALUES
('recency-test-002', 'test-machine',
'test-project', 'codex',
'hello again',
'2026-04-01T10:00:00Z'::timestamptz,
'2026-04-01T10:30:00Z'::timestamptz,
1, 1)
ON CONFLICT (id) DO NOTHING
`)
require.NoError(t, err, "inserting newer session")
_, err = pg.Exec(`
INSERT INTO messages
(session_id, ordinal, role, content,
timestamp, content_length)
VALUES
('recency-test-002', 0, 'user',
'hello again newer',
'2026-04-01T10:00:00Z'::timestamptz, 17)
ON CONFLICT DO NOTHING
`)
require.NoError(t, err, "inserting newer message")
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: 10,
Sort: "recency",
})
require.NoError(t, err, "Search")
require.GreaterOrEqual(t, len(page.Results), 2)
// recency-test-002 has ended_at 2026-04-01, store-test-001 has 2026-03-12
assert.Equal(t, "recency-test-002", page.Results[0].SessionID)
}
func TestPGSearchRelevanceSort(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
pg, err := Open(pgURL, testSchema, false)
require.NoError(t, err, "Open")
defer pg.Close()
// Insert two sessions:
// - relevance-early: match appears at position 1 (start of content)
// - relevance-late: match appears after 50 chars of prefix
_, err = pg.Exec(`
INSERT INTO sessions
(id, machine, project, agent, first_message,
started_at, ended_at, message_count,
user_message_count)
VALUES
('relevance-early', 'test-machine',
'test-project', 'claude',
'needle at start',
'2025-01-01T10:00:00Z'::timestamptz,
'2025-01-01T10:30:00Z'::timestamptz,
1, 1),
('relevance-late', 'test-machine',
'test-project', 'claude',
'lots of text before needle',
'2025-01-02T10:00:00Z'::timestamptz,
'2025-01-02T10:30:00Z'::timestamptz,
1, 1)
ON CONFLICT (id) DO NOTHING
`)
require.NoError(t, err, "inserting sessions")
_, err = pg.Exec(`
INSERT INTO messages
(session_id, ordinal, role, content, timestamp, content_length)
VALUES
('relevance-early', 0, 'user',
'needleunique at the very beginning of content',
'2025-01-01T10:00:00Z'::timestamptz, 45),
('relevance-late', 0, 'user',
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaneedleunique at the end',
'2025-01-02T10:00:00Z'::timestamptz, 73)
ON CONFLICT DO NOTHING
`)
require.NoError(t, err, "inserting messages")
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: "needleunique",
Limit: 10,
Sort: "relevance",
})
require.NoError(t, err, "Search")
require.GreaterOrEqual(t, len(page.Results), 2)
// relevance-early has match at position 1; relevance-late has it after 50 chars
// relevance sort = match_pos ASC, so relevance-early must come first
assert.Equal(t, "relevance-early", page.Results[0].SessionID)
}
func TestPGSearchNullTimestampSorting(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
pg, err := Open(pgURL, testSchema, false)
require.NoError(t, err, "Open")
defer pg.Close()
// Insert a session with NULL ended_at and started_at.
_, err = pg.Exec(`
INSERT INTO sessions
(id, machine, project, agent, first_message,
message_count, user_message_count)
VALUES
('null-ts-001', 'test-machine',
'test-project', 'claude',
'nullsort keyword here',
1, 1)
ON CONFLICT (id) DO NOTHING
`)
require.NoError(t, err, "inserting null-ts session")
_, err = pg.Exec(`
INSERT INTO messages
(session_id, ordinal, role, content,
timestamp, content_length)
VALUES
('null-ts-001', 0, 'user',
'nullsort keyword here',
'2026-01-01T00:00:00Z'::timestamptz, 21)
ON CONFLICT DO NOTHING
`)
require.NoError(t, err, "inserting null-ts message")
// Insert a session with real timestamps.
_, err = pg.Exec(`
INSERT INTO sessions
(id, machine, project, agent, first_message,
started_at, ended_at,
message_count, user_message_count)
VALUES
('null-ts-002', 'test-machine',
'test-project', 'claude',
'nullsort keyword here',
'2026-03-10T10:00:00Z'::timestamptz,
'2026-03-10T10:30:00Z'::timestamptz,
1, 1)
ON CONFLICT (id) DO NOTHING
`)
require.NoError(t, err, "inserting real-ts session")
_, err = pg.Exec(`
INSERT INTO messages
(session_id, ordinal, role, content,
timestamp, content_length)
VALUES
('null-ts-002', 0, 'user',
'nullsort keyword here',
'2026-03-10T10:00:00Z'::timestamptz, 21)
ON CONFLICT DO NOTHING
`)
require.NoError(t, err, "inserting real-ts message")
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
ctx := context.Background()
// Test recency sort: NULL-timestamp session must not be first.
page, err := store.Search(ctx, db.SearchFilter{
Query: "nullsort",
Limit: 10,
Sort: "recency",
})
require.NoError(t, err, "recency search")
require.GreaterOrEqual(t, len(page.Results), 2)
assert.NotEqual(t, "null-ts-001", page.Results[0].SessionID,
"recency: NULL-timestamp session appeared first, want last")
// Test relevance sort: both have same match_pos, so
// session_ended_at DESC is the tie-breaker — NULL must not win.
page, err = store.Search(ctx, db.SearchFilter{
Query: "nullsort",
Limit: 10,
Sort: "relevance",
})
require.NoError(t, err, "relevance search")
require.GreaterOrEqual(t, len(page.Results), 2)
assert.NotEqual(t, "null-ts-001", page.Results[0].SessionID,
"relevance: NULL-timestamp session appeared first, want last")
}
func TestPGSearchSystemMessageExcluded(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
pg, err := Open(pgURL, testSchema, false)
require.NoError(t, err, "Open")
defer pg.Close()
// Insert a session whose only matching message is a system message.
// first_message intentionally does NOT contain the search term so the
// name branch does not accidentally surface this session.
_, err = pg.Exec(`
INSERT INTO sessions
(id, machine, project, agent, first_message,
started_at, ended_at, message_count,
user_message_count)
VALUES
('sysonly-session', 'test-machine',
'test-project', 'claude',
'hello world',
'2026-03-01T10:00:00Z'::timestamptz,
'2026-03-01T10:30:00Z'::timestamptz,
1, 0)
ON CONFLICT (id) DO NOTHING
`)
require.NoError(t, err, "inserting session")
_, err = pg.Exec(`
INSERT INTO messages
(session_id, ordinal, role, content,
timestamp, content_length, is_system)
VALUES
('sysonly-session', 0, 'user',
'sysonly unique term',
'2026-03-01T10:00:00Z'::timestamptz, 19, TRUE)
ON CONFLICT DO NOTHING
`)
require.NoError(t, err, "inserting system message")
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: "sysonly unique",
Limit: 10,
})
require.NoError(t, err, "Search")
assert.Empty(t, page.Results,
"system-only session should not appear in search results")
}
// TestPGSearchNameBranchExcludesSystemOnlySessions verifies that a
// session whose display_name or first_message matches the search query
// does not appear in global search results when all its messages are
// system messages (the EXISTS guard in the name branch).
func TestPGSearchNameBranchExcludesSystemOnlySessions(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
pg, err := Open(pgURL, testSchema, false)
require.NoError(t, err, "Open")
defer pg.Close()
// Session with only display_name matching, system messages only.
_, err = pg.Exec(`
INSERT INTO sessions
(id, machine, project, agent, first_message,
display_name, started_at, ended_at,
message_count, user_message_count)
VALUES
('name-sysonly-dn', 'test-machine',
'test-project', 'claude',
'no match here',
'pgdnguardterm display',
'2026-03-10T10:00:00Z'::timestamptz,
'2026-03-10T10:30:00Z'::timestamptz,
1, 0)
ON CONFLICT (id) DO NOTHING
`)
require.NoError(t, err, "inserting dn session")
_, err = pg.Exec(`
INSERT INTO messages
(session_id, ordinal, role, content,
timestamp, content_length, is_system)
VALUES
('name-sysonly-dn', 0, 'user',
'irrelevant content',
'2026-03-10T10:00:00Z'::timestamptz, 19, TRUE)
ON CONFLICT DO NOTHING
`)
require.NoError(t, err, "inserting dn system message")
// Session with only first_message matching, system messages only.
_, err = pg.Exec(`
INSERT INTO sessions
(id, machine, project, agent,
first_message, started_at, ended_at,
message_count, user_message_count)
VALUES
('name-sysonly-fm', 'test-machine',
'test-project', 'claude',
'pgfmguardterm first msg',
'2026-03-11T10:00:00Z'::timestamptz,
'2026-03-11T10:30:00Z'::timestamptz,
1, 0)
ON CONFLICT (id) DO NOTHING
`)
require.NoError(t, err, "inserting fm session")
_, err = pg.Exec(`
INSERT INTO messages
(session_id, ordinal, role, content,
timestamp, content_length, is_system)
VALUES
('name-sysonly-fm', 0, 'user',
'irrelevant content',
'2026-03-11T10:00:00Z'::timestamptz, 19, TRUE)
ON CONFLICT DO NOTHING
`)
require.NoError(t, err, "inserting fm system message")
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
ctx := context.Background()
t.Run("display_name path", func(t *testing.T) {
page, err := store.Search(ctx, db.SearchFilter{
Query: "pgdnguardterm",
Limit: 10,
})
require.NoError(t, err, "Search")
assert.Empty(t, page.Results,
"system-only session via display_name should not appear")
})
t.Run("first_message path", func(t *testing.T) {
page, err := store.Search(ctx, db.SearchFilter{
Query: "pgfmguardterm",
Limit: 10,
})
require.NoError(t, err, "Search")
assert.Empty(t, page.Results,
"system-only session via first_message should not appear")
})
}
// TestPGSearchSessionExcludesSystemMessages verifies that SearchSession
// (the in-session Cmd+F find-bar) excludes system messages since the
// frontend hides them and matching would produce phantom highlights.
func TestPGSearchSessionExcludesSystemMessages(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
pg, err := Open(pgURL, testSchema, false)
require.NoError(t, err, "Open")
defer pg.Close()
// Insert a session with one regular and one system message, both
// containing the search term "syssearch".
_, err = pg.Exec(`
INSERT INTO sessions
(id, machine, project, agent, first_message,
started_at, message_count, user_message_count)
VALUES
('sess-syssearch', 'test-machine',
'test-project', 'claude',
'syssearch regular',
NOW(), 2, 1)
ON CONFLICT (id) DO NOTHING
`)
require.NoError(t, err, "inserting session")
_, err = pg.Exec(`
INSERT INTO messages
(session_id, ordinal, role, content,
timestamp, content_length, is_system)
VALUES
('sess-syssearch', 0, 'user',
'syssearch regular content',
NOW(), 25, FALSE),
('sess-syssearch', 1, 'assistant',
'syssearch system-only content',
NOW(), 29, TRUE)
ON CONFLICT DO NOTHING
`)
require.NoError(t, err, "inserting messages")
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
ctx := context.Background()
ordinals, err := store.SearchSession(ctx, "sess-syssearch", "syssearch")
require.NoError(t, err, "SearchSession")
require.Len(t, ordinals, 1,
"session search excludes system messages: %v", ordinals)
assert.Equal(t, 0, ordinals[0])
}
func TestPGSearchSessionNameMatch(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
pg, err := Open(pgURL, testSchema, false)
require.NoError(t, err, "Open")
defer pg.Close()
// Insert session with display_name containing unique search term,
// no messages match.
_, err = pg.Exec(`
INSERT INTO sessions
(id, machine, project, agent, first_message, display_name,
started_at, message_count, user_message_count)
VALUES
('name-match-001', 'test-machine', 'test-project', 'claude',
'first msg text', 'uniquedisplayterm session',
'2026-03-15T10:00:00Z'::timestamptz, 1, 1)
ON CONFLICT (id) DO NOTHING`)
require.NoError(t, err, "insert session")
_, err = pg.Exec(`
INSERT INTO messages
(session_id, ordinal, role, content, timestamp, content_length)
VALUES
('name-match-001', 0, 'user', 'no match here',
'2026-03-15T10:00:00Z'::timestamptz, 13)
ON CONFLICT DO NOTHING`)
require.NoError(t, err, "insert message")
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
page, err := store.Search(context.Background(), db.SearchFilter{
Query: "uniquedisplayterm", Limit: 10,
})
require.NoError(t, err, "Search")
require.Len(t, page.Results, 1)
r := page.Results[0]
assert.Equal(t, "name-match-001", r.SessionID)
assert.Equal(t, -1, r.Ordinal, "name-only match")
assert.NotEmpty(t, r.Name, "Name field is empty")
}
func TestPGSearchRecencyNameOnlyBeatsOlderContent(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
pg, err := Open(pgURL, testSchema, false)
require.NoError(t, err, "Open")
defer pg.Close()
// older-content-001: message matches "recencytestterm", older timestamp
// newer-name-001: display_name matches "recencytestterm", newer timestamp
_, err = pg.Exec(`
INSERT INTO sessions
(id, machine, project, agent, first_message,
started_at, message_count, user_message_count)
VALUES
('older-content-recency', 'test-machine', 'test-project', 'claude',
'first msg', '2026-01-01T10:00:00Z'::timestamptz, 1, 1),
('newer-name-recency', 'test-machine', 'test-project', 'claude',
'first msg', '2026-01-02T10:00:00Z'::timestamptz, 1, 1)
ON CONFLICT (id) DO NOTHING`)
require.NoError(t, err, "insert sessions")
_, err = pg.Exec(`
UPDATE sessions SET display_name = 'recencytestterm session'
WHERE id = 'newer-name-recency'`)
require.NoError(t, err, "set display_name")
_, err = pg.Exec(`
INSERT INTO messages
(session_id, ordinal, role, content, timestamp, content_length)
VALUES
('older-content-recency', 0, 'user', 'recencytestterm content',
'2026-01-01T10:00:00Z'::timestamptz, 22),
('newer-name-recency', 0, 'user', 'no match here',
'2026-01-02T10:00:00Z'::timestamptz, 13)
ON CONFLICT DO NOTHING`)
require.NoError(t, err, "insert messages")
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
page, err := store.Search(context.Background(), db.SearchFilter{
Query: "recencytestterm", Limit: 10, Sort: "recency",
})
require.NoError(t, err, "Search")
require.GreaterOrEqual(t, len(page.Results), 2)
// Recency mode: newer session (name-only) must appear before older content match.
assert.Equal(t, "newer-name-recency", page.Results[0].SessionID,
"name-only but newer should win")
}
// TestPGSearchSnippetMatchingField verifies that when a session has a
// display_name set but the search term only matches first_message, the
// snippet returned is the first_message (the matching field), not the
// display_name.
func TestPGSearchSnippetMatchingField(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
pg, err := Open(pgURL, testSchema, false)
require.NoError(t, err, "Open")
defer pg.Close()
// Session: display_name is set to something unrelated; only first_message
// contains the search term.
_, err = pg.Exec(`
INSERT INTO sessions
(id, machine, project, agent, first_message, display_name,
started_at, message_count, user_message_count)
VALUES
('snippet-field-001', 'test-machine', 'test-project', 'claude',
'snippetfieldterm in first message', 'unrelated display name',
'2026-03-16T10:00:00Z'::timestamptz, 1, 1)
ON CONFLICT (id) DO NOTHING`)
require.NoError(t, err, "insert session")
// Message that does NOT contain the search term.
_, err = pg.Exec(`
INSERT INTO messages
(session_id, ordinal, role, content, timestamp, content_length)
VALUES
('snippet-field-001', 0, 'user', 'no match here',
'2026-03-16T10:00:00Z'::timestamptz, 13)
ON CONFLICT DO NOTHING`)
require.NoError(t, err, "insert message")
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
page, err := store.Search(context.Background(), db.SearchFilter{
Query: "snippetfieldterm", Limit: 10,
})
require.NoError(t, err, "Search")
require.Len(t, page.Results, 1)
r := page.Results[0]
assert.Equal(t, "snippet-field-001", r.SessionID)
assert.Equal(t, -1, r.Ordinal, "name-only match")
// Snippet must be first_message (the matching field), not display_name.
assert.Equal(t, "snippetfieldterm in first message", r.Snippet)
}
// TestGetMessagesIsSystemField verifies that GetMessages and GetAllMessages
// correctly populate db.Message.IsSystem from the is_system column.
func TestGetMessagesIsSystemField(t *testing.T) {
pgURL := testPGURL(t)
const schema = "agentsview_is_system_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")
_, err = pg.Exec(`
INSERT INTO sessions
(id, machine, project, agent, first_message,
started_at, message_count, user_message_count)
VALUES
('is-system-001', 'test-machine', 'test-project', 'claude',
'hello', '2026-03-16T10:00:00Z'::timestamptz, 2, 1)
ON CONFLICT (id) DO NOTHING`)
require.NoError(t, err, "insert session")
_, err = pg.Exec(`
INSERT INTO messages
(session_id, ordinal, role, content, timestamp, content_length, is_system)
VALUES
('is-system-001', 0, 'user', 'normal message',
'2026-03-16T10:00:00Z'::timestamptz, 14, FALSE),
('is-system-001', 1, 'user', 'system message',
'2026-03-16T10:00:01Z'::timestamptz, 14, TRUE)
ON CONFLICT DO NOTHING`)
require.NoError(t, err, "insert messages")
store, err := NewStore(pgURL, schema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
// GetMessages
msgs, err := store.GetMessages(ctx, "is-system-001", 0, 10, true)
require.NoError(t, err, "GetMessages")
require.Len(t, msgs, 2)
assert.False(t, msgs[0].IsSystem)
assert.True(t, msgs[1].IsSystem)
// GetAllMessages
all, err := store.GetAllMessages(ctx, "is-system-001")
require.NoError(t, err, "GetAllMessages")
require.Len(t, all, 2)
assert.False(t, all[0].IsSystem)
assert.True(t, all[1].IsSystem)
}
// TestGetMessagesToolCallFilePathAndCallIndex verifies the PG message
// hydrator populates db.ToolCall.FilePath and CallIndex, mirroring the SQLite
// round-trip coverage (db.TestResolveToolCallsDerivesPositionalCallIndex) so
// GetMessages/GetAllMessages consumers see these fields at parity.
func TestGetMessagesToolCallFilePathAndCallIndex(t *testing.T) {
pgURL := testPGURL(t)
const schema = "agentsview_toolcall_fields_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")
_, err = pg.Exec(`
INSERT INTO sessions
(id, machine, project, agent, first_message,
started_at, message_count, user_message_count)
VALUES
('tc-fields-001', 'test-machine', 'test-project', 'claude',
'tools', '2026-03-16T10:00:00Z'::timestamptz, 1, 0)`)
require.NoError(t, err, "insert session")
_, err = pg.Exec(`
INSERT INTO messages
(session_id, ordinal, role, content,
timestamp, content_length, has_tool_use)
VALUES
('tc-fields-001', 0, 'assistant', 'tools',
'2026-03-16T10:00:00Z'::timestamptz, 5, TRUE)`)
require.NoError(t, err, "insert message")
// Three tool calls on one message, call_index 0/1/2, distinct file_path.
_, err = pg.Exec(`
INSERT INTO tool_calls
(session_id, tool_name, category, call_index,
message_ordinal, file_path)
VALUES
('tc-fields-001', 'Read', 'Read', 0, 0, 'a.go'),
('tc-fields-001', 'Edit', 'Edit', 1, 0, 'b.go'),
('tc-fields-001', 'Write', 'Write', 2, 0, 'c.go')`)
require.NoError(t, err, "insert tool_calls")
store, err := NewStore(pgURL, schema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
all, err := store.GetAllMessages(ctx, "tc-fields-001")
require.NoError(t, err, "GetAllMessages")
require.Len(t, all, 1)
calls := all[0].ToolCalls
require.Len(t, calls, 3)
for i, tc := range calls {
assert.Equal(t, i, tc.CallIndex, "call %d index", i)
}
assert.Equal(t, "a.go", calls[0].FilePath)
assert.Equal(t, "b.go", calls[1].FilePath)
assert.Equal(t, "c.go", calls[2].FilePath)
}
// TestGetMessagesIDPopulated regresses #439: scanPGMessages must
// populate db.Message.ID with a unique-within-session value that
// matches int64(ordinal). The frontend keys {#each messages
// (message.id)} on it, and joins with TurnRow.MessageID via
// turnByMessage.get(message.id) — so collisions or zero-fill
// crash the message panel with each_key_duplicate. PG has no id
// column (composite PK on session_id, ordinal), so the synthetic
// ID matches the existing convention used by TurnRow.MessageID
// and CallRow.MessageID in session_timing.go.
func TestGetMessagesIDPopulated(t *testing.T) {
pgURL := testPGURL(t)
const schema = "agentsview_msg_id_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")
_, err = pg.Exec(`
INSERT INTO sessions
(id, machine, project, agent, first_message,
started_at, message_count, user_message_count)
VALUES
('msg-id-001', 'test-machine', 'test-project', 'claude',
'hello', '2026-03-16T10:00:00Z'::timestamptz, 4, 2)`)
require.NoError(t, err, "insert session")
_, err = pg.Exec(`
INSERT INTO messages
(session_id, ordinal, role, content,
timestamp, content_length, has_tool_use)
VALUES
('msg-id-001', 0, 'user', 'first',
'2026-03-16T10:00:00Z'::timestamptz, 5, FALSE),
('msg-id-001', 1, 'assistant', 'second',
'2026-03-16T10:00:01Z'::timestamptz, 6, TRUE),
('msg-id-001', 2, 'user', 'third',
'2026-03-16T10:00:02Z'::timestamptz, 5, FALSE),
('msg-id-001', 3, 'assistant', 'fourth',
'2026-03-16T10:00:03Z'::timestamptz, 6, TRUE)`)
require.NoError(t, err, "insert messages")
store, err := NewStore(pgURL, schema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
check := func(t *testing.T, label string, msgs []db.Message) {
t.Helper()
require.Len(t, msgs, 4, label)
seen := map[int64]int{}
for i, m := range msgs {
prev, ok := seen[m.ID]
assert.False(t, ok,
"%s: msgs[%d].ID == msgs[%d].ID == %d (regression of #439)",
label, prev, i, m.ID)
seen[m.ID] = i
assert.Equal(t, int64(m.Ordinal), m.ID,
"%s: msgs[%d].ID = %d, want int64(ordinal=%d)",
label, i, m.ID, m.Ordinal)
}
}
msgs, err := store.GetMessages(ctx, "msg-id-001", 0, 100, true)
require.NoError(t, err, "GetMessages")
check(t, "GetMessages", msgs)
all, err := store.GetAllMessages(ctx, "msg-id-001")
require.NoError(t, err, "GetAllMessages")
check(t, "GetAllMessages", all)
// Cross-check: every TurnRow.MessageID must correspond to a message
// returned by GetMessages with the same ID. The frontend looks up
// turns via turnByMessage.get(message.id); messages without a turn
// are allowed (lookup returns undefined), but a turn whose
// MessageID has no matching message means the join will silently
// drop timing data.
timing, err := store.GetSessionTiming(ctx, "msg-id-001")
require.NoError(t, err, "GetSessionTiming")
require.NotNil(t, timing, "GetSessionTiming returned nil")
require.NotEmpty(t, timing.Turns, "GetSessionTiming.Turns empty")
msgIDs := map[int64]bool{}
for _, m := range msgs {
msgIDs[m.ID] = true
}
for _, turn := range timing.Turns {
assert.True(t, msgIDs[turn.MessageID],
"turn.MessageID=%d has no matching message.ID; "+
"frontend turnByMessage join will drop this turn",
turn.MessageID)
}
}
// TestPGSearchOperatorTokenNoError mirrors the SQLite FTS 500 regression on the
// PostgreSQL/ILIKE backend: a single token containing operator characters
// (hyphen, colon), prepared the way the HTTP handler does, must match content
// and not error. ILIKE has no FTS-operator hazard, but this pins parity.
func TestPGSearchOperatorTokenNoError(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
pg, err := Open(pgURL, testSchema, false)
require.NoError(t, err, "Open")
defer pg.Close()
_, err = pg.Exec(`
INSERT INTO sessions
(id, machine, project, agent, first_message,
started_at, message_count, user_message_count)
VALUES
('optok-001', 'test-machine', 'test-project', 'claude',
'first msg text',
'2026-03-20T10:00:00Z'::timestamptz, 2, 2)
ON CONFLICT (id) DO NOTHING`)
require.NoError(t, err, "insert session")
_, err = pg.Exec(`
INSERT INTO messages
(session_id, ordinal, role, content, timestamp, content_length)
VALUES
('optok-001', 0, 'user', 'hit error-401 from the api',
'2026-03-20T10:00:00Z'::timestamptz, 26),
('optok-001', 1, 'assistant', 'returned status:500 to client',
'2026-03-20T10:00:01Z'::timestamptz, 30)
ON CONFLICT DO NOTHING`)
require.NoError(t, err, "insert messages")
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
for _, raw := range []string{"error-401", "status:500"} {
page, err := store.Search(context.Background(), db.SearchFilter{
Query: db.PrepareFTSQuery(raw), Limit: 10,
})
require.NoError(t, err, "Search(%q)", raw)
require.Len(t, page.Results, 1, "results for %q", raw)
assert.Equal(t, "optok-001", page.Results[0].SessionID, "session for %q", raw)
}
}
// TestPGSearchMultiTermAND verifies that a multi-term query matches a session
// only when every term appears in its content (AND), matching SQLite FTS5's
// implicit AND so the same user query behaves identically across backends.
func TestPGSearchMultiTermAND(t *testing.T) {
pgURL := testPGURL(t)
ensureStoreSchema(t, pgURL)
pg, err := Open(pgURL, testSchema, false)
require.NoError(t, err, "Open")
defer pg.Close()
_, err = pg.Exec(`
INSERT INTO sessions
(id, machine, project, agent, first_message,
started_at, message_count, user_message_count)
VALUES
('andboth-001', 'test-machine', 'test-project', 'claude',
'first msg text',
'2026-03-21T10:00:00Z'::timestamptz, 2, 2),
('andone-001', 'test-machine', 'test-project', 'claude',
'first msg text',
'2026-03-21T11:00:00Z'::timestamptz, 2, 2)
ON CONFLICT (id) DO NOTHING`)
require.NoError(t, err, "insert sessions")
_, err = pg.Exec(`
INSERT INTO messages
(session_id, ordinal, role, content, timestamp, content_length)
VALUES
('andboth-001', 0, 'user', 'pgquickterm and pgfoxterm both here',
'2026-03-21T10:00:00Z'::timestamptz, 35),
('andone-001', 0, 'user', 'only pgquickterm present',
'2026-03-21T11:00:00Z'::timestamptz, 24)
ON CONFLICT DO NOTHING`)
require.NoError(t, err, "insert messages")
store, err := NewStore(pgURL, testSchema, true)
require.NoError(t, err, "NewStore")
defer store.Close()
page, err := store.Search(context.Background(), db.SearchFilter{
Query: db.PrepareFTSQuery("pgquickterm pgfoxterm"), Limit: 10,
})
require.NoError(t, err, "Search")
require.Len(t, page.Results, 1, "only the session containing both terms")
assert.Equal(t, "andboth-001", page.Results[0].SessionID, "session")
}