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
229 lines
7.2 KiB
Go
229 lines
7.2 KiB
Go
//go:build pgtest
|
|
|
|
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"go.kenn.io/agentsview/internal/db"
|
|
)
|
|
|
|
// TestPushSessionGuardsAgainstCrossMachineCollision verifies that when two
|
|
// machines share the same session ID (from dotfile sync, directory restore, etc.),
|
|
// the second machine's push is skipped if the session is already owned by a
|
|
// different machine. This prevents the ping-pong effect where two pushers
|
|
// fight over the same row on every push cycle.
|
|
//
|
|
// Steps:
|
|
// 1. Insert a session with id="clash-001" and machine="machine-a" directly into PG.
|
|
// 2. Call pushSession with machine="machine-b" and a db.Session{ID: "clash-001", Machine: "machine-b", ...}.
|
|
// 3. Assert that the row's machine column is still "machine-a" (not overwritten).
|
|
// 4. Assert that no messages were written for the conflicting session.
|
|
func TestPushSessionGuardsAgainstCrossMachineCollision(t *testing.T) {
|
|
pgURL := testPGURL(t)
|
|
|
|
const schema = "agentsview_collision_guard_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 SQLite DB.
|
|
localDB, err := db.Open(filepath.Join(t.TempDir(), "local.db"))
|
|
require.NoError(t, err, "db.Open")
|
|
defer localDB.Close()
|
|
|
|
sync := &Sync{
|
|
pg: pg,
|
|
local: localDB,
|
|
machine: "machine-b",
|
|
schema: schema,
|
|
schemaDone: true,
|
|
}
|
|
|
|
const clashID = "clash-001"
|
|
|
|
// Step 1: Insert a session owned by machine-a directly into PG.
|
|
markerID, err := sync.pushMarkerID()
|
|
require.NoError(t, err, "pushMarkerID")
|
|
|
|
_, err = pg.ExecContext(ctx, `
|
|
INSERT INTO sessions (
|
|
id, machine, owner_marker, project, agent, created_at
|
|
) VALUES ($1, $2, $3, $4, $5, NOW())
|
|
`, clashID, "machine-a", "different-owner", "test-proj", "claude")
|
|
require.NoError(t, err, "insert existing session")
|
|
|
|
// Step 2: Attempt to push the same session from machine-b.
|
|
sess := db.Session{
|
|
ID: clashID,
|
|
Project: "test-proj",
|
|
Machine: "machine-b",
|
|
Agent: "claude",
|
|
MessageCount: 1,
|
|
CreatedAt: "2026-01-01T00:00:00Z",
|
|
}
|
|
require.NoError(t, localDB.UpsertSession(sess), "UpsertSession")
|
|
require.NoError(t, localDB.InsertMessages([]db.Message{{
|
|
SessionID: clashID,
|
|
Ordinal: 0,
|
|
Role: "user",
|
|
Content: "test",
|
|
ContentLength: 4,
|
|
}}), "InsertMessages")
|
|
|
|
// Execute pushSession.
|
|
tx, err := pg.BeginTx(ctx, nil)
|
|
require.NoError(t, err, "BeginTx")
|
|
err = sync.pushSession(ctx, tx, sess, markerID, nil)
|
|
require.ErrorIs(t, err, errSessionOwnershipConflict, "pushSession should return ownership conflict sentinel")
|
|
require.NoError(t, tx.Commit(), "Commit")
|
|
|
|
// Step 3: Verify the machine column is still "machine-a".
|
|
var existingMachine string
|
|
err = pg.QueryRowContext(ctx,
|
|
`SELECT machine FROM sessions WHERE id = $1`, clashID,
|
|
).Scan(&existingMachine)
|
|
require.NoError(t, err, "read back machine")
|
|
assert.Equal(t, "machine-a", existingMachine,
|
|
"machine should remain as 'machine-a', not overwritten by 'machine-b'")
|
|
|
|
// Step 4: Verify no messages were written for this session.
|
|
var messageCount int
|
|
err = pg.QueryRowContext(ctx,
|
|
`SELECT COUNT(*) FROM messages WHERE session_id = $1`, clashID,
|
|
).Scan(&messageCount)
|
|
require.NoError(t, err, "count messages")
|
|
assert.Equal(t, 0, messageCount,
|
|
"no messages should be written when session is skipped due to collision")
|
|
|
|
assert.NotEqual(t, markerID, "different-owner", "precondition: foreign owner marker differs from local marker")
|
|
}
|
|
|
|
func TestPushSessionAllowsMachineRenameForSameOwnerMarker(t *testing.T) {
|
|
pgURL := testPGURL(t)
|
|
|
|
const schema = "agentsview_collision_owner_marker_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")
|
|
|
|
localDB, err := db.Open(filepath.Join(t.TempDir(), "local.db"))
|
|
require.NoError(t, err, "db.Open")
|
|
defer localDB.Close()
|
|
|
|
sync := &Sync{
|
|
pg: pg,
|
|
local: localDB,
|
|
machine: "renamed-host",
|
|
schema: schema,
|
|
schemaDone: true,
|
|
}
|
|
markerID, err := sync.pushMarkerID()
|
|
require.NoError(t, err, "pushMarkerID")
|
|
|
|
const sessID = "rename-001"
|
|
_, err = pg.ExecContext(ctx, `
|
|
INSERT INTO sessions (
|
|
id, machine, owner_marker, project, agent, created_at
|
|
) VALUES ($1, $2, $3, $4, $5, NOW())
|
|
`, sessID, "old-host", markerID, "test-proj", "claude")
|
|
require.NoError(t, err, "insert existing session")
|
|
|
|
sess := db.Session{
|
|
ID: sessID,
|
|
Project: "test-proj",
|
|
Machine: "local",
|
|
Agent: "claude",
|
|
MessageCount: 1,
|
|
CreatedAt: "2026-01-01T00:00:00Z",
|
|
}
|
|
require.NoError(t, localDB.UpsertSession(sess), "UpsertSession")
|
|
|
|
tx, err := pg.BeginTx(ctx, nil)
|
|
require.NoError(t, err, "BeginTx")
|
|
require.NoError(t, sync.pushSession(ctx, tx, sess, markerID, nil), "pushSession")
|
|
require.NoError(t, tx.Commit(), "Commit")
|
|
|
|
var machine, ownerMarker string
|
|
err = pg.QueryRowContext(ctx,
|
|
`SELECT machine, owner_marker FROM sessions WHERE id = $1`, sessID,
|
|
).Scan(&machine, &ownerMarker)
|
|
require.NoError(t, err, "read back session")
|
|
assert.Equal(t, "renamed-host", machine)
|
|
assert.Equal(t, markerID, ownerMarker)
|
|
}
|
|
|
|
func TestPushSessionAdoptsLegacyLocalSentinelRow(t *testing.T) {
|
|
pgURL := testPGURL(t)
|
|
|
|
const schema = "agentsview_collision_legacy_local_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")
|
|
|
|
localDB, err := db.Open(filepath.Join(t.TempDir(), "local.db"))
|
|
require.NoError(t, err, "db.Open")
|
|
defer localDB.Close()
|
|
|
|
sync := &Sync{
|
|
pg: pg,
|
|
local: localDB,
|
|
machine: "host-a",
|
|
schema: schema,
|
|
schemaDone: true,
|
|
}
|
|
|
|
const sessID = "legacy-local-001"
|
|
_, err = pg.ExecContext(ctx, `
|
|
INSERT INTO sessions (
|
|
id, machine, owner_marker, project, agent, created_at
|
|
) VALUES ($1, $2, $3, $4, $5, NOW())
|
|
`, sessID, "local", "", "test-proj", "claude")
|
|
require.NoError(t, err, "insert legacy local sentinel row")
|
|
|
|
sess := db.Session{
|
|
ID: sessID,
|
|
Project: "test-proj",
|
|
Machine: "local",
|
|
Agent: "claude",
|
|
MessageCount: 1,
|
|
CreatedAt: "2026-01-01T00:00:00Z",
|
|
}
|
|
require.NoError(t, localDB.UpsertSession(sess), "UpsertSession")
|
|
|
|
tx, err := pg.BeginTx(ctx, nil)
|
|
require.NoError(t, err, "BeginTx")
|
|
markerID, err := sync.pushMarkerID()
|
|
require.NoError(t, err, "pushMarkerID")
|
|
require.NoError(t, sync.pushSession(ctx, tx, sess, markerID, nil), "pushSession")
|
|
require.NoError(t, tx.Commit(), "Commit")
|
|
|
|
var machine, ownerMarker string
|
|
err = pg.QueryRowContext(ctx,
|
|
`SELECT machine, owner_marker FROM sessions WHERE id = $1`, sessID,
|
|
).Scan(&machine, &ownerMarker)
|
|
require.NoError(t, err, "read back session")
|
|
assert.Equal(t, "host-a", machine)
|
|
assert.Equal(t, markerID, ownerMarker)
|
|
}
|