diff --git a/cmd/gochat/main.go b/cmd/gochat/main.go index 07e17cb1..8c2cdf19 100644 --- a/cmd/gochat/main.go +++ b/cmd/gochat/main.go @@ -107,17 +107,20 @@ func closeDB(db *gorm.DB) { } type smokeSeedSummary struct { - AdminEmail string `json:"admin_email"` - AdminPassword string `json:"admin_password"` - AccountID uint `json:"account_id"` - InboxID uint `json:"inbox_id"` - ContactID uint `json:"contact_id"` - ConversationID uint `json:"conversation_id"` - ConversationUID string `json:"conversation_uuid"` - SlaPolicyID uint `json:"sla_policy_id"` - CustomRoleID uint `json:"custom_role_id"` - CapacityPolicyID uint `json:"capacity_policy_id"` - CaptainAssistantID uint `json:"captain_assistant_id"` + AdminEmail string `json:"admin_email"` + AdminPassword string `json:"admin_password"` + AccountID uint `json:"account_id"` + InboxID uint `json:"inbox_id"` + ContactID uint `json:"contact_id"` + CompanyID uint `json:"company_id"` + ConversationID uint `json:"conversation_id"` + ConversationDisplayID uint `json:"conversation_display_id"` + ConversationUID string `json:"conversation_uuid"` + CsatMessageID uint `json:"csat_message_id"` + SlaPolicyID uint `json:"sla_policy_id"` + CustomRoleID uint `json:"custom_role_id"` + CapacityPolicyID uint `json:"capacity_policy_id"` + CaptainAssistantID uint `json:"captain_assistant_id"` } func seedSmokeData(ctx context.Context, db *gorm.DB) (*smokeSeedSummary, error) { @@ -150,17 +153,30 @@ func seedSmokeData(ctx context.Context, db *gorm.DB) (*smokeSeedSummary, error) } inbox := &model.Inbox{} - if err := db.WithContext(ctx).Where("account_id = ? AND name = ?", account.ID, inboxName).FirstOrCreate(inbox, model.Inbox{AccountID: account.ID, Name: inboxName, ChannelType: "web_widget", Enabled: true, EnableAutoAssignment: true, GreetingEnabled: true, GreetingMessage: "Hello from GoChat", EnableEmailCollect: true, CsatSurveyEnabled: true, Timezone: "UTC", AllowMessagesAfterResolved: true, SenderNameType: "friendly_name", ChannelConfig: `{"website_url":"http://localhost:3036","website_token":"gochat-smoke-widget-token","widget_color":"#1f93ff"}`}).Error; err != nil { + channelConfig := `{"website_url":"http://localhost:3036","website_token":"gochat-smoke-widget-token","widget_color":"#1f93ff"}` + csatConfig := `{"display_type":"emoji","message":"Rate this chat"}` + if err := db.WithContext(ctx).Where("account_id = ? AND name = ?", account.ID, inboxName).FirstOrCreate(inbox, model.Inbox{AccountID: account.ID, Name: inboxName, ChannelType: "web_widget", Enabled: true, EnableAutoAssignment: true, GreetingEnabled: true, GreetingMessage: "Hello from GoChat", EnableEmailCollect: true, CsatSurveyEnabled: true, CsatConfig: csatConfig, Timezone: "UTC", AllowMessagesAfterResolved: true, SenderNameType: "friendly_name", ChannelConfig: channelConfig}).Error; err != nil { return nil, fmt.Errorf("seed inbox: %w", err) } + if err := db.WithContext(ctx).Model(inbox).Updates(map[string]any{"enabled": true, "csat_survey_enabled": true, "csat_config": csatConfig, "channel_config": channelConfig}).Error; err != nil { + return nil, fmt.Errorf("update inbox smoke settings: %w", err) + } if err := db.WithContext(ctx).Where("inbox_id = ? AND user_id = ?", inbox.ID, admin.ID).FirstOrCreate(&model.InboxMember{}, model.InboxMember{InboxID: inbox.ID, UserID: admin.ID, Role: "administrator"}).Error; err != nil { return nil, fmt.Errorf("seed inbox member: %w", err) } + company := &model.Company{} + if err := db.WithContext(ctx).Where("account_id = ? AND name = ?", account.ID, "Smoke Company").FirstOrCreate(company, model.Company{AccountID: account.ID, Name: "Smoke Company", Domain: "gochat.local", WebsiteURL: "https://gochat.local", CustomAttributes: datatypes.JSON([]byte(`{"tier":"enterprise"}`))}).Error; err != nil { + return nil, fmt.Errorf("seed company: %w", err) + } + contact := &model.Contact{} - if err := db.WithContext(ctx).Where("account_id = ? AND email = ?", account.ID, "customer@gochat.local").FirstOrCreate(contact, model.Contact{AccountID: account.ID, Name: "Smoke Customer", Email: "customer@gochat.local", Identifier: "gochat-smoke-customer", ContactType: "lead", AdditionalAttributes: datatypes.JSON([]byte(`{}`)), CustomAttributes: datatypes.JSON([]byte(`{"plan":"enterprise"}`))}).Error; err != nil { + if err := db.WithContext(ctx).Where("account_id = ? AND email = ?", account.ID, "customer@gochat.local").FirstOrCreate(contact, model.Contact{AccountID: account.ID, CompanyID: &company.ID, Name: "Smoke Customer", Email: "customer@gochat.local", Identifier: "gochat-smoke-customer", ContactType: "lead", AdditionalAttributes: datatypes.JSON([]byte(`{}`)), CustomAttributes: datatypes.JSON([]byte(`{"plan":"enterprise"}`))}).Error; err != nil { return nil, fmt.Errorf("seed contact: %w", err) } + if err := db.WithContext(ctx).Model(contact).Updates(map[string]any{"company_id": company.ID}).Error; err != nil { + return nil, fmt.Errorf("update contact company: %w", err) + } contactInbox := &model.ContactInbox{} if err := db.WithContext(ctx).Where("contact_id = ? AND inbox_id = ?", contact.ID, inbox.ID).FirstOrCreate(contactInbox, model.ContactInbox{ContactID: contact.ID, InboxID: inbox.ID, SourceID: "gochat-smoke-source", PubsubToken: "gochat-smoke-contact-pubsub"}).Error; err != nil { return nil, fmt.Errorf("seed contact inbox: %w", err) @@ -172,10 +188,20 @@ func seedSmokeData(ctx context.Context, db *gorm.DB) (*smokeSeedSummary, error) if err := db.WithContext(ctx).Where("account_id = ? AND inbox_id = ? AND contact_id = ?", account.ID, inbox.ID, contact.ID).FirstOrCreate(conversation, model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, ContactInboxID: &contactInbox.ID, DisplayID: &displayID, AssigneeID: &admin.ID, Status: "open", Priority: "medium", ChannelType: "web_widget", Channel: "web_widget", Labels: "vip", LastActivityAt: &lastActivity}).Error; err != nil { return nil, fmt.Errorf("seed conversation: %w", err) } - if err := seedMessage(ctx, db, conversation, inbox.ID, contact.ID, "incoming", "Hello, I need help with my order."); err != nil { + if err := db.WithContext(ctx).Model(conversation).Updates(map[string]any{"contact_inbox_id": contactInbox.ID, "display_id": displayID, "assignee_id": admin.ID, "status": "open", "priority": "medium", "channel_type": "web_widget", "channel": "web_widget", "labels": "vip", "last_activity_at": lastActivity}).Error; err != nil { + return nil, fmt.Errorf("update smoke conversation: %w", err) + } + conversation.ContactInboxID = &contactInbox.ID + conversation.DisplayID = &displayID + conversation.AssigneeID = &admin.ID + if _, err := seedMessage(ctx, db, conversation, inbox.ID, contact.ID, "incoming", "text", "Hello, I need help with my order."); err != nil { return nil, err } - if err := seedMessage(ctx, db, conversation, inbox.ID, admin.ID, "outgoing", "I can help with that."); err != nil { + if _, err := seedMessage(ctx, db, conversation, inbox.ID, admin.ID, "outgoing", "text", "I can help with that."); err != nil { + return nil, err + } + csatMessage, err := seedMessage(ctx, db, conversation, inbox.ID, admin.ID, "template", "input_csat", "Rate this chat") + if err != nil { return nil, err } @@ -204,26 +230,34 @@ func seedSmokeData(ctx context.Context, db *gorm.DB) (*smokeSeedSummary, error) return nil, fmt.Errorf("seed captain assistant: %w", err) } - return &smokeSeedSummary{AdminEmail: adminEmail, AdminPassword: adminPassword, AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, ConversationID: conversation.ID, ConversationUID: conversation.UUID, SlaPolicyID: sla.ID, CustomRoleID: customRole.ID, CapacityPolicyID: capacity.ID, CaptainAssistantID: assistant.ID}, nil + conversationDisplayID := uint(0) + if conversation.DisplayID != nil { + conversationDisplayID = *conversation.DisplayID + } + return &smokeSeedSummary{AdminEmail: adminEmail, AdminPassword: adminPassword, AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, CompanyID: company.ID, ConversationID: conversation.ID, ConversationDisplayID: conversationDisplayID, ConversationUID: conversation.UUID, CsatMessageID: csatMessage.ID, SlaPolicyID: sla.ID, CustomRoleID: customRole.ID, CapacityPolicyID: capacity.ID, CaptainAssistantID: assistant.ID}, nil } -func seedMessage(ctx context.Context, db *gorm.DB, conversation *model.Conversation, inboxID, senderID uint, messageType, content string) error { +func seedMessage(ctx context.Context, db *gorm.DB, conversation *model.Conversation, inboxID, senderID uint, messageType, contentType, content string) (*model.Message, error) { + message := &model.Message{} var count int64 if err := db.WithContext(ctx).Model(&model.Message{}).Where("conversation_id = ? AND content = ?", conversation.ID, content).Count(&count).Error; err != nil { - return fmt.Errorf("check message: %w", err) + return nil, fmt.Errorf("check message: %w", err) } if count > 0 { - return nil + if err := db.WithContext(ctx).Where("conversation_id = ? AND content = ?", conversation.ID, content).First(message).Error; err != nil { + return nil, fmt.Errorf("load existing message: %w", err) + } + return message, nil } senderType := "contact" - if messageType == "outgoing" { + if messageType == "outgoing" || messageType == "template" { senderType = "user" } - msg := &model.Message{AccountID: conversation.AccountID, InboxID: inboxID, ConversationID: conversation.ID, SenderID: &senderID, SenderType: senderType, MessageType: messageType, ContentType: "text", Content: content, Status: "sent"} - if err := db.WithContext(ctx).Create(msg).Error; err != nil { - return fmt.Errorf("seed message: %w", err) + message = &model.Message{AccountID: conversation.AccountID, InboxID: inboxID, ConversationID: conversation.ID, SenderID: &senderID, SenderType: senderType, MessageType: messageType, ContentType: contentType, Content: content, Status: "sent", ContentAttributes: datatypes.JSON([]byte(`{"display_type":"emoji"}`))} + if err := db.WithContext(ctx).Create(message).Error; err != nil { + return nil, fmt.Errorf("seed message: %w", err) } - return nil + return message, nil } func getenvDefault(key, fallback string) string { diff --git a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md index 6804fc4a..a2b14206 100644 --- a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md +++ b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md @@ -17,9 +17,9 @@ Build GoChat as a Go backend that can directly reuse the frontend from `referenc ## Current Baseline - Plan freeze checkpoint: 2026-06-05 docs-only tracker landing after `13cb750 feat(captain): align task payload persistence`. -- Latest implementation checkpoint: this checkpoint, prepared as `test(parity): add frontend smoke harness`. +- Latest implementation checkpoint: this checkpoint, prepared as `test(parity): add api smoke assertions`. - Latest documentation checkpoint before this freeze: `3263ed9 docs: land copilot task execution plan`; this document now carries the active follow-up plan directly. -- Worktree status at this implementation checkpoint: B11.1a aligns Captain assistant CRUD/tools/inbox bindings; B11.1b aligns Captain scenarios and custom tools; B11.1c aligns Captain documents, assistant responses, bulk actions, and custom-tool test payloads; B11.2 aligns Copilot thread/message create/list/get/delete payloads, account/user scoping, and no-LLM fallback persistence; B11.3a aligns Captain preferences show/update payloads and account-level model/feature storage; B11.3b aligns Captain playground request/response payloads, account scoping, v2 history handling, and no-LLM fallback; B11.3c adds the fakeable Captain document sync backend gate with disabled, failed, and fake-success states; B11.3d aligns Captain task request/response payloads, no-provider disabled states, follow-up context, suggestion persistence, and Copilot message tool-call key validation; B11.3e aligns Captain stream DTOs/disabled SSE fallbacks and Copilot push-event payload shapes; B12.1 adds the reusable GoChat server/seed entrypoint plus a Meilisearch-first reused Chatwoot frontend smoke harness and report. Next active implementation slice is B12.2 browser/API path assertions. +- Worktree status at this implementation checkpoint: B11.1a aligns Captain assistant CRUD/tools/inbox bindings; B11.1b aligns Captain scenarios and custom tools; B11.1c aligns Captain documents, assistant responses, bulk actions, and custom-tool test payloads; B11.2 aligns Copilot thread/message create/list/get/delete payloads, account/user scoping, and no-LLM fallback persistence; B11.3a aligns Captain preferences show/update payloads and account-level model/feature storage; B11.3b aligns Captain playground request/response payloads, account scoping, v2 history handling, and no-LLM fallback; B11.3c adds the fakeable Captain document sync backend gate with disabled, failed, and fake-success states; B11.3d aligns Captain task request/response payloads, no-provider disabled states, follow-up context, suggestion persistence, and Copilot message tool-call key validation; B11.3e aligns Captain stream DTOs/disabled SSE fallbacks and Copilot push-event payload shapes; B12.1 adds the reusable GoChat server/seed entrypoint plus a Meilisearch-first reused Chatwoot frontend smoke harness and report; B12.2a adds API smoke assertions for auth/profile, inbox, conversation/messages, contact/company, widget config/message, and public CSAT. Next active implementation slice is B12.2b live browser navigation against the reused frontend. - `go test ./...` passes. - Route dump succeeds with `TOTAL: 830` after adding the Chatwoot-compatible applied-SLA index route. - Route parity artifacts now exist under `docs/parity/` and are generated by `cmd/route_parity`. @@ -80,7 +80,7 @@ Open work after the current checkpoint: | Phase 4 | Enterprise feature completion | Doing | B7, B8, B9, B10, and B11 are in Review; B12 reused frontend smoke is the next broad verification gate | | Phase 5 | Background jobs and integrations | Planned | durable worker choice and job parity are open | | Phase 6 | Core placeholder burn-down | Doing | account/contact/conversation/message/inbox placeholder groups remain broad | -| Phase 7 | Verification harness | Doing | B12.1 boot/readiness harness exists; browser/API assertions and optional live Meilisearch run remain open | +| Phase 7 | Verification harness | Doing | B12.1 boot/readiness and B12.2a API assertions exist; live browser navigation and optional live Meilisearch run remain open | ## Tracking Artifacts @@ -101,6 +101,7 @@ This ledger records the committed parity checkpoints that future slices should b | Commit | Scope | Verification summary | Follow-up state | | --- | --- | --- | --- | +| `test(parity): add api smoke assertions` | Advances B12.2 with a repeatable `scripts/parity_frontend_smoke.sh --api-smoke` path that seeds deterministic frontend data, signs in through Chatwoot auth headers, asserts current-user/profile, inbox list, conversation/message list and create, contact/company show, widget config/message, and public CSAT show. The seed now includes company, conversation display ID, and CSAT message IDs for smoke assertions. | `bash -n scripts/parity_frontend_smoke.sh`; `scripts/parity_frontend_smoke.sh --print`; `scripts/parity_frontend_smoke.sh --check`; `go test ./cmd/gochat -count=1`; full verification recorded in the B12.2a section. Live API smoke requires a running DB/backend and is not run in default check mode. | Continue B12.2b with live browser navigation/assertions against the reused Vite frontend; B12.3 enterprise screen smoke remains open. | | `test(parity): add frontend smoke harness` | Completes B12.1 readiness tooling: adds `cmd/gochat` as the reusable server/seed entrypoint, adds `scripts/parity_frontend_smoke.sh` to print/check/boot GoChat plus the reused `reference/chatwoot` Vite frontend, keeps the boot command Meilisearch-first, and writes `docs/parity/frontend_smoke_report.md` with seed credentials, command lines, logs, and the smoke matrix. | `bash -n scripts/parity_frontend_smoke.sh`; `scripts/parity_frontend_smoke.sh --print`; `scripts/parity_frontend_smoke.sh --check`; `go test ./cmd/gochat -count=1`; full verification recorded in the B12.1 section. | Continue B12.2 with browser/API assertions for auth, inbox, conversation, CRM, widget, CSAT, and enterprise screens. | | `feat(captain): align streaming fallbacks` | Completed B11.3e streaming/realtime compatibility: Captain task stream routes now reuse non-stream request DTOs, resolve conversations by account-scoped display ID or legacy ID, guard missing LLM providers with deterministic SSE `error`/`done` events, validate rewrite operations, and keep stream errors frontend-readable. Copilot message nested thread payloads now use Chatwoot `push_event_data` shape and event payload helpers omit REST-only fields. | Focused CaptainTask/Captain/Copilot handler and service tests passed; Copilot/Captain repository tests passed; full verification recorded below. | B11 moves to Review; start B12 reused frontend smoke. | | `docs: land parity tracker handoff` | Froze the active tracker after B11.3d, copied the remaining Hermes-derived work into explicit B11.3e/B12/Phase 5/Phase 6 tracking rows, and clarified the commit/update rules for future checkpoints. | Documentation-only checkpoint; `git diff --check` is sufficient. | Start B11.3e streaming/realtime compatibility. | @@ -453,7 +454,7 @@ Upcoming enterprise task boards: | B11 | B11.2 | Align Copilot threads, messages, tasks, preferences, playground/tool-call behavior, and disabled-state feature gates. | Copilot controllers/services/frontend clients under `reference/chatwoot`. | Copilot handler/service tests for persistence, disabled LLM state, and frontend payloads. | Review; thread/message payloads, account/user scoping, assistant scope, and no-LLM fallback are landed; tasks/preferences/tool-call/playground depth remains in B11.3 follow-up | | B11 | B11.3 | Add document sync/embedding/LLM job boundaries where external dependencies are required and finish remaining Copilot task/preference/tool-call/streaming depth. | Captain/Copilot jobs, document services, Copilot controllers/services/frontend clients. | Worker tests or explicit feature-gated fallback tests plus Copilot task/preference/tool-call fixtures. | Doing; B11.3a Captain preferences show/update payloads are landed, while document sync/indexing and remaining Copilot task/tool-call depth remain active | | B12 | B12.1 | Add a repeatable command to run the reused Chatwoot frontend against GoChat. | `reference/chatwoot` frontend boot/auth/API clients. | Smoke command documented and runnable locally. | Done by `test(parity): add frontend smoke harness` | -| B12 | B12.2 | Cover login, inbox list/settings, conversation list/detail/message send, contact/company views, widget init/message, public CSAT, SLA/CSAT reports, and enterprise admin screens. | Dashboard route usage and frontend stores/API modules. | Smoke report checked into `docs/parity/` with pass/fail gaps. | Todo; next active slice | +| B12 | B12.2 | Cover login, inbox list/settings, conversation list/detail/message send, contact/company views, widget init/message, public CSAT, SLA/CSAT reports, and enterprise admin screens. | Dashboard route usage and frontend stores/API modules. | Smoke report checked into `docs/parity/` with pass/fail gaps. | Doing; B12.2a API assertion command landed, B12.2b browser navigation pending | B8 CSAT execution breakdown: @@ -1008,7 +1009,7 @@ B12 reused frontend verification breakdown: | Step | Implementation target | Reference source | Required tests | Status | | --- | --- | --- | --- | --- | | B12.1 | Add a repeatable smoke command that boots GoChat plus the reused `reference/chatwoot` frontend without adapters. | Local app boot scripts, Chatwoot dashboard frontend. | Checked command and gap report under `docs/parity/`. | Done by `scripts/parity_frontend_smoke.sh`, `cmd/gochat seed`, and `docs/parity/frontend_smoke_report.md` | -| B12.2 | Cover login, current user, inbox list, conversation list/detail, message send, contact/company view, and widget init/message. | Dashboard/widget frontend routes and API clients. | Smoke output records pass/fail and links failed API calls to route/serializer tasks. | Todo; next active slice | +| B12.2 | Cover login, current user, inbox list, conversation list/detail, message send, contact/company view, widget init/message, and public CSAT. | Dashboard/widget frontend routes and API clients. | Smoke output records pass/fail and links failed API calls to route/serializer tasks. | Doing; B12.2a API smoke command landed, B12.2b reused-frontend browser navigation pending | | B12.3 | Add enterprise smoke coverage as B8-B11 land: SLA reports, CSAT public/account reports, automation/macros, audit/custom roles, Captain/Copilot. | Enterprise frontend screens and clients. | Smoke output keeps enterprise failures as named follow-up tasks, not hidden browser-only debt. | Todo | B12 smoke harness contract: @@ -1040,6 +1041,30 @@ env TMPDIR=/home/rogee/Projects/gochat/.tmp/test-tmp GOCACHE=/tmp/gochat-gocache git diff --check ``` +B12.2a current checkpoint: + +- `scripts/parity_frontend_smoke.sh --api-smoke` now runs against a live GoChat backend. It seeds deterministic data, logs in with `/auth/sign_in`, captures DeviseTokenAuth-compatible headers, and exercises the frontend-critical API paths without frontend adapters. +- Covered API assertions: `/auth/validate_token`, `/api/v1/profile`, inbox list, conversation list/messages/message create, contact show, company show, widget config, widget message create, and public CSAT show. +- `cmd/gochat seed` now emits `company_id`, `conversation_display_id`, and `csat_message_id`; it also creates a deterministic company relation and an `input_csat` message so B12 API smoke can verify CRM and public CSAT surfaces. +- This is still not a browser-navigation pass. B12.2b must drive the reused Vite frontend and capture failed API calls or UI blockers in the report; B12.3 must add enterprise screens. + +B12.2a verification: + +```bash +bash -n scripts/parity_frontend_smoke.sh +scripts/parity_frontend_smoke.sh --print +scripts/parity_frontend_smoke.sh --check +env GOCACHE=/tmp/gochat-gocache GOMODCACHE=/tmp/gochat-gomodcache go test ./cmd/gochat -count=1 +env TMPDIR=/home/rogee/Projects/gochat/.tmp/test-tmp GOCACHE=/tmp/gochat-gocache GOMODCACHE=/tmp/gochat-gomodcache go test ./... +git diff --check +``` + +Optional live API smoke, when PostgreSQL/Redis/Meilisearch and GoChat are running: + +```bash +scripts/parity_frontend_smoke.sh --api-smoke +``` + Hermes plan material now mapped: - `.hermes/plans/2025-05-24-global-search-meilisearch.md` maps to Phase 1/B6. The Meilisearch interface, config, documents, indexing hooks, reindex command, payload shape, and live gate are already tracked here. Remaining search work is only future payload gaps discovered by frontend smoke or route expansion. @@ -1685,3 +1710,4 @@ Verification milestone gates: - 2026-06-05: Parity tracker handoff checkpoint prepared as `docs: land parity tracker handoff`; the plan now has a front-loaded handoff contract, explicit open-work table for B11.3e/B12/Phase 5/Phase 2/3/Phase 6, exact Hermes source-plan mapping, and a rule that every future checkpoint updates this tracker before commit. Documentation-only checkpoint; `git diff --check` passed. - 2026-06-05: B11.3e streaming/realtime checkpoint prepared as `feat(captain): align streaming fallbacks`; Captain stream task routes now share non-stream DTO/account-scope behavior, return deterministic SSE disabled/error states for missing providers and validation errors, and preserve success chunk/done shapes. Copilot message REST payloads now nest thread `push_event_data`, dedicated push payload helpers match Chatwoot event data, and the legacy Copilot SSE route has a no-provider disabled guard. Focused CaptainTask/Captain/Copilot handler/service tests and Copilot/Captain repository tests passed; full verification is recorded in the B11.3e section. B11 moves to Review; next active slice is B12 reused Chatwoot frontend smoke. - 2026-06-05: B12.1 frontend smoke harness checkpoint prepared as `test(parity): add frontend smoke harness`; `cmd/gochat` now provides `serve` and deterministic `seed`, `.gitignore` no longer hides `cmd/gochat`, `scripts/parity_frontend_smoke.sh` can print/check/boot GoChat plus the reused `reference/chatwoot` Vite frontend with Meilisearch-first defaults, and `docs/parity/frontend_smoke_report.md` records commands, seed data, logs, and pending smoke matrix owners. Verification: `bash -n scripts/parity_frontend_smoke.sh`, `scripts/parity_frontend_smoke.sh --print`, `scripts/parity_frontend_smoke.sh --check`, `go test ./cmd/gochat -count=1`, full `go test ./...`, and `git diff --check`. Next slice is B12.2 browser/API path assertions. +- 2026-06-05: B12.2a API smoke assertion checkpoint prepared as `test(parity): add api smoke assertions`; the smoke script now has `--api-smoke` for live GoChat API checks covering auth/profile, inbox, conversation/message list and create, contact/company show, widget config/message, and public CSAT show. Seed output now includes company, conversation display ID, and CSAT message identifiers. Default verification keeps `--api-smoke` optional because it needs a running backend/DB; `bash -n`, `--print`, `--check`, `go test ./cmd/gochat`, full `go test ./...`, and `git diff --check` passed locally. Next slice is B12.2b reused-frontend browser navigation. diff --git a/docs/parity/frontend_smoke_report.md b/docs/parity/frontend_smoke_report.md index 6b735a7c..3b9b26d2 100644 --- a/docs/parity/frontend_smoke_report.md +++ b/docs/parity/frontend_smoke_report.md @@ -1,6 +1,6 @@ # Frontend Smoke Report -Updated: 2026-06-05T06:08:51Z +Updated: 2026-06-05T06:19:13Z ## Status @@ -12,6 +12,12 @@ Harness readiness check passed; live frontend smoke not run in this mode. scripts/parity_frontend_smoke.sh --boot-only ``` +## API Smoke Command + +```bash +scripts/parity_frontend_smoke.sh --api-smoke +``` + ## Backend - URL: http://127.0.0.1:3000 @@ -36,7 +42,7 @@ GOCHAT_SEED_INBOX_NAME="B12 Smoke Website Inbox" \ go run ./cmd/gochat seed ``` -The seed command creates deterministic login/account/inbox/contact/conversation data plus SLA, CustomRole, AgentCapacity, and Captain fixtures for the smoke paths. +The seed command creates deterministic login/account/inbox/contact/company/conversation data plus CSAT, SLA, CustomRole, AgentCapacity, and Captain fixtures for the smoke paths. ## Smoke Matrix diff --git a/scripts/parity_frontend_smoke.sh b/scripts/parity_frontend_smoke.sh index 67aeaa52..74b7758f 100755 --- a/scripts/parity_frontend_smoke.sh +++ b/scripts/parity_frontend_smoke.sh @@ -19,12 +19,14 @@ KEEP_ALIVE="true" usage() { cat <&2; usage; exit 2 ;; esac @@ -77,6 +83,12 @@ $status scripts/parity_frontend_smoke.sh --boot-only \`\`\` +## API Smoke Command + +\`\`\`bash +scripts/parity_frontend_smoke.sh --api-smoke +\`\`\` + ## Backend - URL: http://$API_HOST:$API_PORT @@ -101,21 +113,21 @@ GOCHAT_SEED_INBOX_NAME="B12 Smoke Website Inbox" \\ go run ./cmd/gochat seed \`\`\` -The seed command creates deterministic login/account/inbox/contact/conversation data plus SLA, CustomRole, AgentCapacity, and Captain fixtures for the smoke paths. +The seed command creates deterministic login/account/inbox/contact/company/conversation data plus CSAT, SLA, CustomRole, AgentCapacity, and Captain fixtures for the smoke paths. ## Smoke Matrix | Area | Current result | Owner if failing | | --- | --- | --- | -| Boot GoChat backend | Not run in check mode | B12.1 | -| Boot reused Chatwoot Vite frontend | Not run in check mode | B12.1 | -| Auth/profile | Pending browser/API smoke | B2/B12.2 | -| Inbox list/settings | Pending browser/API smoke | B5/B12.2 | -| Conversation list/detail/message send | Pending browser/API smoke | B3/B12.2 | -| Contact/company views | Pending browser/API smoke | B4/B12.2 | -| Widget config/message | Pending browser/API smoke | B12.2 | -| Public CSAT | Pending browser/API smoke | B8/B12.2 | -| Enterprise screens | Pending browser/API smoke | B7-B11/B12.3 | +| Boot GoChat backend | ${BOOT_BACKEND_RESULT:-Not run in check mode} | B12.1 | +| Boot reused Chatwoot Vite frontend | ${BOOT_FRONTEND_RESULT:-Not run in check mode} | B12.1 | +| Auth/profile | ${AUTH_PROFILE_RESULT:-Pending browser/API smoke} | B2/B12.2 | +| Inbox list/settings | ${INBOX_RESULT:-Pending browser/API smoke} | B5/B12.2 | +| Conversation list/detail/message send | ${CONVERSATION_RESULT:-Pending browser/API smoke} | B3/B12.2 | +| Contact/company views | ${CRM_RESULT:-Pending browser/API smoke} | B4/B12.2 | +| Widget config/message | ${WIDGET_RESULT:-Pending browser/API smoke} | B12.2 | +| Public CSAT | ${CSAT_RESULT:-Pending browser/API smoke} | B8/B12.2 | +| Enterprise screens | ${ENTERPRISE_RESULT:-Pending browser/API smoke} | B7-B11/B12.3 | ## Notes @@ -154,9 +166,166 @@ wait_url() { print_commands() { echo "Backend: ${backend_cmd[*]}" echo "Frontend: (cd $CHATWOOT_DIR && ${frontend_cmd[*]})" + echo "API smoke: scripts/parity_frontend_smoke.sh --api-smoke" echo "Report: $REPORT_PATH" } +tmp_file() { + local file + file="$(mktemp "$LOG_DIR/smoke.XXXXXX")" + echo "$file" +} + +json_value() { + local file="$1" + local expr="$2" + node -e 'const fs = require("fs"); const data = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const value = Function("data", "return " + process.argv[2])(data); if (value === undefined || value === null) process.exit(2); if (typeof value === "object") console.log(JSON.stringify(value)); else console.log(String(value));' "$file" "$expr" +} + +json_assert() { + local file="$1" + local expr="$2" + local label="$3" + node -e 'const fs = require("fs"); const data = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); if (!Function("data", "return " + process.argv[2])(data)) { console.error("assertion failed: " + process.argv[3]); process.exit(1); }' "$file" "$expr" "$label" + echo "ok: $label" +} + +header_value() { + local file="$1" + local name="$2" + awk -v key="${name,,}" 'BEGIN { FS=":" } { current=tolower($1); if (current == key) { sub(/^[[:space:]]+/, "", $2); sub(/\r$/, "", $2); print $2; exit } }' "$file" +} + +authed_curl() { + curl -fsS \ + -H "access-token: $ACCESS_TOKEN" \ + -H "client: $CLIENT" \ + -H "uid: $UID_HEADER" \ + -H "token-type: $TOKEN_TYPE" \ + -H "Content-Type: application/json" \ + "$@" +} + +run_api_smoke() { + mkdir -p "$LOG_DIR" + + local seed_file account_id inbox_id contact_id company_id conversation_display_id conversation_uuid + seed_file="$(tmp_file)" + if [[ "$RUN_SEED" == "true" ]]; then + echo "seeding smoke data..." + (cd "$ROOT" && env GOCACHE="${GOCACHE:-/tmp/gochat-gocache}" GOMODCACHE="${GOMODCACHE:-/tmp/gochat-gomodcache}" go run ./cmd/gochat seed) >"$seed_file" + else + cat >"$seed_file" <&2 + return 1 + fi + + body="$(tmp_file)" + authed_curl -o "$body" "http://$API_HOST:$API_PORT/auth/validate_token" + cp "$body" "$LOG_DIR/validate_token.json" + json_assert "$body" 'data.payload && data.payload.success === true' "auth validate_token accepts frontend headers" + + body="$(tmp_file)" + authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/profile" + cp "$body" "$LOG_DIR/profile.json" + json_assert "$body" 'data.email === "'"$admin_email"'"' "profile returns Chatwoot user serializer" + + body="$(tmp_file)" + authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/inboxes" + cp "$body" "$LOG_DIR/inboxes.json" + json_assert "$body" 'Array.isArray(data.payload) && data.payload.some(inbox => Number(inbox.id) === Number("'"$inbox_id"'"))' "inbox list includes seeded widget inbox" + + body="$(tmp_file)" + authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/conversations?status=open" + cp "$body" "$LOG_DIR/conversations.json" + json_assert "$body" 'data.data && Array.isArray(data.data.payload) && data.data.payload.length >= 1' "conversation list returns Chatwoot data payload" + + body="$(tmp_file)" + authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/conversations/$conversation_display_id/messages" + cp "$body" "$LOG_DIR/conversation_messages.json" + json_assert "$body" 'data.payload && Array.isArray(data.payload) && data.payload.length >= 1' "conversation messages return payload" + + local message_body + message_body="$(tmp_file)" + node -e 'const fs = require("fs"); fs.writeFileSync(process.argv[1], JSON.stringify({ content: "B12 API smoke outgoing message", message_type: "outgoing", private: false }));' "$message_body" + body="$(tmp_file)" + authed_curl -X POST --data-binary "@$message_body" -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/conversations/$conversation_display_id/messages" + cp "$body" "$LOG_DIR/message_create.json" + json_assert "$body" 'data.content === "B12 API smoke outgoing message"' "message create accepts dashboard payload" + + body="$(tmp_file)" + authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/contacts/$contact_id" + cp "$body" "$LOG_DIR/contact.json" + json_assert "$body" 'data.payload && Number(data.payload.id) === Number("'"$contact_id"'")' "contact show returns payload" + + body="$(tmp_file)" + authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/companies/$company_id" + cp "$body" "$LOG_DIR/company.json" + json_assert "$body" 'data.payload && Number(data.payload.id) === Number("'"$company_id"'")' "company show returns payload" + + body="$(tmp_file)" + curl -fsS -X POST -o "$body" "http://$API_HOST:$API_PORT/api/v1/widget/config?website_token=gochat-smoke-widget-token" + cp "$body" "$LOG_DIR/widget_config.json" + json_assert "$body" 'data.website_channel_config && data.website_channel_config.website_token === "gochat-smoke-widget-token" && data.contact && data.contact.pubsub_token' "widget config returns auth token" + local widget_token + widget_token="$(json_value "$body" 'data.contact.pubsub_token')" + + body="$(tmp_file)" + curl -fsS -H "X-Auth-Token: $widget_token" -H "Content-Type: application/json" -X POST --data-binary '{"message":{"content":"B12 widget smoke message"}}' -o "$body" "http://$API_HOST:$API_PORT/api/v1/widget/messages" + cp "$body" "$LOG_DIR/widget_message.json" + json_assert "$body" 'data.content === "B12 widget smoke message" && data.conversation_id' "widget message create works" + + if [[ -n "$conversation_uuid" ]]; then + body="$(tmp_file)" + curl -fsS -o "$body" "http://$API_HOST:$API_PORT/public/api/v1/csat_survey/$conversation_uuid" + cp "$body" "$LOG_DIR/public_csat.json" + json_assert "$body" 'data.display_type === "emoji" && data.content' "public CSAT show returns survey payload" + fi + + BOOT_BACKEND_RESULT="Passed /health during API smoke" \ + AUTH_PROFILE_RESULT="Passed API smoke" \ + INBOX_RESULT="Passed API smoke" \ + CONVERSATION_RESULT="Passed API smoke" \ + CRM_RESULT="Passed contact/company API smoke" \ + WIDGET_RESULT="Passed API smoke" \ + CSAT_RESULT="Passed public show API smoke" \ + ENTERPRISE_RESULT="Pending B12.3 browser/API smoke" \ + write_report "API smoke passed for reused Chatwoot frontend core paths; live browser smoke not run in this mode." "Command run: \`scripts/parity_frontend_smoke.sh --api-smoke\`. Logs and payload captures are under \`$LOG_DIR\`. B12.2 still needs browser navigation assertions for the reused Vite app, and B12.3 must add enterprise screen assertions." + echo "api smoke passed; report written to $REPORT_PATH" +} + if [[ "$MODE" == "print" ]]; then print_commands exit 0 @@ -171,6 +340,11 @@ if [[ "$MODE" == "check" ]]; then exit 0 fi +if [[ "$MODE" == "api-smoke" ]]; then + run_api_smoke + exit 0 +fi + mkdir -p "$LOG_DIR" trap 'jobs -pr | xargs -r kill 2>/dev/null || true' EXIT @@ -182,6 +356,8 @@ echo "starting reused Chatwoot frontend..." (cd "$CHATWOOT_DIR" && "${frontend_cmd[@]}") >"$LOG_DIR/chatwoot-vite.log" 2>&1 & wait_url "http://$FRONTEND_HOST:$FRONTEND_PORT" "Chatwoot Vite" +BOOT_BACKEND_RESULT="Passed boot readiness" \ +BOOT_FRONTEND_RESULT="Passed boot readiness" \ write_report "Boot smoke passed for backend and reused Chatwoot frontend." "This run verified process boot and HTTP readiness only. B12.2 must add browser/API path assertions and update the smoke matrix." echo "smoke boot passed; report written to $REPORT_PATH"