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

466 lines
14 KiB
Go

//go:build !(windows && arm64)
package duckdb
import (
"context"
"database/sql"
"path/filepath"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.kenn.io/agentsview/internal/export"
)
func TestOpenCreatesLocalDuckDBFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "agentsview.duckdb")
db, err := Open(path)
require.NoError(t, err, "Open")
t.Cleanup(func() {
require.NoError(t, db.Close(), "close DuckDB")
})
require.NoError(t, db.PingContext(context.Background()))
assert.FileExists(t, path)
}
func TestOpenRejectsEmptyPath(t *testing.T) {
db, err := Open("")
require.Error(t, err)
assert.Nil(t, db)
assert.Contains(t, err.Error(), "duckdb path is required")
}
func TestEnsureSchemaCreatesRequiredMirrorTables(t *testing.T) {
ctx := context.Background()
db := openTestDuckDB(t)
require.NoError(t, EnsureSchema(ctx, db), "EnsureSchema")
for _, table := range []string{
"sync_metadata",
"sessions",
"messages",
"usage_events",
"cursor_usage_events",
"model_pricing",
"tool_calls",
"tool_result_events",
"secret_findings",
"starred_sessions",
"pinned_messages",
} {
assert.True(t, tableExists(t, db, table), "missing table %s", table)
}
var version string
require.NoError(t, db.QueryRowContext(ctx,
`SELECT value FROM sync_metadata WHERE key = ?`,
schemaVersionMetadataKey,
).Scan(&version))
assert.Equal(t, strconv.Itoa(SchemaVersion), version)
var repaired string
require.NoError(t, db.QueryRowContext(ctx,
`SELECT value FROM sync_metadata WHERE key = ?`,
defaultRepairMetadataKey,
).Scan(&repaired))
assert.Equal(t, "1", repaired)
var dedupIndex string
require.NoError(t, db.QueryRowContext(ctx,
`SELECT value FROM sync_metadata WHERE key = ?`,
usageDedupIndexMetadataKey,
).Scan(&dedupIndex))
assert.Equal(t, "1", dedupIndex)
var remoteScrubbed string
require.NoError(t, db.QueryRowContext(ctx,
`SELECT value FROM sync_metadata WHERE key = ?`,
projectIdentityRemoteScrubMetadataKey,
).Scan(&remoteScrubbed))
assert.Equal(t, "1", remoteScrubbed)
}
func TestUsageEventsDedupIndexAllowsRepeatedKeys(t *testing.T) {
ctx := context.Background()
db := openTestDuckDB(t)
require.NoError(t, EnsureSchema(ctx, db), "EnsureSchema")
_, err := db.ExecContext(ctx, `
INSERT INTO usage_events (id, session_id, source, model, dedup_key)
VALUES
(1, 's1', 'hermes', 'claude-test', ''),
(2, 's1', 'hermes', 'claude-test', '')`)
require.NoError(t, err)
_, err = db.ExecContext(ctx, `
INSERT INTO usage_events (id, session_id, source, model, dedup_key)
VALUES
(3, 's1', 'hermes', 'claude-test', 'same-key'),
(4, 's1', 'hermes', 'claude-test', 'same-key')`)
require.NoError(t, err)
}
func TestEnsureSchemaIsIdempotent(t *testing.T) {
ctx := context.Background()
db := openTestDuckDB(t)
require.NoError(t, EnsureSchema(ctx, db), "first EnsureSchema")
require.NoError(t, EnsureSchema(ctx, db), "second EnsureSchema")
assert.True(t, columnExists(t, db, "sessions", "secret_leak_count"))
assert.True(t, columnExists(t, db, "messages", "thinking_text"))
}
func TestEnsureSchemaAddsMissingColumnsNonDestructively(t *testing.T) {
ctx := context.Background()
db := openTestDuckDB(t)
_, err := db.ExecContext(ctx,
`CREATE TABLE sessions (
id TEXT PRIMARY KEY,
machine TEXT NOT NULL,
project TEXT NOT NULL,
agent TEXT NOT NULL,
message_count INTEGER,
relationship_type TEXT,
is_automated BOOLEAN
)`,
)
require.NoError(t, err)
_, err = db.ExecContext(ctx,
`INSERT INTO sessions (
id, machine, project, agent,
message_count, relationship_type, is_automated
) VALUES (?, ?, ?, ?, NULL, NULL, NULL)`,
"kept", "mac", "alpha", "claude",
)
require.NoError(t, err)
require.NoError(t, EnsureSchema(ctx, db), "EnsureSchema")
assert.True(t, columnExists(t, db, "sessions", "ended_at"))
var project string
require.NoError(t, db.QueryRowContext(ctx,
`SELECT project FROM sessions WHERE id = ?`, "kept",
).Scan(&project))
assert.Equal(t, "alpha", project)
var messageCount int
var relationshipType string
var isAutomated bool
require.NoError(t, db.QueryRowContext(ctx,
`SELECT message_count, relationship_type, is_automated
FROM sessions WHERE id = ?`, "kept",
).Scan(&messageCount, &relationshipType, &isAutomated))
assert.Equal(t, 0, messageCount)
assert.Equal(t, "", relationshipType)
assert.False(t, isAutomated)
}
func TestEnsureSchemaMigratesMessagesIDPrimaryKey(t *testing.T) {
ctx := context.Background()
db := openTestDuckDB(t)
_, err := db.ExecContext(ctx, `
CREATE TABLE messages (
id BIGINT PRIMARY KEY,
session_id TEXT NOT NULL,
ordinal INTEGER NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
UNIQUE(session_id, ordinal)
)`)
require.NoError(t, err)
_, err = db.ExecContext(ctx, `
INSERT INTO messages (id, session_id, ordinal, role, content)
VALUES (1, 'from-other-machine', 0, 'user', 'kept')`)
require.NoError(t, err)
require.NoError(t, EnsureSchema(ctx, db), "EnsureSchema")
hasPrimary, err := tableHasPrimaryKey(ctx, db, "messages")
require.NoError(t, err)
assert.False(t, hasPrimary)
_, err = db.ExecContext(ctx, `
INSERT INTO messages (id, session_id, ordinal, role, content)
VALUES (1, 'from-this-machine', 0, 'user', 'same local rowid')`)
require.NoError(t, err)
assertDuckDBCountWhere(t, db, "messages", "id = ?", int64(1), 2)
}
func TestCheckSchemaCompatRejectsPendingNonIndexRepairs(t *testing.T) {
ctx := context.Background()
t.Run("messages id primary key", func(t *testing.T) {
db := openTestDuckDB(t)
require.NoError(t, EnsureSchema(ctx, db), "EnsureSchema")
recreateMessagesWithIDPrimaryKey(t, ctx, db)
err := CheckSchemaCompat(ctx, db)
require.Error(t, err)
assert.Contains(t, err.Error(), "messages.id primary key")
})
t.Run("missing default repair metadata", func(t *testing.T) {
db := openTestDuckDB(t)
require.NoError(t, EnsureSchema(ctx, db), "EnsureSchema")
_, err := db.ExecContext(ctx,
`DELETE FROM sync_metadata WHERE key = ?`,
defaultRepairMetadataKey,
)
require.NoError(t, err)
err = CheckSchemaCompat(ctx, db)
require.Error(t, err)
assert.Contains(t, err.Error(), defaultRepairMetadataKey)
})
t.Run("missing usage repair metadata", func(t *testing.T) {
db := openTestDuckDB(t)
require.NoError(t, EnsureSchema(ctx, db), "EnsureSchema")
_, err := db.ExecContext(ctx,
`DELETE FROM sync_metadata WHERE key = ?`,
usageDedupIndexMetadataKey,
)
require.NoError(t, err)
err = CheckSchemaCompat(ctx, db)
require.Error(t, err)
assert.Contains(t, err.Error(), usageDedupIndexMetadataKey)
})
t.Run("missing project identity remote scrub metadata", func(t *testing.T) {
db := openTestDuckDB(t)
require.NoError(t, EnsureSchema(ctx, db), "EnsureSchema")
_, err := db.ExecContext(ctx,
`DELETE FROM sync_metadata WHERE key = ?`,
projectIdentityRemoteScrubMetadataKey,
)
require.NoError(t, err)
err = CheckSchemaCompat(ctx, db)
require.Error(t, err)
assert.Contains(t, err.Error(), projectIdentityRemoteScrubMetadataKey)
})
t.Run("quack incompatible timestamp default", func(t *testing.T) {
db := openTestDuckDB(t)
require.NoError(t, EnsureSchema(ctx, db), "EnsureSchema")
_, err := db.ExecContext(ctx,
`ALTER TABLE starred_sessions ALTER COLUMN created_at SET DEFAULT current_timestamp`,
)
require.NoError(t, err)
err = CheckSchemaCompat(ctx, db)
require.Error(t, err)
assert.Contains(t, err.Error(), "starred_sessions.created_at")
})
}
func TestEnsureSchemaScrubsProjectIdentityGitRemoteCredentials(t *testing.T) {
ctx := context.Background()
db := openTestDuckDB(t)
require.NoError(t, EnsureSchema(ctx, db), "initial EnsureSchema")
_, err := db.ExecContext(ctx,
`DELETE FROM sync_metadata WHERE key = ?`,
projectIdentityRemoteScrubMetadataKey,
)
require.NoError(t, err)
rawRemote := "https://" + "user:token@" + "github.com/acme/app.git"
storedRemote := "https://github.com/acme/app.git"
_, err = db.ExecContext(ctx, `
INSERT INTO source_project_identity_observations (
project, machine, root_path, git_remote, git_remote_name,
worktree_name, worktree_root_path, observed_at,
normalized_remote, key_source, key
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
"app", "laptop", "/tmp/app", rawRemote, "origin",
"", "", "2026-07-03T12:00:00Z", "", "", "",
)
require.NoError(t, err)
_, err = db.ExecContext(ctx, `
INSERT INTO source_project_identity_observations (
project, machine, root_path, git_remote, git_remote_name,
worktree_name, worktree_root_path, observed_at,
normalized_remote, key_source, key
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
"app", "laptop", "/tmp/app", "", "",
"app", "/tmp/app", "2026-07-03T11:00:00Z", "", "", "",
)
require.NoError(t, err)
require.NoError(t, EnsureSchema(ctx, db), "repair EnsureSchema")
rows, err := db.QueryContext(ctx, `
SELECT git_remote, normalized_remote, key_source, key
FROM source_project_identity_observations
WHERE project = ? AND machine = ? AND root_path = ?
ORDER BY git_remote`,
"app", "laptop", "/tmp/app",
)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, rows.Close()) })
var got []export.ProjectIdentityObservation
for rows.Next() {
var obs export.ProjectIdentityObservation
require.NoError(t, rows.Scan(
&obs.GitRemote,
&obs.NormalizedRemote,
&obs.KeySource,
&obs.Key,
))
got = append(got, obs)
}
require.NoError(t, rows.Err())
require.Len(t, got, 1)
assert.Equal(t, storedRemote, got[0].GitRemote)
assert.Equal(t, "github.com/acme/app", got[0].NormalizedRemote)
assert.Equal(t, export.ProjectIdentityKeySourceGitRemote, got[0].KeySource)
assert.NotEmpty(t, got[0].Key)
}
func TestEnsureSchemaDropsQuackIncompatibleTimestampDefaults(t *testing.T) {
ctx := context.Background()
db := openTestDuckDB(t)
_, err := db.ExecContext(ctx, `
CREATE TABLE starred_sessions (
session_id TEXT PRIMARY KEY,
created_at TIMESTAMP NOT NULL DEFAULT current_timestamp
)`)
require.NoError(t, err)
require.NoError(t, EnsureSchema(ctx, db), "EnsureSchema")
assert.NotContains(
t,
strings.ToLower(columnDefaultValue(t, db, "starred_sessions", "created_at")),
"current_timestamp",
)
_, err = db.ExecContext(ctx,
`INSERT INTO starred_sessions (session_id, created_at)
VALUES (?, current_timestamp)`,
"kept",
)
require.NoError(t, err)
}
func TestCheckSchemaCompatReportsMissingTablesAndColumns(t *testing.T) {
ctx := context.Background()
db := openTestDuckDB(t)
err := CheckSchemaCompat(ctx, db)
require.Error(t, err)
assert.Contains(t, err.Error(), "missing table sessions")
db = openTestDuckDB(t)
_, err = db.ExecContext(ctx,
`CREATE TABLE sessions (
id TEXT PRIMARY KEY,
machine TEXT NOT NULL,
project TEXT NOT NULL,
agent TEXT NOT NULL
)`,
)
require.NoError(t, err)
err = CheckSchemaCompat(ctx, db)
require.Error(t, err)
assert.Contains(t, err.Error(), "sessions.secret_leak_count")
}
func TestCheckSchemaCompatPassesAfterEnsureSchema(t *testing.T) {
ctx := context.Background()
db := openTestDuckDB(t)
require.NoError(t, EnsureSchema(ctx, db), "EnsureSchema")
require.NoError(t, CheckSchemaCompat(ctx, db), "CheckSchemaCompat")
}
// TestEnsureSchemaCreatesToolCallsFilePathIndex verifies the DuckDB mirror
// builds idx_tool_calls_file_path, the parity counterpart to SQLite's
// Recent Edits index. DuckDB has no partial indexes, so it omits the
// WHERE file_path IS NOT NULL clause but indexes the same column.
func TestEnsureSchemaCreatesToolCallsFilePathIndex(t *testing.T) {
ctx := context.Background()
db := openTestDuckDB(t)
require.NoError(t, EnsureSchema(ctx, db), "EnsureSchema")
var count int
require.NoError(t, db.QueryRowContext(ctx, `
SELECT count(*) FROM duckdb_indexes()
WHERE table_name = 'tool_calls'
AND index_name = 'idx_tool_calls_file_path'`).Scan(&count),
"query duckdb_indexes")
assert.Equal(t, 1, count,
"idx_tool_calls_file_path must exist for Recent Edits parity")
}
func openTestDuckDB(t *testing.T) *sql.DB {
t.Helper()
db, err := openDuckDB("")
require.NoError(t, err, "open in-memory DuckDB")
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
require.NoError(t, configureDuckDBThreads(db))
t.Cleanup(func() {
require.NoError(t, db.Close(), "close DuckDB")
})
return db
}
func recreateMessagesWithIDPrimaryKey(t *testing.T, ctx context.Context, db *sql.DB) {
t.Helper()
_, err := db.ExecContext(ctx, `DROP TABLE messages`)
require.NoError(t, err)
create := strings.Replace(
mirrorTableCreate("messages"),
"id BIGINT,",
"id BIGINT PRIMARY KEY,",
1,
)
require.NotEqual(t, mirrorTableCreate("messages"), create)
_, err = db.ExecContext(ctx, create)
require.NoError(t, err)
_, err = db.ExecContext(ctx, `
INSERT INTO messages (id, session_id, ordinal, role, content)
VALUES (1, 'from-other-machine', 0, 'user', 'kept')`)
require.NoError(t, err)
}
func tableExists(t *testing.T, db *sql.DB, table string) bool {
t.Helper()
var exists bool
require.NoError(t, db.QueryRow(
`SELECT count(*) > 0
FROM information_schema.tables
WHERE table_schema = current_schema()
AND table_name = ?`,
strings.ToLower(table),
).Scan(&exists))
return exists
}
func columnExists(t *testing.T, db *sql.DB, table, column string) bool {
t.Helper()
var exists bool
require.NoError(t, db.QueryRow(
`SELECT count(*) > 0
FROM information_schema.columns
WHERE table_schema = current_schema()
AND table_name = ?
AND column_name = ?`,
strings.ToLower(table),
strings.ToLower(column),
).Scan(&exists))
return exists
}
func columnDefaultValue(t *testing.T, db *sql.DB, table, column string) string {
t.Helper()
value, err := columnDefault(context.Background(), db, table, column)
require.NoError(t, err)
return value
}