wipe: one command empties the box and leaves it standing (V-494)
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
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user