Files
claw-email/internal/ai_client/client_test.go
T
thuanleandClaude Opus 4.7 8815c20c13 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>
2026-04-27 18:16:57 +07:00

115 lines
3.0 KiB
Go

package ai_client_test
import (
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"thuanle.me/claw-email-bridge/internal/ai_client"
"thuanle.me/claw-email-bridge/internal/config"
"thuanle.me/claw-email-bridge/internal/database"
)
// Test matrix #2: OpenClaw fail 2 lần, lần 3 thành công → COMPLETED, attempt_openclaw = 3.
func TestDispatch_RetryThenSuccess(t *testing.T) {
tdb := database.NewTestDB(t)
var callCount atomic.Int32
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
n := callCount.Add(1)
if n <= 2 {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("temporary error"))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok"}`))
}))
defer mockServer.Close()
cfg := &config.Config{
OpenClawURL: mockServer.URL,
ListenAddr: ":9999",
}
task := &database.Task{
TaskUUID: "uuid-retry-test",
ThreadID: "thread-1",
MessageID: "msg-retry@test.com",
Sender: "user@test.com",
Subject: "Retry test",
BodyPlain: "Hello",
Status: database.StatusReceived,
}
if err := database.CreateTask(tdb.DB, task); err != nil {
t.Fatalf("create task: %v", err)
}
dispatcher := ai_client.NewDispatcher(cfg, tdb.DB)
dispatcher.Dispatch(task) // Synchronous call for testing.
// Verify 3 attempts were made.
if callCount.Load() != 3 {
t.Errorf("expected 3 API calls, got %d", callCount.Load())
}
// Reload task from DB.
updated, err := database.FindByTaskUUID(tdb.DB, "uuid-retry-test")
if err != nil {
t.Fatalf("find task: %v", err)
}
if updated.Status != database.StatusAIProcessing {
t.Errorf("expected AI_PROCESSING (waiting for callback), got %s", updated.Status)
}
if updated.AttemptOpenClaw != 3 {
t.Errorf("expected attempt_openclaw = 3, got %d", updated.AttemptOpenClaw)
}
}
// OpenClaw fail all 3 retries → FAILED with last_error.
func TestDispatch_AllRetriesFailed(t *testing.T) {
tdb := database.NewTestDB(t)
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("server down"))
}))
defer mockServer.Close()
cfg := &config.Config{
OpenClawURL: mockServer.URL,
ListenAddr: ":9999",
}
task := &database.Task{
TaskUUID: "uuid-fail-all",
ThreadID: "thread-1",
MessageID: "msg-fail@test.com",
Sender: "user@test.com",
Subject: "Fail test",
BodyPlain: "Hello",
Status: database.StatusReceived,
}
if err := database.CreateTask(tdb.DB, task); err != nil {
t.Fatalf("create task: %v", err)
}
dispatcher := ai_client.NewDispatcher(cfg, tdb.DB)
dispatcher.Dispatch(task)
updated, err := database.FindByTaskUUID(tdb.DB, "uuid-fail-all")
if err != nil {
t.Fatalf("find task: %v", err)
}
if updated.Status != database.StatusFailed {
t.Errorf("expected FAILED, got %s", updated.Status)
}
if updated.LastError == nil {
t.Error("expected last_error to be set")
}
if updated.AttemptOpenClaw != 3 {
t.Errorf("expected attempt_openclaw = 3, got %d", updated.AttemptOpenClaw)
}
}