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:
@@ -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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user