feat(conversations): align search sender names
This commit is contained in:
@@ -49,11 +49,11 @@ Hermes task landing checklist:
|
||||
|
||||
## Current Baseline
|
||||
|
||||
- Current tracking checkpoint: 2026-06-07 P3.60 conversation search message-type parity, prepared as `feat(conversations): align search message types`.
|
||||
- Latest implementation checkpoint: this checkpoint, prepared as `feat(conversations): align search message types`.
|
||||
- Latest documentation/tooling checkpoint: this tracker update records Chatwoot search model serializer `message_type_before_type_cast` behavior for conversation search results. No route artifacts change.
|
||||
- Current tracking checkpoint: 2026-06-07 P3.61 conversation search sender-name parity, prepared as `feat(conversations): align search sender names`.
|
||||
- Latest implementation checkpoint: this checkpoint, prepared as `feat(conversations): align search sender names`.
|
||||
- Latest documentation/tooling checkpoint: this tracker update records Chatwoot search model serializer `sender_name message.sender.name if message.sender` behavior for conversation search results. No route artifacts change.
|
||||
- Plan landing status: complete for the current known Hermes plans and user-confirmed scope. Future work should update this file directly instead of opening a parallel tracker.
|
||||
- Worktree status at this implementation checkpoint: conversation search messages now serialize `message_type` as Chatwoot enum integers via `message_type_before_type_cast` semantics instead of GoChat's local string enum. P3.59 search finder payload parity, P3.58 index finder parity, P3.57/P3.56/P3.55 conversation meta parity, P3.54 unread-count parity, P5.8b contact import DataImportJob parity, P3.53 first-response distribution parity, and prior report/profile/channel/contact checkpoints remain implemented. Live API/browser/enterprise smoke still needs the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack.
|
||||
- Worktree status at this implementation checkpoint: conversation search messages now serialize `sender_name` from persisted contact/user senders when Chatwoot's search model serializer would expose `message.sender.name`, while retaining P3.60 numeric `message_type` parity. P3.59 search finder payload parity, P3.58 index finder parity, P3.57/P3.56/P3.55 conversation meta parity, P3.54 unread-count parity, P5.8b contact import DataImportJob parity, P3.53 first-response distribution parity, and prior report/profile/channel/contact checkpoints remain implemented. Live API/browser/enterprise smoke still needs the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack.
|
||||
- Next executable implementation checkpoint: continue Phase 2/3 drift audit for deeper report data-source/metric-builder drift or the next reused-frontend mismatch, or run B12 live smoke when the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack is available. Re-run Phase 6 placeholder audit after future route/smoke changes.
|
||||
- `go test ./...` passes when run outside the restricted socket sandbox for the latest implementation baseline; the latest docs/tooling checkpoint verified `scripts/parity_frontend_smoke.sh --check` with workspace-local temp/cache dirs after `/tmp` was full.
|
||||
- Route dump succeeds with `967` registered routes after profile MFA route tracking.
|
||||
@@ -156,8 +156,9 @@ This table is the shortest authoritative handoff view. If an older lower section
|
||||
|
||||
| Priority | Workstream | Current state | Next checkpoint | Commit close rule |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 0 | P3.60 conversation search message-type parity | Implemented for reused dashboard conversation search results: GoChat now follows inspected `api/v1/models/_conversation.json.jbuilder` by serializing each search result message `message_type` as Chatwoot enum integers (`incoming=0`, `outgoing=1`, `activity=2`, `template=3`) instead of local string values. | Keep in Review; reopen from B12 conversation search smoke or fresh reference evidence for sender-name serialization, message ordering, or additional search model serializer fields. | Focused conversation search handler test, full `go test ./...`, and `git diff --check` must pass. No route artifacts change. |
|
||||
| 0 | P3.59 conversation search finder parity | Implemented for reused dashboard conversation search calls: GET `/conversations/search` now follows inspected Chatwoot `ConversationsController#search`, `ConversationFinder#perform`, and `search.json.jbuilder`; it uses the finder filters/counts, accepts missing `q`, returns root `{ meta, payload }`, omits `assigned_count`, and emits search-specific contact/inbox/message payloads. | Keep in Review; reopen from B12 conversation search smoke or fresh reference evidence for exact model search serializer drift, PostgreSQL `ILIKE` case-folding drift, duplicate message join count drift, or sender-name serialization depth. | Focused conversation search handler tests, full `go test ./...`, and `git diff --check` must pass. No route artifacts change. |
|
||||
| 0 | P3.61 conversation search sender-name parity | Implemented for reused dashboard conversation search results: GoChat now follows inspected `api/v1/models/_conversation.json.jbuilder` by emitting `sender_name` from `message.sender.name` for persisted contact/user senders in search result messages. | Keep in Review; reopen from B12 conversation search smoke or fresh reference evidence for message ordering, sender polymorphic types beyond Contact/User, or additional search model serializer fields. | Focused conversation search handler test, full `go test ./...`, and `git diff --check` must pass. No route artifacts change. |
|
||||
| 0 | P3.60 conversation search message-type parity | Implemented for reused dashboard conversation search results: GoChat now follows inspected `api/v1/models/_conversation.json.jbuilder` by serializing each search result message `message_type` as Chatwoot enum integers (`incoming=0`, `outgoing=1`, `activity=2`, `template=3`) instead of local string values. | Keep in Review; reopen from B12 conversation search smoke or fresh reference evidence for message ordering or additional search model serializer fields. | Focused conversation search handler test, full `go test ./...`, and `git diff --check` must pass. No route artifacts change. |
|
||||
| 0 | P3.59 conversation search finder parity | Implemented for reused dashboard conversation search calls: GET `/conversations/search` now follows inspected Chatwoot `ConversationsController#search`, `ConversationFinder#perform`, and `search.json.jbuilder`; it uses the finder filters/counts, accepts missing `q`, returns root `{ meta, payload }`, omits `assigned_count`, and emits search-specific contact/inbox/message payloads. | Keep in Review; reopen from B12 conversation search smoke or fresh reference evidence for exact model search serializer drift, PostgreSQL `ILIKE` case-folding drift, or duplicate message join count drift. | Focused conversation search handler tests, full `go test ./...`, and `git diff --check` must pass. No route artifacts change. |
|
||||
| 0 | P3.58 conversation index finder parity | Implemented for reused dashboard conversation list calls: GET `/conversations` now follows inspected Chatwoot `ConversationsController#index` and `ConversationFinder#perform` for the frontend query params, including default-open status, status skip while `q` is present, pre-assignee meta counts, post-count `assignee_type` payload filtering, labels/source/query filters, and finder sort/pagination behavior. | Keep in Review; reopen from B12 conversation list smoke or fresh reference evidence for full `Conversations::PermissionFilterService` custom-role depth, PostgreSQL `ILIKE` case-folding drift, duplicate message join count drift, or custom-filter POST `/conversations/filter` behavior. | Focused repository/list handler tests, full `go test ./...`, and `git diff --check` must pass. No route artifacts change. |
|
||||
| 0 | P3.57 conversation meta query/status parity | Implemented for reused conversation sidebar message search counts: GoChat now follows Chatwoot `ConversationFinder#set_up` by skipping status filtering whenever `q` is present, including explicit `status=open`, so query counts span all statuses before the raw `{ meta }` payload is returned. | Keep in Review; reopen from B12 conversation search smoke or fresh reference evidence for PostgreSQL `ILIKE` case-folding drift, duplicate message join count drift, or full conversation list `perform` query/status drift beyond meta. | Focused repository/handler meta query/status tests, full `go test ./...`, and `git diff --check` must pass. No route artifacts change. |
|
||||
| 0 | P3.56 conversation meta query/source parity | Implemented for reused conversation sidebar search/source counts: GoChat now follows inspected Chatwoot `ConversationFinder#filter_by_query` and `#filter_by_source_id` inside meta-only counts, joining public incoming/outgoing messages for `q` and contact inboxes for `source_id` before returning raw `{ meta }`. | Keep in Review; reopen from B12 conversation sidebar smoke or fresh reference evidence for PostgreSQL `ILIKE` case-folding drift, duplicate message join count drift, or deeper custom-role permission filtering. | Focused repository/handler meta query/source tests, full `go test ./...`, and `git diff --check` must pass. No route artifacts change. |
|
||||
@@ -223,6 +224,7 @@ These rows are the executable development plan from this point forward. A checkp
|
||||
|
||||
| ID | Owner files | Reference files | Work to land | Exit gate |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| P3.61 conversation search sender-name parity | `internal/handler/api/v1/conversation_serializer.go`, conversation search handler tests | `reference/chatwoot/app/views/api/v1/models/_conversation.json.jbuilder`, `reference/chatwoot/app/views/api/v1/accounts/conversations/search.json.jbuilder` | Conversation search result messages now serialize `sender_name` when the persisted sender can be resolved, matching `json.sender_name message.sender.name if message.sender` for contact and user senders. | Review by `feat(conversations): align search sender names`; focused search handler tests assert contact and user `sender_name`; full `go test ./...` and `git diff --check` must pass. No route artifacts change. |
|
||||
| P3.60 conversation search message-type parity | `internal/handler/api/v1/conversation_serializer.go`, conversation search handler tests | `reference/chatwoot/app/views/api/v1/models/_conversation.json.jbuilder`, `reference/chatwoot/app/views/api/v1/accounts/conversations/search.json.jbuilder` | Conversation search result messages now serialize `message_type` using Chatwoot `message_type_before_type_cast` integer semantics, sharing the existing message-type mapping used by normal message serializers. | Review by `feat(conversations): align search message types`; focused search handler tests assert numeric `message_type`; full `go test ./...` and `git diff --check` must pass. No route artifacts change. |
|
||||
| P3.59 conversation search finder parity | `internal/handler/api/v1/conversation_handler.go`, `internal/handler/api/v1/conversation_serializer.go`, `internal/service/conversation_service.go`, `internal/repository/conversation_repo.go`, conversation search handler tests | `reference/chatwoot/app/controllers/api/v1/accounts/conversations_controller.rb#search`, `reference/chatwoot/app/finders/conversation_finder.rb`, `reference/chatwoot/app/views/api/v1/accounts/conversations/search.json.jbuilder`, `reference/chatwoot/app/views/api/v1/models/_conversation.json.jbuilder`, `reference/chatwoot/app/javascript/dashboard/api/inbox/conversation.js` | GET `/api/v1/accounts/:account_id/conversations/search` now uses the same finder path as index/search in Chatwoot, accepts missing `q`, preserves query/status skip semantics, and returns the root search payload shape `{ meta: { mine_count, unassigned_count, all_count }, payload }` with search-specific contact/inbox/messages fields instead of the local list/data envelope. | Review by `feat(conversations): align search finder payload`; focused search handler tests cover root payload shape, omitted `assigned_count`, missing-`q` behavior, all-status query search, and search serializer fields; full `go test ./...` and `git diff --check` must pass. No route artifacts change. |
|
||||
| P3.58 conversation index finder parity | `internal/handler/api/v1/conversation_handler.go`, `internal/service/conversation_service.go`, `internal/repository/conversation_repo.go`, conversation CRUD/repository tests | `reference/chatwoot/app/controllers/api/v1/accounts/conversations_controller.rb#index`, `reference/chatwoot/app/finders/conversation_finder.rb`, `reference/chatwoot/app/views/api/v1/accounts/conversations/index.json.jbuilder`, `reference/chatwoot/app/javascript/dashboard/api/inbox/conversation.js` | GET `/api/v1/accounts/:account_id/conversations` now uses a ConversationFinder-style path for reused `ConversationApi.get` params: default status is open, `q` skips status filtering, counts are computed before `assignee_type`, payload is filtered by `assignee_type` after counts, labels/source/query filters share the meta base, and finder sort/pagination behavior replaces the old status-only list path. | Review by `feat(conversations): align index finder filters`; focused repository/list handler tests cover default-open, query/status skip, labels, pre-assignee counts, and post-count payload filtering; full `go test ./...` and `git diff --check` must pass. No route artifacts change. |
|
||||
@@ -308,6 +310,7 @@ This ledger records the committed parity checkpoints that future slices should b
|
||||
|
||||
| Commit | Scope | Verification summary | Follow-up state |
|
||||
| --- | --- | --- | --- |
|
||||
| `feat(conversations): align search sender names` | Advances P3.61 with Chatwoot conversation search model serializer parity. Search result messages now resolve persisted contact/user senders and emit `sender_name` using the same `message.sender.name if message.sender` behavior as the reference `_conversation` Jbuilder, while preserving P3.60 numeric `message_type` and P3.59 root search payload behavior. | `go test ./internal/handler/api/v1 -run 'ConversationCrudTestSuite/TestSearch' -count=1`; `go test ./internal/repository ./internal/service ./internal/handler/api/v1 -run 'ConversationRepo_(GetMeta\|ListForFinder)\|Conversation.*(Meta\|List\|Search)\|ConversationCrudTestSuite/Test(Search\|List)' -count=1`; `go test ./...`; `git diff --check`. No route artifacts change. | Move P3.61 to Review; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. |
|
||||
| `feat(conversations): align search message types` | Advances P3.60 with Chatwoot conversation search message serializer parity. Search result messages now emit integer `message_type` values matching `message_type_before_type_cast`, instead of GoChat string enums, while preserving the P3.59 root search payload and finder behavior. | `go test ./internal/handler/api/v1 -run 'ConversationCrudTestSuite/TestSearch' -count=1`; `go test ./internal/repository ./internal/service ./internal/handler/api/v1 -run 'ConversationRepo_(GetMeta\|ListForFinder)\|Conversation.*(Meta\|List\|Search)\|ConversationCrudTestSuite/Test(Search\|List)' -count=1`; `go test ./...`; `git diff --check`. No route artifacts change. | Move P3.60 to Review; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. |
|
||||
| `feat(conversations): align search finder payload` | Advances P3.59 with Chatwoot conversation search finder parity. GET `/conversations/search` now calls the same ConversationFinder-style service path as index, accepts missing `q`, preserves query/status-skip semantics, and returns the root search Jbuilder payload with `mine_count`, `unassigned_count`, `all_count`, and search-specific conversation contact/inbox/message fields. | `go test ./internal/handler/api/v1 -run 'ConversationCrudTestSuite/TestSearch' -count=1`; `go test ./internal/repository ./internal/service ./internal/handler/api/v1 -run 'ConversationRepo_(GetMeta\|ListForFinder)\|Conversation.*(Meta\|List\|Search)\|ConversationCrudTestSuite/Test(Search\|List)' -count=1`; `go test ./...`; `git diff --check`. No route artifacts change. | Move P3.59 to Review; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. |
|
||||
| `feat(conversations): align index finder filters` | Advances P3.58 with Chatwoot conversation index finder parity. Reused frontend GET `/conversations` query params now flow through a ConversationFinder-style service/repository path: default-open status, status skip while `q` is present, labels/source/query filters, pre-assignee meta counts, post-count `assignee_type` payload filtering, and finder sort/pagination replace the previous status-only list behavior. | `go test ./internal/repository ./internal/handler/api/v1 -run 'ConversationRepo_(GetMeta\|ListForFinder)\|ConversationCrudTestSuite/TestList' -count=1`; `go test ./internal/repository ./internal/service ./internal/handler/api/v1 -run 'Conversation.*(Meta\|List)\|ConversationRepo\|ConversationService\|ConversationCrudTestSuite' -count=1`; `go test ./...`; `git diff --check`. No route artifacts change. | Move P3.58 to Review; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. |
|
||||
@@ -2621,3 +2624,4 @@ Verification milestone gates:
|
||||
- 2026-06-07: P3.58 conversation index finder checkpoint prepared as `feat(conversations): align index finder filters`; audited Chatwoot `ConversationsController#index`, `ConversationFinder#perform`, `index.json.jbuilder`, and reused dashboard `ConversationApi.get`. GoChat GET `/conversations` now uses the finder-style filtered base instead of the old status-only list path: default-open status, `q` status-skip, labels/source/query filters, pre-assignee meta counts, post-count `assignee_type` payload filtering, and finder sort/pagination are covered. Focused repository/list handler tests passed; full `go test ./...` and `git diff --check` passed. No route artifacts change.
|
||||
- 2026-06-07: P3.59 conversation search finder checkpoint prepared as `feat(conversations): align search finder payload`; audited Chatwoot `ConversationsController#search`, `ConversationFinder#perform`, `search.json.jbuilder`, `_conversation` search model serializer, and reused dashboard `ConversationApi.search`. GoChat GET `/conversations/search` now uses the finder-style path, accepts missing `q`, keeps query/status-skip behavior, returns root `{ meta, payload }` without `assigned_count` or `data`, and serializes search-specific contact/inbox/messages fields. Focused search/list handler and finder tests passed; full `go test ./...` and `git diff --check` passed. No route artifacts change.
|
||||
- 2026-06-07: P3.60 conversation search message-type checkpoint prepared as `feat(conversations): align search message types`; audited Chatwoot search `_conversation` model serializer and its `message_type_before_type_cast` field. GoChat conversation search result messages now emit integer message types (`incoming=0`, `outgoing=1`, `activity=2`, `template=3`) instead of local string enums. Focused search handler tests passed; full `go test ./...` and `git diff --check` passed. No route artifacts change.
|
||||
- 2026-06-07: P3.61 conversation search sender-name checkpoint prepared as `feat(conversations): align search sender names`; audited Chatwoot search `_conversation` model serializer and its `json.sender_name message.sender.name if message.sender` field. GoChat conversation search result messages now resolve persisted contact/user senders and emit `sender_name` alongside the existing numeric `message_type` payload. Focused search handler tests passed; full `go test ./...` and `git diff --check` passed. No route artifacts change.
|
||||
|
||||
@@ -181,6 +181,7 @@ func (s *ConversationCrudTestSuite) TearDownTest() {
|
||||
s.db.Exec("DELETE FROM inboxes")
|
||||
s.db.Exec("DELETE FROM messages")
|
||||
s.db.Exec("DELETE FROM account_users")
|
||||
s.db.Exec("DELETE FROM users")
|
||||
s.db.Exec("DELETE FROM accounts")
|
||||
s.db.Exec("DELETE FROM teams")
|
||||
s.db.Exec("DELETE FROM team_members")
|
||||
@@ -897,10 +898,12 @@ func (s *ConversationCrudTestSuite) TestUpdateLabels_NotFound() {
|
||||
// ========== Search Handler Tests ==========
|
||||
|
||||
func (s *ConversationCrudTestSuite) TestSearch_Success() {
|
||||
s.Require().NoError(s.db.Create(&model.Message{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ConversationID: s.testConv.ID, Content: "needle open", MessageType: string(model.MessageTypeIncoming)}).Error)
|
||||
s.Require().NoError(s.db.Create(&model.Message{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ConversationID: s.testConv.ID, SenderID: &s.testContact.ID, SenderType: string(model.SenderTypeContact), Content: "needle open", MessageType: string(model.MessageTypeIncoming)}).Error)
|
||||
agent := &model.User{AccountID: s.testAccount.ID, Name: "Search Agent", Email: "search-agent-" + strconv.FormatUint(uint64(s.testAccount.ID), 10) + "@example.com", Password: "secret", Role: "agent"}
|
||||
s.Require().NoError(s.db.Create(agent).Error)
|
||||
resolved := &model.Conversation{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ContactID: s.testContact.ID, Status: "resolved", ChannelType: "web_widget", Channel: "web_widget"}
|
||||
s.Require().NoError(s.db.Create(resolved).Error)
|
||||
s.Require().NoError(s.db.Create(&model.Message{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ConversationID: resolved.ID, Content: "needle resolved", MessageType: string(model.MessageTypeOutgoing)}).Error)
|
||||
s.Require().NoError(s.db.Create(&model.Message{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ConversationID: resolved.ID, SenderID: &agent.ID, SenderType: string(model.SenderTypeUser), Content: "needle resolved", MessageType: string(model.MessageTypeOutgoing)}).Error)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", s.accountURL()+"/conversations/search?q=needle&status=open", nil)
|
||||
@@ -936,6 +939,16 @@ func (s *ConversationCrudTestSuite) TestSearch_Success() {
|
||||
assert.Equal(s.T(), s.testInbox.Name, resp.Payload[0].Inbox["name"])
|
||||
assert.NotEmpty(s.T(), resp.Payload[0].Messages)
|
||||
assert.Equal(s.T(), float64(0), resp.Payload[0].Messages[0]["message_type"])
|
||||
senderNamesByContent := map[string]string{}
|
||||
for _, conversation := range resp.Payload {
|
||||
for _, message := range conversation.Messages {
|
||||
if senderName, ok := message["sender_name"].(string); ok {
|
||||
senderNamesByContent[message["content"].(string)] = senderName
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.Equal(s.T(), s.testContact.Name, senderNamesByContent["needle open"])
|
||||
assert.Equal(s.T(), agent.Name, senderNamesByContent["needle resolved"])
|
||||
}
|
||||
|
||||
func (s *ConversationCrudTestSuite) TestSearch_InvalidAccountID() {
|
||||
|
||||
@@ -93,7 +93,7 @@ type chatwootConversationSearchPayload struct {
|
||||
type chatwootConversationSearchMessage struct {
|
||||
Content string `json:"content"`
|
||||
ID uint `json:"id"`
|
||||
SenderName string `json:"sender_name,omitempty"`
|
||||
SenderName *string `json:"sender_name,omitempty"`
|
||||
MessageType int `json:"message_type"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
@@ -189,6 +189,7 @@ func serializeConversationSearch(ctx context.Context, db *gorm.DB, conversation
|
||||
payload.Messages = append(payload.Messages, chatwootConversationSearchMessage{
|
||||
Content: messages[i].Content,
|
||||
ID: messages[i].ID,
|
||||
SenderName: messageSenderName(ctx, db, &messages[i]),
|
||||
MessageType: messageTypeValue(messages[i].MessageType),
|
||||
CreatedAt: messages[i].CreatedAt.Unix(),
|
||||
})
|
||||
@@ -463,6 +464,15 @@ func serializeMessageSender(ctx context.Context, db *gorm.DB, message *model.Mes
|
||||
return nil
|
||||
}
|
||||
|
||||
func messageSenderName(ctx context.Context, db *gorm.DB, message *model.Message) *string {
|
||||
sender := serializeMessageSender(ctx, db, message)
|
||||
if sender == nil {
|
||||
return nil
|
||||
}
|
||||
name, _ := sender["name"].(string)
|
||||
return &name
|
||||
}
|
||||
|
||||
func serializeContact(contact *model.Contact) map[string]any {
|
||||
return map[string]any{
|
||||
"additional_attributes": jsonObject(contact.AdditionalAttributes),
|
||||
|
||||
Reference in New Issue
Block a user