Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
This commit is contained in:
@@ -0,0 +1,581 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdtempSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { createServer } from 'node:http';
|
||||
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, 'reference/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';
|
||||
const frontendHost = process.env.GOCHAT_SMOKE_FRONTEND_HOST || '127.0.0.1';
|
||||
const frontendPort = process.env.GOCHAT_SMOKE_FRONTEND_PORT || '3036';
|
||||
const shellHost = process.env.GOCHAT_SMOKE_SHELL_HOST || frontendHost;
|
||||
const shellPort = process.env.GOCHAT_SMOKE_SHELL_PORT || String(Number(frontendPort) + 1);
|
||||
const chromePath = process.env.GOCHAT_SMOKE_CHROME || '/usr/bin/google-chrome';
|
||||
const viteBaseURL = `http://${frontendHost}:${frontendPort}`;
|
||||
const frontendBaseURL = `http://${shellHost}:${shellPort}`;
|
||||
const apiBaseURL = `http://${apiHost}:${apiPort}`;
|
||||
const enterpriseMode = process.argv.includes('--enterprise');
|
||||
|
||||
mkdirSync(logDir, { recursive: true });
|
||||
|
||||
const seed = JSON.parse(readFileSync(path.join(logDir, 'seed.json'), 'utf8'));
|
||||
const widgetConfig = JSON.parse(readFileSync(path.join(logDir, 'widget_config.json'), 'utf8'));
|
||||
const signInHeaders = readFileSync(path.join(logDir, 'sign_in.headers'), 'utf8');
|
||||
const report = {
|
||||
started_at: new Date().toISOString(),
|
||||
mode: enterpriseMode ? 'enterprise' : 'core',
|
||||
frontend_base_url: frontendBaseURL,
|
||||
api_base_url: apiBaseURL,
|
||||
account_id: seed.account_id,
|
||||
requests: [],
|
||||
console: [],
|
||||
checks: [],
|
||||
};
|
||||
|
||||
function smokeHTML(entrypoint, route) {
|
||||
const config = {
|
||||
apiHost: apiBaseURL,
|
||||
hostURL: frontendBaseURL,
|
||||
helpCenterURL: '',
|
||||
allowedLoginMethods: ['email'],
|
||||
signupEnabled: 'false',
|
||||
isEnterprise: 'true',
|
||||
isMfaEnabled: 'false',
|
||||
enabledLanguages: [{ iso_639_1_code: 'en', name: 'English' }],
|
||||
helpUrls: {},
|
||||
selectedLocale: 'en',
|
||||
};
|
||||
const globalConfig = {
|
||||
INSTALLATION_NAME: 'GoChat',
|
||||
BRAND_NAME: 'GoChat',
|
||||
LOGO: '/logo.png',
|
||||
LOGO_DARK: '',
|
||||
LOGO_THUMBNAIL: '/logo.png',
|
||||
IS_ENTERPRISE: 'true',
|
||||
DISABLE_USER_PROFILE_UPDATE: 'false',
|
||||
DIRECT_UPLOADS_ENABLED: 'false',
|
||||
MAXIMUM_FILE_UPLOAD_SIZE: '40',
|
||||
ACTIVE_PLATFORM_BANNERS: [],
|
||||
LOGOUT_REDIRECT_LINK: '/app/login',
|
||||
};
|
||||
return `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>GoChat Smoke</title>
|
||||
<script>
|
||||
history.replaceState({}, '', ${JSON.stringify(route)});
|
||||
window.chatwootConfig = ${JSON.stringify(config)};
|
||||
window.globalConfig = ${JSON.stringify(globalConfig)};
|
||||
window.browserConfig = { browser_name: 'chrome' };
|
||||
window.errorLoggingConfig = '';
|
||||
window.analyticsConfig = { token: '' };
|
||||
</script>
|
||||
<script type="module" src="/vite-dev/entrypoints/${entrypoint}.js"></script>
|
||||
</head>
|
||||
<body class="text-slate-600"><div id="app"></div></body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function widgetSmokeHTML(route) {
|
||||
const websiteChannelConfig = widgetConfig.website_channel_config || {};
|
||||
const contact = widgetConfig.contact || {};
|
||||
const globalConfig = widgetConfig.global_config || {};
|
||||
const chatwootWebChannel = {
|
||||
...websiteChannelConfig,
|
||||
websiteToken: websiteChannelConfig.website_token,
|
||||
enabledLanguages: [{ iso_639_1_code: 'en', name: 'English' }],
|
||||
locale: 'en',
|
||||
portal: null,
|
||||
hasAConnectedAgentBot: false,
|
||||
allowMessagesAfterResolved: true,
|
||||
disableBranding: false,
|
||||
};
|
||||
return `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>GoChat Widget Smoke</title>
|
||||
<script>
|
||||
history.replaceState({}, '', ${JSON.stringify(route)});
|
||||
window.chatwootWebChannel = ${JSON.stringify(chatwootWebChannel)};
|
||||
window.chatwootPubsubToken = ${JSON.stringify(contact.pubsub_token || websiteChannelConfig.auth_token || '')};
|
||||
window.authToken = ${JSON.stringify(websiteChannelConfig.auth_token || contact.pubsub_token || '')};
|
||||
window.globalConfig = ${JSON.stringify(globalConfig)};
|
||||
window.referrerURL = ${JSON.stringify(frontendBaseURL)};
|
||||
window.browserConfig = { browser_name: 'chrome' };
|
||||
window.errorLoggingConfig = '';
|
||||
</script>
|
||||
<script type="module" src="/vite-dev/entrypoints/widget.js"></script>
|
||||
</head>
|
||||
<body class="text-slate-600"><div id="app"></div></body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
const smokeShells = new Map();
|
||||
|
||||
function writeSmokeShell(name, entrypoint, route) {
|
||||
smokeShells.set(`/gochat-smoke/${name}.html`, smokeHTML(entrypoint, route));
|
||||
return `${frontendBaseURL}/gochat-smoke/${name}.html`;
|
||||
}
|
||||
|
||||
function writeWidgetSmokeShell(name, route) {
|
||||
smokeShells.set(`/gochat-smoke/${name}.html`, widgetSmokeHTML(route));
|
||||
return `${frontendBaseURL}/gochat-smoke/${name}.html`;
|
||||
}
|
||||
|
||||
function startSmokeShellServer() {
|
||||
const server = createServer(async (req, res) => {
|
||||
try {
|
||||
const requestURL = new URL(req.url || '/', frontendBaseURL);
|
||||
if (smokeShells.has(requestURL.pathname)) {
|
||||
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
||||
res.end(smokeShells.get(requestURL.pathname));
|
||||
return;
|
||||
}
|
||||
if (requestURL.pathname.startsWith('/app/')) {
|
||||
const entrypoint = requestURL.pathname === '/app/login' ? 'v3app' : 'dashboard';
|
||||
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
||||
res.end(smokeHTML(entrypoint, requestURL.pathname));
|
||||
return;
|
||||
}
|
||||
if (requestURL.pathname.startsWith('/vite-dev/')) {
|
||||
const nonEnglishLocaleModule = requestURL.pathname.match(/^\/vite-dev\/dashboard\/i18n\/locale\/([^/]+)\/index\.js$/);
|
||||
if (nonEnglishLocaleModule && nonEnglishLocaleModule[1] !== 'en') {
|
||||
res.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8' });
|
||||
res.end('export default {};');
|
||||
return;
|
||||
}
|
||||
const upstream = await fetch(`${viteBaseURL}${requestURL.pathname}${requestURL.search}`);
|
||||
const headers = Object.fromEntries(upstream.headers.entries());
|
||||
headers['access-control-allow-origin'] = '*';
|
||||
res.writeHead(upstream.status, headers);
|
||||
res.end(Buffer.from(await upstream.arrayBuffer()));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
requestURL.pathname.startsWith('/api/') ||
|
||||
requestURL.pathname.startsWith('/public/') ||
|
||||
requestURL.pathname.startsWith('/auth/') ||
|
||||
requestURL.pathname.startsWith('/rails/')
|
||||
) {
|
||||
const chunks = [];
|
||||
for await (const chunk of req) chunks.push(chunk);
|
||||
const upstream = await fetch(`${apiBaseURL}${requestURL.pathname}${requestURL.search}`, {
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
body: ['GET', 'HEAD'].includes(req.method || 'GET') ? undefined : Buffer.concat(chunks),
|
||||
});
|
||||
const headers = Object.fromEntries(upstream.headers.entries());
|
||||
headers['access-control-allow-origin'] = '*';
|
||||
res.writeHead(upstream.status, headers);
|
||||
res.end(Buffer.from(await upstream.arrayBuffer()));
|
||||
return;
|
||||
}
|
||||
if (requestURL.pathname === '/sw.js') {
|
||||
res.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8' });
|
||||
res.end('self.addEventListener("install", event => self.skipWaiting());');
|
||||
return;
|
||||
}
|
||||
if (requestURL.pathname === '/favicon.ico' || requestURL.pathname === '/logo.png') {
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
res.writeHead(404, { 'content-type': 'text/plain' });
|
||||
res.end('not found');
|
||||
} catch (error) {
|
||||
res.writeHead(502, { 'content-type': 'text/plain' });
|
||||
res.end(error.message);
|
||||
}
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(Number(shellPort), shellHost, () => resolve(server));
|
||||
});
|
||||
}
|
||||
|
||||
const smokePages = {
|
||||
dashboard: writeSmokeShell('gochat-smoke-dashboard', 'dashboard', `/app/accounts/${seed.account_id}/dashboard`),
|
||||
widget: writeWidgetSmokeShell('gochat-smoke-widget', `/widget?website_token=${encodeURIComponent(widgetConfig.website_channel_config?.website_token || 'gochat-smoke-widget-token')}#/messages`),
|
||||
};
|
||||
|
||||
function headerValue(headers, name) {
|
||||
const needle = `${name.toLowerCase()}:`;
|
||||
const line = headers.split(/\r?\n/).find(header => header.toLowerCase().startsWith(needle));
|
||||
return line ? line.slice(line.indexOf(':') + 1).trim() : '';
|
||||
}
|
||||
|
||||
const sessionCookie = JSON.stringify({
|
||||
'access-token': headerValue(signInHeaders, 'access-token'),
|
||||
client: headerValue(signInHeaders, 'client'),
|
||||
uid: headerValue(signInHeaders, 'uid'),
|
||||
'token-type': headerValue(signInHeaders, 'token-type') || 'Bearer',
|
||||
});
|
||||
|
||||
const enterprisePages = [
|
||||
{
|
||||
label: 'SLA reports screen',
|
||||
name: 'gochat-smoke-enterprise-sla-reports',
|
||||
route: `/app/accounts/${seed.account_id}/reports/sla`,
|
||||
requests: ['/applied_slas', '/applied_slas/metrics'],
|
||||
},
|
||||
{
|
||||
label: 'CSAT reports screen',
|
||||
name: 'gochat-smoke-enterprise-csat-reports',
|
||||
route: `/app/accounts/${seed.account_id}/reports/csat`,
|
||||
requests: ['/csat_survey_responses', '/csat_survey_responses/metrics'],
|
||||
},
|
||||
{
|
||||
label: 'automation rules screen',
|
||||
name: 'gochat-smoke-enterprise-automation',
|
||||
route: `/app/accounts/${seed.account_id}/settings/automation/list`,
|
||||
requests: ['/automation_rules'],
|
||||
},
|
||||
{
|
||||
label: 'macros screen',
|
||||
name: 'gochat-smoke-enterprise-macros',
|
||||
route: `/app/accounts/${seed.account_id}/settings/macros`,
|
||||
requests: ['/macros'],
|
||||
},
|
||||
{
|
||||
label: 'audit logs screen',
|
||||
name: 'gochat-smoke-enterprise-audit-logs',
|
||||
route: `/app/accounts/${seed.account_id}/settings/audit-logs/list`,
|
||||
requests: ['/audit_logs'],
|
||||
},
|
||||
{
|
||||
label: 'custom roles screen',
|
||||
name: 'gochat-smoke-enterprise-custom-roles',
|
||||
route: `/app/accounts/${seed.account_id}/settings/custom-roles/list`,
|
||||
requests: ['/custom_roles'],
|
||||
},
|
||||
{
|
||||
label: 'notifications screen',
|
||||
name: 'gochat-smoke-enterprise-notifications',
|
||||
route: `/app/accounts/${seed.account_id}/notifications`,
|
||||
requests: [`/api/v1/accounts/${seed.account_id}/notifications?page=1`],
|
||||
},
|
||||
{
|
||||
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`],
|
||||
},
|
||||
{
|
||||
label: 'agent capacity screen',
|
||||
name: 'gochat-smoke-enterprise-agent-capacity',
|
||||
route: `/app/accounts/${seed.account_id}/settings/assignment-policy/capacity`,
|
||||
requests: ['/agent_capacity_policies'],
|
||||
},
|
||||
{
|
||||
label: 'agent capacity edit screen',
|
||||
name: 'gochat-smoke-enterprise-agent-capacity-edit',
|
||||
route: `/app/accounts/${seed.account_id}/settings/assignment-policy/capacity/edit/${seed.capacity_policy_id}`,
|
||||
requests: [`/agent_capacity_policies/${seed.capacity_policy_id}`, `/agent_capacity_policies/${seed.capacity_policy_id}/users`],
|
||||
},
|
||||
{
|
||||
label: 'Captain settings screen',
|
||||
name: 'gochat-smoke-enterprise-captain-settings',
|
||||
route: `/app/accounts/${seed.account_id}/settings/captain`,
|
||||
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'],
|
||||
},
|
||||
].map(page => ({
|
||||
...page,
|
||||
url: writeSmokeShell(page.name, 'dashboard', page.route),
|
||||
}));
|
||||
|
||||
class CDPPage {
|
||||
constructor(ws, chrome) {
|
||||
this.ws = ws;
|
||||
this.chrome = chrome;
|
||||
this.nextID = 1;
|
||||
this.pending = new Map();
|
||||
this.listeners = new Map();
|
||||
this.requestURLs = new Map();
|
||||
ws.onmessage = event => this.handleMessage(JSON.parse(event.data));
|
||||
}
|
||||
|
||||
handleMessage(message) {
|
||||
if (message.id && this.pending.has(message.id)) {
|
||||
const { resolve, reject } = this.pending.get(message.id);
|
||||
this.pending.delete(message.id);
|
||||
if (message.error) reject(new Error(message.error.message));
|
||||
else resolve(message.result || {});
|
||||
return;
|
||||
}
|
||||
const handlers = this.listeners.get(message.method) || [];
|
||||
handlers.forEach(handler => handler(message.params || {}));
|
||||
}
|
||||
|
||||
send(method, params = {}) {
|
||||
const id = this.nextID++;
|
||||
this.ws.send(JSON.stringify({ id, method, params }));
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pending.set(id, { resolve, reject });
|
||||
setTimeout(() => {
|
||||
if (this.pending.has(id)) {
|
||||
this.pending.delete(id);
|
||||
reject(new Error(`CDP command timed out: ${method}`));
|
||||
}
|
||||
}, 30000);
|
||||
});
|
||||
}
|
||||
|
||||
on(method, handler) {
|
||||
const handlers = this.listeners.get(method) || [];
|
||||
handlers.push(handler);
|
||||
this.listeners.set(method, handlers);
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.send('Page.enable');
|
||||
await this.send('Runtime.enable');
|
||||
await this.send('Network.enable');
|
||||
this.on('Network.requestWillBeSent', params => {
|
||||
if (params.requestId && params.request?.url) {
|
||||
this.requestURLs.set(params.requestId, params.request.url);
|
||||
}
|
||||
});
|
||||
this.on('Runtime.consoleAPICalled', params => {
|
||||
report.console.push({
|
||||
type: params.type,
|
||||
text: (params.args || []).map(arg => arg.value || arg.description || '').join(' '),
|
||||
});
|
||||
});
|
||||
this.on('Runtime.exceptionThrown', params => {
|
||||
report.console.push({ type: 'exception', text: params.exceptionDetails?.text || 'exception' });
|
||||
});
|
||||
this.on('Network.responseReceived', params => {
|
||||
report.requests.push({
|
||||
url: params.response.url,
|
||||
status: params.response.status,
|
||||
type: params.type,
|
||||
});
|
||||
});
|
||||
this.on('Network.loadingFailed', params => {
|
||||
report.requests.push({
|
||||
url: this.requestURLs.get(params.requestId) || params.requestId,
|
||||
status: 0,
|
||||
errorText: params.errorText,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async navigate(url) {
|
||||
const loaded = new Promise(resolve => this.on('Page.loadEventFired', resolve));
|
||||
await this.send('Page.navigate', { url });
|
||||
await loaded;
|
||||
}
|
||||
|
||||
async eval(expression) {
|
||||
const result = await this.send('Runtime.evaluate', {
|
||||
expression,
|
||||
awaitPromise: true,
|
||||
returnByValue: true,
|
||||
});
|
||||
if (result.exceptionDetails) {
|
||||
throw new Error(result.exceptionDetails.text || 'Runtime.evaluate failed');
|
||||
}
|
||||
return result.result?.value;
|
||||
}
|
||||
|
||||
async waitFor(expression, label, timeout = 30000) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
if (await this.eval(expression)) {
|
||||
report.checks.push({ label, status: 'passed' });
|
||||
return;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 250));
|
||||
}
|
||||
throw new Error(`Timed out waiting for ${label}`);
|
||||
}
|
||||
|
||||
async waitForAppMounted(label, timeout = 90000) {
|
||||
try {
|
||||
await this.waitFor(
|
||||
'document.querySelector("#app") && document.querySelector("#app").children.length > 0',
|
||||
label,
|
||||
timeout
|
||||
);
|
||||
} catch (error) {
|
||||
const state = await this.eval(`JSON.stringify({
|
||||
href: location.href,
|
||||
readyState: document.readyState,
|
||||
appHTMLLength: document.querySelector('#app')?.innerHTML?.length || 0,
|
||||
appChildCount: document.querySelector('#app')?.children?.length || 0,
|
||||
title: document.title,
|
||||
})`);
|
||||
report.console.push({ type: 'diagnostic', text: `${label}: ${state}` });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
waitForRequest(substring, label, timeout = 30000) {
|
||||
return this.waitFor(
|
||||
`performance.getEntriesByType('resource').some(entry => entry.name.includes(${JSON.stringify(substring)}))`,
|
||||
label,
|
||||
timeout
|
||||
);
|
||||
}
|
||||
|
||||
async waitForCapturedRequest(substring, label, timeout = 30000) {
|
||||
return this.waitForCapturedRequestAfter(substring, 0, label, timeout);
|
||||
}
|
||||
|
||||
async waitForCapturedRequestAfter(substring, requestIndex, label, timeout = 30000) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
if (report.requests.slice(requestIndex).some(request => request.url.includes(substring))) {
|
||||
report.checks.push({ label, status: 'passed' });
|
||||
return;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 250));
|
||||
}
|
||||
throw new Error(`Timed out waiting for ${label}`);
|
||||
}
|
||||
|
||||
assertNoFailedAPIRequests(requestIndex = 0) {
|
||||
const failures = report.requests.slice(requestIndex).filter(request => {
|
||||
const isBackendAPI = request.url.includes(apiBaseURL);
|
||||
const isShellProxiedAPI = request.url.startsWith(frontendBaseURL) && (
|
||||
request.url.includes('/api/') ||
|
||||
request.url.includes('/public/') ||
|
||||
request.url.includes('/auth/') ||
|
||||
request.url.includes('/rails/')
|
||||
);
|
||||
if (!isBackendAPI && !isShellProxiedAPI) return false;
|
||||
if (request.type === 'Preflight') return false;
|
||||
if (request.status === 0 && ['net::ERR_ABORTED', 'net::ERR_FAILED'].includes(request.errorText)) {
|
||||
return false;
|
||||
}
|
||||
return request.status >= 400 || request.status === 0;
|
||||
});
|
||||
if (failures.length > 0) {
|
||||
throw new Error(`Frontend API requests failed: ${failures.map(request => `${request.status} ${request.url}`).join('; ')}`);
|
||||
}
|
||||
report.checks.push({ label: 'no failed frontend API requests', status: 'passed' });
|
||||
}
|
||||
|
||||
async close() {
|
||||
this.ws.close();
|
||||
this.chrome.kill('SIGTERM');
|
||||
}
|
||||
}
|
||||
|
||||
async function launchChrome() {
|
||||
const userDataDir = mkdtempSync(path.join(tmpdir(), 'gochat-chrome-'));
|
||||
const chrome = spawn(chromePath, [
|
||||
'--headless=new',
|
||||
'--disable-gpu',
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check',
|
||||
'--disable-dev-shm-usage',
|
||||
'--remote-debugging-port=0',
|
||||
`--user-data-dir=${userDataDir}`,
|
||||
'about:blank',
|
||||
], { stdio: ['ignore', 'ignore', 'pipe'] });
|
||||
const portFile = path.join(userDataDir, 'DevToolsActivePort');
|
||||
for (let i = 0; i < 80; i += 1) {
|
||||
try {
|
||||
const [port] = readFileSync(portFile, 'utf8').trim().split('\n');
|
||||
const target = await fetch(`http://127.0.0.1:${port}/json/new?about:blank`, { method: 'PUT' }).then(r => r.json());
|
||||
const ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||
await new Promise((resolve, reject) => {
|
||||
ws.onopen = resolve;
|
||||
ws.onerror = reject;
|
||||
});
|
||||
const page = new CDPPage(ws, chrome);
|
||||
await page.init();
|
||||
return page;
|
||||
} catch {
|
||||
await new Promise(resolve => setTimeout(resolve, 250));
|
||||
}
|
||||
}
|
||||
chrome.kill('SIGTERM');
|
||||
throw new Error('Chrome DevTools did not become ready');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const shellServer = await startSmokeShellServer();
|
||||
const page = await launchChrome();
|
||||
try {
|
||||
await page.send('Network.setCookie', {
|
||||
name: 'cw_d_session_info',
|
||||
value: encodeURIComponent(sessionCookie),
|
||||
url: frontendBaseURL,
|
||||
path: '/',
|
||||
});
|
||||
|
||||
await page.navigate(smokePages.dashboard);
|
||||
await page.waitForAppMounted('dashboard app mounted');
|
||||
await page.waitForCapturedRequest('/auth/validate_token', 'dashboard validates auth token');
|
||||
await page.waitForCapturedRequest(`/api/v1/accounts/${seed.account_id}/conversations`, 'dashboard requests conversations');
|
||||
page.assertNoFailedAPIRequests();
|
||||
|
||||
const widgetRequestIndex = report.requests.length;
|
||||
await page.navigate(smokePages.widget);
|
||||
await page.waitForAppMounted('widget app mounted');
|
||||
await page.waitForCapturedRequest('/api/v1/widget/messages', 'widget requests messages');
|
||||
await page.waitForCapturedRequest('/api/v1/widget/inbox_members', 'widget requests inbox members');
|
||||
await page.eval(`fetch('/api/v1/widget/campaigns?website_token=${encodeURIComponent(widgetConfig.website_channel_config?.website_token || 'gochat-smoke-widget-token')}').then(response => response.ok)`);
|
||||
await page.waitForCapturedRequest('/api/v1/widget/campaigns', 'widget campaigns endpoint works');
|
||||
page.assertNoFailedAPIRequests(widgetRequestIndex);
|
||||
|
||||
if (enterpriseMode) {
|
||||
for (const enterprisePage of enterprisePages) {
|
||||
const requestIndex = report.requests.length;
|
||||
await page.navigate(enterprisePage.url);
|
||||
await page.waitForAppMounted(`${enterprisePage.label} app mounted`);
|
||||
for (const request of enterprisePage.requests) {
|
||||
await page.waitForCapturedRequestAfter(request, requestIndex, `${enterprisePage.label} requests ${request}`);
|
||||
}
|
||||
}
|
||||
await page.eval(`fetch(${JSON.stringify(`${apiBaseURL}/api/v1/accounts/${seed.account_id}/captain/copilot_threads`)}, {
|
||||
method: 'POST',
|
||||
headers: (() => {
|
||||
const sessionCookie = document.cookie.split('; ').find(cookie => cookie.startsWith('cw_d_session_info='));
|
||||
const session = sessionCookie ? JSON.parse(decodeURIComponent(sessionCookie.split('=').slice(1).join('='))) : {};
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'access-token': session['access-token'] || '',
|
||||
client: session.client || '',
|
||||
uid: session.uid || '',
|
||||
'token-type': session['token-type'] || 'Bearer',
|
||||
};
|
||||
})(),
|
||||
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');
|
||||
}
|
||||
report.finished_at = new Date().toISOString();
|
||||
report.status = 'passed';
|
||||
} catch (error) {
|
||||
report.finished_at = new Date().toISOString();
|
||||
report.status = 'failed';
|
||||
report.error = error.message;
|
||||
throw error;
|
||||
} finally {
|
||||
writeFileSync(path.join(logDir, 'browser-smoke-report.json'), JSON.stringify(report, null, 2));
|
||||
await page.close();
|
||||
await new Promise(resolve => shellServer.close(resolve));
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user