package state_test
import (
"context"
"testing"
"sourcecraft.dev/bigbes/lethe/internal/collector/state"
)
func openStore(t *testing.T) *state.Store {
t.Helper()
ctx := context.Background()
dir := t.TempDir()
s, err := state.Open(ctx, dir+"/test.db")
if err != nil {
t.Fatalf("Open: %v", err)
}
t.Cleanup(func() { _ = s.Close() })
return s
}
func TestOpen_CreatesTables(t *testing.T) {
ctx := context.Background()
dir := t.TempDir()
s, err := state.Open(ctx, dir+"/test.db")
if err != nil {
t.Fatalf("Open: %v", err)
}
defer func() { _ = s.Close() }()
// Verify we can query schema.
var n int
if err := s.DB().Get(&n, `SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'ingestion_state'`); err != nil {
t.Fatalf("query schema: %v", err)
}
if n != 1 {
t.Fatalf("expected ingestion_state table, got %d", n)
}
if err := s.DB().Get(&n, `SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'outbox'`); err != nil {
t.Fatalf("query schema: %v", err)
}
if n != 1 {
t.Fatalf("expected outbox table, got %d", n)
}
}
func TestOpen_Idempotent(t *testing.T) {
ctx := context.Background()
dir := t.TempDir()
path := dir + "/test.db"
if _, err := state.Open(ctx, path); err != nil {
t.Fatalf("first open: %v", err)
}
if _, err := state.Open(ctx, path); err != nil {
t.Fatalf("second open: %v", err)
}
}
func TestGetOffset_MissingReturnsZero(t *testing.T) {
s := openStore(t)
ctx := context.Background()
off, err := s.GetOffset(ctx, "claude-code", "/tmp/foo.jsonl")
if err != nil {
t.Fatalf("GetOffset: %v", err)
}
if off != 0 {
t.Errorf("GetOffset = %d, want 0", off)
}
}
func TestGetOffset_DBErrorPropagated(t *testing.T) {
s := openStore(t)
ctx := context.Background()
if err := s.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
_, err := s.GetOffset(ctx, "claude-code", "/tmp/foo.jsonl")
if err == nil {
t.Fatal("expected error after closing database, got nil")
}
}
func TestSaveOffsetAndGetOffset(t *testing.T) {
s := openStore(t)
ctx := context.Background()
if err := s.SaveOffset(ctx, "claude-code", "/tmp/foo.jsonl", 1234); err != nil {
t.Fatalf("SaveOffset: %v", err)
}
off, err := s.GetOffset(ctx, "claude-code", "/tmp/foo.jsonl")
if err != nil {
t.Fatalf("GetOffset: %v", err)
}
if off != 1234 {
t.Errorf("GetOffset = %d, want 1234", off)
}
}
func TestSaveOffset_UpdatesExisting(t *testing.T) {
s := openStore(t)
ctx := context.Background()
if err := s.SaveOffset(ctx, "claude-code", "/tmp/foo.jsonl", 100); err != nil {
t.Fatalf("SaveOffset: %v", err)
}
if err := s.SaveOffset(ctx, "claude-code", "/tmp/foo.jsonl", 200); err != nil {
t.Fatalf("SaveOffset update: %v", err)
}
off, err := s.GetOffset(ctx, "claude-code", "/tmp/foo.jsonl")
if err != nil {
t.Fatalf("GetOffset: %v", err)
}
if off != 200 {
t.Errorf("GetOffset = %d, want 200", off)
}
}
func TestEnqueueAndOldest(t *testing.T) {
s := openStore(t)
ctx := context.Background()
item := state.OutboxItem{
Tool: "claude-code",
Host: "laptop",
SourceFile: "/tmp/foo.jsonl",
Payload: []byte(`{"test":1}`),
}
if err := s.Enqueue(ctx, item); err != nil {
t.Fatalf("Enqueue: %v", err)
}
rows, err := s.Oldest(ctx, 10)
if err != nil {
t.Fatalf("Oldest: %v", err)
}
if len(rows) != 1 {
t.Fatalf("expected 1 row, got %d", len(rows))
}
if string(rows[0].Payload) != `{"test":1}` {
t.Errorf("Payload = %s", rows[0].Payload)
}
}
func TestOldest_Limit(t *testing.T) {
s := openStore(t)
ctx := context.Background()
for i := range 3 {
item := state.OutboxItem{
Tool: "claude-code",
Host: "laptop",
SourceFile: "/tmp/foo.jsonl",
Payload: []byte(`x`),
}
_ = item // avoid unused if we change loop; we need i for unique payload
item.Payload = []byte(string(rune('a' + i)))
if err := s.Enqueue(ctx, item); err != nil {
t.Fatalf("Enqueue: %v", err)
}
}
rows, err := s.Oldest(ctx, 2)
if err != nil {
t.Fatalf("Oldest: %v", err)
}
if len(rows) != 2 {
t.Errorf("expected 2 rows, got %d", len(rows))
}
}
func TestDelete(t *testing.T) {
s := openStore(t)
ctx := context.Background()
item := state.OutboxItem{
Tool: "claude-code",
Host: "laptop",
SourceFile: "/tmp/foo.jsonl",
Payload: []byte(`x`),
}
if err := s.Enqueue(ctx, item); err != nil {
t.Fatalf("Enqueue: %v", err)
}
rows, _ := s.Oldest(ctx, 10)
if len(rows) != 1 {
t.Fatalf("expected 1 row before delete")
}
if err := s.Delete(ctx, []int64{rows[0].ID}); err != nil {
t.Fatalf("Delete: %v", err)
}
rows, err := s.Oldest(ctx, 10)
if err != nil {
t.Fatalf("Oldest after delete: %v", err)
}
if len(rows) != 0 {
t.Errorf("expected 0 rows after delete, got %d", len(rows))
}
}
func TestStats(t *testing.T) {
s := openStore(t)
ctx := context.Background()
st, err := s.Stats(ctx)
if err != nil {
t.Fatalf("Stats: %v", err)
}
if st.OutboxCount != 0 {
t.Errorf("OutboxCount = %d, want 0", st.OutboxCount)
}
if st.OutboxBytes != 0 {
t.Errorf("OutboxBytes = %d, want 0", st.OutboxBytes)
}
if st.SourceOffsets != 0 {
t.Errorf("SourceOffsets = %d, want 0", st.SourceOffsets)
}
// Add one offset and one outbox item.
if err := s.SaveOffset(ctx, "cc", "/a.jsonl", 42); err != nil {
t.Fatalf("SaveOffset: %v", err)
}
if err := s.Enqueue(ctx, state.OutboxItem{Tool: "cc", Host: "h", SourceFile: "/a.jsonl", Payload: []byte("hello")}); err != nil {
t.Fatalf("Enqueue: %v", err)
}
st, err = s.Stats(ctx)
if err != nil {
t.Fatalf("Stats: %v", err)
}
if st.OutboxCount != 1 {
t.Errorf("OutboxCount = %d, want 1", st.OutboxCount)
}
if st.OutboxBytes != 5 {
t.Errorf("OutboxBytes = %d, want 5", st.OutboxBytes)
}
if st.SourceOffsets != 1 {
t.Errorf("SourceOffsets = %d, want 1", st.SourceOffsets)
}
}