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>
This commit is contained in:
509
internal/mail/mailbox_test.go
Normal file
509
internal/mail/mailbox_test.go
Normal file
@@ -0,0 +1,509 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
221
internal/mail/router_test.go
Normal file
221
internal/mail/router_test.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDetectTownRoot(t *testing.T) {
|
||||
// Create temp directory structure
|
||||
tmpDir := t.TempDir()
|
||||
townRoot := filepath.Join(tmpDir, "town")
|
||||
mayorDir := filepath.Join(townRoot, "mayor")
|
||||
rigDir := filepath.Join(townRoot, "gastown", "polecats", "test")
|
||||
|
||||
// Create mayor/town.json marker
|
||||
if err := os.MkdirAll(mayorDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(mayorDir, "town.json"), []byte("{}"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(rigDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
startDir string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "from town root",
|
||||
startDir: townRoot,
|
||||
want: townRoot,
|
||||
},
|
||||
{
|
||||
name: "from rig subdirectory",
|
||||
startDir: rigDir,
|
||||
want: townRoot,
|
||||
},
|
||||
{
|
||||
name: "from mayor directory",
|
||||
startDir: mayorDir,
|
||||
want: townRoot,
|
||||
},
|
||||
{
|
||||
name: "from non-town directory",
|
||||
startDir: tmpDir,
|
||||
want: "", // No town.json marker above tmpDir
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := detectTownRoot(tt.startDir)
|
||||
if got != tt.want {
|
||||
t.Errorf("detectTownRoot(%q) = %q, want %q", tt.startDir, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsTownLevelAddress(t *testing.T) {
|
||||
tests := []struct {
|
||||
address string
|
||||
want bool
|
||||
}{
|
||||
{"mayor", true},
|
||||
{"mayor/", true},
|
||||
{"deacon", true},
|
||||
{"deacon/", true},
|
||||
{"gastown/refinery", false},
|
||||
{"gastown/polecats/Toast", false},
|
||||
{"gastown/", false},
|
||||
{"", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.address, func(t *testing.T) {
|
||||
got := isTownLevelAddress(tt.address)
|
||||
if got != tt.want {
|
||||
t.Errorf("isTownLevelAddress(%q) = %v, want %v", tt.address, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddressToSessionID(t *testing.T) {
|
||||
tests := []struct {
|
||||
address string
|
||||
want string
|
||||
}{
|
||||
{"mayor", "gt-mayor"},
|
||||
{"mayor/", "gt-mayor"},
|
||||
{"gastown/refinery", "gt-gastown-refinery"},
|
||||
{"gastown/Toast", "gt-gastown-Toast"},
|
||||
{"beads/witness", "gt-beads-witness"},
|
||||
{"gastown/", ""}, // Empty target
|
||||
{"gastown", ""}, // No slash
|
||||
{"", ""}, // Empty address
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.address, func(t *testing.T) {
|
||||
got := addressToSessionID(tt.address)
|
||||
if got != tt.want {
|
||||
t.Errorf("addressToSessionID(%q) = %q, want %q", tt.address, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSelfMail(t *testing.T) {
|
||||
tests := []struct {
|
||||
from string
|
||||
to string
|
||||
want bool
|
||||
}{
|
||||
{"mayor/", "mayor/", true},
|
||||
{"mayor", "mayor/", true},
|
||||
{"mayor/", "mayor", true},
|
||||
{"gastown/Toast", "gastown/Toast", true},
|
||||
{"gastown/Toast/", "gastown/Toast", true},
|
||||
{"mayor/", "deacon/", false},
|
||||
{"gastown/Toast", "gastown/Nux", false},
|
||||
{"", "", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.from+"->"+tt.to, func(t *testing.T) {
|
||||
got := isSelfMail(tt.from, tt.to)
|
||||
if got != tt.want {
|
||||
t.Errorf("isSelfMail(%q, %q) = %v, want %v", tt.from, tt.to, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldBeWisp(t *testing.T) {
|
||||
r := &Router{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
msg *Message
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "explicit wisp flag",
|
||||
msg: &Message{Subject: "Regular message", Wisp: true},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "POLECAT_STARTED subject",
|
||||
msg: &Message{Subject: "POLECAT_STARTED: Toast"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "polecat_done subject (lowercase)",
|
||||
msg: &Message{Subject: "polecat_done: work complete"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "NUDGE subject",
|
||||
msg: &Message{Subject: "NUDGE: check your hook"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "START_WORK subject",
|
||||
msg: &Message{Subject: "START_WORK: gt-123"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "regular message",
|
||||
msg: &Message{Subject: "Please review this PR"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "handoff message (not auto-wisp)",
|
||||
msg: &Message{Subject: "HANDOFF: context notes"},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := r.shouldBeWisp(tt.msg)
|
||||
if got != tt.want {
|
||||
t.Errorf("shouldBeWisp(%v) = %v, want %v", tt.msg.Subject, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveBeadsDir(t *testing.T) {
|
||||
// With town root set
|
||||
r := NewRouterWithTownRoot("/work/dir", "/home/user/gt")
|
||||
got := r.resolveBeadsDir("gastown/Toast")
|
||||
want := "/home/user/gt/.beads"
|
||||
if got != want {
|
||||
t.Errorf("resolveBeadsDir with townRoot = %q, want %q", got, want)
|
||||
}
|
||||
|
||||
// Without town root (fallback to workDir)
|
||||
r2 := &Router{workDir: "/work/dir", townRoot: ""}
|
||||
got2 := r2.resolveBeadsDir("mayor/")
|
||||
want2 := "/work/dir/.beads"
|
||||
if got2 != want2 {
|
||||
t.Errorf("resolveBeadsDir without townRoot = %q, want %q", got2, want2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRouterWithTownRoot(t *testing.T) {
|
||||
r := NewRouterWithTownRoot("/work/rig", "/home/gt")
|
||||
if r.workDir != "/work/rig" {
|
||||
t.Errorf("workDir = %q, want '/work/rig'", r.workDir)
|
||||
}
|
||||
if r.townRoot != "/home/gt" {
|
||||
t.Errorf("townRoot = %q, want '/home/gt'", r.townRoot)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package mail
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAddressToIdentity(t *testing.T) {
|
||||
tests := []struct {
|
||||
@@ -66,3 +69,283 @@ func TestIdentityToAddress(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPriorityToBeads(t *testing.T) {
|
||||
tests := []struct {
|
||||
priority Priority
|
||||
expected int
|
||||
}{
|
||||
{PriorityUrgent, 0},
|
||||
{PriorityHigh, 1},
|
||||
{PriorityNormal, 2},
|
||||
{PriorityLow, 3},
|
||||
{Priority("unknown"), 2}, // Default to normal
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.priority), func(t *testing.T) {
|
||||
got := PriorityToBeads(tt.priority)
|
||||
if got != tt.expected {
|
||||
t.Errorf("PriorityToBeads(%q) = %d, want %d", tt.priority, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPriorityFromInt(t *testing.T) {
|
||||
tests := []struct {
|
||||
p int
|
||||
expected Priority
|
||||
}{
|
||||
{0, PriorityUrgent},
|
||||
{1, PriorityHigh},
|
||||
{2, PriorityNormal},
|
||||
{3, PriorityLow},
|
||||
{4, PriorityLow}, // Out of range maps to low
|
||||
{-1, PriorityNormal}, // Negative maps to normal
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := PriorityFromInt(tt.p)
|
||||
if got != tt.expected {
|
||||
t.Errorf("PriorityFromInt(%d) = %q, want %q", tt.p, got, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePriority(t *testing.T) {
|
||||
tests := []struct {
|
||||
s string
|
||||
expected Priority
|
||||
}{
|
||||
{"urgent", PriorityUrgent},
|
||||
{"high", PriorityHigh},
|
||||
{"normal", PriorityNormal},
|
||||
{"low", PriorityLow},
|
||||
{"unknown", PriorityNormal}, // Default
|
||||
{"", PriorityNormal}, // Empty
|
||||
{"URGENT", PriorityNormal}, // Case-sensitive, defaults to normal
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.s, func(t *testing.T) {
|
||||
got := ParsePriority(tt.s)
|
||||
if got != tt.expected {
|
||||
t.Errorf("ParsePriority(%q) = %q, want %q", tt.s, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMessageType(t *testing.T) {
|
||||
tests := []struct {
|
||||
s string
|
||||
expected MessageType
|
||||
}{
|
||||
{"task", TypeTask},
|
||||
{"scavenge", TypeScavenge},
|
||||
{"notification", TypeNotification},
|
||||
{"reply", TypeReply},
|
||||
{"unknown", TypeNotification}, // Default
|
||||
{"", TypeNotification}, // Empty
|
||||
{"TASK", TypeNotification}, // Case-sensitive, defaults to notification
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.s, func(t *testing.T) {
|
||||
got := ParseMessageType(tt.s)
|
||||
if got != tt.expected {
|
||||
t.Errorf("ParseMessageType(%q) = %q, want %q", tt.s, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewMessage(t *testing.T) {
|
||||
msg := NewMessage("mayor/", "gastown/Toast", "Test Subject", "Test Body")
|
||||
|
||||
if msg.From != "mayor/" {
|
||||
t.Errorf("From = %q, want 'mayor/'", msg.From)
|
||||
}
|
||||
if msg.To != "gastown/Toast" {
|
||||
t.Errorf("To = %q, want 'gastown/Toast'", msg.To)
|
||||
}
|
||||
if msg.Subject != "Test Subject" {
|
||||
t.Errorf("Subject = %q, want 'Test Subject'", msg.Subject)
|
||||
}
|
||||
if msg.Body != "Test Body" {
|
||||
t.Errorf("Body = %q, want 'Test Body'", msg.Body)
|
||||
}
|
||||
if msg.ID == "" {
|
||||
t.Error("ID should be generated")
|
||||
}
|
||||
if msg.ThreadID == "" {
|
||||
t.Error("ThreadID should be generated")
|
||||
}
|
||||
if msg.Timestamp.IsZero() {
|
||||
t.Error("Timestamp should be set")
|
||||
}
|
||||
if msg.Priority != PriorityNormal {
|
||||
t.Errorf("Priority = %q, want PriorityNormal", msg.Priority)
|
||||
}
|
||||
if msg.Type != TypeNotification {
|
||||
t.Errorf("Type = %q, want TypeNotification", msg.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewReplyMessage(t *testing.T) {
|
||||
original := &Message{
|
||||
ID: "orig-001",
|
||||
ThreadID: "thread-001",
|
||||
From: "gastown/Toast",
|
||||
To: "mayor/",
|
||||
Subject: "Original Subject",
|
||||
}
|
||||
|
||||
reply := NewReplyMessage("mayor/", "gastown/Toast", "Re: Original Subject", "Reply body", original)
|
||||
|
||||
if reply.ThreadID != "thread-001" {
|
||||
t.Errorf("ThreadID = %q, want 'thread-001'", reply.ThreadID)
|
||||
}
|
||||
if reply.ReplyTo != "orig-001" {
|
||||
t.Errorf("ReplyTo = %q, want 'orig-001'", reply.ReplyTo)
|
||||
}
|
||||
if reply.From != "mayor/" {
|
||||
t.Errorf("From = %q, want 'mayor/'", reply.From)
|
||||
}
|
||||
if reply.To != "gastown/Toast" {
|
||||
t.Errorf("To = %q, want 'gastown/Toast'", reply.To)
|
||||
}
|
||||
if reply.Subject != "Re: Original Subject" {
|
||||
t.Errorf("Subject = %q, want 'Re: Original Subject'", reply.Subject)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBeadsMessageToMessage(t *testing.T) {
|
||||
now := time.Now()
|
||||
bm := BeadsMessage{
|
||||
ID: "hq-test",
|
||||
Title: "Test Subject",
|
||||
Description: "Test Body",
|
||||
Status: "open",
|
||||
Assignee: "gastown/Toast",
|
||||
Labels: []string{"from:mayor/", "thread:t-001"},
|
||||
CreatedAt: now,
|
||||
Priority: 1,
|
||||
}
|
||||
|
||||
msg := bm.ToMessage()
|
||||
|
||||
if msg.ID != "hq-test" {
|
||||
t.Errorf("ID = %q, want 'hq-test'", msg.ID)
|
||||
}
|
||||
if msg.Subject != "Test Subject" {
|
||||
t.Errorf("Subject = %q, want 'Test Subject'", msg.Subject)
|
||||
}
|
||||
if msg.Body != "Test Body" {
|
||||
t.Errorf("Body = %q, want 'Test Body'", msg.Body)
|
||||
}
|
||||
if msg.From != "mayor/" {
|
||||
t.Errorf("From = %q, want 'mayor/'", msg.From)
|
||||
}
|
||||
if msg.ThreadID != "t-001" {
|
||||
t.Errorf("ThreadID = %q, want 't-001'", msg.ThreadID)
|
||||
}
|
||||
if msg.To != "gastown/Toast" {
|
||||
t.Errorf("To = %q, want 'gastown/Toast'", msg.To)
|
||||
}
|
||||
if msg.Priority != PriorityHigh {
|
||||
t.Errorf("Priority = %q, want PriorityHigh", msg.Priority)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBeadsMessageToMessageWithReplyTo(t *testing.T) {
|
||||
bm := BeadsMessage{
|
||||
ID: "hq-reply",
|
||||
Title: "Reply Subject",
|
||||
Description: "Reply Body",
|
||||
Status: "open",
|
||||
Assignee: "gastown/Toast",
|
||||
Labels: []string{"from:mayor/", "thread:t-002", "reply-to:orig-001", "msg-type:reply"},
|
||||
CreatedAt: time.Now(),
|
||||
Priority: 2,
|
||||
}
|
||||
|
||||
msg := bm.ToMessage()
|
||||
|
||||
if msg.ReplyTo != "orig-001" {
|
||||
t.Errorf("ReplyTo = %q, want 'orig-001'", msg.ReplyTo)
|
||||
}
|
||||
if msg.Type != TypeReply {
|
||||
t.Errorf("Type = %q, want TypeReply", msg.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBeadsMessageToMessagePriorities(t *testing.T) {
|
||||
tests := []struct {
|
||||
priority int
|
||||
expected Priority
|
||||
}{
|
||||
{0, PriorityUrgent},
|
||||
{1, PriorityHigh},
|
||||
{2, PriorityNormal},
|
||||
{3, PriorityLow},
|
||||
{4, PriorityNormal}, // Out of range defaults to normal
|
||||
{99, PriorityNormal}, // Out of range defaults to normal
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
bm := BeadsMessage{
|
||||
ID: "hq-test",
|
||||
Priority: tt.priority,
|
||||
}
|
||||
msg := bm.ToMessage()
|
||||
if msg.Priority != tt.expected {
|
||||
t.Errorf("Priority %d -> %q, want %q", tt.priority, msg.Priority, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBeadsMessageToMessageTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
msgType string
|
||||
expected MessageType
|
||||
}{
|
||||
{"task", TypeTask},
|
||||
{"scavenge", TypeScavenge},
|
||||
{"reply", TypeReply},
|
||||
{"notification", TypeNotification},
|
||||
{"", TypeNotification}, // Default
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
bm := BeadsMessage{
|
||||
ID: "hq-test",
|
||||
Labels: []string{"msg-type:" + tt.msgType},
|
||||
}
|
||||
msg := bm.ToMessage()
|
||||
if msg.Type != tt.expected {
|
||||
t.Errorf("msg-type:%s -> %q, want %q", tt.msgType, msg.Type, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBeadsMessageToMessageEmptyLabels(t *testing.T) {
|
||||
bm := BeadsMessage{
|
||||
ID: "hq-empty",
|
||||
Title: "Empty Labels",
|
||||
Description: "Test with empty labels",
|
||||
Assignee: "gastown/Toast",
|
||||
Labels: []string{}, // No labels
|
||||
Priority: 2,
|
||||
}
|
||||
|
||||
msg := bm.ToMessage()
|
||||
|
||||
if msg.From != "" {
|
||||
t.Errorf("From should be empty, got %q", msg.From)
|
||||
}
|
||||
if msg.ThreadID != "" {
|
||||
t.Errorf("ThreadID should be empty, got %q", msg.ThreadID)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user