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
282 lines
8.0 KiB
Go
282 lines
8.0 KiB
Go
//go:build pgtest
|
|
|
|
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"go.kenn.io/agentsview/internal/db"
|
|
)
|
|
|
|
// TestPGPushSecrets verifies that pg push carries secret_leak_count
|
|
// and secret_findings rows to PostgreSQL. It also verifies idempotency
|
|
// (delete-before-insert prevents duplicates on re-push).
|
|
func TestPGPushSecrets(t *testing.T) {
|
|
pgURL := testPGURL(t)
|
|
cleanPGSchema(t, pgURL)
|
|
t.Cleanup(func() { cleanPGSchema(t, pgURL) })
|
|
|
|
local := testDB(t)
|
|
ps, err := New(
|
|
pgURL, "agentsview", local, "machine-secrets", true,
|
|
SyncOptions{},
|
|
)
|
|
require.NoError(t, err, "creating sync")
|
|
defer ps.Close()
|
|
|
|
ctx := context.Background()
|
|
require.NoError(t, ps.EnsureSchema(ctx), "ensure schema")
|
|
|
|
// Seed a session with a message.
|
|
started := time.Now().UTC().Format(time.RFC3339)
|
|
firstMsg := "push secrets test"
|
|
sess := db.Session{
|
|
ID: "secrets-sess-001",
|
|
Project: "secrets-project",
|
|
Machine: "local",
|
|
Agent: "claude",
|
|
FirstMessage: &firstMsg,
|
|
StartedAt: &started,
|
|
MessageCount: 1,
|
|
}
|
|
require.NoError(t, local.UpsertSession(sess), "upsert session")
|
|
require.NoError(t, local.InsertMessages([]db.Message{{
|
|
SessionID: "secrets-sess-001",
|
|
Ordinal: 0,
|
|
Role: "user",
|
|
Content: firstMsg,
|
|
}}), "insert message")
|
|
|
|
// Add a secret finding to the session.
|
|
findings := []db.SecretFinding{
|
|
{
|
|
SessionID: "secrets-sess-001",
|
|
RuleName: "aws-access-key",
|
|
Confidence: "definite",
|
|
LocationKind: "message",
|
|
MessageOrdinal: 0,
|
|
MatchStart: 5,
|
|
MatchEnd: 25,
|
|
MatchIndex: 0,
|
|
RedactedMatch: "AKIA[REDACTED]",
|
|
RulesVersion: "v1.0",
|
|
},
|
|
}
|
|
require.NoError(t, local.ReplaceSessionSecretFindings(
|
|
"secrets-sess-001", findings, 1, "v1.0",
|
|
), "replace secret findings")
|
|
|
|
// Push to PG.
|
|
pushResult, err := ps.Push(ctx, false, nil)
|
|
require.NoError(t, err, "push")
|
|
require.Equal(t, 1, pushResult.SessionsPushed)
|
|
|
|
// Open a read Store and verify via ListSessions + ListSecretFindings.
|
|
store, err := NewStore(pgURL, "agentsview", true)
|
|
require.NoError(t, err, "opening store")
|
|
defer store.Close()
|
|
|
|
// ListSessions with HasSecret=true must return the session with
|
|
// SecretLeakCount=1.
|
|
page, err := store.ListSessions(ctx, db.SessionFilter{
|
|
HasSecret: true, Limit: 10,
|
|
})
|
|
require.NoError(t, err, "ListSessions HasSecret")
|
|
require.Len(t, page.Sessions, 1)
|
|
assert.Equal(t, 1, page.Sessions[0].SecretLeakCount)
|
|
|
|
// ListSecretFindings must return the pushed finding with
|
|
// Project and Agent populated.
|
|
fpage, err := store.ListSecretFindings(ctx, db.SecretFindingFilter{
|
|
Limit: 10,
|
|
})
|
|
require.NoError(t, err, "ListSecretFindings")
|
|
require.Len(t, fpage.Findings, 1)
|
|
f := fpage.Findings[0]
|
|
assert.Equal(t, "aws-access-key", f.RuleName)
|
|
assert.Equal(t, "secrets-project", f.Project)
|
|
assert.Equal(t, "claude", f.Agent)
|
|
assert.Equal(t, "AKIA[REDACTED]", f.RedactedMatch)
|
|
|
|
// Second push with no changes must be a no-op (fingerprint
|
|
// unchanged) and must not duplicate findings.
|
|
r2, err := ps.Push(ctx, false, nil)
|
|
require.NoError(t, err, "second push")
|
|
assert.Equal(t, 0, r2.SessionsPushed, "second push should be no-op")
|
|
fpage2, err := store.ListSecretFindings(ctx, db.SecretFindingFilter{
|
|
Limit: 10,
|
|
})
|
|
require.NoError(t, err, "ListSecretFindings after re-push")
|
|
assert.Len(t, fpage2.Findings, 1, "no duplicate findings after re-push")
|
|
}
|
|
|
|
// TestPushSecretFindingsReportsChange verifies pushSecretFindings reports
|
|
// whether it changed any rows, so the caller can bump sessions.updated_at
|
|
// for secret-only changes that pushSession and pushMessages would miss.
|
|
func TestPushSecretFindingsReportsChange(t *testing.T) {
|
|
pgURL := testPGURL(t)
|
|
cleanPGSchema(t, pgURL)
|
|
t.Cleanup(func() { cleanPGSchema(t, pgURL) })
|
|
|
|
local := testDB(t)
|
|
ps, err := New(
|
|
pgURL, "agentsview", local, "machine-findings-change", true,
|
|
SyncOptions{},
|
|
)
|
|
require.NoError(t, err, "creating sync")
|
|
defer ps.Close()
|
|
|
|
ctx := context.Background()
|
|
require.NoError(t, ps.EnsureSchema(ctx), "ensure schema")
|
|
|
|
// Seed a session (no findings yet) and push it so the PG row exists
|
|
// for the secret_findings foreign key.
|
|
started := time.Now().UTC().Format(time.RFC3339)
|
|
firstMsg := "findings change test"
|
|
const sessID = "findings-change-001"
|
|
sess := db.Session{
|
|
ID: sessID,
|
|
Project: "findings-project",
|
|
Machine: "local",
|
|
Agent: "claude",
|
|
FirstMessage: &firstMsg,
|
|
StartedAt: &started,
|
|
MessageCount: 1,
|
|
}
|
|
require.NoError(t, local.UpsertSession(sess), "upsert session")
|
|
require.NoError(t, local.InsertMessages([]db.Message{{
|
|
SessionID: sessID,
|
|
Ordinal: 0,
|
|
Role: "user",
|
|
Content: firstMsg,
|
|
}}), "insert message")
|
|
_, err = ps.Push(ctx, false, nil)
|
|
require.NoError(t, err, "seed push")
|
|
|
|
// pushOnce runs pushSecretFindings in its own transaction and returns
|
|
// whether it reported a change.
|
|
pushOnce := func() bool {
|
|
tx, err := ps.pg.BeginTx(ctx, nil)
|
|
require.NoError(t, err, "begin tx")
|
|
changed, err := ps.pushSecretFindings(ctx, tx, sessID)
|
|
if err != nil {
|
|
_ = tx.Rollback()
|
|
t.Fatalf("pushSecretFindings: %v", err)
|
|
}
|
|
require.NoError(t, tx.Commit(), "commit tx")
|
|
return changed
|
|
}
|
|
|
|
finding := db.SecretFinding{
|
|
SessionID: sessID,
|
|
RuleName: "aws-access-key",
|
|
Confidence: "definite",
|
|
LocationKind: "message",
|
|
MessageOrdinal: 0,
|
|
MatchStart: 5,
|
|
MatchEnd: 25,
|
|
MatchIndex: 0,
|
|
RedactedMatch: "AKIA[REDACTED]",
|
|
RulesVersion: "v1.0",
|
|
}
|
|
|
|
// No PG rows, no local findings: nothing changes.
|
|
assert.False(t, pushOnce(), "empty -> empty reported a change")
|
|
|
|
// Local gains a finding: the insert is a change.
|
|
require.NoError(t, local.ReplaceSessionSecretFindings(
|
|
sessID, []db.SecretFinding{finding}, 1, "v1.0",
|
|
), "seed finding")
|
|
assert.True(t, pushOnce(), "insert should report change")
|
|
|
|
// Re-pushing the same finding still rewrites rows (delete + insert),
|
|
// which counts as a change.
|
|
assert.True(t, pushOnce(), "rewrite should report change")
|
|
|
|
// Clearing local findings deletes the PG row: that is a change.
|
|
require.NoError(t, local.ReplaceSessionSecretFindings(
|
|
sessID, nil, 0, "v1.0",
|
|
), "clear findings")
|
|
assert.True(t, pushOnce(), "delete should report change")
|
|
|
|
// Back to empty on both sides: nothing changes.
|
|
assert.False(t, pushOnce(), "post-clear empty -> empty reported a change")
|
|
}
|
|
|
|
func TestPGConnectivity(t *testing.T) {
|
|
pgURL := testPGURL(t)
|
|
|
|
local := testDB(t)
|
|
ps, err := New(
|
|
pgURL, "agentsview", local,
|
|
"connectivity-test-machine", true,
|
|
SyncOptions{},
|
|
)
|
|
require.NoError(t, err, "creating sync")
|
|
defer ps.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(
|
|
context.Background(), 10*time.Second,
|
|
)
|
|
defer cancel()
|
|
|
|
require.NoError(t, ps.EnsureSchema(ctx), "ensure schema")
|
|
|
|
status, err := ps.Status(ctx)
|
|
require.NoError(t, err, "get status")
|
|
|
|
t.Logf("PG Sync Status: %+v", status)
|
|
}
|
|
|
|
func TestPGPushCycle(t *testing.T) {
|
|
pgURL := testPGURL(t)
|
|
|
|
cleanPGSchema(t, pgURL)
|
|
t.Cleanup(func() { cleanPGSchema(t, pgURL) })
|
|
|
|
local := testDB(t)
|
|
ps, err := New(
|
|
pgURL, "agentsview", local, "machine-a", true,
|
|
SyncOptions{},
|
|
)
|
|
require.NoError(t, err, "creating sync")
|
|
defer ps.Close()
|
|
|
|
ctx := context.Background()
|
|
require.NoError(t, ps.EnsureSchema(ctx), "ensure schema")
|
|
|
|
started := time.Now().UTC().Format(time.RFC3339)
|
|
firstMsg := "hello from pg"
|
|
sess := db.Session{
|
|
ID: "pg-sess-001",
|
|
Project: "pg-project",
|
|
Machine: "local",
|
|
Agent: "test-agent",
|
|
FirstMessage: &firstMsg,
|
|
StartedAt: &started,
|
|
MessageCount: 1,
|
|
}
|
|
require.NoError(t, local.UpsertSession(sess), "upsert session")
|
|
require.NoError(t, local.InsertMessages([]db.Message{{
|
|
SessionID: "pg-sess-001",
|
|
Ordinal: 0,
|
|
Role: "user",
|
|
Content: firstMsg,
|
|
}}), "insert message")
|
|
|
|
pushResult, err := ps.Push(ctx, false, nil)
|
|
require.NoError(t, err, "push")
|
|
require.Equal(t, 1, pushResult.SessionsPushed)
|
|
require.Equal(t, 1, pushResult.MessagesPushed)
|
|
|
|
status, err := ps.Status(ctx)
|
|
require.NoError(t, err, "status")
|
|
assert.Equal(t, 1, status.PGSessions)
|
|
assert.Equal(t, 1, status.PGMessages)
|
|
}
|