internal/store/history_test.go
Rama main · réplica de lectura del repositorio original.
package store
import (
"context"
"database/sql"
"errors"
"fmt"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"sync"
"testing"
"time"
)
const migrationsInV1 = 7
const p1 = "https://10.0.0.5:6443"
const p2 = "https://10.0.0.6:6443"
var noon = time.Date(2026, 8, 29, 12, 0, 0, 0, time.UTC)
func dbPath(t *testing.T) string {
t.Helper()
return filepath.Join(t.TempDir(), "spinoza", "history.db")
}
func openHistory(t *testing.T, path string) *Store {
t.Helper()
store, err := Open(t.Context(), path)
if err != nil {
t.Fatalf("open: %v", err)
}
t.Cleanup(func() { _ = store.Close() })
return store
}
func entry(cluster string, at time.Time, name string) Entry {
return Entry{
Cluster: cluster,
At: at,
Verb: "delete",
Actor: "[email protected]",
Group: "apps",
Version: "v1",
Resource: "deployments",
Kind: "Deployment",
Namespace: "default",
Name: name,
Detail: "deleted the deployment",
Outcome: "ok",
Message: "",
}
}
func record(t *testing.T, store *Store, held Entry) {
t.Helper()
if err := store.For(held.Cluster).Record(t.Context(), held); err != nil {
t.Fatalf("record: %v", err)
}
}
func recent(t *testing.T, store *Store, query Query) Page {
t.Helper()
page, err := store.Recent(t.Context(), query)
if err != nil {
t.Fatalf("recent: %v", err)
}
return page
}
func names(page Page) []string {
out := make([]string, 0, len(page.Entries))
for _, held := range page.Entries {
out = append(out, held.Name)
}
return out
}
func pragma(t *testing.T, store *Store, name string) string {
t.Helper()
var value string
err := store.reads.QueryRowContext(t.Context(), "PRAGMA "+name).Scan(&value)
if err != nil {
t.Fatalf("pragma %s: %v", name, err)
}
return value
}
func TestTheDefaultPathSitsBesideTheOtherStores(t *testing.T) {
path, err := DefaultPath()
if err != nil {
t.Skipf("this platform names no config directory: %v", err)
}
if filepath.Base(path) != "history.db" {
t.Fatalf("path = %q, want it to end in history.db", path)
}
if filepath.Base(filepath.Dir(path)) != "spinoza" {
t.Fatalf("path = %q, want it beside settings.json and protected.json", path)
}
}
func TestTheDefaultPathNeedsAConfigDirectory(t *testing.T) {
t.Setenv("HOME", "")
t.Setenv("XDG_CONFIG_HOME", "")
_, err := DefaultPath()
if err == nil {
t.Skip("this platform still names a config directory without HOME")
}
if !strings.Contains(err.Error(), "store") {
t.Fatalf("error = %q, want it to say what it was doing", err.Error())
}
}
func TestTheDatabaseIsOpenedInWalMode(t *testing.T) {
store := openHistory(t, dbPath(t))
if mode := pragma(t, store, "journal_mode"); !strings.EqualFold(mode, "wal") {
t.Fatalf("journal_mode = %q, want wal; two spinoza windows share this file", mode)
}
}
func TestAWriterWaitsForAnotherRatherThanFailing(t *testing.T) {
store := openHistory(t, dbPath(t))
if timeout := pragma(t, store, "busy_timeout"); timeout != "5000" {
t.Fatalf("busy_timeout = %q, want 5000; without it a second window's write fails outright", timeout)
}
}
func TestAWorkingStoreHasNothingToApologiseFor(t *testing.T) {
if reason := openHistory(t, dbPath(t)).Reason(); reason != "" {
t.Fatalf("reason = %q, want none from a healthy store", reason)
}
}
func TestAStoreWithNowhereToWriteStillWorks(t *testing.T) {
store, err := Open(t.Context(), "")
if err != nil {
t.Fatalf("open: %v, want a degraded store rather than a failure", err)
}
if store.Reason() == "" {
t.Fatal("the store did not say why it is not recording")
}
record(t, store, entry(p1, noon, "web"))
if len(recent(t, store, Query{}).Entries) != 0 {
t.Fatal("a store with no database returned rows")
}
if forgetErr := store.Forget(t.Context(), ""); forgetErr != nil {
t.Fatalf("forget: %v", forgetErr)
}
if closeErr := store.Close(); closeErr != nil {
t.Fatalf("close: %v", closeErr)
}
}
func TestAReadOnlyDirectoryDoesNotStopSpinoza(t *testing.T) {
root := t.TempDir()
if err := os.Chmod(root, 0o500); err != nil {
t.Fatalf("chmod: %v", err)
}
t.Cleanup(func() { _ = os.Chmod(root, 0o700) })
store, err := Open(t.Context(), filepath.Join(root, "spinoza", "history.db"))
if err == nil {
t.Skip("this filesystem let the write through")
}
if store == nil {
t.Fatal("Open returned no store; spinoza must still start without history")
}
if !strings.Contains(store.Reason(), "history.db") {
t.Fatalf("reason = %q, want it to name the file it could not use", store.Reason())
}
record(t, store, entry(p1, noon, "web"))
}
func TestACorruptFileDoesNotStopSpinoza(t *testing.T) {
path := dbPath(t)
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
t.Fatalf("mkdir: %v", err)
}
if err := os.WriteFile(path, []byte("this is not a database"), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
store, err := Open(t.Context(), path)
if err == nil {
t.Fatal("a corrupt file opened cleanly")
}
if store.Reason() == "" {
t.Fatal("the store did not say why it is not recording")
}
record(t, store, entry(p1, noon, "web"))
if len(recent(t, store, Query{}).Entries) != 0 {
t.Fatal("a corrupt store returned rows")
}
}
func TestAFileFromANewerSpinozaIsRefusedRatherThanMigrated(t *testing.T) {
path := dbPath(t)
store := openHistory(t, path)
if _, err := store.writes.ExecContext(t.Context(), "PRAGMA user_version = 99"); err != nil {
t.Fatalf("bump: %v", err)
}
if err := store.Close(); err != nil {
t.Fatalf("close: %v", err)
}
reopened, reopenErr := Open(t.Context(), path)
if !errors.Is(reopenErr, errFromTheFuture) {
t.Fatalf("error = %v, want it to refuse a newer schema rather than migrate over it", reopenErr)
}
if !strings.Contains(reopened.Reason(), "newer spinoza") {
t.Fatalf("reason = %q, want it to say the file is from a newer spinoza", reopened.Reason())
}
}
func TestAFileWithAnAdditiveMigrationAheadIsStillOpened(t *testing.T) {
path := dbPath(t)
store := openHistory(t, path)
tabs := store.Tabs()
if err := tabs.Remember(t.Context(), Tab{ID: p1, Context: "p-mk1", Seen: noon}); err != nil {
t.Fatalf("remember: %v", err)
}
if err := tabs.Recording(t.Context(), p1, "workloads"); err != nil {
t.Fatalf("recording: %v", err)
}
if err := store.Close(); err != nil {
t.Fatalf("close: %v", err)
}
saved := migrations
migrations = slices.Clone(saved[:migrationsInV1])
t.Cleanup(func() { migrations = saved })
reopened, reopenErr := Open(t.Context(), path)
t.Cleanup(func() { _ = reopened.Close() })
if reopenErr != nil {
t.Fatalf("error = %v, want v1.30.0 to open a file this spinoza wrote", reopenErr)
}
if reopened.Reason() != "" {
t.Fatalf("reason = %q, want an older spinoza to keep recording", reopened.Reason())
}
found, readErr := reopened.Tabs().All(t.Context())
if readErr != nil {
t.Fatalf("all: %v", readErr)
}
if len(found) != 1 || found[0].Timeline != "workloads" {
t.Fatalf("the tab came back as %+v, want what it recorded before the upgrade", found)
}
}
func TestAFileStampedBeforeTheFloorWasKeptIsRepaired(t *testing.T) {
path := dbPath(t)
store := openHistory(t, path)
if _, err := store.writes.ExecContext(t.Context(), "DROP TABLE schema_progress"); err != nil {
t.Fatalf("drop: %v", err)
}
bump := fmt.Sprintf("PRAGMA user_version = %d", len(migrations))
if _, err := store.writes.ExecContext(t.Context(), bump); err != nil {
t.Fatalf("bump: %v", err)
}
if err := store.Close(); err != nil {
t.Fatalf("close: %v", err)
}
reopened := openHistory(t, path)
var floor int
if err := reopened.writes.QueryRowContext(t.Context(), "PRAGMA user_version").Scan(&floor); err != nil {
t.Fatalf("floor: %v", err)
}
if floor != readableFrom {
t.Fatalf("floor = %d, want it lowered to %d so an older spinoza can still read the file", floor, readableFrom)
}
var applied int
if err := reopened.writes.QueryRowContext(t.Context(), selectProgress).Scan(&applied); err != nil {
t.Fatalf("progress: %v", err)
}
if applied != len(migrations) {
t.Fatalf("applied = %d, want all %d migrations counted", applied, len(migrations))
}
}
func TestTheReadableFloorNamesAMigrationThatExists(t *testing.T) {
if readableFrom < 1 || readableFrom > len(migrations) {
t.Fatalf("readableFrom = %d, want it between 1 and %d, or no file will open", readableFrom, len(migrations))
}
}
func TestASchemaThatWillNotApplyIsReported(t *testing.T) {
saved := migrations
migrations = []string{"CREATE TABLE ("}
t.Cleanup(func() { migrations = saved })
store, err := Open(t.Context(), dbPath(t))
if err == nil {
t.Fatal("a broken migration was accepted")
}
if store.Reason() == "" {
t.Fatal("the store did not say why it is not recording")
}
}
func TestAnAbsentLimitFallsBackToTheDefault(t *testing.T) {
store := openHistory(t, dbPath(t))
for at := range defaultLimit + 5 {
record(t, store, entry(p1, noon.Add(time.Duration(at)*time.Second), "one"))
}
page := recent(t, store, Query{})
if len(page.Entries) != defaultLimit {
t.Fatalf("returned %d entries with no limit asked for, want the default %d", len(page.Entries), defaultLimit)
}
}
func TestAnEnormousLimitIsClampedRatherThanObeyed(t *testing.T) {
store := openHistory(t, dbPath(t))
record(t, store, entry(p1, noon, "web"))
page, err := store.Recent(t.Context(), Query{Limit: maxLimit * 100})
if err != nil {
t.Fatalf("recent: %v", err)
}
if len(page.Entries) != 1 {
t.Fatalf("returned %d entries, want the one that exists", len(page.Entries))
}
}
func TestWhatWasDoneComesBackUnchanged(t *testing.T) {
store := openHistory(t, dbPath(t))
written := entry(p1, noon, "web")
written.Message = "the api server said no"
written.Outcome = "refused"
record(t, store, written)
page := recent(t, store, Query{})
if len(page.Entries) != 1 {
t.Fatalf("found %d entries, want 1", len(page.Entries))
}
got := page.Entries[0]
got.ID = 0
if got != written {
t.Fatalf("entry = %+v, want %+v", got, written)
}
}
func TestTheInstantSurvivesToTheMillisecond(t *testing.T) {
store := openHistory(t, dbPath(t))
at := time.Date(2026, 8, 29, 12, 0, 0, 123_000_000, time.UTC)
record(t, store, entry(p1, at, "web"))
got := recent(t, store, Query{}).Entries[0].At
if !got.Equal(at) {
t.Fatalf("at = %s, want %s; the stored unit must be milliseconds", got, at)
}
}
func TestAnInstantInAnotherZoneComesBackAsUTC(t *testing.T) {
store := openHistory(t, dbPath(t))
zone := time.FixedZone("CEST", 2*60*60)
at := time.Date(2026, 8, 29, 14, 0, 0, 0, zone)
record(t, store, entry(p1, at, "web"))
got := recent(t, store, Query{}).Entries[0].At
if !got.Equal(at) {
t.Fatalf("at = %s, want the same instant as %s", got, at)
}
if got.Location() != time.UTC {
t.Fatalf("location = %s, want UTC so two windows agree on what a row says", got.Location())
}
}
func TestEveryRowCarriesAnIdentityOfItsOwn(t *testing.T) {
store := openHistory(t, dbPath(t))
record(t, store, entry(p1, noon, "first"))
record(t, store, entry(p1, noon, "second"))
page := recent(t, store, Query{})
if page.Entries[0].ID == page.Entries[1].ID {
t.Fatal("two rows share an id; a list has no stable key to draw them by")
}
if page.Entries[0].ID == 0 {
t.Fatal("a row came back without an id")
}
}
func TestHistorySurvivesARestart(t *testing.T) {
path := dbPath(t)
store := openHistory(t, path)
record(t, store, entry(p1, noon, "web"))
if err := store.Close(); err != nil {
t.Fatalf("close: %v", err)
}
page := recent(t, openHistory(t, path), Query{})
if len(page.Entries) != 1 {
t.Fatalf("found %d entries after reopening, want 1", len(page.Entries))
}
}
func TestTheNewestIsFirst(t *testing.T) {
store := openHistory(t, dbPath(t))
record(t, store, entry(p1, noon, "older"))
record(t, store, entry(p1, noon.Add(time.Minute), "newer"))
page := recent(t, store, Query{})
if got := names(page); got[0] != "newer" {
t.Fatalf("order = %v, want the newest first", got)
}
}
func TestTwoEntriesAtTheSameInstantKeepAStableOrder(t *testing.T) {
store := openHistory(t, dbPath(t))
record(t, store, entry(p1, noon, "first"))
record(t, store, entry(p1, noon, "second"))
page := recent(t, store, Query{})
if got := names(page); got[0] != "second" || got[1] != "first" {
t.Fatalf("order = %v, want the later write first so rows do not shuffle between polls", got)
}
}
func TestOneClusterAtATime(t *testing.T) {
store := openHistory(t, dbPath(t))
record(t, store, entry(p1, noon, "here"))
record(t, store, entry(p2, noon, "elsewhere"))
page := recent(t, store, Query{Cluster: p1})
if got := names(page); len(got) != 1 || got[0] != "here" {
t.Fatalf("entries = %v, want only the one from the cluster asked for", got)
}
}
func TestNoClusterMeansEveryCluster(t *testing.T) {
store := openHistory(t, dbPath(t))
record(t, store, entry(p1, noon, "here"))
record(t, store, entry(p2, noon, "elsewhere"))
if got := names(recent(t, store, Query{})); len(got) != 2 {
t.Fatalf("entries = %v, want both clusters", got)
}
}
func TestAPageThatLeftSomethingOutSaysSo(t *testing.T) {
store := openHistory(t, dbPath(t))
for at := range 4 {
record(t, store, entry(p1, noon.Add(time.Duration(at)*time.Second), "one"))
}
page := recent(t, store, Query{Limit: 2})
if len(page.Entries) != 2 {
t.Fatalf("returned %d entries, want 2", len(page.Entries))
}
if !page.More {
t.Fatal("the page dropped two entries and did not say so")
}
}
func TestAPageThatHeldEverythingSaysThatToo(t *testing.T) {
store := openHistory(t, dbPath(t))
record(t, store, entry(p1, noon, "web"))
page := recent(t, store, Query{Limit: 2})
if page.More {
t.Fatal("a complete page claimed there was more")
}
}
func TestForgettingClearsEverything(t *testing.T) {
store := openHistory(t, dbPath(t))
record(t, store, entry(p1, noon, "web"))
record(t, store, entry(p2, noon, "elsewhere"))
if err := store.Forget(t.Context(), ""); err != nil {
t.Fatalf("forget: %v", err)
}
if got := names(recent(t, store, Query{})); len(got) != 0 {
t.Fatalf("entries = %v, want none after clearing", got)
}
}
func TestForgettingRollsBackWhenOneHistoryTableCannotBeCleared(t *testing.T) {
store := openHistory(t, dbPath(t))
record(t, store, entry(p1, noon, "web"))
err := store.Timeline(p1).Note(t.Context(), []Change{{At: noon, Name: "web"}})
if err != nil {
t.Fatalf("record change: %v", err)
}
_, triggerErr := store.writes.ExecContext(t.Context(), `
CREATE TRIGGER refuse_change_delete
BEFORE DELETE ON changes
BEGIN
SELECT RAISE(ABORT, 'changes cannot be deleted');
END`)
if triggerErr != nil {
t.Fatalf("create trigger: %v", triggerErr)
}
forgetErr := store.Forget(t.Context(), p1)
if forgetErr == nil {
t.Fatal("forgetting succeeded despite the second table refusing its delete")
}
if got := names(recent(t, store, Query{Cluster: p1})); len(got) != 1 {
t.Fatalf("audit entries = %v, want the first delete rolled back", got)
}
changes, changedErr := store.Changed(t.Context(), Query{Cluster: p1})
if changedErr != nil {
t.Fatalf("read changes: %v", changedErr)
}
if len(changes.Rows) != 1 {
t.Fatalf("changes = %d, want the original row", len(changes.Rows))
}
}
func TestTwoStoresWritingAtOnceBothLand(t *testing.T) {
path := dbPath(t)
one := openHistory(t, path)
two := openHistory(t, path)
const each = 25
var wg sync.WaitGroup
for _, store := range []*Store{one, two} {
wg.Add(1)
go func(into *Store) {
defer wg.Done()
for at := range each {
held := entry(p1, noon.Add(time.Duration(at)*time.Second), "web")
if err := into.For(p1).Record(context.Background(), held); err != nil {
t.Errorf("record: %v", err)
return
}
}
}(store)
}
wg.Wait()
page := recent(t, one, Query{Limit: maxLimit})
if len(page.Entries) != each*2 {
t.Fatalf("found %d entries, want %d; a second window's writes were lost", len(page.Entries), each*2)
}
}
func TestAWindowSeesWhatTheOtherWrote(t *testing.T) {
path := dbPath(t)
one := openHistory(t, path)
two := openHistory(t, path)
record(t, one, entry(p1, noon, "written by the first"))
if got := names(recent(t, two, Query{})); len(got) != 1 {
t.Fatalf("entries = %v, want the other window's write to be visible", got)
}
}
func TestMigratingTwiceChangesNothing(t *testing.T) {
path := dbPath(t)
store := openHistory(t, path)
record(t, store, entry(p1, noon, "web"))
if err := store.Close(); err != nil {
t.Fatalf("close: %v", err)
}
again := openHistory(t, path)
if len(recent(t, again, Query{}).Entries) != 1 {
t.Fatal("reopening re-ran the schema and lost what was recorded")
}
}
func TestAnOlderAuditRowIsKeptWithAnUnknownActor(t *testing.T) {
path := dbPath(t)
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
t.Fatalf("mkdir: %v", err)
}
db := sql.OpenDB(connector{dsn: path + pragmas})
for version, statements := range migrations[:len(migrations)-1] {
if err := apply(t.Context(), db, statements, version+1); err != nil {
t.Fatalf("apply version %d: %v", version+1, err)
}
}
_, err := db.ExecContext(
t.Context(), `
INSERT INTO audit (
cluster, at, verb, api_group, api_version, resource, kind,
namespace, name, detail, outcome, message
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
p1, noon.UnixMilli(), "delete", "apps", "v1", "deployments", "Deployment",
"default", "web", "deleted the deployment", "done", "",
)
if err != nil {
t.Fatalf("insert legacy audit row: %v", err)
}
if err := db.Close(); err != nil {
t.Fatalf("close legacy database: %v", err)
}
got := recent(t, openHistory(t, path), Query{}).Entries
if len(got) != 1 {
t.Fatalf("entries = %d, want the legacy row", len(got))
}
if got[0].Actor != "unknown" {
t.Fatalf("actor = %q, want unknown for a row written before actors were recorded", got[0].Actor)
}
if got[0].Name != "web" {
t.Fatalf("name = %q, want the legacy row to survive", got[0].Name)
}
}
func TestClosingTwiceIsFine(t *testing.T) {
store := openHistory(t, dbPath(t))
if err := store.Close(); err != nil {
t.Fatalf("close: %v", err)
}
if err := store.Close(); err != nil {
t.Fatalf("second close: %v", err)
}
}
func TestADatabaseThatStopsAnsweringIsReported(t *testing.T) {
store := openHistory(t, dbPath(t))
store.mu.Lock()
for _, db := range []*sql.DB{store.writes, store.reads} {
if err := db.Close(); err != nil {
t.Fatalf("close: %v", err)
}
}
store.mu.Unlock()
if err := store.For(p1).Record(t.Context(), entry(p1, noon, "web")); err == nil {
t.Fatal("recording into a closed database reported success")
}
if _, err := store.Recent(t.Context(), Query{}); err == nil {
t.Fatal("reading a closed database reported success")
}
if err := store.Forget(t.Context(), ""); err == nil {
t.Fatal("clearing a closed database reported success")
}
}
func TestAnEntryWithNoRoomForItIsReported(t *testing.T) {
store := openHistory(t, dbPath(t))
if _, err := store.writes.ExecContext(t.Context(), "DROP TABLE audit"); err != nil {
t.Fatalf("drop: %v", err)
}
if recordErr := store.For(p1).Record(t.Context(), entry(p1, noon, "web")); recordErr == nil {
t.Fatal("recording into a missing table reported success")
}
if _, readErr := store.Recent(t.Context(), Query{}); readErr == nil {
t.Fatal("reading a missing table reported success")
}
}
func TestAColumnThatChangedShapeIsReported(t *testing.T) {
store := openHistory(t, dbPath(t))
record(t, store, entry(p1, noon, "web"))
if _, err := store.writes.ExecContext(t.Context(), "UPDATE audit SET at = 'not a number'"); err != nil {
t.Fatalf("corrupt: %v", err)
}
_, err := store.Recent(t.Context(), Query{})
if err == nil {
t.Fatal("a row that could not be read came back as a silent zero")
}
if !strings.Contains(err.Error(), "store") {
t.Fatalf("error = %q, want it to say what it was doing", err.Error())
}
}
func TestTheAuditHalfIsBoundedByItsOwnCursor(t *testing.T) {
store := openHistory(t, dbPath(t))
for at := range 5 {
record(t, store, entry(p1, noon.Add(time.Duration(at)*time.Minute), "row-"+strconv.Itoa(at)))
}
whole := recent(t, store, Query{})
if len(whole.Entries) != 5 {
t.Fatalf("read %d rows", len(whole.Entries))
}
boundary := whole.Entries[1].ID
below := recent(t, store, Query{AfterAction: boundary})
if len(below.Entries) != 3 {
t.Fatalf("read %d rows below the cursor, want the three older ones", len(below.Entries))
}
for _, one := range below.Entries {
if one.ID >= boundary {
t.Fatalf("row %d came back at or above the cursor %d", one.ID, boundary)
}
}
}
func TestAuditPaginationFollowsTimestampOrder(t *testing.T) {
store := openHistory(t, dbPath(t))
record(t, store, entry(p1, noon, "oldest"))
record(t, store, entry(p1, noon.Add(2*time.Minute), "newest"))
record(t, store, entry(p1, noon.Add(time.Minute), "middle"))
first := recent(t, store, Query{Limit: 2})
second := recent(t, store, Query{Limit: 2, AfterAction: first.Entries[1].ID})
if got := names(first); !slices.Equal(got, []string{"newest", "middle"}) {
t.Fatalf("first page = %v", got)
}
if got := names(second); !slices.Equal(got, []string{"oldest"}) {
t.Fatalf("second page = %v", got)
}
}
func TestNoAuditCursorReadsFromTheTop(t *testing.T) {
store := openHistory(t, dbPath(t))
record(t, store, entry(p1, noon, "only"))
if len(recent(t, store, Query{AfterAction: 0}).Entries) != 1 {
t.Fatal("a zero cursor hid the newest row")
}
}