feat(parity): align Chatwoot 4.15.1 contracts
This commit is contained in:
@@ -6,7 +6,7 @@ import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
const root = process.env.GOCHAT_ROOT || process.cwd();
|
||||
const chatwootDir = process.env.CHATWOOT_DIR || path.join(root, 'frontend');
|
||||
const chatwootDir = process.env.CHATWOOT_DIR || path.join(root, '..', 'docs', 'chatwoot');
|
||||
const logDir = process.env.GOCHAT_SMOKE_LOG_DIR || path.join(root, '.tmp/frontend-smoke');
|
||||
const apiHost = process.env.GOCHAT_SMOKE_API_HOST || '127.0.0.1';
|
||||
const apiPort = process.env.GOCHAT_SMOKE_API_PORT || '3000';
|
||||
@@ -47,6 +47,8 @@ function smokeHTML(entrypoint, route) {
|
||||
enabledLanguages: [{ iso_639_1_code: 'en', name: 'English' }],
|
||||
helpUrls: {},
|
||||
selectedLocale: 'en',
|
||||
isEnterprise: 'true',
|
||||
enterprisePlanName: 'enterprise',
|
||||
};
|
||||
const globalConfig = {
|
||||
INSTALLATION_NAME: 'GoChat',
|
||||
@@ -59,6 +61,8 @@ function smokeHTML(entrypoint, route) {
|
||||
MAXIMUM_FILE_UPLOAD_SIZE: '40',
|
||||
ACTIVE_PLATFORM_BANNERS: [],
|
||||
LOGOUT_REDIRECT_LINK: '/app/login',
|
||||
DEPLOYMENT_ENV: 'cloud',
|
||||
IS_ENTERPRISE: 'true',
|
||||
};
|
||||
return `<!doctype html>
|
||||
<html>
|
||||
@@ -159,6 +163,7 @@ function startSmokeShellServer() {
|
||||
}
|
||||
if (
|
||||
requestURL.pathname.startsWith('/api/') ||
|
||||
requestURL.pathname.startsWith('/enterprise/') ||
|
||||
requestURL.pathname.startsWith('/public/') ||
|
||||
requestURL.pathname.startsWith('/auth/') ||
|
||||
requestURL.pathname.startsWith('/rails/')
|
||||
@@ -264,7 +269,10 @@ const enterprisePages = [
|
||||
label: 'profile notification preferences screen',
|
||||
name: 'gochat-smoke-enterprise-profile-notification-preferences',
|
||||
route: `/app/accounts/${seed.account_id}/profile/settings`,
|
||||
requests: [`/api/v1/accounts/${seed.account_id}/notification_settings`],
|
||||
requests: [
|
||||
`/api/v1/accounts/${seed.account_id}/notification_settings`,
|
||||
'/api/v1/profile/sessions',
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'agent capacity screen',
|
||||
@@ -285,10 +293,28 @@ const enterprisePages = [
|
||||
requests: ['/captain/preferences'],
|
||||
},
|
||||
{
|
||||
label: 'Captain assistants screen',
|
||||
name: 'gochat-smoke-enterprise-captain-assistants',
|
||||
route: `/app/accounts/${seed.account_id}/captain/captain_assistants_responses_index`,
|
||||
requests: ['/captain/assistants'],
|
||||
label: 'Captain assistant overview screen',
|
||||
name: 'gochat-smoke-enterprise-captain-overview',
|
||||
route: `/app/accounts/${seed.account_id}/captain/${seed.captain_assistant_id}/overview`,
|
||||
requests: [
|
||||
`/captain/assistants/${seed.captain_assistant_id}/stats`,
|
||||
`/captain/assistants/${seed.captain_assistant_id}/summary`,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'billing screen',
|
||||
name: 'gochat-smoke-enterprise-billing',
|
||||
route: `/app/accounts/${seed.account_id}/settings/billing`,
|
||||
requests: [
|
||||
`/enterprise/api/v1/accounts/${seed.account_id}/subscription`,
|
||||
`/enterprise/api/v1/accounts/${seed.account_id}/limits`,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'voice inbox settings screen',
|
||||
name: 'gochat-smoke-enterprise-voice-inbox',
|
||||
route: `/app/accounts/${seed.account_id}/settings/inboxes/${seed.voice_inbox_id}/voice-configuration`,
|
||||
requests: [`/api/v1/accounts/${seed.account_id}/inboxes`],
|
||||
},
|
||||
].map(page => ({
|
||||
...page,
|
||||
@@ -451,6 +477,7 @@ class CDPPage {
|
||||
const isBackendAPI = request.url.includes(apiBaseURL);
|
||||
const isShellProxiedAPI = request.url.startsWith(frontendBaseURL) && (
|
||||
request.url.includes('/api/') ||
|
||||
request.url.includes('/enterprise/') ||
|
||||
request.url.includes('/public/') ||
|
||||
request.url.includes('/auth/') ||
|
||||
request.url.includes('/rails/')
|
||||
@@ -541,7 +568,9 @@ async function main() {
|
||||
for (const request of enterprisePage.requests) {
|
||||
await page.waitForCapturedRequestAfter(request, requestIndex, `${enterprisePage.label} requests ${request}`);
|
||||
}
|
||||
page.assertNoFailedAPIRequests(requestIndex);
|
||||
}
|
||||
const copilotRequestIndex = report.requests.length;
|
||||
await page.eval(`fetch(${JSON.stringify(`${apiBaseURL}/api/v1/accounts/${seed.account_id}/captain/copilot_threads`)}, {
|
||||
method: 'POST',
|
||||
headers: (() => {
|
||||
@@ -557,7 +586,8 @@ async function main() {
|
||||
})(),
|
||||
body: JSON.stringify({ message: 'B12 enterprise browser copilot smoke', assistant_id: ${Number(seed.captain_assistant_id)}, conversation_id: ${Number(seed.conversation_id)} })
|
||||
}).then(response => response.ok)`);
|
||||
await page.waitForCapturedRequest('/captain/copilot_threads', 'browser context requests Copilot threads');
|
||||
await page.waitForCapturedRequestAfter('/captain/copilot_threads', copilotRequestIndex, 'browser context requests Copilot threads');
|
||||
page.assertNoFailedAPIRequests(copilotRequestIndex);
|
||||
}
|
||||
report.finished_at = new Date().toISOString();
|
||||
report.status = 'passed';
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
CHATWOOT_DIR="${CHATWOOT_DIR:-$ROOT/../frontend}"
|
||||
CHATWOOT_DIR="${CHATWOOT_DIR:-$ROOT/../docs/chatwoot}"
|
||||
LOG_DIR="${GOCHAT_SMOKE_LOG_DIR:-$ROOT/.tmp/frontend-smoke}"
|
||||
REPORT_PATH="${GOCHAT_SMOKE_REPORT:-$ROOT/docs/parity/frontend-smoke-report.md}"
|
||||
REPORT_PATH="${GOCHAT_SMOKE_REPORT:-$ROOT/../docs/parity/frontend-smoke-report.md}"
|
||||
API_HOST="${GOCHAT_SMOKE_API_HOST:-127.0.0.1}"
|
||||
API_PORT="${GOCHAT_SMOKE_API_PORT:-3000}"
|
||||
FRONTEND_HOST="${GOCHAT_SMOKE_FRONTEND_HOST:-localhost}"
|
||||
@@ -33,7 +33,7 @@ Modes:
|
||||
--no-seed Skip seed during --api-smoke and use GOCHAT_SEED_* values already present in DB.
|
||||
|
||||
Environment:
|
||||
CHATWOOT_DIR Chatwoot checkout path. Default: ../frontend (repo root frontend/)
|
||||
CHATWOOT_DIR Chatwoot checkout path. Default: ../docs/chatwoot (4.15.1 upstream source)
|
||||
GOCHAT_SMOKE_API_PORT GoChat backend port. Default: 3000
|
||||
GOCHAT_SMOKE_FRONTEND_HOST Vite frontend host. Default: localhost
|
||||
GOCHAT_SMOKE_FRONTEND_PORT Vite frontend port. Default: 3036
|
||||
@@ -312,7 +312,7 @@ run_api_smoke() {
|
||||
extract_json_object "$seed_raw_file" "$seed_file"
|
||||
else
|
||||
cat >"$seed_file" <<SEED
|
||||
{"admin_email":"${GOCHAT_SEED_ADMIN_EMAIL:-admin@gochat.local}","admin_password":"${GOCHAT_SEED_ADMIN_PASSWORD:-changeme}","account_id":"${GOCHAT_SMOKE_ACCOUNT_ID:-1}","inbox_id":"${GOCHAT_SMOKE_INBOX_ID:-1}","contact_id":"${GOCHAT_SMOKE_CONTACT_ID:-1}","company_id":"${GOCHAT_SMOKE_COMPANY_ID:-1}","portal_id":"${GOCHAT_SMOKE_PORTAL_ID:-1}","article_id":"${GOCHAT_SMOKE_ARTICLE_ID:-1}","conversation_id":"${GOCHAT_SMOKE_CONVERSATION_ID:-1}","conversation_display_id":"${GOCHAT_SMOKE_CONVERSATION_DISPLAY_ID:-1}","conversation_uuid":"${GOCHAT_SMOKE_CONVERSATION_UUID:-}","sla_policy_id":"${GOCHAT_SMOKE_SLA_POLICY_ID:-1}","custom_role_id":"${GOCHAT_SMOKE_CUSTOM_ROLE_ID:-1}","capacity_policy_id":"${GOCHAT_SMOKE_CAPACITY_POLICY_ID:-1}","captain_assistant_id":"${GOCHAT_SMOKE_CAPTAIN_ASSISTANT_ID:-1}"}
|
||||
{"admin_id":"${GOCHAT_SMOKE_ADMIN_ID:-1}","admin_email":"${GOCHAT_SEED_ADMIN_EMAIL:-admin@gochat.local}","admin_password":"${GOCHAT_SEED_ADMIN_PASSWORD:-changeme}","account_id":"${GOCHAT_SMOKE_ACCOUNT_ID:-1}","inbox_id":"${GOCHAT_SMOKE_INBOX_ID:-1}","voice_inbox_id":"${GOCHAT_SMOKE_VOICE_INBOX_ID:-1}","contact_id":"${GOCHAT_SMOKE_CONTACT_ID:-1}","company_id":"${GOCHAT_SMOKE_COMPANY_ID:-1}","portal_id":"${GOCHAT_SMOKE_PORTAL_ID:-1}","article_id":"${GOCHAT_SMOKE_ARTICLE_ID:-1}","conversation_id":"${GOCHAT_SMOKE_CONVERSATION_ID:-1}","conversation_display_id":"${GOCHAT_SMOKE_CONVERSATION_DISPLAY_ID:-1}","conversation_uuid":"${GOCHAT_SMOKE_CONVERSATION_UUID:-}","sla_policy_id":"${GOCHAT_SMOKE_SLA_POLICY_ID:-1}","custom_role_id":"${GOCHAT_SMOKE_CUSTOM_ROLE_ID:-1}","capacity_policy_id":"${GOCHAT_SMOKE_CAPACITY_POLICY_ID:-1}","captain_assistant_id":"${GOCHAT_SMOKE_CAPTAIN_ASSISTANT_ID:-1}","captain_message_id":"${GOCHAT_SMOKE_CAPTAIN_MESSAGE_ID:-1}","agent_bot_id":"${GOCHAT_SMOKE_AGENT_BOT_ID:-1}"}
|
||||
SEED
|
||||
fi
|
||||
cp "$seed_file" "$LOG_DIR/seed.json"
|
||||
@@ -361,6 +361,21 @@ SEED
|
||||
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/profile/sessions"
|
||||
cp "$body" "$LOG_DIR/profile_sessions.json"
|
||||
json_assert "$body" 'Array.isArray(data) && data.some(session => session.current === true && session.id && Object.prototype.hasOwnProperty.call(session, "browser_name") && Object.prototype.hasOwnProperty.call(session, "last_activity_at"))' "profile sessions expose the current Chatwoot session"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/onboarding/help_center_generation"
|
||||
cp "$body" "$LOG_DIR/help_center_generation.json"
|
||||
json_assert "$body" 'Object.prototype.hasOwnProperty.call(data, "generation_id") && Object.prototype.hasOwnProperty.call(data, "state") && typeof data.articles_count === "number" && typeof data.categories_count === "number"' "help center generation returns the Chatwoot onboarding contract"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/calls?page=1"
|
||||
cp "$body" "$LOG_DIR/calls.json"
|
||||
json_assert "$body" 'data.meta && typeof data.meta.count === "number" && Number(data.meta.current_page) === 1 && typeof data.meta.total_pages === "number" && Array.isArray(data.payload)' "calls index returns Chatwoot meta and payload"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/inboxes"
|
||||
cp "$body" "$LOG_DIR/inboxes.json"
|
||||
@@ -489,20 +504,49 @@ run_browser_smoke() {
|
||||
run_enterprise_api_smoke() {
|
||||
run_api_smoke
|
||||
|
||||
local seed_file account_id inbox_id conversation_id conversation_display_id conversation_uuid custom_role_id capacity_policy_id captain_assistant_id sla_policy_id
|
||||
local seed_file admin_id account_id inbox_id voice_inbox_id conversation_id conversation_display_id conversation_uuid custom_role_id capacity_policy_id captain_assistant_id captain_message_id agent_bot_id sla_policy_id
|
||||
seed_file="$LOG_DIR/seed.json"
|
||||
admin_id="$(json_value "$seed_file" 'data.admin_id')"
|
||||
account_id="$(json_value "$seed_file" 'data.account_id')"
|
||||
inbox_id="$(json_value "$seed_file" 'data.inbox_id')"
|
||||
voice_inbox_id="$(json_value "$seed_file" 'data.voice_inbox_id')"
|
||||
conversation_id="$(json_value "$seed_file" 'data.conversation_id')"
|
||||
conversation_display_id="$(json_value "$seed_file" 'data.conversation_display_id || 1')"
|
||||
conversation_uuid="$(json_value "$seed_file" 'data.conversation_uuid || ""')"
|
||||
custom_role_id="$(json_value "$seed_file" 'data.custom_role_id')"
|
||||
capacity_policy_id="$(json_value "$seed_file" 'data.capacity_policy_id')"
|
||||
captain_assistant_id="$(json_value "$seed_file" 'data.captain_assistant_id')"
|
||||
captain_message_id="$(json_value "$seed_file" 'data.captain_message_id')"
|
||||
agent_bot_id="$(json_value "$seed_file" 'data.agent_bot_id')"
|
||||
sla_policy_id="$(json_value "$seed_file" 'data.sla_policy_id')"
|
||||
|
||||
local body request_body created_id csv_file
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/assignable_agents?inbox_ids[]=$inbox_id&include_agent_bots=true"
|
||||
cp "$body" "$LOG_DIR/assignable_agents_with_bots.json"
|
||||
json_assert "$body" 'Array.isArray(data.payload) && data.payload.some(owner => Number(owner.id) === Number("'"$agent_bot_id"'") && owner.assignee_type === "AgentBot" && owner.icon === "i-lucide-bot")' "assignable agents include typed AgentBot owners"
|
||||
|
||||
request_body="$(tmp_file)"
|
||||
node -e 'const fs = require("fs"); fs.writeFileSync(process.argv[1], JSON.stringify({ assignee_id: Number(process.argv[2]), assignee_type: "AgentBot" }));' "$request_body" "$agent_bot_id"
|
||||
body="$(tmp_file)"
|
||||
authed_curl -X POST --data-binary "@$request_body" -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/conversations/$conversation_display_id/assign"
|
||||
cp "$body" "$LOG_DIR/assign_agent_bot.json"
|
||||
json_assert "$body" 'Number(data.id) === Number("'"$agent_bot_id"'") && data.name === "Smoke Agent Bot"' "conversation assignment returns the Chatwoot AgentBot slim payload"
|
||||
|
||||
node -e 'const fs = require("fs"); fs.writeFileSync(process.argv[1], JSON.stringify({ assignee_id: Number(process.argv[2]), assignee_type: "User" }));' "$request_body" "$admin_id"
|
||||
body="$(tmp_file)"
|
||||
authed_curl -X POST --data-binary "@$request_body" -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/conversations/$conversation_display_id/assign"
|
||||
cp "$body" "$LOG_DIR/assign_agent_restore.json"
|
||||
json_assert "$body" 'Number(data.id) === Number("'"$admin_id"'")' "conversation assignment restores a User owner"
|
||||
|
||||
request_body="$(tmp_file)"
|
||||
printf '{"inbound_calls_enabled":false}' >"$request_body"
|
||||
authed_curl -X POST --data-binary "@$request_body" -o /dev/null "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/inboxes/$voice_inbox_id/set_inbound_calls"
|
||||
printf '{"inbound_calls_enabled":true}' >"$request_body"
|
||||
authed_curl -X POST --data-binary "@$request_body" -o /dev/null "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/inboxes/$voice_inbox_id/set_inbound_calls"
|
||||
echo "ok: inbound calls toggle accepts the Chatwoot voice inbox payload"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/applied_slas?page=1&sla_policy_id=$sla_policy_id"
|
||||
cp "$body" "$LOG_DIR/enterprise_applied_slas.json"
|
||||
@@ -608,6 +652,53 @@ run_enterprise_api_smoke() {
|
||||
cp "$body" "$LOG_DIR/enterprise_captain_assistants.json"
|
||||
json_assert "$body" 'data.payload && data.payload.some(assistant => Number(assistant.id) === Number("'$captain_assistant_id'"))' "Captain assistants list returns seeded assistant"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/captain/assistants/$captain_assistant_id/stats?range=7&timezone_offset=0"
|
||||
cp "$body" "$LOG_DIR/enterprise_captain_stats.json"
|
||||
json_assert "$body" '["conversations_handled", "auto_resolution_rate", "handoff_rate", "hours_saved", "reopen_rate", "conversation_depth", "knowledge"].every(key => Object.prototype.hasOwnProperty.call(data, key))' "Captain stats return all 4.15.1 overview metrics"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/captain/assistants/$captain_assistant_id/summary?range=7&timezone_offset=0"
|
||||
cp "$body" "$LOG_DIR/enterprise_captain_summary.json"
|
||||
json_assert "$body" 'typeof data.message === "string" && data.message.length > 0' "Captain summary returns the overview message contract"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/captain/assistants/$captain_assistant_id/drilldown?metric=conversations_handled&range=7&timezone_offset=0&page=1"
|
||||
cp "$body" "$LOG_DIR/enterprise_captain_drilldown.json"
|
||||
json_assert "$body" 'data.meta && Array.isArray(data.payload) && data.payload.some(item => item.conversation && Number(item.conversation.id) === Number("'"$conversation_id"'"))' "Captain drilldown returns the seeded handled conversation"
|
||||
|
||||
request_body="$(tmp_file)"
|
||||
node -e 'const fs = require("fs"); fs.writeFileSync(process.argv[1], JSON.stringify({ message_id: Number(process.argv[2]), report_reason: "incorrect_information", description: "4.15.1 parity smoke" }));' "$request_body" "$captain_message_id"
|
||||
body="$(tmp_file)"
|
||||
authed_curl -X POST --data-binary "@$request_body" -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/captain/message_reports"
|
||||
cp "$body" "$LOG_DIR/enterprise_captain_message_report.json"
|
||||
json_assert "$body" 'data.id && Number(data.message_id) === Number("'"$captain_message_id"'") && data.report_reason === "incorrect_information"' "Captain message reports return the 4.15.1 Jbuilder shape"
|
||||
|
||||
local now since until bucket_timestamp
|
||||
now="$(date +%s)"
|
||||
since="$((now - 86400))"
|
||||
until="$((now + 86400))"
|
||||
bucket_timestamp="$(date -u -d 'today 00:00:00' +%s)"
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v2/accounts/$account_id/reports/drilldown?metric=incoming_messages_count&type=account&group_by=day&timezone_offset=0&page=1&per_page=25&since=$since&until=$until&bucket_timestamp=$bucket_timestamp"
|
||||
cp "$body" "$LOG_DIR/enterprise_reports_drilldown.json"
|
||||
json_assert "$body" 'data.meta && typeof data.meta.total_count === "number" && Array.isArray(data.payload) && data.payload.some(record => record.record_type === "message" && record.conversation && record.message)' "reports drilldown returns the shared 4.15.1 record envelope"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id"
|
||||
cp "$body" "$LOG_DIR/enterprise_billing_account.json"
|
||||
if json_matches "$body" 'data.custom_attributes && data.custom_attributes.billing_currency === "usd"'; then
|
||||
echo "ok: billing currency remains locked to the previously selected currency"
|
||||
else
|
||||
request_body="$(tmp_file)"
|
||||
printf '{"currency":"usd"}' >"$request_body"
|
||||
authed_curl -X POST --data-binary "@$request_body" -o /dev/null "http://$API_HOST:$API_PORT/enterprise/api/v1/accounts/$account_id/select_billing_currency"
|
||||
fi
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/enterprise/api/v1/accounts/$account_id/topup_options"
|
||||
cp "$body" "$LOG_DIR/enterprise_topup_options.json"
|
||||
json_assert "$body" 'Number(data.id) === Number("'"$account_id"'") && data.currency === "usd" && Array.isArray(data.options)' "billing currency and topup options match the Chatwoot enterprise account contract"
|
||||
|
||||
request_body="$(tmp_file)"
|
||||
node -e 'const fs = require("fs"); fs.writeFileSync(process.argv[1], JSON.stringify({ message: "B12 enterprise copilot smoke", assistant_id: Number(process.argv[2]), conversation_id: Number(process.argv[3]) }));' "$request_body" "$captain_assistant_id" "$conversation_id"
|
||||
body="$(tmp_file)"
|
||||
|
||||
Reference in New Issue
Block a user