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) } }