Files
gastown/internal/mail/mailbox_test.go
Steve Yegge 9c718b0d7d test: Add unit tests for mail router and mailbox (gt-1pelm)
Add comprehensive tests for:
- router_test.go: 7 tests covering detectTownRoot, isTownLevelAddress,
  addressToSessionID, isSelfMail, shouldBeWisp, resolveBeadsDir
- mailbox_test.go: 17 tests for legacy mailbox operations
- types_test.go: Enhanced with additional priority and message tests

Coverage: 57.8% for mail package (legacy mailbox fully tested,
beads integration requires runtime infrastructure)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 16:16:24 -08:00

510 lines
12 KiB
Go

package mail
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"testing"
"time"
)
func TestNewMailbox(t *testing.T) {
m := NewMailbox("/tmp/test")
if m.path != "/tmp/test/inbox.jsonl" {
t.Errorf("NewMailbox path = %q, want %q", m.path, "/tmp/test/inbox.jsonl")
}
if !m.legacy {
t.Error("NewMailbox should create legacy mailbox")
}
}
func TestNewMailboxBeads(t *testing.T) {
m := NewMailboxBeads("gastown/Toast", "/work/dir")
if m.identity != "gastown/Toast" {
t.Errorf("identity = %q, want %q", m.identity, "gastown/Toast")
}
if m.legacy {
t.Error("NewMailboxBeads should not create legacy mailbox")
}
}
func TestMailboxLegacyAppend(t *testing.T) {
tmpDir := t.TempDir()
m := NewMailbox(tmpDir)
msg := &Message{
ID: "msg-001",
From: "mayor/",
To: "gastown/Toast",
Subject: "Test message",
Body: "Hello world",
Timestamp: time.Now(),
}
if err := m.Append(msg); err != nil {
t.Fatalf("Append error: %v", err)
}
// Verify file exists
if _, err := os.Stat(m.path); os.IsNotExist(err) {
t.Fatal("inbox.jsonl was not created")
}
// Verify content
content, err := os.ReadFile(m.path)
if err != nil {
t.Fatalf("ReadFile error: %v", err)
}
var readMsg Message
if err := json.Unmarshal(content[:len(content)-1], &readMsg); err != nil { // -1 for newline
t.Fatalf("Unmarshal error: %v", err)
}
if readMsg.ID != msg.ID {
t.Errorf("ID = %q, want %q", readMsg.ID, msg.ID)
}
}
func TestMailboxLegacyList(t *testing.T) {
tmpDir := t.TempDir()
m := NewMailbox(tmpDir)
// Append multiple messages
msgs := []*Message{
{ID: "msg-001", Subject: "First", Timestamp: time.Now().Add(-2 * time.Hour)},
{ID: "msg-002", Subject: "Second", Timestamp: time.Now().Add(-1 * time.Hour)},
{ID: "msg-003", Subject: "Third", Timestamp: time.Now()},
}
for _, msg := range msgs {
if err := m.Append(msg); err != nil {
t.Fatalf("Append error: %v", err)
}
}
// List should return newest first
listed, err := m.List()
if err != nil {
t.Fatalf("List error: %v", err)
}
if len(listed) != 3 {
t.Fatalf("List returned %d messages, want 3", len(listed))
}
// Verify order (newest first)
if listed[0].ID != "msg-003" {
t.Errorf("First message ID = %q, want msg-003 (newest)", listed[0].ID)
}
if listed[2].ID != "msg-001" {
t.Errorf("Last message ID = %q, want msg-001 (oldest)", listed[2].ID)
}
}
func TestMailboxLegacyGet(t *testing.T) {
tmpDir := t.TempDir()
m := NewMailbox(tmpDir)
msg := &Message{
ID: "msg-001",
Subject: "Test",
Body: "Content",
}
if err := m.Append(msg); err != nil {
t.Fatalf("Append error: %v", err)
}
// Get existing message
got, err := m.Get("msg-001")
if err != nil {
t.Fatalf("Get error: %v", err)
}
if got.Subject != "Test" {
t.Errorf("Subject = %q, want %q", got.Subject, "Test")
}
// Get non-existent message
_, err = m.Get("msg-nonexistent")
if err != ErrMessageNotFound {
t.Errorf("Get non-existent = %v, want ErrMessageNotFound", err)
}
}
func TestMailboxLegacyMarkRead(t *testing.T) {
tmpDir := t.TempDir()
m := NewMailbox(tmpDir)
msg := &Message{
ID: "msg-001",
Read: false,
}
if err := m.Append(msg); err != nil {
t.Fatalf("Append error: %v", err)
}
// Mark as read
if err := m.MarkRead("msg-001"); err != nil {
t.Fatalf("MarkRead error: %v", err)
}
// Verify it's now read
got, err := m.Get("msg-001")
if err != nil {
t.Fatalf("Get error: %v", err)
}
if !got.Read {
t.Error("Message should be marked as read")
}
// Mark non-existent
err = m.MarkRead("msg-nonexistent")
if err != ErrMessageNotFound {
t.Errorf("MarkRead non-existent = %v, want ErrMessageNotFound", err)
}
}
func TestMailboxLegacyDelete(t *testing.T) {
tmpDir := t.TempDir()
m := NewMailbox(tmpDir)
msgs := []*Message{
{ID: "msg-001", Subject: "First"},
{ID: "msg-002", Subject: "Second"},
}
for _, msg := range msgs {
if err := m.Append(msg); err != nil {
t.Fatalf("Append error: %v", err)
}
}
// Delete one
if err := m.Delete("msg-001"); err != nil {
t.Fatalf("Delete error: %v", err)
}
// Verify only one remains
listed, err := m.List()
if err != nil {
t.Fatalf("List error: %v", err)
}
if len(listed) != 1 {
t.Fatalf("List returned %d messages, want 1", len(listed))
}
if listed[0].ID != "msg-002" {
t.Errorf("Remaining message ID = %q, want msg-002", listed[0].ID)
}
// Delete non-existent
err = m.Delete("msg-nonexistent")
if err != ErrMessageNotFound {
t.Errorf("Delete non-existent = %v, want ErrMessageNotFound", err)
}
}
func TestMailboxLegacyCount(t *testing.T) {
tmpDir := t.TempDir()
m := NewMailbox(tmpDir)
// Empty inbox
total, unread, err := m.Count()
if err != nil {
t.Fatalf("Count error: %v", err)
}
if total != 0 || unread != 0 {
t.Errorf("Empty inbox count = (%d, %d), want (0, 0)", total, unread)
}
// Add messages
msgs := []*Message{
{ID: "msg-001", Read: false},
{ID: "msg-002", Read: true},
{ID: "msg-003", Read: false},
}
for _, msg := range msgs {
if err := m.Append(msg); err != nil {
t.Fatalf("Append error: %v", err)
}
}
total, unread, err = m.Count()
if err != nil {
t.Fatalf("Count error: %v", err)
}
if total != 3 {
t.Errorf("total = %d, want 3", total)
}
if unread != 2 {
t.Errorf("unread = %d, want 2", unread)
}
}
func TestMailboxLegacyListUnread(t *testing.T) {
tmpDir := t.TempDir()
m := NewMailbox(tmpDir)
msgs := []*Message{
{ID: "msg-001", Read: false},
{ID: "msg-002", Read: true},
{ID: "msg-003", Read: false},
}
for _, msg := range msgs {
if err := m.Append(msg); err != nil {
t.Fatalf("Append error: %v", err)
}
}
unread, err := m.ListUnread()
if err != nil {
t.Fatalf("ListUnread error: %v", err)
}
if len(unread) != 2 {
t.Errorf("ListUnread returned %d, want 2", len(unread))
}
}
func TestMailboxLegacyListByThread(t *testing.T) {
tmpDir := t.TempDir()
m := NewMailbox(tmpDir)
msgs := []*Message{
{ID: "msg-001", ThreadID: "thread-A", Timestamp: time.Now().Add(-2 * time.Hour)},
{ID: "msg-002", ThreadID: "thread-B", Timestamp: time.Now().Add(-1 * time.Hour)},
{ID: "msg-003", ThreadID: "thread-A", Timestamp: time.Now()},
}
for _, msg := range msgs {
if err := m.Append(msg); err != nil {
t.Fatalf("Append error: %v", err)
}
}
// Get thread A
thread, err := m.ListByThread("thread-A")
if err != nil {
t.Fatalf("ListByThread error: %v", err)
}
if len(thread) != 2 {
t.Fatalf("thread-A has %d messages, want 2", len(thread))
}
// Verify oldest first
if thread[0].ID != "msg-001" {
t.Errorf("First thread message = %q, want msg-001 (oldest)", thread[0].ID)
}
// Non-existent thread
empty, err := m.ListByThread("thread-nonexistent")
if err != nil {
t.Fatalf("ListByThread error: %v", err)
}
if len(empty) != 0 {
t.Errorf("Non-existent thread has %d messages, want 0", len(empty))
}
}
func TestMailboxLegacyEmptyInbox(t *testing.T) {
tmpDir := t.TempDir()
m := NewMailbox(tmpDir)
// List on non-existent file should return empty, not error
msgs, err := m.List()
if err != nil {
t.Fatalf("List on empty inbox error: %v", err)
}
if len(msgs) != 0 {
t.Errorf("Empty inbox returned %d messages, want 0", len(msgs))
}
}
func TestMailboxBeadsAppendError(t *testing.T) {
m := NewMailboxBeads("gastown/Toast", "/work/dir")
err := m.Append(&Message{})
if err == nil {
t.Error("Append on beads mailbox should error")
}
}
func TestMailboxIdentityAndPath(t *testing.T) {
// Legacy mailbox
legacy := NewMailbox("/tmp/test")
if legacy.Identity() != "" {
t.Errorf("Legacy mailbox identity = %q, want empty", legacy.Identity())
}
if legacy.Path() != "/tmp/test/inbox.jsonl" {
t.Errorf("Legacy mailbox path = %q, want /tmp/test/inbox.jsonl", legacy.Path())
}
// Beads mailbox
beads := NewMailboxBeads("gastown/Toast", "/work/dir")
if beads.Identity() != "gastown/Toast" {
t.Errorf("Beads mailbox identity = %q, want gastown/Toast", beads.Identity())
}
if beads.Path() != "" {
t.Errorf("Beads mailbox path = %q, want empty", beads.Path())
}
}
func TestMailboxPersistence(t *testing.T) {
tmpDir := t.TempDir()
// Create mailbox and add message
m1 := NewMailbox(tmpDir)
msg := &Message{
ID: "persist-001",
Subject: "Persistent message",
Body: "Should survive reload",
}
if err := m1.Append(msg); err != nil {
t.Fatalf("Append error: %v", err)
}
// Create new mailbox pointing to same location
m2 := NewMailbox(tmpDir)
msgs, err := m2.List()
if err != nil {
t.Fatalf("List error: %v", err)
}
if len(msgs) != 1 {
t.Fatalf("Reloaded mailbox has %d messages, want 1", len(msgs))
}
if msgs[0].Subject != "Persistent message" {
t.Errorf("Subject = %q, want 'Persistent message'", msgs[0].Subject)
}
}
func TestNewMailboxWithBeadsDir(t *testing.T) {
m := NewMailboxWithBeadsDir("gastown/Toast", "/work/dir", "/custom/.beads")
if m.identity != "gastown/Toast" {
t.Errorf("identity = %q, want 'gastown/Toast'", m.identity)
}
if m.beadsDir != "/custom/.beads" {
t.Errorf("beadsDir = %q, want '/custom/.beads'", m.beadsDir)
}
}
func TestMailboxLegacyMultipleOperations(t *testing.T) {
tmpDir := t.TempDir()
m := NewMailbox(tmpDir)
// Append multiple messages
for i := 0; i < 5; i++ {
msg := &Message{
ID: fmt.Sprintf("msg-%03d", i),
Subject: fmt.Sprintf("Subject %d", i),
Body: fmt.Sprintf("Body %d", i),
Read: i%2 == 0, // Alternate read/unread
Timestamp: time.Now().Add(time.Duration(i) * time.Minute),
}
if err := m.Append(msg); err != nil {
t.Fatalf("Append error: %v", err)
}
}
// Delete middle message
if err := m.Delete("msg-002"); err != nil {
t.Fatalf("Delete error: %v", err)
}
// Mark one as read
if err := m.MarkRead("msg-001"); err != nil {
t.Fatalf("MarkRead error: %v", err)
}
// Verify counts
total, unread, err := m.Count()
if err != nil {
t.Fatalf("Count error: %v", err)
}
if total != 4 {
t.Errorf("total = %d, want 4", total)
}
// After marking msg-001 as read, we have: msg-000 (read), msg-001 (read), msg-003 (unread), msg-004 (read)
// So unread = 1
if unread != 1 {
t.Errorf("unread = %d, want 1", unread)
}
}
func TestMailboxLegacyAppendWithMissingDir(t *testing.T) {
tmpDir := t.TempDir()
deepPath := filepath.Join(tmpDir, "deep", "nested", "inbox")
m := NewMailbox(deepPath)
msg := &Message{
ID: "msg-001",
Subject: "Test",
}
// Should create directories
if err := m.Append(msg); err != nil {
t.Fatalf("Append error: %v", err)
}
// Verify file exists
if _, err := os.Stat(m.path); os.IsNotExist(err) {
t.Fatal("inbox.jsonl was not created")
}
}
func TestMailboxLegacyDeleteAll(t *testing.T) {
tmpDir := t.TempDir()
m := NewMailbox(tmpDir)
// Add messages
msgs := []*Message{
{ID: "msg-001"},
{ID: "msg-002"},
}
for _, msg := range msgs {
if err := m.Append(msg); err != nil {
t.Fatalf("Append error: %v", err)
}
}
// Delete all
for _, msg := range msgs {
if err := m.Delete(msg.ID); err != nil {
t.Fatalf("Delete error: %v", err)
}
}
// Should be empty
total, _, err := m.Count()
if err != nil {
t.Fatalf("Count error: %v", err)
}
if total != 0 {
t.Errorf("total = %d, want 0", total)
}
}
func TestMailboxLegacyMarkReadTwice(t *testing.T) {
tmpDir := t.TempDir()
m := NewMailbox(tmpDir)
msg := &Message{ID: "msg-001", Read: false}
if err := m.Append(msg); err != nil {
t.Fatalf("Append error: %v", err)
}
// Mark as read twice
if err := m.MarkRead("msg-001"); err != nil {
t.Fatalf("First MarkRead error: %v", err)
}
if err := m.MarkRead("msg-001"); err != nil {
t.Fatalf("Second MarkRead error: %v", err)
}
// Should still be read
got, err := m.Get("msg-001")
if err != nil {
t.Fatalf("Get error: %v", err)
}
if !got.Read {
t.Error("Message should be marked as read")
}
}