package store import ( "context" "path/filepath" "testing" "time" ) func TestOpenCreatesSchema(t *testing.T) { ctx := context.Background() path := filepath.Join(t.TempDir(), "nested", "email_agent.db") s, err := Open(ctx, path) if err != nil { t.Fatalf("Open() error = %v", err) } defer s.Close() v, err := s.SchemaVersion(ctx) if err != nil { t.Fatalf("SchemaVersion() error = %v", err) } if v < 1 { t.Fatalf("SchemaVersion() = %d, want >= 1", v) } wantTables := []string{ "messages", "classifications", "rules", "actions_log", "unsubscribe", "drafts", "summaries", } for _, table := range wantTables { var name string err := s.DB().QueryRowContext(ctx, `SELECT name FROM sqlite_master WHERE type='table' AND name=?`, table).Scan(&name) if err != nil { t.Errorf("table %q missing: %v", table, err) } } } func TestOpenIsIdempotent(t *testing.T) { ctx := context.Background() path := filepath.Join(t.TempDir(), "email_agent.db") s1, err := Open(ctx, path) if err != nil { t.Fatalf("first Open() error = %v", err) } _ = s1.Close() s2, err := Open(ctx, path) if err != nil { t.Fatalf("second Open() error = %v", err) } defer s2.Close() if err := s2.Ping(ctx); err != nil { t.Fatalf("Ping() error = %v", err) } } func TestMessagesInsertAndRead(t *testing.T) { ctx := context.Background() s, err := Open(ctx, filepath.Join(t.TempDir(), "email_agent.db")) if err != nil { t.Fatalf("Open() error = %v", err) } defer s.Close() _, err = s.DB().ExecContext(ctx, `INSERT INTO messages (gmail_id, subject, source_address) VALUES (?, ?, ?)`, "abc123", "Hello", "hello@where-woof.com") if err != nil { t.Fatalf("insert message: %v", err) } var subject string if err := s.DB().QueryRowContext(ctx, `SELECT subject FROM messages WHERE gmail_id = ?`, "abc123").Scan(&subject); err != nil { t.Fatalf("select message: %v", err) } if subject != "Hello" { t.Errorf("subject = %q, want %q", subject, "Hello") } } func TestUpsertAndListMessages(t *testing.T) { ctx := context.Background() s, err := Open(ctx, filepath.Join(t.TempDir(), "email_agent.db")) if err != nil { t.Fatalf("Open() error = %v", err) } defer s.Close() m := &Message{ GmailID: "1234000000000000000", ThreadID: "999", FromAddr: "favrolfe@gmail.com", ToAddr: "Hello@where-woof.com", Subject: "A forwarded hello", BodyText: "body text", SourceAddress: "Hello@where-woof.com", DateSent: time.Date(2026, 10, 1, 9, 30, 0, 0, time.UTC), IMAPUID: 42, IMAPUIDValidity: 7, } id, err := s.UpsertMessage(ctx, m) if err != nil { t.Fatalf("UpsertMessage() error = %v", err) } if id == 0 { t.Fatal("UpsertMessage() returned id 0") } // A second upsert with the same Gmail id must update, not duplicate, and // must not move received_at. m.Subject = "Updated subject" id2, err := s.UpsertMessage(ctx, m) if err != nil { t.Fatalf("second UpsertMessage() error = %v", err) } if id2 != id { t.Errorf("second upsert id = %d, want %d", id2, id) } n, err := s.CountMessages(ctx) if err != nil { t.Fatalf("CountMessages() error = %v", err) } if n != 1 { t.Fatalf("CountMessages() = %d, want 1", n) } list, err := s.ListMessages(ctx, 10) if err != nil { t.Fatalf("ListMessages() error = %v", err) } if len(list) != 1 { t.Fatalf("ListMessages() len = %d, want 1", len(list)) } got := list[0] if got.Subject != "Updated subject" { t.Errorf("Subject = %q, want updated value", got.Subject) } if got.SourceAddress != "Hello@where-woof.com" { t.Errorf("SourceAddress = %q", got.SourceAddress) } if got.IMAPUID != 42 || got.IMAPUIDValidity != 7 { t.Errorf("IMAP bookkeeping = (%d, %d), want (42, 7)", got.IMAPUID, got.IMAPUIDValidity) } if got.HasClassification { t.Error("HasClassification = true with no classifications row") } } func TestMigrationAddsIMAPColumns(t *testing.T) { ctx := context.Background() s, err := Open(ctx, filepath.Join(t.TempDir(), "email_agent.db")) if err != nil { t.Fatalf("Open() error = %v", err) } defer s.Close() v, err := s.SchemaVersion(ctx) if err != nil { t.Fatalf("SchemaVersion() error = %v", err) } if v < 2 { t.Errorf("SchemaVersion() = %d, want >= 2", v) } for _, col := range []string{"imap_uid", "imap_uidvalidity"} { var found bool rows, err := s.DB().QueryContext(ctx, `SELECT name FROM pragma_table_info('messages')`) if err != nil { t.Fatalf("pragma_table_info: %v", err) } for rows.Next() { var name string if err := rows.Scan(&name); err != nil { t.Fatalf("scan: %v", err) } if name == col { found = true } } _ = rows.Close() if !found { t.Errorf("column %q missing after migration", col) } } }