package store import ( "context" "database/sql" "errors" "fmt" "reflect" "testing" "time" "banger/internal/model" ) func sampleGuestSession(id, vmID, name string) model.GuestSession { now := fixedTime() exit := 7 return model.GuestSession{ ID: id, VMID: vmID, Name: name, Backend: "ssh", AttachBackend: "vsock", AttachMode: "rpc", Command: "pi", Args: []string{"--mode", "rpc"}, CWD: "/root/repo", Env: map[string]string{"FOO": "bar"}, StdinMode: model.GuestSessionStdinMode("pipe"), Status: model.GuestSessionStatus("exited"), ExitCode: &exit, GuestPID: 1234, GuestStateDir: "/tmp/guest-" + id, StdoutLogPath: "/tmp/" + id + ".stdout", StderrLogPath: "/tmp/" + id + ".stderr", Tags: map[string]string{"role": "planner"}, LastError: "", Attachable: true, Reattachable: true, LaunchStage: "started", LaunchMessage: "ok", LaunchRawLog: "boot log...", CreatedAt: now, StartedAt: now, UpdatedAt: now, EndedAt: now.Add(time.Minute), } } // openTestStoreWithVMs opens a fresh store seeded with the given VM IDs so // guest_sessions FK constraints are satisfied. Each VM gets a minimal // image it references. func openTestStoreWithVMs(t *testing.T, vmIDs ...string) *Store { t.Helper() ctx := context.Background() store := openTestStore(t) image := sampleImage("stub-image") if err := store.UpsertImage(ctx, image); err != nil { t.Fatalf("UpsertImage: %v", err) } for i, id := range vmIDs { vm := sampleVM(id, image.ID, fmt.Sprintf("172.16.0.%d", i+2)) vm.ID = id if err := store.UpsertVM(ctx, vm); err != nil { t.Fatalf("UpsertVM(%s): %v", id, err) } } return store } func TestGuestSessionUpsertAndGetByID(t *testing.T) { t.Parallel() ctx := context.Background() store := openTestStoreWithVMs(t, "vm-1") session := sampleGuestSession("sess-1", "vm-1", "planner") if err := store.UpsertGuestSession(ctx, session); err != nil { t.Fatalf("UpsertGuestSession: %v", err) } got, err := store.GetGuestSessionByID(ctx, "sess-1") if err != nil { t.Fatalf("GetGuestSessionByID: %v", err) } if !reflect.DeepEqual(got, session) { t.Fatalf("round-trip mismatch:\n got %+v\n want %+v", got, session) } } func TestGuestSessionUpsertIsIdempotent(t *testing.T) { t.Parallel() ctx := context.Background() store := openTestStoreWithVMs(t, "vm-1") session := sampleGuestSession("sess-1", "vm-1", "planner") if err := store.UpsertGuestSession(ctx, session); err != nil { t.Fatalf("UpsertGuestSession (first): %v", err) } // Mutate + re-upsert → existing row updated. session.Command = "pi --other" session.Status = model.GuestSessionStatus("running") session.ExitCode = nil if err := store.UpsertGuestSession(ctx, session); err != nil { t.Fatalf("UpsertGuestSession (second): %v", err) } got, err := store.GetGuestSessionByID(ctx, "sess-1") if err != nil { t.Fatalf("GetGuestSessionByID: %v", err) } if got.Command != "pi --other" { t.Errorf("command = %q, want 'pi --other'", got.Command) } if got.Status != model.GuestSessionStatus("running") { t.Errorf("status = %q, want running", got.Status) } if got.ExitCode != nil { t.Errorf("ExitCode = %v, want nil after clearing", got.ExitCode) } } func TestGetGuestSessionByIDOrName(t *testing.T) { t.Parallel() ctx := context.Background() store := openTestStoreWithVMs(t, "vm-1") session := sampleGuestSession("sess-1", "vm-1", "planner") if err := store.UpsertGuestSession(ctx, session); err != nil { t.Fatalf("UpsertGuestSession: %v", err) } byID, err := store.GetGuestSession(ctx, "vm-1", "sess-1") if err != nil { t.Fatalf("GetGuestSession by ID: %v", err) } if byID.ID != "sess-1" { t.Errorf("by-ID: got %q, want sess-1", byID.ID) } byName, err := store.GetGuestSession(ctx, "vm-1", "planner") if err != nil { t.Fatalf("GetGuestSession by name: %v", err) } if byName.Name != "planner" { t.Errorf("by-name: got %q, want planner", byName.Name) } // Scoped to the VM. if _, err := store.GetGuestSession(ctx, "vm-unknown", "sess-1"); !errors.Is(err, sql.ErrNoRows) { t.Errorf("wrong-vm lookup = %v, want sql.ErrNoRows", err) } } func TestListGuestSessionsByVMOrdersByCreatedAt(t *testing.T) { t.Parallel() ctx := context.Background() store := openTestStoreWithVMs(t, "vm-1", "vm-2") base := fixedTime() first := sampleGuestSession("sess-early", "vm-1", "first") first.CreatedAt = base second := sampleGuestSession("sess-late", "vm-1", "second") second.CreatedAt = base.Add(time.Hour) other := sampleGuestSession("sess-other", "vm-2", "other") for _, s := range []model.GuestSession{second, first, other} { if err := store.UpsertGuestSession(ctx, s); err != nil { t.Fatalf("UpsertGuestSession: %v", err) } } sessions, err := store.ListGuestSessionsByVM(ctx, "vm-1") if err != nil { t.Fatalf("ListGuestSessionsByVM: %v", err) } if len(sessions) != 2 { t.Fatalf("len = %d, want 2 (vm-1 only)", len(sessions)) } if sessions[0].ID != "sess-early" || sessions[1].ID != "sess-late" { t.Fatalf("order: got %q, %q; want sess-early, sess-late", sessions[0].ID, sessions[1].ID) } empty, err := store.ListGuestSessionsByVM(ctx, "vm-unknown") if err != nil { t.Fatalf("ListGuestSessionsByVM (unknown vm): %v", err) } if len(empty) != 0 { t.Fatalf("unknown vm sessions = %+v, want empty", empty) } } func TestDeleteGuestSession(t *testing.T) { t.Parallel() ctx := context.Background() store := openTestStoreWithVMs(t, "vm-1") session := sampleGuestSession("sess-1", "vm-1", "planner") if err := store.UpsertGuestSession(ctx, session); err != nil { t.Fatalf("UpsertGuestSession: %v", err) } if err := store.DeleteGuestSession(ctx, "sess-1"); err != nil { t.Fatalf("DeleteGuestSession: %v", err) } if _, err := store.GetGuestSessionByID(ctx, "sess-1"); !errors.Is(err, sql.ErrNoRows) { t.Fatalf("after delete err = %v, want sql.ErrNoRows", err) } // Deleting something that doesn't exist is a no-op (matches SQL DELETE semantics). if err := store.DeleteGuestSession(ctx, "sess-nope"); err != nil { t.Fatalf("DeleteGuestSession on missing row: %v", err) } }