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
576 lines
18 KiB
Go
576 lines
18 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 TestStoreStarsAndPins(t *testing.T) {
|
|
pgURL := testPGURL(t)
|
|
|
|
const schema = "agentsview_curation_test"
|
|
pg, err := Open(pgURL, schema, true)
|
|
require.NoError(t, err, "Open")
|
|
defer pg.Close()
|
|
defer func() {
|
|
_, _ = pg.ExecContext(
|
|
context.Background(),
|
|
`DROP SCHEMA IF EXISTS `+schema+` CASCADE`,
|
|
)
|
|
}()
|
|
|
|
ctx := context.Background()
|
|
_, err = pg.ExecContext(ctx, `DROP SCHEMA IF EXISTS `+schema+` CASCADE`)
|
|
require.NoError(t, err, "drop schema")
|
|
require.NoError(t, EnsureSchema(ctx, pg, schema), "EnsureSchema")
|
|
|
|
_, err = pg.ExecContext(ctx, `
|
|
INSERT INTO sessions
|
|
(id, machine, project, agent, first_message,
|
|
started_at, message_count, user_message_count)
|
|
VALUES
|
|
('cur-star-1', 'machine-a', 'proj-curation',
|
|
'codex', 'star one',
|
|
'2026-05-01T00:00:00Z'::timestamptz, 0, 0),
|
|
('cur-star-2', 'machine-a', 'proj-curation',
|
|
'codex', 'star two',
|
|
'2026-05-01T00:01:00Z'::timestamptz, 0, 0),
|
|
('cur-pin-1', 'machine-a', 'proj-curation',
|
|
'claude', 'pin source',
|
|
'2026-05-01T00:02:00Z'::timestamptz, 2, 1)`)
|
|
require.NoError(t, err, "insert sessions")
|
|
_, err = pg.ExecContext(ctx, `
|
|
INSERT INTO messages
|
|
(session_id, ordinal, role, content, timestamp,
|
|
content_length, source_uuid)
|
|
VALUES
|
|
('cur-pin-1', 0, 'user', 'question',
|
|
'2026-05-01T00:02:00Z'::timestamptz, 8,
|
|
'uuid-question'),
|
|
('cur-pin-1', 1, 'assistant', 'answer',
|
|
'2026-05-01T00:02:01Z'::timestamptz, 6,
|
|
'uuid-answer')`)
|
|
require.NoError(t, err, "insert messages")
|
|
|
|
store, err := NewStore(pgURL, schema, true)
|
|
require.NoError(t, err, "NewStore")
|
|
defer store.Close()
|
|
|
|
ok, err := store.StarSession("cur-star-1")
|
|
require.NoError(t, err, "StarSession existing")
|
|
require.True(t, ok, "StarSession existing")
|
|
ok, err = store.StarSession("missing")
|
|
require.NoError(t, err, "StarSession missing")
|
|
assert.False(t, ok, "StarSession missing")
|
|
require.NoError(t, store.BulkStarSessions(
|
|
[]string{"cur-star-2", "missing"},
|
|
), "BulkStarSessions")
|
|
|
|
ids, err := store.ListStarredSessionIDs(ctx)
|
|
require.NoError(t, err, "ListStarredSessionIDs")
|
|
wantStars := map[string]bool{
|
|
"cur-star-1": true,
|
|
"cur-star-2": true,
|
|
}
|
|
require.Len(t, ids, len(wantStars), "starred ids = %v", ids)
|
|
for _, id := range ids {
|
|
assert.True(t, wantStars[id], "unexpected starred id %q in %v", id, ids)
|
|
}
|
|
require.NoError(t, store.UnstarSession("cur-star-1"), "UnstarSession")
|
|
ids, err = store.ListStarredSessionIDs(ctx)
|
|
require.NoError(t, err, "ListStarredSessionIDs after unstar")
|
|
require.Len(t, ids, 1)
|
|
assert.Equal(t, "cur-star-2", ids[0])
|
|
|
|
note := "keep this"
|
|
pinID, err := store.PinMessage("cur-pin-1", 1, ¬e)
|
|
require.NoError(t, err, "PinMessage")
|
|
require.NotZero(t, pinID, "PinMessage returned 0, want row id")
|
|
updatedNote := "updated"
|
|
pinID2, err := store.PinMessage("cur-pin-1", 1, &updatedNote)
|
|
require.NoError(t, err, "PinMessage update")
|
|
assert.Equal(t, pinID, pinID2)
|
|
missingPin, err := store.PinMessage("cur-pin-1", 99, nil)
|
|
require.NoError(t, err, "PinMessage missing message")
|
|
assert.Zero(t, missingPin)
|
|
|
|
pins, err := store.ListPinnedMessages(ctx, "cur-pin-1", "")
|
|
require.NoError(t, err, "ListPinnedMessages session")
|
|
require.Len(t, pins, 1)
|
|
assert.Equal(t, int64(1), pins[0].MessageID)
|
|
assert.Equal(t, 1, pins[0].Ordinal)
|
|
require.NotNil(t, pins[0].Note)
|
|
assert.Equal(t, updatedNote, *pins[0].Note)
|
|
|
|
allPins, err := store.ListPinnedMessages(ctx, "", "proj-curation")
|
|
require.NoError(t, err, "ListPinnedMessages all")
|
|
require.Len(t, allPins, 1)
|
|
require.NotNil(t, allPins[0].Content)
|
|
assert.Equal(t, "answer", *allPins[0].Content)
|
|
require.NotNil(t, allPins[0].Role)
|
|
assert.Equal(t, "assistant", *allPins[0].Role)
|
|
require.NotNil(t, allPins[0].SessionProject)
|
|
assert.Equal(t, "proj-curation", *allPins[0].SessionProject)
|
|
|
|
require.NoError(t, store.UnpinMessage("cur-pin-1", 1), "UnpinMessage")
|
|
pins, err = store.ListPinnedMessages(ctx, "cur-pin-1", "")
|
|
require.NoError(t, err, "ListPinnedMessages after unpin")
|
|
assert.Empty(t, pins)
|
|
}
|
|
|
|
func TestPushPreservesMultiplePGPinsBySourceUUID(t *testing.T) {
|
|
pgURL := testPGURL(t)
|
|
cleanPGSchema(t, pgURL)
|
|
t.Cleanup(func() { cleanPGSchema(t, pgURL) })
|
|
|
|
local := testDB(t)
|
|
ps, err := New(
|
|
pgURL, "agentsview", local,
|
|
"curation-machine", true,
|
|
SyncOptions{},
|
|
)
|
|
require.NoError(t, err, "New sync")
|
|
defer ps.Close()
|
|
|
|
ctx := context.Background()
|
|
require.NoError(t, ps.EnsureSchema(ctx), "EnsureSchema")
|
|
|
|
sess := db.Session{
|
|
ID: "pg-pin-rewrite",
|
|
Project: "proj-curation",
|
|
Machine: "local",
|
|
Agent: "codex",
|
|
MessageCount: 3,
|
|
CreatedAt: "2026-05-01T00:00:00Z",
|
|
}
|
|
require.NoError(t, local.UpsertSession(sess), "UpsertSession first")
|
|
require.NoError(t, local.InsertMessages([]db.Message{
|
|
{
|
|
SessionID: "pg-pin-rewrite",
|
|
Ordinal: 0,
|
|
Role: "user",
|
|
Content: "question",
|
|
SourceUUID: "uuid-question",
|
|
},
|
|
{
|
|
SessionID: "pg-pin-rewrite",
|
|
Ordinal: 1,
|
|
Role: "assistant",
|
|
Content: "answer one",
|
|
SourceUUID: "uuid-answer-one",
|
|
},
|
|
{
|
|
SessionID: "pg-pin-rewrite",
|
|
Ordinal: 2,
|
|
Role: "assistant",
|
|
Content: "answer two",
|
|
SourceUUID: "uuid-answer-two",
|
|
},
|
|
}), "InsertMessages first")
|
|
_, err = ps.Push(ctx, false, nil)
|
|
require.NoError(t, err, "Push first")
|
|
|
|
store, err := NewStore(pgURL, "agentsview", true)
|
|
require.NoError(t, err, "NewStore")
|
|
defer store.Close()
|
|
|
|
noteOne := "important one"
|
|
_, err = store.PinMessage("pg-pin-rewrite", 1, ¬eOne)
|
|
require.NoError(t, err, "PinMessage one")
|
|
noteTwo := "important two"
|
|
_, err = store.PinMessage("pg-pin-rewrite", 2, ¬eTwo)
|
|
require.NoError(t, err, "PinMessage two")
|
|
|
|
sess.MessageCount = 4
|
|
require.NoError(t, local.UpsertSession(sess), "UpsertSession second")
|
|
require.NoError(t, local.ReplaceSessionMessages(
|
|
"pg-pin-rewrite",
|
|
[]db.Message{
|
|
{
|
|
SessionID: "pg-pin-rewrite",
|
|
Ordinal: 0,
|
|
Role: "user",
|
|
Content: "question",
|
|
SourceUUID: "uuid-question",
|
|
},
|
|
{
|
|
SessionID: "pg-pin-rewrite",
|
|
Ordinal: 1,
|
|
Role: "user",
|
|
Content: "[compact]",
|
|
SourceUUID: "uuid-boundary",
|
|
IsCompactBoundary: true,
|
|
},
|
|
{
|
|
SessionID: "pg-pin-rewrite",
|
|
Ordinal: 2,
|
|
Role: "assistant",
|
|
Content: "answer one",
|
|
SourceUUID: "uuid-answer-one",
|
|
},
|
|
{
|
|
SessionID: "pg-pin-rewrite",
|
|
Ordinal: 3,
|
|
Role: "assistant",
|
|
Content: "answer two",
|
|
SourceUUID: "uuid-answer-two",
|
|
},
|
|
},
|
|
), "ReplaceSessionMessages")
|
|
|
|
_, err = ps.Push(ctx, true, nil)
|
|
require.NoError(t, err, "Push rewrite")
|
|
|
|
pins, err := store.ListPinnedMessages(ctx, "pg-pin-rewrite", "")
|
|
require.NoError(t, err, "ListPinnedMessages")
|
|
require.Len(t, pins, 2)
|
|
|
|
byNote := map[string]db.PinnedMessage{}
|
|
for _, pin := range pins {
|
|
require.NotNil(t, pin.Note, "pin note should be populated")
|
|
byNote[*pin.Note] = pin
|
|
}
|
|
pin, ok := byNote[noteOne]
|
|
require.True(t, ok, "pin for %q missing: %v", noteOne, pins)
|
|
assert.Equal(t, int64(2), pin.MessageID)
|
|
assert.Equal(t, 2, pin.Ordinal)
|
|
pin, ok = byNote[noteTwo]
|
|
require.True(t, ok, "pin for %q missing: %v", noteTwo, pins)
|
|
assert.Equal(t, int64(3), pin.MessageID)
|
|
assert.Equal(t, 3, pin.Ordinal)
|
|
}
|
|
|
|
func TestReconcilePinnedMessagesPrefersCurrentTargetPin(t *testing.T) {
|
|
pgURL := testPGURL(t)
|
|
|
|
const schema = "agentsview_pin_duplicate_test"
|
|
pg, err := Open(pgURL, schema, true)
|
|
require.NoError(t, err, "Open")
|
|
defer pg.Close()
|
|
defer func() {
|
|
_, _ = pg.ExecContext(
|
|
context.Background(),
|
|
`DROP SCHEMA IF EXISTS `+schema+` CASCADE`,
|
|
)
|
|
}()
|
|
|
|
ctx := context.Background()
|
|
_, err = pg.ExecContext(ctx, `DROP SCHEMA IF EXISTS `+schema+` CASCADE`)
|
|
require.NoError(t, err, "drop schema")
|
|
require.NoError(t, EnsureSchema(ctx, pg, schema), "EnsureSchema")
|
|
|
|
_, err = pg.ExecContext(ctx, `
|
|
INSERT INTO sessions
|
|
(id, machine, project, agent, first_message,
|
|
started_at, message_count, user_message_count)
|
|
VALUES
|
|
('pg-pin-duplicate', 'machine-a', 'proj-curation',
|
|
'codex', 'duplicate source repair',
|
|
'2026-05-01T00:00:00Z'::timestamptz, 3, 1)`)
|
|
require.NoError(t, err, "insert session")
|
|
_, err = pg.ExecContext(ctx, `
|
|
INSERT INTO messages
|
|
(session_id, ordinal, role, content, timestamp,
|
|
content_length, source_uuid)
|
|
VALUES
|
|
('pg-pin-duplicate', 0, 'user', 'question',
|
|
'2026-05-01T00:00:00Z'::timestamptz, 8,
|
|
'uuid-question'),
|
|
('pg-pin-duplicate', 1, 'user', '[compact]',
|
|
'2026-05-01T00:00:01Z'::timestamptz, 9,
|
|
'uuid-boundary'),
|
|
('pg-pin-duplicate', 2, 'assistant', 'answer',
|
|
'2026-05-01T00:00:02Z'::timestamptz, 6,
|
|
'uuid-answer')`)
|
|
require.NoError(t, err, "insert messages")
|
|
_, err = pg.ExecContext(ctx, `
|
|
INSERT INTO pinned_messages
|
|
(session_id, message_id, ordinal, source_uuid,
|
|
note, created_at)
|
|
VALUES
|
|
('pg-pin-duplicate', 1, 1, 'uuid-answer',
|
|
'stale note',
|
|
'2026-05-01T00:01:00Z'::timestamptz),
|
|
('pg-pin-duplicate', 2, 2, 'uuid-answer',
|
|
'current note',
|
|
'2026-05-01T00:02:00Z'::timestamptz)`)
|
|
require.NoError(t, err, "insert pins")
|
|
|
|
tx, err := pg.BeginTx(ctx, nil)
|
|
require.NoError(t, err, "begin tx")
|
|
if err := reconcilePinnedMessages(
|
|
ctx, tx, "pg-pin-duplicate",
|
|
); err != nil {
|
|
_ = tx.Rollback()
|
|
t.Fatalf("reconcilePinnedMessages: %v", err)
|
|
}
|
|
require.NoError(t, tx.Commit(), "commit tx")
|
|
|
|
store, err := NewStore(pgURL, schema, true)
|
|
require.NoError(t, err, "NewStore")
|
|
defer store.Close()
|
|
|
|
pins, err := store.ListPinnedMessages(ctx, "pg-pin-duplicate", "")
|
|
require.NoError(t, err, "ListPinnedMessages")
|
|
require.Len(t, pins, 1, "pins = %v", pins)
|
|
assert.Equal(t, int64(2), pins[0].MessageID)
|
|
assert.Equal(t, 2, pins[0].Ordinal)
|
|
require.NotNil(t, pins[0].Note)
|
|
assert.Equal(t, "current note", *pins[0].Note)
|
|
}
|
|
|
|
// TestReconcilePinnedMessagesPrunesPinWhenSourceUUIDGone covers the
|
|
// case where a source-backed pin's source_uuid no longer exists in
|
|
// the messages table, but a different message now occupies the
|
|
// pin's original ordinal. The pin must be deleted: otherwise it
|
|
// would silently re-anchor on an unrelated message.
|
|
func TestReconcilePinnedMessagesPrunesPinWhenSourceUUIDGone(t *testing.T) {
|
|
pgURL := testPGURL(t)
|
|
|
|
const schema = "agentsview_pin_source_gone_test"
|
|
pg, err := Open(pgURL, schema, true)
|
|
require.NoError(t, err, "Open")
|
|
defer pg.Close()
|
|
defer func() {
|
|
_, _ = pg.ExecContext(
|
|
context.Background(),
|
|
`DROP SCHEMA IF EXISTS `+schema+` CASCADE`,
|
|
)
|
|
}()
|
|
|
|
ctx := context.Background()
|
|
_, err = pg.ExecContext(ctx, `DROP SCHEMA IF EXISTS `+schema+` CASCADE`)
|
|
require.NoError(t, err, "drop schema")
|
|
require.NoError(t, EnsureSchema(ctx, pg, schema), "EnsureSchema")
|
|
|
|
_, err = pg.ExecContext(ctx, `
|
|
INSERT INTO sessions
|
|
(id, machine, project, agent, first_message,
|
|
started_at, message_count, user_message_count)
|
|
VALUES
|
|
('pg-pin-source-gone', 'machine-a', 'proj-curation',
|
|
'codex', 'source uuid gone',
|
|
'2026-05-01T00:00:00Z'::timestamptz, 2, 1)`)
|
|
require.NoError(t, err, "insert session")
|
|
_, err = pg.ExecContext(ctx, `
|
|
INSERT INTO messages
|
|
(session_id, ordinal, role, content, timestamp,
|
|
content_length, source_uuid)
|
|
VALUES
|
|
('pg-pin-source-gone', 0, 'user', 'question',
|
|
'2026-05-01T00:00:00Z'::timestamptz, 8,
|
|
'uuid-question'),
|
|
('pg-pin-source-gone', 1, 'assistant', 'new answer',
|
|
'2026-05-01T00:00:01Z'::timestamptz, 10,
|
|
'uuid-new-answer')`)
|
|
require.NoError(t, err, "insert messages")
|
|
_, err = pg.ExecContext(ctx, `
|
|
INSERT INTO pinned_messages
|
|
(session_id, message_id, ordinal, source_uuid,
|
|
note, created_at)
|
|
VALUES
|
|
('pg-pin-source-gone', 1, 1, 'uuid-gone-forever',
|
|
'stale pin',
|
|
'2026-05-01T00:01:00Z'::timestamptz)`)
|
|
require.NoError(t, err, "insert pin")
|
|
|
|
tx, err := pg.BeginTx(ctx, nil)
|
|
require.NoError(t, err, "begin tx")
|
|
if err := reconcilePinnedMessages(
|
|
ctx, tx, "pg-pin-source-gone",
|
|
); err != nil {
|
|
_ = tx.Rollback()
|
|
t.Fatalf("reconcilePinnedMessages: %v", err)
|
|
}
|
|
require.NoError(t, tx.Commit(), "commit tx")
|
|
|
|
store, err := NewStore(pgURL, schema, true)
|
|
require.NoError(t, err, "NewStore")
|
|
defer store.Close()
|
|
|
|
pins, err := store.ListPinnedMessages(ctx, "pg-pin-source-gone", "")
|
|
require.NoError(t, err, "ListPinnedMessages")
|
|
assert.Empty(t, pins,
|
|
"stale source_uuid should be pruned: %v", pins)
|
|
}
|
|
|
|
// TestReconcilePinnedMessagesKeepsPinOnLaterDuplicateSourceUUID
|
|
// covers the case where multiple messages in the same session share
|
|
// the same source_uuid (the schema permits it) and the pin sits on
|
|
// the later duplicate. Reconciliation must keep the pin where it is
|
|
// rather than relocating it to the lowest-ordinal duplicate.
|
|
func TestReconcilePinnedMessagesKeepsPinOnLaterDuplicateSourceUUID(t *testing.T) {
|
|
pgURL := testPGURL(t)
|
|
|
|
const schema = "agentsview_pin_dup_uuid_test"
|
|
pg, err := Open(pgURL, schema, true)
|
|
require.NoError(t, err, "Open")
|
|
defer pg.Close()
|
|
defer func() {
|
|
_, _ = pg.ExecContext(
|
|
context.Background(),
|
|
`DROP SCHEMA IF EXISTS `+schema+` CASCADE`,
|
|
)
|
|
}()
|
|
|
|
ctx := context.Background()
|
|
_, err = pg.ExecContext(ctx, `DROP SCHEMA IF EXISTS `+schema+` CASCADE`)
|
|
require.NoError(t, err, "drop schema")
|
|
require.NoError(t, EnsureSchema(ctx, pg, schema), "EnsureSchema")
|
|
|
|
_, err = pg.ExecContext(ctx, `
|
|
INSERT INTO sessions
|
|
(id, machine, project, agent, first_message,
|
|
started_at, message_count, user_message_count)
|
|
VALUES
|
|
('pg-pin-dup-uuid', 'machine-a', 'proj-curation',
|
|
'claude', 'duplicate source uuid',
|
|
'2026-05-01T00:00:00Z'::timestamptz, 3, 1)`)
|
|
require.NoError(t, err, "insert session")
|
|
_, err = pg.ExecContext(ctx, `
|
|
INSERT INTO messages
|
|
(session_id, ordinal, role, content, timestamp,
|
|
content_length, source_uuid)
|
|
VALUES
|
|
('pg-pin-dup-uuid', 0, 'user', 'question',
|
|
'2026-05-01T00:00:00Z'::timestamptz, 8,
|
|
'uuid-question'),
|
|
('pg-pin-dup-uuid', 1, 'assistant', 'first answer',
|
|
'2026-05-01T00:00:01Z'::timestamptz, 12,
|
|
'uuid-shared'),
|
|
('pg-pin-dup-uuid', 2, 'assistant', 'second answer',
|
|
'2026-05-01T00:00:02Z'::timestamptz, 13,
|
|
'uuid-shared')`)
|
|
require.NoError(t, err, "insert messages")
|
|
_, err = pg.ExecContext(ctx, `
|
|
INSERT INTO pinned_messages
|
|
(session_id, message_id, ordinal, source_uuid,
|
|
note, created_at)
|
|
VALUES
|
|
('pg-pin-dup-uuid', 2, 2, 'uuid-shared',
|
|
'pin on later duplicate',
|
|
'2026-05-01T00:01:00Z'::timestamptz)`)
|
|
require.NoError(t, err, "insert pin")
|
|
|
|
tx, err := pg.BeginTx(ctx, nil)
|
|
require.NoError(t, err, "begin tx")
|
|
if err := reconcilePinnedMessages(
|
|
ctx, tx, "pg-pin-dup-uuid",
|
|
); err != nil {
|
|
_ = tx.Rollback()
|
|
t.Fatalf("reconcilePinnedMessages: %v", err)
|
|
}
|
|
require.NoError(t, tx.Commit(), "commit tx")
|
|
|
|
store, err := NewStore(pgURL, schema, true)
|
|
require.NoError(t, err, "NewStore")
|
|
defer store.Close()
|
|
|
|
pins, err := store.ListPinnedMessages(ctx, "pg-pin-dup-uuid", "")
|
|
require.NoError(t, err, "ListPinnedMessages")
|
|
require.Len(t, pins, 1, "pins = %v", pins)
|
|
assert.Equal(t, int64(2), pins[0].MessageID,
|
|
"must not relocate to lower-ordinal duplicate")
|
|
assert.Equal(t, 2, pins[0].Ordinal,
|
|
"must not relocate to lower-ordinal duplicate")
|
|
}
|
|
|
|
// TestPinMessageRepinRefreshesSourceUUID covers re-pinning the same
|
|
// (session_id, message_id). The stored source_uuid must reflect the
|
|
// message currently at message_id; otherwise the next reconciliation
|
|
// would follow the stale uuid away from where the user just pinned.
|
|
func TestPinMessageRepinRefreshesSourceUUID(t *testing.T) {
|
|
pgURL := testPGURL(t)
|
|
|
|
const schema = "agentsview_pin_repin_test"
|
|
pg, err := Open(pgURL, schema, true)
|
|
require.NoError(t, err, "Open")
|
|
defer pg.Close()
|
|
defer func() {
|
|
_, _ = pg.ExecContext(
|
|
context.Background(),
|
|
`DROP SCHEMA IF EXISTS `+schema+` CASCADE`,
|
|
)
|
|
}()
|
|
|
|
ctx := context.Background()
|
|
_, err = pg.ExecContext(ctx, `DROP SCHEMA IF EXISTS `+schema+` CASCADE`)
|
|
require.NoError(t, err, "drop schema")
|
|
require.NoError(t, EnsureSchema(ctx, pg, schema), "EnsureSchema")
|
|
|
|
_, err = pg.ExecContext(ctx, `
|
|
INSERT INTO sessions
|
|
(id, machine, project, agent, first_message,
|
|
started_at, message_count, user_message_count)
|
|
VALUES
|
|
('pg-pin-repin', 'machine-a', 'proj-curation',
|
|
'claude', 'repin refreshes uuid',
|
|
'2026-05-01T00:00:00Z'::timestamptz, 2, 1)`)
|
|
require.NoError(t, err, "insert session")
|
|
_, err = pg.ExecContext(ctx, `
|
|
INSERT INTO messages
|
|
(session_id, ordinal, role, content, timestamp,
|
|
content_length, source_uuid)
|
|
VALUES
|
|
('pg-pin-repin', 0, 'user', 'question',
|
|
'2026-05-01T00:00:00Z'::timestamptz, 8,
|
|
'uuid-question'),
|
|
('pg-pin-repin', 1, 'assistant', 'original',
|
|
'2026-05-01T00:00:01Z'::timestamptz, 8,
|
|
'uuid-original')`)
|
|
require.NoError(t, err, "insert messages")
|
|
|
|
store, err := NewStore(pgURL, schema, true)
|
|
require.NoError(t, err, "NewStore")
|
|
defer store.Close()
|
|
|
|
originalNote := "first"
|
|
_, err = store.PinMessage("pg-pin-repin", 1, &originalNote)
|
|
require.NoError(t, err, "PinMessage initial")
|
|
var initialSourceUUID, initialCreatedAt string
|
|
require.NoError(t, pg.QueryRowContext(ctx, `
|
|
SELECT source_uuid, created_at::text
|
|
FROM pinned_messages
|
|
WHERE session_id = $1 AND message_id = $2`,
|
|
"pg-pin-repin", 1,
|
|
).Scan(&initialSourceUUID, &initialCreatedAt), "query initial pin")
|
|
assert.Equal(t, "uuid-original", initialSourceUUID)
|
|
|
|
// Simulate a session rewrite that replaces the message at
|
|
// ordinal 1 with a different message (different source_uuid)
|
|
// while reusing the ordinal.
|
|
_, err = pg.ExecContext(ctx, `
|
|
UPDATE messages
|
|
SET source_uuid = 'uuid-replacement',
|
|
content = 'replaced'
|
|
WHERE session_id = $1 AND ordinal = $2`,
|
|
"pg-pin-repin", 1,
|
|
)
|
|
require.NoError(t, err, "update message source_uuid")
|
|
|
|
updatedNote := "second"
|
|
_, err = store.PinMessage("pg-pin-repin", 1, &updatedNote)
|
|
require.NoError(t, err, "PinMessage repin")
|
|
|
|
var gotSourceUUID, gotCreatedAt string
|
|
var gotNote *string
|
|
require.NoError(t, pg.QueryRowContext(ctx, `
|
|
SELECT source_uuid, note, created_at::text
|
|
FROM pinned_messages
|
|
WHERE session_id = $1 AND message_id = $2`,
|
|
"pg-pin-repin", 1,
|
|
).Scan(&gotSourceUUID, &gotNote, &gotCreatedAt), "query repinned pin")
|
|
assert.Equal(t, "uuid-replacement", gotSourceUUID,
|
|
"stale source_uuid would steer the next reconciliation away from the current message")
|
|
require.NotNil(t, gotNote)
|
|
assert.Equal(t, updatedNote, *gotNote)
|
|
assert.Equal(t, initialCreatedAt, gotCreatedAt, "created_at must be preserved")
|
|
}
|