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) } }