Files
kandev/apps/backend/internal/system/database/reset_test.go
T

153 lines
4.6 KiB
Go

package database
import (
"context"
"errors"
"net/http"
"os"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"github.com/gin-gonic/gin"
"github.com/kandev/kandev/internal/system/jobs"
)
func TestFactoryReset_WrongConfirm_ReturnsError(t *testing.T) {
svc, tracker, _, _ := newTestService(t)
id, err := svc.FactoryReset(context.Background(), "WRONG")
if err == nil {
t.Fatal("expected error for wrong confirm, got nil")
}
if !errors.Is(err, ErrResetNotConfirmed) {
t.Errorf("err = %v, want ErrResetNotConfirmed", err)
}
if id != "" {
t.Errorf("expected empty id when not confirmed, got %q", id)
}
if len(tracker.List()) != 0 {
t.Errorf("no jobs should be started when confirm fails; got %d", len(tracker.List()))
}
}
func TestFactoryReset_Confirmed_RunsFullSequence(t *testing.T) {
svc, tracker, _, dataDir := newTestService(t)
var shutdownCalls atomic.Int32
svc.OrchestratorShutdown = func() { shutdownCalls.Add(1) }
id, err := svc.FactoryReset(context.Background(), "RESET")
if err != nil {
t.Fatalf("FactoryReset: %v", err)
}
if id == "" {
t.Fatal("expected non-empty job id")
}
job := waitForState(t, tracker, id, jobs.StateSucceeded)
if job.State != jobs.StateSucceeded {
t.Fatalf("state = %s, want succeeded; message=%s", job.State, job.Message)
}
// Snapshot path should be in the result map and the file must exist.
rawPath, ok := job.Result["snapshot_path"]
if !ok {
t.Fatalf("result missing snapshot_path: %+v", job.Result)
}
snapshotPath, _ := rawPath.(string)
if snapshotPath == "" {
t.Fatalf("snapshot_path empty")
}
if !strings.HasPrefix(filepath.Base(snapshotPath), "kandev-pre-reset-") {
t.Errorf("snapshot filename %q should start with kandev-pre-reset-", filepath.Base(snapshotPath))
}
if _, err := os.Stat(snapshotPath); err != nil {
t.Errorf("snapshot file missing: %v", err)
}
// Path must live inside <dataDir>/backups
if filepath.Dir(snapshotPath) != filepath.Join(dataDir, "backups") {
t.Errorf("snapshot dir = %s, want %s", filepath.Dir(snapshotPath), filepath.Join(dataDir, "backups"))
}
// tables_dropped must reflect both seeded user tables.
dropped, _ := job.Result["tables_dropped"].(int)
if dropped < 2 {
t.Errorf("tables_dropped = %d, want >= 2 (users, sessions_t)", dropped)
}
// Verify user tables are gone, kandev_meta is kept.
rows, err := svc.pool.Reader().Query(`
SELECT name FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%'
`)
if err != nil {
t.Fatalf("query sqlite_master: %v", err)
}
defer func() { _ = rows.Close() }()
var remaining []string
for rows.Next() {
var n string
if scanErr := rows.Scan(&n); scanErr != nil {
t.Fatalf("scan: %v", scanErr)
}
remaining = append(remaining, n)
}
if len(remaining) != 1 || remaining[0] != "kandev_meta" {
t.Errorf("remaining tables = %v, want [kandev_meta]", remaining)
}
// Wiped subdirs must be gone.
for _, p := range []string{svc.dirs.Worktrees, svc.dirs.Repos, svc.dirs.Sessions, svc.dirs.Tasks, svc.dirs.QuickChat} {
if _, err := os.Stat(p); !os.IsNotExist(err) {
t.Errorf("subdir %s still exists (err=%v)", p, err)
}
}
// restart_required must be set so the frontend dialog prompts the user
// to quit and relaunch Kandev (no auto re-exec).
if got, _ := job.Result["restart_required"].(bool); !got {
t.Errorf("restart_required missing or false in job result: %+v", job.Result)
}
if shutdownCalls.Load() != 1 {
t.Errorf("OrchestratorShutdown called %d times, want 1", shutdownCalls.Load())
}
}
func TestHandleReset_WrongConfirm_Returns400(t *testing.T) {
svc, _, _, _ := newTestService(t)
gin.SetMode(gin.TestMode)
r := gin.New()
r.POST("/reset", HandleReset(svc))
w := serveHTTP(r, httpPost(t, "/reset", `{"confirm":"NOPE"}`))
if w.Code != http.StatusBadRequest {
t.Fatalf("status = %d body=%s", w.Code, w.Body.String())
}
}
func TestHandleReset_Confirmed_Returns202WithJobID(t *testing.T) {
svc, tracker, _, _ := newTestService(t)
// FactoryReset no longer re-execs; just install a no-op shutdown so the
// orchestrator hook fires without side effects.
svc.OrchestratorShutdown = func() {}
gin.SetMode(gin.TestMode)
r := gin.New()
r.POST("/reset", HandleReset(svc))
w := serveHTTP(r, httpPost(t, "/reset", `{"confirm":"RESET"}`))
if w.Code != http.StatusAccepted {
t.Fatalf("status = %d body=%s", w.Code, w.Body.String())
}
if !contains(w.Body.String(), `"job_id"`) {
t.Errorf("body missing job_id: %s", w.Body.String())
}
// Make sure the spawned job finishes before the test exits so the temp dir cleanup
// doesn't race with VACUUM INTO.
for _, j := range tracker.List() {
waitForState(t, tracker, j.ID, jobs.StateSucceeded)
}
}