chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,440 @@
|
||||
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"))
|
||||
}
|
||||
Reference in New Issue
Block a user