Files
LWLT-AIBOT/LianSyn-platform/app-operations-dashboard.test.mjs
inman b11dc7b771 merge: integrate lightweight dashboard candidate queries
# Conflicts:
#	control-plane/src/task-service.ts
2026-09-09 18:54:16 +08:00

156 lines
9.1 KiB
JavaScript

import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
const [app, index, styles, taskService, server] = await Promise.all([
readFile(new URL('./app.js', import.meta.url), 'utf8'),
readFile(new URL('./index.html', import.meta.url), 'utf8'),
readFile(new URL('./styles.css', import.meta.url), 'utf8'),
readFile(new URL('../control-plane/src/task-service.ts', import.meta.url), 'utf8'),
readFile(new URL('../control-plane/src/server.ts', import.meta.url), 'utf8')
]);
test('dashboard summary cards are uniform display-only totals and leave filtering to the explicit controls', () => {
const summaryRenderer = app.slice(
app.indexOf('function renderOperationsDashboardSummary()'),
app.indexOf('function operationsDashboardRankScale')
);
assert.match(app, /label: '操作次数'[^\n]+tone: 'total'/);
assert.doesNotMatch(app, /tone: 'primary'/);
assert.match(summaryRenderer, /const card = el\('article', `operations-dashboard-stat operations-dashboard-stat-\$\{item\.tone\}`\)/);
assert.doesNotMatch(summaryRenderer, /selectedStatus|isSelected|operationsStatus|aria-pressed/);
assert.doesNotMatch(app, /\$\('#operationsDashboardSummary'\)\?\.addEventListener\('click'/);
assert.match(index, /<option value="all">全部结果<\/option>/);
assert.match(app, /function resetOperationsDashboardFilters\(\)[\s\S]*?\$\('#operationsDashboardStatus'\)\.value = 'all';/);
assert.match(styles, /\.operations-dashboard-summary\s*{[\s\S]*?grid-template-columns: repeat\(5, minmax\(0, 1fr\)\)/);
assert.doesNotMatch(styles, /\.operations-dashboard-stat\[data-operations-status\]/);
assert.doesNotMatch(styles, /\.operations-dashboard-stat\.is-active/);
assert.doesNotMatch(styles, /operations-dashboard-stat-primary/);
});
test('waiting-for-input work is folded into in-progress instead of exposing a follow-up state', () => {
assert.doesNotMatch(index, /<option value="attention">/);
assert.doesNotMatch(app, /label: '待跟进'/);
assert.match(app, /outcomeKind === 'attention'\) return '进行中'/);
assert.match(app, /Number\(summary\.active \|\| 0\) \+ Number\(summary\.attention \|\| 0\)/);
const activeSql = taskService.slice(
taskService.indexOf('const OPERATIONS_ACTIVE_SQL'),
taskService.indexOf('const OPERATIONS_COMPLETED_STATUSES')
);
assert.doesNotMatch(activeSql, /OPERATIONS_ATTENTION_SQL/);
});
test('dashboard route has a focused leadership header and phone-safe navigation shell', () => {
assert.match(index, /content="width=device-width, initial-scale=1, viewport-fit=cover"/);
assert.match(index, /class="operations-dashboard-page-head"[\s\S]*?<h1>平台运行看板<\/h1>/);
assert.match(index, /class="operations-dashboard-back-link" href="\/">返回操作台<\/a>/);
assert.match(
styles,
/body\.operations-dashboard-page-active \.top-nav,[\s\S]*?#statusDetailsPopover\s*{[\s\S]*?display: none !important;/
);
assert.match(
styles,
/@media \(max-width: 920px\)[\s\S]*?body\.operations-dashboard-page-active \.app-shell\s*{[\s\S]*?width: 100%;/
);
});
test('phone dashboard turns metrics and task rows into touch-friendly cards', () => {
assert.match(
styles,
/\.operations-dashboard-summary\s*{[\s\S]*?display: flex;[\s\S]*?overflow-x: auto;[\s\S]*?scroll-snap-type: x proximity;/
);
assert.match(styles, /\.operations-dashboard-business-row,[\s\S]*?min-height: 48px;/);
assert.match(styles, /\.operations-dashboard-table thead\s*{\s*display: none;/);
assert.match(styles, /content: attr\(data-label\);/);
assert.match(styles, /\.operations-dashboard-detail\s*{[\s\S]*?env\(safe-area-inset-top\)/);
for (const label of ['操作时间', '员工名称', '任务类型', '原始输入', '最终输出', '任务结果']) {
assert.match(app, new RegExp(`dataset\\.label = '${label}'`));
}
assert.match(app, /document\.body\.classList\.add\('operations-dashboard-detail-open'\)/);
assert.match(app, /document\.body\.classList\.remove\('operations-dashboard-detail-open'\)/);
});
test('table filters use a bounded single-connection read and default to 20 rows', () => {
const dashboardQuery = taskService.slice(
taskService.indexOf('async listOperationsDashboard('),
taskService.indexOf('async getOperationsDashboardTask(')
);
const dashboardRoute = server.slice(
server.indexOf("app.get('/api/operations-dashboard'"),
server.indexOf("app.get('/api/operations-dashboard/tasks/:taskId'")
);
assert.match(dashboardQuery, /businessRouteId === 'unclassified'[\s\S]*?t\.business_route_id IS NULL/);
assert.match(dashboardQuery, /t\.assigned_user_id IS NOT NULL[\s\S]*?t\.source IN \('manual', 'agentbus'\)/);
assert.match(dashboardQuery, /actorUserId[\s\S]*?t\.assigned_user_id = \$\$\{params\.length\}/);
assert.match(dashboardQuery, /LEFT JOIN users assignee ON assignee\.id = t\.assigned_user_id/);
assert.match(dashboardQuery, /t\.business_route_id = \$\$\{params\.length\} OR t\.business_route_id IS NULL/);
assert.match(dashboardQuery, /const taskSearchSelect = `[\s\S]*?t\.success_receipt = '\{\}'::jsonb[\s\S]*?END AS execution_result/);
assert.match(dashboardQuery, /withTransaction\(this\.config/);
assert.match(dashboardQuery, /SET TRANSACTION READ ONLY/);
assert.match(dashboardQuery, /SET LOCAL statement_timeout/);
assert.match(dashboardQuery, /SELECT \$\{search \? taskSearchSelect : taskCandidateSelect\}/);
assert.match(dashboardQuery, /CASE WHEN t\.business_route_id IS NULL THEN t\.original_text_ciphertext ELSE NULL END/);
const baseSelect = dashboardQuery.slice(
dashboardQuery.indexOf('const taskBaseSelect'),
dashboardQuery.indexOf('const taskLegacyOperationSelect')
);
const legacyOperationSelect = dashboardQuery.slice(
dashboardQuery.indexOf('const taskLegacyOperationSelect'),
dashboardQuery.indexOf('const taskDetailSelect')
);
const detailSelect = dashboardQuery.slice(
dashboardQuery.indexOf('const taskDetailSelect'),
dashboardQuery.indexOf('const taskSearchSelect')
);
const searchSelect = dashboardQuery.slice(
dashboardQuery.indexOf('const taskSearchSelect'),
dashboardQuery.indexOf('const taskCandidateSelect')
);
const candidateSelect = dashboardQuery.slice(
dashboardQuery.indexOf('const taskCandidateSelect'),
dashboardQuery.indexOf('try {', dashboardQuery.indexOf('const taskCandidateSelect'))
);
assert.doesNotMatch(baseSelect, /t\.operation/);
assert.match(legacyOperationSelect, /'action', t\.operation -> 'action'/);
assert.match(legacyOperationSelect, /'kind', t\.operation #> '\{data,existing_refs,kind\}'/);
assert.match(legacyOperationSelect, /'mode', t\.operation #> '\{data,arrangement,mode\}'/);
assert.match(detailSelect, /t\.operation, t\.original_text_ciphertext/);
assert.match(searchSelect, /t\.operation, t\.original_text_ciphertext/);
assert.doesNotMatch(candidateSelect, /t\.operation/);
assert.doesNotMatch(candidateSelect, /taskLegacyOperationSelect/);
assert.match(dashboardQuery, /filter\(\(\{ projection \}\) => !projection\.task\.business_route_id\)/);
assert.match(dashboardQuery, /'legacy_classification'/);
assert.match(dashboardQuery, /SELECT t\.id, \$\{taskLegacyOperationSelect\}/);
assert.match(dashboardQuery, /legacyOperationById/);
assert.match(dashboardQuery, /'search_messages'/);
assert.match(dashboardQuery, /filter\(\(\{ directSearchMatch \}\) => !directSearchMatch\)/);
assert.doesNotMatch(dashboardQuery, /const pool = getPool\(this\.config\)/);
assert.doesNotMatch(dashboardQuery, /Promise\.all/);
assert.doesNotMatch(dashboardQuery, /loadSearchRows|classificationResult/);
assert.match(dashboardQuery, /if \(pageMatches\.length\)[\s\S]*?loadDetailedRows\(pageMatches\.map/);
assert.match(dashboardQuery, /options\.limit \|\| 20/);
assert.match(app, /const OPERATIONS_DASHBOARD_PAGE_SIZE = 20/);
assert.match(server, /operationsDashboardQuerySchema[\s\S]*?limit:[^\n]+default\(20\)/);
assert.match(dashboardRoute, /new AbortController\(\)/);
assert.match(dashboardRoute, /request\.raw\.once\('aborted', abortRequest\)/);
assert.match(dashboardRoute, /signal: controller\.signal/);
assert.match(dashboardRoute, /request\.raw\.off\('aborted', abortRequest\)/);
assert.match(dashboardQuery, /operations_dashboard_query_timeout/);
const dashboardDetail = taskService.slice(taskService.indexOf('async getOperationsDashboardTask('));
assert.match(dashboardDetail, /t\.assigned_user_id IS NOT NULL/);
assert.match(dashboardDetail, /t\.source IN \('manual', 'agentbus'\)/);
assert.doesNotMatch(dashboardDetail, /t\.source = 'manual'/);
const sync = app.slice(
app.indexOf('async function syncOperationsDashboard()'),
app.indexOf('async function loadOperationsDashboardTask(')
);
assert.match(sync, /filterForm\.setAttribute\('aria-busy', 'true'\)/);
assert.match(sync, /queryButton\.disabled = true/);
assert.match(sync, /filterForm\.removeAttribute\('aria-busy'\)/);
assert.match(sync, /queryButton\.disabled = false/);
assert.match(app, /看板请求超时,请稍后重试;若正在筛选,请缩短日期范围或增加筛选条件/);
assert.match(app, /error\?\.errorCode === 'operations_dashboard_query_timeout'/);
});