Initial commit: email-openclaw bridge v1

Full pipeline: IMAP ingress -> OpenClaw dispatch -> callback -> SMTP reply.
SQLite stateful storage with idempotency, threading, and retry logic.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-27 18:16:57 +07:00
co-authored by Claude Opus 4.7
commit 8815c20c13
35 changed files with 2808 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
package database_test
import (
"testing"
"thuanle.me/claw-email-bridge/internal/database"
)
// Test matrix #5: Duplicate message_id → không tạo task mới.
func TestDuplicateMessageID(t *testing.T) {
tdb := database.NewTestDB(t)
task1 := &database.Task{
TaskUUID: "uuid-1",
ThreadID: "thread-1",
MessageID: "msg-duplicate@test.com",
Sender: "user@test.com",
Subject: "First email",
Status: database.StatusReceived,
}
if err := database.CreateTask(tdb.DB, task1); err != nil {
t.Fatalf("create first task: %v", err)
}
// Lookup should find the existing task.
found, err := database.FindByMessageID(tdb.DB, "msg-duplicate@test.com")
if err != nil {
t.Fatalf("find by message_id: %v", err)
}
if found == nil {
t.Fatal("expected to find existing task")
}
if found.TaskUUID != "uuid-1" {
t.Errorf("expected uuid-1, got %s", found.TaskUUID)
}
// Attempting to create another task with same task_uuid should fail (unique constraint).
task2 := &database.Task{
TaskUUID: "uuid-1", // Same UUID — unique constraint violation.
ThreadID: "thread-1",
MessageID: "msg-duplicate@test.com",
Sender: "user@test.com",
Subject: "Duplicate",
Status: database.StatusReceived,
}
err = database.CreateTask(tdb.DB, task2)
if err == nil {
t.Error("expected error creating duplicate task_uuid, got nil")
}
}
func TestFindByMessageID_NotFound(t *testing.T) {
tdb := database.NewTestDB(t)
found, err := database.FindByMessageID(tdb.DB, "nonexistent@test.com")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if found != nil {
t.Error("expected nil for nonexistent message_id")
}
}
func TestThreadHistory(t *testing.T) {
tdb := database.NewTestDB(t)
// Create two completed tasks in same thread.
for i, uuid := range []string{"uuid-a", "uuid-b"} {
task := &database.Task{
TaskUUID: uuid,
ThreadID: "thread-history",
MessageID: uuid + "@test.com",
Sender: "user@test.com",
Subject: "Thread test",
BodyPlain: "Message " + uuid,
AIResponse: "Reply " + uuid,
Status: database.StatusCompleted,
}
_ = i
if err := database.CreateTask(tdb.DB, task); err != nil {
t.Fatalf("create task %s: %v", uuid, err)
}
}
// Create one non-completed task — should not appear in history.
task3 := &database.Task{
TaskUUID: "uuid-c",
ThreadID: "thread-history",
MessageID: "uuid-c@test.com",
Sender: "user@test.com",
Subject: "Pending",
Status: database.StatusAIProcessing,
}
if err := database.CreateTask(tdb.DB, task3); err != nil {
t.Fatalf("create task: %v", err)
}
history, err := database.ThreadHistory(tdb.DB, "thread-history")
if err != nil {
t.Fatalf("thread history: %v", err)
}
if len(history) != 2 {
t.Errorf("expected 2 completed tasks in history, got %d", len(history))
}
}