Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

441 lines
14 KiB
Go

package persistence
import (
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func openTempSidecar(t *testing.T) *SidecarStore {
t.Helper()
path := filepath.Join(t.TempDir(), "sidecar.sqlite")
st, err := OpenSidecar(path)
require.NoError(t, err)
require.NotNil(t, st)
t.Cleanup(func() { _ = st.Close() })
return st
}
func TestSidecar_OpenEmptyPathIsNoOp(t *testing.T) {
st, err := OpenSidecar("")
require.NoError(t, err)
require.Nil(t, st)
}
func TestSidecar_SameAbsPathReusesHandle(t *testing.T) {
path := filepath.Join(t.TempDir(), "sidecar.sqlite")
a, err := OpenSidecar(path)
require.NoError(t, err)
b, err := OpenSidecar(path)
require.NoError(t, err)
require.Same(t, a, b, "same absolute path must return the cached handle")
t.Cleanup(func() { _ = a.Close() })
}
func TestSidecar_NotesRoundTrip(t *testing.T) {
st := openTempSidecar(t)
now := time.Now().UTC().Truncate(time.Nanosecond)
in := NoteEntry{
ID: "nt-1",
Timestamp: now,
UpdatedAt: now,
SessionID: "sess-1",
ClientName: "claude-code",
Body: "decision: switch to fastpath",
SymbolID: "pkg/foo.go::Bar",
FilePath: "pkg/foo.go",
RepoPrefix: "core",
WorkspaceID: "ws-a",
ProjectID: "proj-a",
Tags: []string{"decision", "perf"},
AutoLinks: []string{"pkg/foo.go::Bar"},
Pinned: true,
}
require.NoError(t, st.UpsertNote("rk", in))
rows, err := st.LoadNotesRows("rk")
require.NoError(t, err)
require.Len(t, rows, 1)
got := rows[0]
assert.Equal(t, in.ID, got.ID)
assert.Equal(t, in.SessionID, got.SessionID)
assert.Equal(t, in.ClientName, got.ClientName)
assert.Equal(t, in.Body, got.Body)
assert.Equal(t, in.SymbolID, got.SymbolID)
assert.Equal(t, in.FilePath, got.FilePath)
assert.Equal(t, in.WorkspaceID, got.WorkspaceID)
assert.Equal(t, in.Tags, got.Tags)
assert.Equal(t, in.AutoLinks, got.AutoLinks)
assert.True(t, got.Pinned)
assert.WithinDuration(t, in.UpdatedAt, got.UpdatedAt, time.Microsecond)
// Scope isolation: another repo_key sees nothing.
other, err := st.LoadNotesRows("other")
require.NoError(t, err)
require.Empty(t, other)
// Delete.
require.NoError(t, st.DeleteNote("rk", "nt-1"))
rows, err = st.LoadNotesRows("rk")
require.NoError(t, err)
require.Empty(t, rows)
}
func TestSidecar_NotesTrimKeepsPinnedAndNewest(t *testing.T) {
st := openTempSidecar(t)
base := time.Now().UTC()
for i := 0; i < 10; i++ {
require.NoError(t, st.UpsertNote("rk", NoteEntry{
ID: noteID(i),
Timestamp: base.Add(time.Duration(i) * time.Second),
UpdatedAt: base.Add(time.Duration(i) * time.Second),
Pinned: i == 0 || i == 5,
}))
}
require.NoError(t, st.TrimNotes("rk", 6))
rows, err := st.LoadNotesRows("rk")
require.NoError(t, err)
require.Len(t, rows, 6)
ids := map[string]bool{}
for _, r := range rows {
ids[r.ID] = true
}
assert.True(t, ids[noteID(0)], "pinned[0] survives")
assert.True(t, ids[noteID(5)], "pinned[5] survives")
assert.True(t, ids[noteID(9)], "newest survives")
assert.False(t, ids[noteID(1)], "oldest non-pinned dropped")
}
func TestSidecar_MemoriesRoundTrip(t *testing.T) {
st := openTempSidecar(t)
now := time.Now().UTC()
in := MemoryEntry{
ID: "mem-1",
Timestamp: now,
UpdatedAt: now,
LastAccessed: now,
AccessCount: 7,
Body: "lock invariant for Bar",
Title: "Bar lock invariant",
Kind: "invariant",
Source: "manual",
Confidence: 0.8,
Importance: 5,
AuthorAgent: "claude-code",
SymbolIDs: []string{"pkg/foo.go::Bar"},
FilePaths: []string{"pkg/foo.go"},
AutoLinks: []string{"pkg/foo.go::Baz"},
Tags: []string{"invariant", "lock"},
WorkspaceID: "ws-a",
ProjectID: "proj-a",
RepoPrefix: "core",
Pinned: true,
SupersededBy: "mem-2",
}
require.NoError(t, st.UpsertMemory("rk", in))
rows, err := st.LoadMemoriesRows("rk")
require.NoError(t, err)
require.Len(t, rows, 1)
got := rows[0]
assert.Equal(t, in.ID, got.ID)
assert.Equal(t, in.Title, got.Title)
assert.Equal(t, in.Kind, got.Kind)
assert.Equal(t, in.Source, got.Source)
assert.InDelta(t, in.Confidence, got.Confidence, 1e-6)
assert.Equal(t, in.Importance, got.Importance)
assert.Equal(t, in.AuthorAgent, got.AuthorAgent)
assert.Equal(t, in.SymbolIDs, got.SymbolIDs)
assert.Equal(t, in.FilePaths, got.FilePaths)
assert.Equal(t, in.AutoLinks, got.AutoLinks)
assert.Equal(t, in.Tags, got.Tags)
assert.Equal(t, uint64(7), got.AccessCount)
assert.Equal(t, "mem-2", got.SupersededBy)
assert.True(t, got.Pinned)
require.NoError(t, st.DeleteMemory("rk", "mem-1"))
rows, err = st.LoadMemoriesRows("rk")
require.NoError(t, err)
require.Empty(t, rows)
}
func TestSidecar_MemoriesTrimTwoPass(t *testing.T) {
st := openTempSidecar(t)
base := time.Now().UTC()
for i := 0; i < 10; i++ {
e := MemoryEntry{
ID: memID(i),
Timestamp: base.Add(time.Duration(i) * time.Second),
UpdatedAt: base.Add(time.Duration(i) * time.Second),
Importance: 4,
}
if i == 2 || i == 4 {
e.Importance = 1
}
if i == 7 {
e.Pinned = true
e.Importance = 1
}
require.NoError(t, st.UpsertMemory("rk", e))
}
require.NoError(t, st.TrimMemories("rk", 6))
rows, err := st.LoadMemoriesRows("rk")
require.NoError(t, err)
require.Len(t, rows, 6)
ids := map[string]bool{}
for _, r := range rows {
ids[r.ID] = true
}
assert.True(t, ids[memID(7)], "pinned low-imp survives")
assert.False(t, ids[memID(2)], "low-imp dropped")
assert.False(t, ids[memID(4)], "low-imp dropped")
}
func TestSidecar_ScopesRoundTrip(t *testing.T) {
st := openTempSidecar(t)
require.NoError(t, st.UpsertScope(ScopeRow{
Name: "backend", Description: "be", Repos: []string{"api", "core"}, Paths: []string{"services/x"},
}))
require.NoError(t, st.UpsertScope(ScopeRow{Name: "frontend", Repos: []string{"web"}}))
rows, err := st.LoadScopes()
require.NoError(t, err)
require.Len(t, rows, 2)
assert.Equal(t, "backend", rows[0].Name)
assert.Equal(t, []string{"api", "core"}, rows[0].Repos)
assert.Equal(t, []string{"services/x"}, rows[0].Paths)
assert.Equal(t, 2, st.ScopeCount())
require.NoError(t, st.DeleteScope("backend"))
rows, err = st.LoadScopes()
require.NoError(t, err)
require.Len(t, rows, 1)
assert.Equal(t, "frontend", rows[0].Name)
}
func TestSidecar_NotebookRoundTrip(t *testing.T) {
st := openTempSidecar(t)
now := time.Now().UTC()
in := NotebookRow{
ID: "nb-1",
Title: "design: sidecar",
Body: "use sqlite\nfor durability",
Tags: []string{"design", "storage"},
SymbolIDs: []string{"pkg/p.go::Q"},
UsedCount: 3,
LastUsed: now,
Created: now,
Updated: now,
}
require.NoError(t, st.UpsertNotebook("rk", in))
got, ok := st.GetNotebookRow("rk", "nb-1")
require.True(t, ok)
assert.Equal(t, in.Title, got.Title)
assert.Equal(t, in.Body, got.Body)
assert.Equal(t, in.Tags, got.Tags)
assert.Equal(t, in.SymbolIDs, got.SymbolIDs)
assert.Equal(t, uint64(3), got.UsedCount)
rows, err := st.LoadNotebookRows("rk")
require.NoError(t, err)
require.Len(t, rows, 1)
require.NoError(t, st.DeleteNotebook("rk", "nb-1"))
_, ok = st.GetNotebookRow("rk", "nb-1")
require.False(t, ok)
}
func TestSidecar_NotebookPrune(t *testing.T) {
st := openTempSidecar(t)
old := time.Now().UTC().Add(-2 * time.Hour)
fresh := time.Now().UTC()
require.NoError(t, st.UpsertNotebook("rk", NotebookRow{ID: "stale", Updated: old}))
require.NoError(t, st.UpsertNotebook("rk", NotebookRow{ID: "fresh", Updated: fresh, LastUsed: fresh}))
require.NoError(t, st.NotebookPrune("rk", time.Now().UTC().Add(-time.Hour)))
rows, err := st.LoadNotebookRows("rk")
require.NoError(t, err)
require.Len(t, rows, 1)
assert.Equal(t, "fresh", rows[0].ID)
}
// ---------------------------------------------------------------------------
// Migration: legacy gob.gz / json → sqlite.
// ---------------------------------------------------------------------------
func TestSidecar_MigrateLegacyNotes(t *testing.T) {
legacyDir := t.TempDir()
require.NoError(t, SaveNotes(legacyDir, &NoteStore{Entries: []NoteEntry{
{ID: "nt-old", Body: "legacy note", SessionID: "s1", Pinned: true},
}}))
st := openTempSidecar(t)
require.NoError(t, st.MigrateLegacyNotes("rk", legacyDir))
rows, err := st.LoadNotesRows("rk")
require.NoError(t, err)
require.Len(t, rows, 1)
assert.Equal(t, "nt-old", rows[0].ID)
assert.Equal(t, "legacy note", rows[0].Body)
assert.True(t, rows[0].Pinned)
// Legacy file renamed to .bak.
_, errOrig := os.Stat(filepath.Join(legacyDir, notesFile))
assert.Error(t, errOrig, "original gob.gz must be renamed away")
_, errBak := os.Stat(filepath.Join(legacyDir, notesFile+".bak"))
assert.NoError(t, errBak, ".bak must exist")
// Idempotent: a second migrate is a no-op (no duplicate rows).
require.NoError(t, st.MigrateLegacyNotes("rk", legacyDir))
rows, err = st.LoadNotesRows("rk")
require.NoError(t, err)
require.Len(t, rows, 1)
}
func TestSidecar_MigrateLegacyMemories(t *testing.T) {
legacyDir := t.TempDir()
require.NoError(t, SaveMemories(legacyDir, &MemoryStore{Entries: []MemoryEntry{
{ID: "mem-old", Body: "legacy memory", Kind: "invariant", Importance: 5},
}}))
st := openTempSidecar(t)
require.NoError(t, st.MigrateLegacyMemories("rk", legacyDir))
rows, err := st.LoadMemoriesRows("rk")
require.NoError(t, err)
require.Len(t, rows, 1)
assert.Equal(t, "mem-old", rows[0].ID)
assert.Equal(t, "invariant", rows[0].Kind)
_, errBak := os.Stat(filepath.Join(legacyDir, memoriesFile+".bak"))
assert.NoError(t, errBak, ".bak must exist")
}
func TestSidecar_MigrateLegacyScopes(t *testing.T) {
legacyPath := filepath.Join(t.TempDir(), "scopes.json")
require.NoError(t, os.WriteFile(legacyPath, []byte(`[{"name":"be","description":"backend","repos":["api"],"paths":["svc/x"]}]`), 0o644))
st := openTempSidecar(t)
require.NoError(t, st.MigrateLegacyScopes(legacyPath))
rows, err := st.LoadScopes()
require.NoError(t, err)
require.Len(t, rows, 1)
assert.Equal(t, "be", rows[0].Name)
assert.Equal(t, []string{"api"}, rows[0].Repos)
assert.Equal(t, []string{"svc/x"}, rows[0].Paths)
_, errBak := os.Stat(legacyPath + ".bak")
assert.NoError(t, errBak)
// Idempotent.
require.NoError(t, st.MigrateLegacyScopes(legacyPath))
assert.Equal(t, 1, st.ScopeCount())
}
func TestSidecar_MigrateLegacyNotebook(t *testing.T) {
legacyDir := t.TempDir()
md := "---\ntitle: old entry\ntags: [a, b]\nused_count: 4\n---\n\nbody text\n"
require.NoError(t, os.WriteFile(filepath.Join(legacyDir, "nbold.md"), []byte(md), 0o644))
st := openTempSidecar(t)
importMD := func(id, contents string) (NotebookRow, bool) {
// Minimal frontmatter parse for the test importer.
return NotebookRow{ID: id, Title: "old entry", Body: "body text\n", Tags: []string{"a", "b"}, UsedCount: 4}, true
}
require.NoError(t, st.MigrateLegacyNotebook("rk", legacyDir, importMD))
rows, err := st.LoadNotebookRows("rk")
require.NoError(t, err)
require.Len(t, rows, 1)
assert.Equal(t, "nbold", rows[0].ID)
assert.Equal(t, "old entry", rows[0].Title)
assert.Equal(t, uint64(4), rows[0].UsedCount)
_, errBak := os.Stat(filepath.Join(legacyDir, "nbold.md.bak"))
assert.NoError(t, errBak)
}
func TestSidecar_MigrateSkippedWhenTableNonEmpty(t *testing.T) {
legacyDir := t.TempDir()
require.NoError(t, SaveNotes(legacyDir, &NoteStore{Entries: []NoteEntry{{ID: "nt-old", Body: "legacy"}}}))
st := openTempSidecar(t)
// Pre-seed the table so the import is skipped (guard on existing rows).
require.NoError(t, st.UpsertNote("rk", NoteEntry{ID: "nt-existing", Body: "already here"}))
require.NoError(t, st.MigrateLegacyNotes("rk", legacyDir))
rows, err := st.LoadNotesRows("rk")
require.NoError(t, err)
require.Len(t, rows, 1, "import must be skipped when the table already has rows")
assert.Equal(t, "nt-existing", rows[0].ID)
}
func TestSidecar_SuppressionsRoundTrip(t *testing.T) {
st := openTempSidecar(t)
now := time.Now().UTC().Truncate(time.Nanosecond)
in := SuppressionEntry{
IdentityKey: "ik-abc",
Rule: "nil-deref",
Category: "nil-deref",
File: "pkg/x.go",
SymbolID: "pkg/x.go::Foo",
Reason: "guarded by a prior nil check",
Author: "me@zzet.org",
HitCount: 0,
Created: now,
}
require.NoError(t, st.UpsertSuppression("rk", in))
// LoadSuppression hits.
got, ok := st.LoadSuppression("rk", "ik-abc")
require.True(t, ok)
assert.Equal(t, in.IdentityKey, got.IdentityKey)
assert.Equal(t, in.Rule, got.Rule)
assert.Equal(t, in.Category, got.Category)
assert.Equal(t, in.File, got.File)
assert.Equal(t, in.SymbolID, got.SymbolID)
assert.Equal(t, in.Reason, got.Reason)
assert.Equal(t, in.Author, got.Author)
assert.WithinDuration(t, now, got.Created, time.Microsecond)
// Missing identity → clean miss, not an error.
_, ok = st.LoadSuppression("rk", "nope")
assert.False(t, ok)
// Scope isolation: another repo_key sees nothing.
_, ok = st.LoadSuppression("other", "ik-abc")
assert.False(t, ok)
// LoadSuppressions for the repo.
all, err := st.LoadSuppressions("rk")
require.NoError(t, err)
require.Len(t, all, 1)
assert.Equal(t, "ik-abc", all[0].IdentityKey)
// Bump hit twice and confirm counters advance.
require.NoError(t, st.BumpSuppressionHit("rk", "ik-abc", now.Add(time.Second)))
require.NoError(t, st.BumpSuppressionHit("rk", "ik-abc", now.Add(2*time.Second)))
bumped, ok := st.LoadSuppression("rk", "ik-abc")
require.True(t, ok)
assert.Equal(t, int64(2), bumped.HitCount)
assert.WithinDuration(t, now.Add(2*time.Second), bumped.LastHit, time.Microsecond)
// Bumping a missing row is a no-op, not an error.
require.NoError(t, st.BumpSuppressionHit("rk", "nope", now))
// Delete.
require.NoError(t, st.DeleteSuppression("rk", "ik-abc"))
_, ok = st.LoadSuppression("rk", "ik-abc")
assert.False(t, ok)
// Deleting a missing row is not an error.
require.NoError(t, st.DeleteSuppression("rk", "ik-abc"))
}