82d9a3324e
There was no documented way to repair a poisoned box. Two invented facts written during QA disabled world answering for every later turn (V-470), and revert voids the SQL row while leaving the vector behind (V-493). This is the operation that undoes both. Store.Wipe drops every table sqlite_master reports and rebuilds from schema.sql plus the migrations, rather than deleting from a hand-written list. A list has to be edited whenever a table is added, and the once it is not, the wipe leaves personal data behind while reporting success. It vacuums afterwards, because free pages still hold readable text. mavend -wipe prints every table and its row count and exits. That alone is a dry run and answers what a QA session actually asks: what is on this box. It deletes only with -confirm-wipe. Two flags, because the destructive reading of one flag is the reading a mistyped command gets. Nothing outside the database moves. Config, models, passkeys.json and the encryption key are files. QA isolation and onboarding are the other two thirds of V-494 and are not here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SoL7EBdYC5Mhz3DJd49GJy
100 lines
2.8 KiB
Go
100 lines
2.8 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// A wipe has to leave a working, empty install: every table still there, every
|
|
// migration still applied, and not one row of his anywhere (Vikunja #494).
|
|
func TestWipeEmptiesEveryTableAndLeavesTheSchemaUsable(t *testing.T) {
|
|
ctx := context.Background()
|
|
path := filepath.Join(t.TempDir(), "wipe.db")
|
|
s, err := Open(ctx, path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
|
|
now := time.Now()
|
|
if _, err := s.WriteFact(ctx, now, KindSelf, "monitor", "новый", "tap:test", 1, sql.NullInt64{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := s.WriteNote(ctx, now, "купил монитор", []float32{0.1, 0.2}, "tap:test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := s.CaptureTask(ctx, Task{Text: "вернуть монитор", Source: "tap:test", Status: TaskOpen, CreatedTs: now}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
before, err := s.WipeCounts(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
populated := 0
|
|
for _, c := range before {
|
|
if c.Rows > 0 {
|
|
populated++
|
|
}
|
|
}
|
|
if populated == 0 {
|
|
t.Fatal("nothing was written, so the wipe proves nothing")
|
|
}
|
|
|
|
if err := s.Wipe(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
after, err := s.WipeCounts(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(after) != len(before) {
|
|
t.Errorf("table count changed across the wipe: %d before, %d after", len(before), len(after))
|
|
}
|
|
for _, c := range after {
|
|
if c.Rows != 0 {
|
|
t.Errorf("table %s still holds %d rows after the wipe", c.Table, c.Rows)
|
|
}
|
|
}
|
|
|
|
// The migrations are what make the schema usable, so the check that matters
|
|
// is a write through the newest columns, not a version number.
|
|
if _, err := s.CaptureTask(ctx, Task{Text: "новая задача", Source: "tap:test", Status: TaskOpen, CreatedTs: now}); err != nil {
|
|
t.Errorf("the store is not usable after a wipe: %v", err)
|
|
}
|
|
if _, err := s.WriteFact(ctx, now, KindSelf, "monitor", "другой", "tap:test", 1, sql.NullInt64{}); err != nil {
|
|
t.Errorf("the store is not usable after a wipe: %v", err)
|
|
}
|
|
}
|
|
|
|
// A wipe that reports success while leaving a table behind is the failure the
|
|
// drop-everything approach exists to prevent, so the count has to see the
|
|
// tables migrations added, not only the ones schema.sql declares.
|
|
func TestWipeCountsSeeMigratedTables(t *testing.T) {
|
|
ctx := context.Background()
|
|
s, err := Open(ctx, filepath.Join(t.TempDir(), "counts.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
|
|
counts, err := s.WipeCounts(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, c := range counts {
|
|
seen[c.Table] = true
|
|
}
|
|
// One from schema.sql, three from migrations, one from a table rebuild.
|
|
for _, want := range []string{"facts", "memory_vectors", "tasks", "ecosystem_traces", "nudges"} {
|
|
if !seen[want] {
|
|
t.Errorf("wipe does not see table %s", want)
|
|
}
|
|
}
|
|
}
|