package main // store_extra_test.go — open failure, closed-handle error branches, corrupt rows. import ( "path/filepath" "strings" "testing" ) func TestOpenStoreBadPath(t *testing.T) { if _, err := OpenStore(filepath.Join(t.TempDir(), "occupied.db", "x.db")); err == nil { t.Fatal("OpenStore under a file path must fail") } } func TestStoreClosedHandleErrors(t *testing.T) { store := openTestStore(t) if err := store.Close(); err != nil { t.Fatalf("close: %v", err) } errStr := func(err error) string { if err == nil { return "" } return err.Error() } if err := store.AppendEvent(Event{SessionID: "s", Seq: 1, Type: evAgentSettled}); errStr(err) == "" { t.Fatal("AppendEvent on closed store must error") } if _, err := store.LastSeq("s"); errStr(err) == "" { t.Fatal("LastSeq on closed store must error") } if _, err := store.EventsAfter("s", 0, 10); errStr(err) == "" { t.Fatal("EventsAfter on closed store must error") } if err := store.UpsertSession(SessionInfo{ID: "s"}); errStr(err) == "" { t.Fatal("UpsertSession on closed store must error") } if err := store.TouchSession("s", 1, 1); errStr(err) == "" { t.Fatal("TouchSession on closed store must error") } if err := store.SetOnline("s", true); errStr(err) == "" { t.Fatal("SetOnline on closed store must error") } if _, err := store.Sessions(); errStr(err) == "" { t.Fatal("Sessions on closed store must error") } if _, _, err := store.GetSetting("k"); errStr(err) == "" { t.Fatal("GetSetting on closed store must error") } if err := store.SetSetting("k", "v"); errStr(err) == "" { t.Fatal("SetSetting on closed store must error") } if err := store.DeleteSetting("k"); errStr(err) == "" { t.Fatal("DeleteSetting on closed store must error") } if err := store.UpsertContainer("s", "c", "r"); errStr(err) == "" { t.Fatal("UpsertContainer on closed store must error") } if _, _, err := store.GetContainer("s"); errStr(err) == "" { t.Fatal("GetContainer on closed store must error") } if err := store.DeleteContainer("s"); errStr(err) == "" { t.Fatal("DeleteContainer on closed store must error") } if _, _, err := store.GetRepoImage("r"); errStr(err) == "" { t.Fatal("GetRepoImage on closed store must error") } if err := store.SetRepoImage("r", "lvmh-worker-r"); errStr(err) == "" { t.Fatal("SetRepoImage on closed store must error") } if err := store.DeleteRepoImage("r"); errStr(err) == "" { t.Fatal("DeleteRepoImage on closed store must error") } if _, err := store.ListRepoImages(); errStr(err) == "" { t.Fatal("ListRepoImages on closed store must error") } } func TestStoreCorruptRows(t *testing.T) { store := openTestStore(t) // undecodable info blob → Sessions errors if _, err := store.db.Exec(`INSERT INTO sessions(id, info) VALUES('bad', 'not json')`); err != nil { t.Fatalf("insert corrupt session: %v", err) } if _, err := store.Sessions(); err == nil || !strings.Contains(err.Error(), "decode session info") { t.Fatalf("Sessions with corrupt info = %v", err) } // non-integer seq → EventsAfter scan error if _, err := store.db.Exec(`INSERT INTO events(sessionId, seq, ts, type, payload) VALUES('s1', 'not-a-number', 1, 'x', '{}')`); err != nil { t.Fatalf("insert corrupt event: %v", err) } if _, err := store.EventsAfter("s1", 0, 10); err == nil { t.Fatal("EventsAfter with text seq must scan-error") } if _, err := store.EventsLatest("s1", 10); err == nil { t.Fatal("EventsLatest with text seq must scan-error") } } func TestStoreAppendEventDefaultPayload(t *testing.T) { store := openTestStore(t) if err := store.AppendEvent(Event{SessionID: "s1", Seq: 1, TS: 1, Type: evBye}); err != nil { t.Fatalf("append empty payload: %v", err) } events, err := store.EventsAfter("s1", 0, 10) if err != nil || len(events) != 1 { t.Fatalf("events = %v %v", events, err) } if string(events[0].Payload) != "{}" { t.Fatalf("payload = %q, want {}", events[0].Payload) } } func seedEvents(t *testing.T, store *Store, sessionID string, seqs ...int64) { t.Helper() for _, seq := range seqs { if err := store.AppendEvent(Event{SessionID: sessionID, Seq: seq, TS: seq, Type: evAgentSettled, Payload: []byte(`{}`)}); err != nil { t.Fatalf("append %d: %v", seq, err) } } } func TestStoreEventsLatestAndBefore(t *testing.T) { store := openTestStore(t) seedEvents(t, store, "s1", 1, 2, 3, 4, 5) latest, err := store.EventsLatest("s1", 3) if err != nil { t.Fatalf("EventsLatest: %v", err) } if len(latest) != 3 { t.Fatalf("latest = %d events, want 3", len(latest)) } for i, want := range []int64{3, 4, 5} { if latest[i].Seq != want { t.Fatalf("latest[%d].Seq = %d, want %d (newest window, ascending)", i, latest[i].Seq, want) } } before, err := store.EventsBefore("s1", 4, 2) if err != nil { t.Fatalf("EventsBefore: %v", err) } if len(before) != 2 { t.Fatalf("before = %d events, want 2", len(before)) } for i, want := range []int64{2, 3} { if before[i].Seq != want { t.Fatalf("before[%d].Seq = %d, want %d (newest below 4, ascending)", i, before[i].Seq, want) } } // windows past the boundaries degrade cleanly if all, _ := store.EventsLatest("s1", 100); len(all) != 5 { t.Fatalf("latest over count = %d, want 5", len(all)) } if none, _ := store.EventsBefore("s1", 1, 10); len(none) != 0 { t.Fatalf("before 1 = %d events, want 0", len(none)) } if missing, _ := store.EventsLatest("nope", 10); len(missing) != 0 { t.Fatalf("latest unknown session = %d events, want 0", len(missing)) } } func TestStoreDeleteSessionEvents(t *testing.T) { store := openTestStore(t) for _, e := range []Event{ {SessionID: "s1", Seq: 1, TS: 10, Type: evMessageStart, Payload: []byte(`{}`)}, {SessionID: "s1", Seq: 2, TS: 11, Type: evMessageEnd, Payload: []byte(`{}`)}, {SessionID: "s2", Seq: 1, TS: 12, Type: evAgentSettled, Payload: []byte(`{}`)}, } { if err := store.AppendEvent(e); err != nil { t.Fatalf("append %s#%d: %v", e.SessionID, e.Seq, err) } } deleted, err := store.DeleteSessionEvents("s1") if err != nil || deleted != 2 { t.Fatalf("delete s1 events = %d %v, want 2 nil", deleted, err) } if evts, _ := store.EventsAfter("s1", 0, 100); len(evts) != 0 { t.Fatalf("s1 events after delete = %d, want 0", len(evts)) } if last, _ := store.LastSeq("s1"); last != 0 { t.Fatalf("s1 lastSeq after delete = %d, want 0", last) } // other sessions untouched evts, err := store.EventsAfter("s2", 0, 100) if err != nil || len(evts) != 1 || evts[0].Seq != 1 { t.Fatalf("s2 events = %v %v, want seq 1 only", evts, err) } // unknown / already-cleared session: 0 rows, no error if deleted, err := store.DeleteSessionEvents("s1"); err != nil || deleted != 0 { t.Fatalf("second delete = %d %v, want 0 nil", deleted, err) } if deleted, err := store.DeleteSessionEvents("ghost"); err != nil || deleted != 0 { t.Fatalf("unknown session = %d %v, want 0 nil", deleted, err) } } func TestStoreDeleteSession(t *testing.T) { store := openTestStore(t) name := "gone" if err := store.UpsertSession(SessionInfo{ID: "s1", Name: &name, Cwd: "/w", Model: "glm-5.3", Provider: "zai-renaud", StartedAt: 1}); err != nil { t.Fatalf("upsert s1: %v", err) } if err := store.UpsertSession(SessionInfo{ID: "s2", Cwd: "/w2", Model: "glm-5.3", Provider: "zai-renaud", StartedAt: 2}); err != nil { t.Fatalf("upsert s2: %v", err) } if err := store.UpsertContainer("s1", "cid", "group/proj"); err != nil { t.Fatalf("upsert container: %v", err) } for _, e := range []Event{ {SessionID: "s1", Seq: 1, TS: 10, Type: evMessageStart, Payload: []byte(`{}`)}, {SessionID: "s2", Seq: 1, TS: 10, Type: evMessageStart, Payload: []byte(`{}`)}, } { if err := store.AppendEvent(e); err != nil { t.Fatalf("append: %v", err) } } if err := store.DeleteSession("s1"); err != nil { t.Fatalf("delete session: %v", err) } rows, err := store.Sessions() if err != nil || len(rows) != 1 || rows[0].ID != "s2" { t.Fatalf("sessions after delete = %v %v, want only s2", rows, err) } if evts, _ := store.EventsAfter("s1", 0, 100); len(evts) != 0 { t.Fatalf("s1 events after delete = %d, want 0", len(evts)) } if _, ok, _ := store.GetContainer("s1"); ok { t.Fatal("s1 container row should be gone") } // s2 untouched if evts, _ := store.EventsAfter("s2", 0, 100); len(evts) != 1 { t.Fatalf("s2 events after delete = %d, want 1", len(evts)) } // idempotent: unknown session deletes cleanly if err := store.DeleteSession("ghost"); err != nil { t.Fatalf("delete unknown session: %v", err) } }