diff --git a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md index bdf30d3c..1de3b463 100644 --- a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md +++ b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md @@ -37,10 +37,10 @@ Hermes plan landing map: ## Current Baseline -- Current tracking checkpoint: 2026-06-06 after this implementation checkpoint, prepared as `feat(conversations): align custom attribute response`. -- Latest implementation checkpoint: this checkpoint, prepared as `feat(conversations): align custom attribute response`. +- Current tracking checkpoint: 2026-06-06 after this implementation checkpoint, prepared as `feat(messages): align retry status parity`. +- Latest implementation checkpoint: this checkpoint, prepared as `feat(messages): align retry status parity`. - Latest documentation/tooling checkpoint: this checkpoint, prepared as `docs: expand help center route parity tracking`; this document is now the active follow-up plan and supersedes `.hermes/plans/*`. -- Worktree status at this implementation checkpoint: P6 conversation custom-attribute response parity now matches Chatwoot `ConversationsController#custom_attributes`, `custom_attributes.json.jbuilder`, and the reused dashboard conversation store action: `POST /conversations/:conversation_id/custom_attributes` returns only `{ custom_attributes: ... }`, omits local conversation/payload/meta wrappers, and serializes empty/null JSON as `{}` for frontend merge stability. Route parity tracking includes the Chatwoot help-center portal/category/article route family from `reference/chatwoot/config/routes.rb:385-404`, so the audited critical set covers portal CRUD/member actions, nested categories, nested articles, and article bulk actions. P3.7c help center article parity matches the reused dashboard `helpCenter/articles.js` client and Chatwoot `ArticlesController`/`Articles::BulkActionsController`/Jbuilder surface. Campaign create/update already mirrors Chatwoot `Campaign#ensure_correct_campaign_attributes`; account campaign CRUD already matches the reused dashboard `campaigns` store and Chatwoot Jbuilder contract. Agent/settings/profile/notification/contact/custom-field/report enterprise drifts listed below remain at their prior checkpoint states. The fresh placeholder audit found remaining `chatwootParityStub` routes only in webhook handler-not-injected fallbacks, not in the reused dashboard account/contact/conversation/message/inbox critical path. P3.6 custom filters/custom attribute definitions and label CRUD are already in Review. B11.1a-B11.3e Captain/Copilot slices remain in Review; B12.1/B12.2/B12.3 smoke harnesses remain in Review; P5 durable job work through P5.13b is in Review. Next active implementation slice is another Phase 6 placeholder burn-down item or a new Phase 2/3 drift slice from fresh reference/smoke evidence. +- Worktree status at this implementation checkpoint: P6 message retry parity now matches Chatwoot `MessagesController#retry`: retry sets the message status back to `sent`, clears provider failure data from `content_attributes`, and queues the existing durable `SendReplyJob` path when a worker pool is configured. P6 conversation custom-attribute response parity already matches Chatwoot `ConversationsController#custom_attributes`, `custom_attributes.json.jbuilder`, and the reused dashboard conversation store action. Route parity tracking includes the Chatwoot help-center portal/category/article route family from `reference/chatwoot/config/routes.rb:385-404`, so the audited critical set covers portal CRUD/member actions, nested categories, nested articles, and article bulk actions. P3.7c help center article parity matches the reused dashboard `helpCenter/articles.js` client and Chatwoot `ArticlesController`/`Articles::BulkActionsController`/Jbuilder surface. Campaign create/update already mirrors Chatwoot `Campaign#ensure_correct_campaign_attributes`; account campaign CRUD already matches the reused dashboard `campaigns` store and Chatwoot Jbuilder contract. Agent/settings/profile/notification/contact/custom-field/report enterprise drifts listed below remain at their prior checkpoint states. The fresh placeholder audit found remaining `chatwootParityStub` routes only in webhook handler-not-injected fallbacks, not in the reused dashboard account/contact/conversation/message/inbox critical path. P3.6 custom filters/custom attribute definitions and label CRUD are already in Review. B11.1a-B11.3e Captain/Copilot slices remain in Review; B12.1/B12.2/B12.3 smoke harnesses remain in Review; P5 durable job work through P5.13b is in Review. Next active implementation slice is another Phase 6 placeholder burn-down item or a new Phase 2/3 drift slice from fresh reference/smoke evidence. - `go test ./...` passes. - Route dump succeeds with `TOTAL: 860` after adding no-trailing-slash article index/create aliases, frontend-used article PATCH update route, and frontend article bulk-action routes. - Route parity artifacts now exist under `docs/parity/` and are generated by `cmd/route_parity`. @@ -110,7 +110,7 @@ This table is the shortest authoritative handoff view. If an older lower section | Priority | Workstream | Current state | Next checkpoint | Commit close rule | | --- | --- | --- | --- | --- | | 1 | Phase 2/3 drift | Tracked route parity is 0 missing for the current 305-route critical set; help-center portal/category/article routes from `routes.rb:385-404` are now explicitly tracked. Notification list/action serializers, user notification-settings raw payloads, campaigns raw payload/display-id routes, Devise password reset/confirmation payloads, CRM shared attachment payloads plus fixed 100-row attachment pagination, account/settings payloads, assignable-agent payloads, agent index full-list behavior, agent create/update/delete defaults/errors/scope, label CRUD payloads, custom filters, custom attribute definitions, and help-center portal/category/article payloads now match the inspected Chatwoot frontend contract. | Run the next evidence-backed route/controller/serializer drift audit. | Regenerate parity artifacts when routes change and add endpoint-family fixture tests. | -| 2 | Phase 6 placeholder audit | Widget/public/webhook critical placeholders are burned down; inbox WhatsApp health/register-webhook and sync-template drift are closed; fresh `rg` audit shows only webhook nil-handler fallbacks still call `chatwootParityStub`; dashboard conversation transcript and custom-attribute response drift are closed. | Continue targeted account/contact/conversation/message/inbox drift from reference/smoke evidence. | `rg` placeholder audit is recorded and no reused-frontend blocker is ownerless. | +| 2 | Phase 6 placeholder audit | Widget/public/webhook critical placeholders are burned down; inbox WhatsApp health/register-webhook and sync-template drift are closed; fresh `rg` audit shows only webhook nil-handler fallbacks still call `chatwootParityStub`; dashboard conversation transcript/custom-attribute response drift and message retry status drift are closed. | Continue targeted account/contact/conversation/message/inbox drift from reference/smoke evidence. | `rg` placeholder audit is recorded and no reused-frontend blocker is ownerless. | | 3 | B12 optional live smoke | API/browser/enterprise smoke commands are checked in; live runs need PostgreSQL, Redis, Meilisearch, Vite, and Chrome. | Run full live smoke when environment is available and map failures to the board. | `docs/parity/frontend_smoke_report.md` records pass/fail and linked owners. | | 4 | B9.3 delayed automation actions | Current reference exposes no delayed automation action params; scheduled-item work is already P5.12; `send_email_to_team` is durable and `add_sla` mutates conversation/applied SLA state. | Keep automation drift closed if future reference/smoke exposes delayed params or unsupported action shapes. | Automation worker/action fixtures verify queued team email replay, retry visibility through worker jobs, and SLA action idempotency. | | 5 | P5.13 reports/analytics | P5.13a derives visible report aggregates from persisted rows; P5.13b adds lazy rollup freshness, durable day rollup jobs, and `/reports` metric timeseries. | Keep report drift closed as frontend smoke or reference inspection exposes additional metrics. | Report fixtures verify timeseries values, cache/freshness behavior, and no hidden placeholder JSON. | @@ -133,6 +133,7 @@ These rows are the executable development plan from this point forward. A checkp | P3.7c help center article and bulk-action parity | `internal/router/router.go`, `internal/handler/api/v1/article_handler.go`, `internal/service/article_service.go`, `internal/repository/article_repo.go`, article tests | `reference/chatwoot/app/controllers/api/v1/accounts/articles_controller.rb`, `app/controllers/api/v1/accounts/articles/bulk_actions_controller.rb`, `app/views/api/v1/accounts/articles/*.json.jbuilder`, dashboard `api/helpCenter/articles.js` | Article APIs now resolve portal slugs account-scoped with numeric fallback, accept raw article bodies plus wrappers, expose no-trailing-slash index/create plus frontend `PATCH`, return Chatwoot `{ payload, meta }` list/search payloads and `{ payload: article }` show/create/update/edit payloads, serialize category/author/views/Unix updated-at/meta/associated articles, make delete/reorder/bulk actions empty `200 OK`, accept Chatwoot `positions_hash`, scope mutations to the resolved portal, and implement frontend bulk update-status/update-category/delete routes plus `translate` `501`. | Review by `feat(help-center): align article payloads`; focused article handler tests, service/repository article tests, router test, route dump/parity regeneration, escalated full `go test ./...`, and `git diff --check` passed. | | P6 conversation transcript response parity | `internal/handler/api/v1/conversation_handler.go`, conversation handler tests | `reference/chatwoot/app/controllers/api/v1/accounts/conversations_controller.rb`, dashboard `api/inbox/conversation.js` | Account conversation transcript now follows Chatwoot's controller contract: missing email returns `422 { error: "email param missing" }`, nonblank email schedules through the existing service boundary and returns empty `200 OK`, and invalid-looking but nonblank email values are not rejected by local email format validation. | Review by `feat(conversations): align transcript responses`; focused transcript handler/service tests, combined handler/service/router tests, full `go test ./...`, and `git diff --check` must pass. | | P6 conversation custom attributes response parity | `internal/handler/api/v1/conversation_handler.go`, conversation handler tests | `reference/chatwoot/app/controllers/api/v1/accounts/conversations_controller.rb`, `app/views/api/v1/accounts/conversations/custom_attributes.json.jbuilder`, dashboard `api/inbox/conversation.js`, conversation store action | Account conversation custom attribute updates now return Chatwoot `{ custom_attributes: ... }` only, matching the store action that reads `response.data.custom_attributes`, instead of returning the full conversation serializer with unrelated fields. Empty/null JSON serializes as `{}`. | Review by `feat(conversations): align custom attribute response`; focused handler tests, combined handler/service/router tests, full `go test ./...`, and `git diff --check` passed. | +| P6 message retry status parity | `internal/service/message_service.go`, `internal/handler/api/v1/message_handler.go`, message service/handler tests | `reference/chatwoot/app/controllers/api/v1/accounts/conversations/messages_controller.rb`, `app/views/api/v1/accounts/conversations/messages/retry.json.jbuilder`, dashboard `api/inbox/message.js` | Account message retry now follows Chatwoot: status resets to `sent`, `content_attributes` is cleared to `{}`, response returns the message serializer, and the existing durable SendReplyJob path is queued when a worker pool is configured. | Review by `feat(messages): align retry status parity`; focused retry service/handler tests, combined handler/service tests, full `go test ./...`, and `git diff --check` must pass. | | Phase 2/3 drift audit | `cmd/route_parity`, `docs/parity/*`, serializer tests | `reference/chatwoot/config/routes.rb`, controller Jbuilder views, reused frontend API clients | Convert any smoke/reference mismatch into a named route, controller, or serializer slice. Static route extraction remains acceptable until Ruby/Bundler is available. | Regenerated route parity shows 0 missing tracked frontend routes; new serializer fixtures cover the drift. | | Phase 6 placeholder burn-down | Account/contact/conversation/message/inbox handlers and services | Matching reference controllers/Jbuilder views plus reused frontend screens | Re-run placeholder audit and assign every frontend-reachable stub to a specific owner. Burn down the highest-impact stubs before broad feature expansion. | `rg` placeholder audit is recorded here; no reused-frontend critical path is ownerless. | | B12 live smoke | `scripts/parity_frontend_smoke.sh`, `docs/parity/frontend_smoke_report.md`, `cmd/gochat` | Reused `reference/chatwoot` Vite frontend, dashboard route/API clients | Run optional live API/browser/enterprise smoke with PostgreSQL, Redis, Meilisearch, GoChat, Vite, and Chrome. Convert failures into named rows above. | Smoke report records command, environment, pass/fail, artifacts, and linked follow-up owners. | @@ -168,6 +169,7 @@ This ledger records the committed parity checkpoints that future slices should b | Commit | Scope | Verification summary | Follow-up state | | --- | --- | --- | --- | +| `feat(messages): align retry status parity` | Advances P6 message API parity by matching Chatwoot `MessagesController#retry`, `messages/retry.json.jbuilder`, and the reused dashboard `MessageApi.retry` path. Message retry now resets failed messages to `sent`, clears `content_attributes` to `{}`, returns the Chatwoot message serializer, and queues the existing durable `message:send_reply` path when a WorkerPool is configured instead of leaving local `retrying` state/provider failure details visible. | `go test ./internal/service -run MessageService_Retry -count=1`; `go test ./internal/handler/api/v1 -run 'MessageHandlerTestSuite/TestRetry' -count=1`; `go test ./internal/handler/api/v1 ./internal/service -run 'Message.*Retry' -count=1`; `go test ./...`; `git diff --check`. | P6.4 message retry drift moves to Review; continue Phase 6 placeholder burn-down or fresh evidence-backed Phase 2/3 drift. | | `feat(conversations): align custom attribute response` | Advances P6 conversation response parity by matching Chatwoot `ConversationsController#custom_attributes`, `custom_attributes.json.jbuilder`, and the reused dashboard `ConversationApi.updateCustomAttributes` store action. The account conversation custom-attributes route now returns only `{ custom_attributes: ... }`, excludes local conversation/payload/meta wrappers, and converts empty/null JSON to `{}` for frontend merge stability. The placeholder audit was rerun and still shows only webhook handler-not-injected fallbacks calling `chatwootParityStub`. | `go test ./internal/handler/api/v1 -run 'ConversationHandler.*UpdateCustomAttributes\|TestConversationHandler' -count=1`; `go test ./internal/handler/api/v1 ./internal/service ./internal/router -run 'UpdateCustomAttributes\|ConversationHandler\|ConversationService\|Router' -count=1`; `rg -n "chatwootParityStub\|TODO.*placeholder\|placeholder\|stub" internal/handler internal/service internal/router -g'*.go'`; `go test ./...`; `git diff --check`. | P6 conversation custom-attribute response drift moves to Review; continue Phase 6 placeholder burn-down from the remaining webhook nil-handler fallbacks or fresh reference/smoke evidence. | | `docs: expand help center route parity tracking` | Audit/tooling checkpoint that expands `cmd/route_parity` to track the full Chatwoot help-center portal/category/article route family from `reference/chatwoot/config/routes.rb:385-404`, including portal member actions, nested category CRUD/reorder, nested article CRUD/reorder, and article bulk actions. Regenerated `docs/parity/route_parity.md` now reports `298 exact, 0 method-compatible, 7 parameter-compatible, 0 missing out of 305 tracked critical routes`; `docs/parity/gochat_routes.txt` remains `TOTAL: 860`. | `go run ./cmd/route_parity`; `go test ./cmd/route_parity -count=1`; `git diff --check`. | Continue Phase 6 placeholder burn-down or the next evidence-backed Phase 2/3 drift slice; keep route artifacts regenerated whenever the tracked set changes. | | `feat(help-center): align article payloads` | Advances P3.7c help-center parity by matching Chatwoot `ArticlesController`, `Articles::BulkActionsController`, article Jbuilder views, and reused dashboard `helpCenter/articles.js`. Article index/create now expose no-trailing-slash routes; update exposes frontend `PATCH`; frontend bulk update-status/update-category/delete and translate routes are registered; portal IDs resolve through account-scoped slug lookup with numeric fallback; create/update accept raw frontend article bodies plus wrappers; list/search return `{ payload, meta }`; show/create/update/edit return `{ payload: article }` with category, author, views, Unix `updated_at`, `meta`, and associated article fields; delete/reorder/bulk actions return empty `200 OK`; reorder accepts Chatwoot `positions_hash`; and bulk action errors return `422 { error }`. | `go test ./internal/handler/api/v1 -run ArticleHandler -count=1`; `go test ./internal/service ./internal/repository -run Article -count=1`; `go test ./internal/router -run Router -count=1`; `go run ./cmd/dump_routes > docs/parity/gochat_routes.txt`; `go run ./cmd/route_parity`; sandboxed full `go test ./...` failed only on local socket restrictions; escalated full `go test ./...` passed; `git diff --check`. Route dump is `TOTAL: 860`; tracked route parity remains `270 exact, 7 parameter-compatible, 0 missing`. | P3.7c moves to Review; continue Phase 2/3 drift audit or Phase 6 placeholder burn-down from fresh reference/smoke evidence. | @@ -2033,7 +2035,7 @@ Tracking table: | P6.1 | Account APIs | `docs/ROUTE_GAP_ANALYSIS.md`, account handlers | Replace placeholder responses with repository-backed behavior and serializer tests. | Todo | | P6.2 | Contact APIs | `docs/ROUTE_GAP_ANALYSIS.md`, contact handlers/services | Finish merge, import/export/data-import, notes serializer depth, and Meilisearch-backed CRM search. | Doing | | P6.3 | Conversation APIs | `docs/ROUTE_GAP_ANALYSIS.md`, conversation handlers/services | Frontend-critical filters, assignment, status, snooze, merge, bulk actions, transcript responses, and custom-attribute responses are implemented or covered by focused drift slices; continue only from fresh reference/smoke evidence. | Review | -| P6.4 | Message APIs | `docs/ROUTE_GAP_ANALYSIS.md`, message handlers/services | Implement create/list/delete, private notes, attachments, source attribution, events. | Todo | +| P6.4 | Message APIs | `docs/ROUTE_GAP_ANALYSIS.md`, message handlers/services | Create/list/delete/private notes/attachments/source attribution have focused coverage; retry now matches Chatwoot status reset/content-attribute clearing and durable send-reply enqueue behavior. Continue only from fresh reference/smoke evidence. | Review | | P6.5 | Inbox APIs | `docs/ROUTE_GAP_ANALYSIS.md`, inbox handlers/services | Implement CRUD, assignable agents, avatar, campaigns, channel settings, reset secret. WhatsApp Cloud health/register-webhook/sync-template member actions now match the reference concern. | Doing | | P6.6 | Widget/public APIs | `docs/ROUTE_GAP_ANALYSIS.md`, widget/channel provider code, `chatwootParityStub` routes | Widget/public frontend-critical route behavior is handler-backed, including public inbox flow, direct uploads/attachments, and public CSAT survey submission. | Done | | P6.7 | Webhook ingress | `internal/router/router.go`, `internal/handler/webhook/*`, channel providers | Replace generic placeholder with provider-specific verified ingestion and dispatch. | Done | @@ -2121,6 +2123,7 @@ Verification milestone gates: ## Progress Log +- 2026-06-06: P6 message retry checkpoint prepared as `feat(messages): align retry status parity`; audited Chatwoot `MessagesController#retry`, `messages/retry.json.jbuilder`, and reused dashboard `api/inbox/message.js`. Account message retry now resets status to `sent`, clears `content_attributes` to `{}`, returns the normal Chatwoot message serializer, and queues the existing durable send-reply job path when a WorkerPool is configured instead of exposing local `retrying` state/provider failure details. Focused retry service/handler tests, combined handler/service retry tests, full `go test ./...`, and `git diff --check` passed; continue Phase 6 placeholder burn-down or a fresh evidence-backed Phase 2/3 drift slice. - 2026-06-06: P6 conversation custom-attribute response checkpoint prepared as `feat(conversations): align custom attribute response`; audited Chatwoot `ConversationsController#custom_attributes`, `custom_attributes.json.jbuilder`, reused dashboard `api/inbox/conversation.js`, and the conversation store action that reads `response.data.custom_attributes`. Account conversation custom-attribute updates now return only `{ custom_attributes: ... }`, exclude local conversation/payload/meta wrappers, and serialize empty/null custom attributes as `{}` for frontend merge stability. Focused handler tests, combined handler/service/router tests, placeholder audit, full `go test ./...`, and `git diff --check` passed; continue Phase 6 placeholder burn-down or a fresh evidence-backed Phase 2/3 drift slice. - 2026-06-06: Audit/tooling checkpoint prepared as `docs: expand help center route parity tracking`; expanded `cmd/route_parity` to cover the full Chatwoot help-center portal/category/article route family from `reference/chatwoot/config/routes.rb:385-404`, including portal CRUD/member actions, nested category CRUD/reorder, nested article CRUD/reorder, and article bulk actions. Regenerated route parity reports `298 exact, 0 method-compatible, 7 parameter-compatible, 0 missing out of 305 tracked critical routes`; route dump remains `TOTAL: 860`. Verification for this checkpoint: `go run ./cmd/route_parity`, `go test ./cmd/route_parity -count=1`, and `git diff --check`; continue Phase 6 placeholder burn-down or a fresh evidence-backed Phase 2/3 drift slice. - 2026-06-06: P3.7c help-center article checkpoint prepared as `feat(help-center): align article payloads`; audited Chatwoot `ArticlesController`, `Articles::BulkActionsController`, article Jbuilder views, and reused dashboard `helpCenter/articles.js`. Account article APIs now accept raw frontend create/update bodies plus wrappers, resolve portal route IDs by account-scoped slug with numeric fallback, return Chatwoot `{ payload, meta }` list/search payloads and `{ payload: article }` mutation/show/edit payloads, expose no-trailing-slash index/create plus frontend `PATCH`, return empty `200 OK` for delete/reorder/bulk actions, support status/category/locale/query/author filters and dashboard article counts, accept Chatwoot reorder `positions_hash`, and implement frontend bulk update-status/update-category/delete routes with `422 { error }` failures. Focused article handler tests, service/repository article tests, router test, route dump/parity, escalated full `go test ./...`, and `git diff --check` passed; continue Phase 2/3 drift audit or Phase 6 placeholder burn-down from fresh evidence. diff --git a/internal/handler/api/v1/message_handler_test.go b/internal/handler/api/v1/message_handler_test.go index 9d11cd02..143dbcdc 100644 --- a/internal/handler/api/v1/message_handler_test.go +++ b/internal/handler/api/v1/message_handler_test.go @@ -13,6 +13,7 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" + "gorm.io/datatypes" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" @@ -560,6 +561,11 @@ func (s *MessageHandlerTestSuite) TestDelete_NotFound() { // --- Retry Tests --- func (s *MessageHandlerTestSuite) TestRetry_Success() { + s.Require().NoError(s.db.Model(s.testMessage).Updates(map[string]interface{}{ + "status": "failed", + "content_attributes": datatypes.JSON([]byte(`{"external_error":"provider failed"}`)), + }).Error) + w := httptest.NewRecorder() url := msgRetryURL(s.testAccount.ID, s.testConv.ID, s.testMessage.ID) req, _ := http.NewRequest("POST", url, nil) @@ -570,6 +576,8 @@ func (s *MessageHandlerTestSuite) TestRetry_Success() { json.Unmarshal(w.Body.Bytes(), &resp) assert.Equal(s.T(), float64(s.testMessage.ID), resp["id"]) assert.Equal(s.T(), float64(1), resp["message_type"]) + assert.Equal(s.T(), "sent", resp["status"]) + assert.Equal(s.T(), map[string]interface{}{}, resp["content_attributes"]) } func (s *MessageHandlerTestSuite) TestRetry_InvalidAccountID() { diff --git a/internal/service/message_service.go b/internal/service/message_service.go index 4e2a5467..1ed2f67e 100644 --- a/internal/service/message_service.go +++ b/internal/service/message_service.go @@ -438,27 +438,33 @@ func attachmentThumbURL(contentType string, messageID uint, fileName string) str } // Retry retries a failed message by resetting its delivery status. +// Reference: Chatwoot MessagesController#retry sets status to sent, clears +// content_attributes, and queues SendReplyJob. func (s *MessageService) Retry(ctx context.Context, accountID, id uint) (*model.Message, error) { message, err := s.repo.FindByAccountAndID(ctx, accountID, id) if err != nil { return nil, err } - // Reset status to indicate retry - message.Status = "retrying" + message.Status = "sent" + message.ContentAttributes = datatypes.JSON([]byte(`{}`)) if err := s.repo.Update(ctx, message); err != nil { return nil, err } + if s.worker != nil { + if _, err := EnqueueSendReply(ctx, s.worker, message.ID); err != nil { + return nil, err + } + } - // Dispatch EventMessageStatusUpdated with "retrying" status event := channel.NewChannelEvent(channel.EventMessageStatusUpdated, channel.ChannelAPI, message.AccountID, message.InboxID) event.ConversationID = message.ConversationID if message.SenderID != nil { event.UserID = *message.SenderID } event.Data["message_id"] = message.ID - event.Data["status"] = "retrying" + event.Data["status"] = "sent" applogger.L().Infof("dispatching retry event for message %d", message.ID) if err := s.dispatcher.Dispatch(ctx, event); err != nil { applogger.L().Errorf("failed to dispatch retry event for message %d: %v", message.ID, err) diff --git a/internal/service/message_service_test.go b/internal/service/message_service_test.go index 83a10a4a..912cc54b 100644 --- a/internal/service/message_service_test.go +++ b/internal/service/message_service_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gorm.io/datatypes" "gorm.io/gorm" "github.com/gochat/gochat/internal/channel" @@ -14,6 +15,7 @@ import ( "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/search" + "github.com/gochat/gochat/internal/worker" ) // mockMessageLLMProvider implements llm.Provider for message service testing. @@ -579,7 +581,7 @@ func TestMessageService_Retry(t *testing.T) { msg := &model.Message{ ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID, Content: "failed message", MessageType: "outgoing", ContentType: "text", SenderType: "user", - Status: "failed", + Status: "failed", ContentAttributes: datatypes.JSON([]byte(`{"external_error":"provider failed"}`)), } require.NoError(t, db.Create(msg).Error) @@ -589,10 +591,43 @@ func TestMessageService_Retry(t *testing.T) { retried, err := svc.Retry(ctx, account.ID, msg.ID) assert.NoError(t, err) - assert.Equal(t, "retrying", retried.Status) + assert.Equal(t, "sent", retried.Status) + assert.JSONEq(t, `{}`, string(retried.ContentAttributes)) assert.True(t, listener.received) - assert.Equal(t, "retrying", listener.lastData["status"]) + assert.Equal(t, "sent", listener.lastData["status"]) assert.Equal(t, msg.ID, listener.lastData["message_id"]) + + var stored model.Message + require.NoError(t, db.First(&stored, msg.ID).Error) + assert.JSONEq(t, `{}`, string(stored.ContentAttributes)) + }) + + t.Run("queues_send_reply_when_worker_configured", func(t *testing.T) { + db, _, _, svc := setupMessageServiceWithDefaultLLM(t) + wp := worker.NewWorkerPool(db) + svc.SetWorkerPool(wp) + + account := createTestAccount(t, db) + inbox := createTestInbox(t, db, account.ID, "web_widget") + contact := createTestContact(t, db, account.ID) + conv := createTestConversation(t, db, account.ID, inbox.ID, contact.ID) + + msg := &model.Message{ + ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID, + Content: "failed message", MessageType: "outgoing", ContentType: "text", SenderType: "user", + Status: "failed", ContentAttributes: datatypes.JSON([]byte(`{"external_error":"provider failed"}`)), + } + require.NoError(t, db.Create(msg).Error) + + retried, err := svc.Retry(ctx, account.ID, msg.ID) + require.NoError(t, err) + assert.Equal(t, "sent", retried.Status) + + var job model.BackgroundJob + require.NoError(t, db.Where("job_type = ?", TaskTypeMessageSendReply).First(&job).Error) + assert.Equal(t, model.BackgroundJobStatusQueued, job.Status) + assert.Equal(t, "message:send_reply", job.JobType) + assert.JSONEq(t, fmt.Sprintf(`{"message_id":%d}`, msg.ID), string(job.Payload)) }) t.Run("message_not_found", func(t *testing.T) {