Files
LWLT-AIBOT/control-plane/test/account-authorization.test.ts
2026-09-09 17:24:16 +08:00

497 lines
29 KiB
TypeScript

import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
async function source(path: string): Promise<string> {
return readFile(new URL(path, import.meta.url), 'utf8');
}
test('account migration adds roles, actor attribution, and reversible archive without purging history', async () => {
const sql = await source('../migrations/015_account_roles_and_task_audit.sql');
assert.match(sql, /CHECK \(role IN \('admin', 'user'\)\)/);
assert.match(sql, /must_change_password boolean NOT NULL DEFAULT false/);
assert.match(sql, /password_changed_at timestamptz NOT NULL DEFAULT now\(\)/);
assert.match(sql, /idempotency_keys[\s\S]+actor_user_id uuid REFERENCES users\(id\)/);
assert.match(sql, /idempotency_keys_actor_unique_idx/);
assert.match(sql, /idempotency_keys_system_unique_idx/);
assert.match(sql, /actor_user_id uuid REFERENCES users\(id\)/);
assert.match(sql, /input_source IN \('manual', 'agentbus', 'reparse', 'system'\)/);
assert.match(sql, /task_input_attachments[\s\S]+created_by uuid REFERENCES users\(id\)/);
assert.match(sql, /archived_at timestamptz/);
assert.match(sql, /archived_by uuid REFERENCES users\(id\)/);
assert.doesNotMatch(sql, /DELETE\s+FROM/i);
});
test('team-lead migration adds the role and bounded dashboard indexes without a tenant concept', async () => {
const sql = await source('../migrations/016_team_lead_operations_dashboard.sql');
assert.match(sql, /CHECK \(role IN \('admin', 'team_lead', 'user'\)\)/);
assert.match(sql, /tasks_operations_dashboard_created_idx/);
assert.match(sql, /tasks_operations_dashboard_actor_idx/);
assert.match(sql, /tasks_operations_dashboard_status_idx/);
assert.match(sql, /tasks_operations_dashboard_business_idx/);
assert.match(sql, /WHERE source = 'manual'/);
assert.doesNotMatch(sql, /CREATE TABLE\s+(?:organizations|tenants)/i);
assert.doesNotMatch(sql, /DELETE\s+FROM/i);
});
test('business authorization migration adds a fail-closed per-user allowlist for all registered routes', async () => {
const sql = await source('../migrations/017_user_business_route_authorizations.sql');
assert.match(sql, /business_authorization_revision integer NOT NULL DEFAULT 0/);
assert.match(sql, /CREATE TABLE IF NOT EXISTS user_business_route_authorizations/);
assert.match(sql, /PRIMARY KEY \(organization_id, user_id, route_id\)/);
assert.match(sql, /granted_by uuid REFERENCES users\(id\)/);
assert.match(sql, /FOREIGN KEY \(organization_id, user_id\)[\s\S]+REFERENCES users \(organization_id, id\)/);
assert.match(sql, /FOREIGN KEY \(organization_id, granted_by\)[\s\S]+REFERENCES users \(organization_id, id\)/);
for (const routeId of [
'team_order_create',
'passenger_list_import_independent',
'arrangement_hotel_create',
'order_update_independent',
'order_cancel',
'confirmation_export'
]) {
assert.match(sql, new RegExp(`'${routeId}'`));
}
assert.doesNotMatch(sql, /INSERT INTO user_business_route_authorizations[\s\S]+SELECT[\s\S]+FROM users/i);
});
test('AgentBus account-worker migration adds fail-closed channel, task, browser, and ERP identity ownership', async () => {
const sql = await source('../migrations/018_agentbus_account_workers.sql');
assert.match(sql, /ADD COLUMN IF NOT EXISTS erp_account text/);
assert.match(sql, /users_org_erp_account_unique_idx/);
assert.match(sql, /ADD COLUMN IF NOT EXISTS owner_user_id uuid/);
assert.match(sql, /FOREIGN KEY \(organization_id, owner_user_id\)[\s\S]+REFERENCES users \(organization_id, id\)/);
assert.match(sql, /user_channels_owner_unique_idx/);
assert.match(sql, /ADD COLUMN IF NOT EXISTS assigned_user_id uuid/);
assert.match(sql, /FOREIGN KEY \(organization_id, assigned_user_id\)[\s\S]+REFERENCES users \(organization_id, id\)/);
assert.match(sql, /source = 'manual'[\s\S]+created_by IS NOT NULL/);
assert.match(sql, /erp_account_verified boolean NOT NULL DEFAULT false/);
assert.match(sql, /browser_connections_active_user_unique_idx/);
assert.match(sql, /status = 'superseded'/);
assert.match(sql, /owner_user_id IS NULL[\s\S]+enabled = true/);
});
test('administrator task-isolation migration removes legacy bindings and rejects future data-plane principals', async () => {
const sql = await source('../migrations/021_admin_task_data_plane_isolation.sql');
assert.match(sql, /UPDATE tasks task[\s\S]+assigned_user_id = NULL[\s\S]+account\.role = 'admin'/);
assert.match(sql, /UPDATE user_channels channel[\s\S]+owner_user_id = NULL[\s\S]+account\.role = 'admin'/);
assert.match(sql, /UPDATE browser_connections connection[\s\S]+status = 'superseded'[\s\S]+account\.role = 'admin'/);
assert.match(sql, /DELETE FROM user_business_route_authorizations[\s\S]+account\.role = 'admin'/);
assert.match(sql, /UPDATE leader_task_summary_subscriptions subscription[\s\S]+enabled = false[\s\S]+account\.role = 'admin'/);
assert.match(sql, /UPDATE leader_task_summary_deliveries delivery[\s\S]+delivery_status = 'cancelled'[\s\S]+account\.role = 'admin'/);
assert.match(sql, /CREATE OR REPLACE FUNCTION reject_admin_task_principal/);
assert.match(sql, /FOR SHARE/);
for (const constraint of [
'tasks_admin_assignee_forbidden',
'tasks_admin_creator_forbidden',
'user_channels_admin_owner_forbidden',
'browser_connections_admin_worker_forbidden',
'user_business_routes_admin_grantee_forbidden',
'leader_task_summary_admin_subscriber_forbidden',
'leader_task_summary_admin_recipient_forbidden'
]) assert.match(sql, new RegExp(constraint));
assert.match(sql, /CREATE OR REPLACE FUNCTION isolate_administrator_from_task_runtime/);
assert.match(sql, /AFTER UPDATE OF role ON users/);
assert.doesNotMatch(sql, /DELETE FROM tasks/);
});
test('AgentBus channel keys and owners are unique so one inbound identity cannot fan out to multiple employees', async () => {
const channels = await source('../src/agentbus-channels.ts');
assert.match(channels, /requireAssignableOwner/);
assert.match(channels, /requireUniqueAgentBusKey/);
assert.match(channels, /pg_advisory_xact_lock/);
assert.match(channels, /sha256Text\(agentbusKey\)/);
assert.match(channels, /channel_key_conflict/);
assert.match(channels, /channel_owner_conflict/);
});
test('account lifecycle is administrator-gated and protects passwords, sessions, and the last administrator', async () => {
const [auth, server] = await Promise.all([
source('../src/auth.ts'),
source('../src/server.ts')
]);
assert.match(auth, /export type AuthRole = 'admin' \| 'team_lead' \| 'user'/);
assert.match(auth, /role: normalizeRole\(row\.role\)/);
assert.match(auth, /argon2\.hash\([^;]+type: argon2\.argon2id/s);
assert.match(auth, /self_lockout_forbidden/);
assert.match(auth, /last_admin_protected/);
assert.match(auth, /UPDATE sessions SET revoked_at = now\(\)/);
assert.match(auth, /must_change_password = false/);
assert.match(auth, /function validatePassword[\s\S]+if \(!password\)/);
assert.doesNotMatch(auth, /password\.length < 12|12—512/);
assert.match(auth, /account\.password_reset/);
assert.match(auth, /account\.password_changed/);
assert.match(auth, /function validateAccountRouting/);
assert.match(auth, /admin_erp_account_forbidden/);
assert.match(auth, /erp_account_conflict/);
assert.match(server, /app\.get\('\/api\/accounts'[\s\S]+requireAdminSession\(request\)/);
assert.match(server, /app\.post\('\/api\/accounts'[\s\S]+requireAdminMutationSession\(request\)/);
assert.match(server, /app\.get\('\/api\/audit'[\s\S]+requireAdminSession\(request\)/);
assert.doesNotMatch(server, /password_change_required|must_change_password|\.min\(12\)/);
const publicUser = server.slice(server.indexOf('function publicUser'), server.indexOf('async function loadExternalParser'));
assert.doesNotMatch(publicUser, /must_change_password/);
assert.doesNotMatch(publicUser, /organization/);
});
test('administrators manage employee task-type grants while remaining outside manual intake', async () => {
const [auth, tasks, server] = await Promise.all([
source('../src/auth.ts'),
source('../src/task-service.ts'),
source('../src/server.ts')
]);
assert.match(auth, /authorized_business_route_ids: BusinessRouteId\[\]/);
assert.match(auth, /async setBusinessRouteAuthorizations\(/);
assert.match(auth, /business_authorization_revision_conflict/);
assert.match(auth, /account\.business_authorizations_updated/);
assert.match(auth, /admin_business_authorization_fixed/);
assert.match(auth, /authorized_business_route_ids: role === 'admin' \? \[\] : storedRouteIds/);
assert.match(server, /task_types: BUSINESS_ROUTES\.map/);
assert.match(server, /app\.put\('\/api\/accounts\/:userId\/business-authorizations'[\s\S]+requireAdminMutationSession\(request\)/);
assert.match(tasks, /export function canExecuteBusinessRoute/);
assert.match(tasks, /task\.business_authorization_denied/);
assert.match(tasks, /business_type_unresolved/);
assert.match(tasks, /当前账号未授权“\$\{route\.directive\}”业务,已禁止执行/);
assert.match(tasks, /no_parse: true/);
assert.match(tasks, /no_plugin_dispatch: true/);
assert.match(tasks, /no_erp_write: true/);
const createTask = tasks.slice(tasks.indexOf('async createTask('), tasks.indexOf('async ingestMessage('));
assert.match(createTask, /requireBusinessAuthorization\(context, requestedRouteId/);
assert.match(createTask, /assertBusinessAuthorizationInTransaction/);
const ingestMessage = tasks.slice(tasks.indexOf('async ingestMessage('), tasks.indexOf('async getTask('));
assert.match(ingestMessage, /requireBusinessAuthorization\(context, preflightRouteId/);
assert.match(ingestMessage, /assertBusinessAuthorizationInTransaction/);
const attachment = tasks.slice(
tasks.indexOf('async attachPassengerRosterAttachment('),
tasks.indexOf('async createTask(')
);
assert.match(attachment, /requireBusinessAuthorization\(context, target\.routeId/);
assert.match(attachment, /assertBusinessAuthorizationInTransaction/);
for (const transition of ['confirmTask', 'claimForBrowser']) {
const start = tasks.indexOf(`async ${transition}(`);
assert.notEqual(start, -1);
assert.match(tasks.slice(start, start + 8_000), /assertTaskCreatorBusinessAuthorizationInTransaction/);
}
const parseResult = tasks.slice(tasks.indexOf('async applyParseResult('), tasks.indexOf('async confirmTask('));
assert.match(parseResult, /taskAuthorization\.allowed && shouldAutomaticallyConfirm/);
});
test('operations dashboard is leadership-gated, cross-source, business-facing, and separate from normal task authority', async () => {
const [tasks, server] = await Promise.all([
source('../src/task-service.ts'),
source('../src/server.ts')
]);
assert.match(tasks, /canViewOperationsDashboard[\s\S]+return role === 'team_lead'/);
assert.match(tasks, /isTaskOwnerRestricted[\s\S]+role === 'team_lead' \|\| role === 'user'/);
assert.match(tasks, /async listOperationsDashboard[\s\S]+t\.assigned_user_id IS NOT NULL[\s\S]+t\.source IN \('manual', 'agentbus'\)/);
assert.match(tasks, /actorUserId[\s\S]+t\.assigned_user_id = \$\$\{params\.length\}/);
assert.match(tasks, /LEFT JOIN users assignee ON assignee\.id = t\.assigned_user_id/);
assert.match(tasks, /operations_dashboard_range_too_large/);
assert.match(tasks, /businessRouteId\?: string/);
assert.match(tasks, /operations_dashboard_business_invalid/);
assert.match(tasks, /operations_dashboard_search_scope_too_large/);
assert.match(tasks, /function operationsDashboardRouteFromInstruction/);
assert.match(tasks, /function operationsDashboardRouteFromOperation/);
assert.match(tasks, /if \(search && candidateResult\.rows\.length > 2_000\)/);
assert.match(tasks, /OPERATIONS_DASHBOARD_FALLBACK_TYPES/);
assert.match(tasks, /dashboard_order_delete/);
assert.match(tasks, /dashboard_arrangement_hotel/);
assert.match(tasks, /dashboard_passenger_list_import/);
assert.match(tasks, /dashboard_order_update/);
assert.match(tasks, /const routeMatches = !businessRouteId/);
assert.match(tasks, /content_ciphertext[\s\S]+decryptText\(this\.config, row\.content_ciphertext/);
assert.match(tasks, /operationsDashboardSearchMatches[\s\S]+projection\.instruction[\s\S]+projection\.result/);
assert.match(tasks, /function operationsDashboardLeadershipResultText/);
assert.match(tasks, /function operationsDashboardLeadershipInstructionText/);
assert.match(tasks, /待跟进/);
assert.match(tasks, /未完成/);
assert.match(tasks, /containsTechnicalLanguage/);
assert.match(tasks, /当前结果需要人工确认/);
assert.match(tasks, /timezone\('Asia\/Shanghai', t\.created_at\)/);
assert.match(tasks, /days,[\s\S]+businesses,[\s\S]+business_options/);
assert.match(tasks, /instructionPreview\.length > 360/);
const detailType = tasks.slice(
tasks.indexOf('export interface PublicOperationsDashboardTaskDetail'),
tasks.indexOf('export interface PublicOperationsDashboardPage')
);
assert.match(detailType, /creator: PublicOperationsDashboardActor/);
assert.match(detailType, /instructions: PublicOperationsDashboardInstruction\[\]/);
assert.match(detailType, /result_summary: string/);
assert.doesNotMatch(detailType, /events|stage|operation|parse_response|execution_result|download_url/);
const detailMethod = tasks.slice(
tasks.indexOf('async getOperationsDashboardTask('),
tasks.indexOf('async listAuditEvents(')
);
assert.match(detailMethod, /t\.assigned_user_id IS NOT NULL/);
assert.match(detailMethod, /t\.source IN \('manual', 'agentbus'\)/);
assert.doesNotMatch(detailMethod, /t\.source = 'manual'/);
assert.match(detailMethod, /getTaskInputHistory/);
assert.match(detailMethod, /instructions,[\s\S]+attachments:/);
assert.doesNotMatch(detailMethod, /this\.getTask\(/);
assert.match(server, /app\.get\('\/api\/operations-dashboard'[\s\S]+requireLeadershipSession\(request\)/);
assert.match(server, /business_route_id: z\.string\(\)\.trim\(\)\.max\(120\)\.optional\(\)/);
assert.match(server, /businessRouteId: query\.business_route_id/);
assert.match(server, /signal: controller\.signal/);
assert.match(server, /app\.get\('\/api\/operations-dashboard\/tasks\/:taskId'[\s\S]+requireLeadershipSession\(request\)/);
assert.match(server, /read_only: true/);
assert.doesNotMatch(server, /app\.(?:post|put|patch|delete)\('\/api\/operations-dashboard/);
});
test('ordinary task access is enforced across reads, mutations, artifacts, events, and browser connections', async () => {
const [tasks, server] = await Promise.all([
source('../src/task-service.ts'),
source('../src/server.ts')
]);
assert.match(tasks, /assigned_user_id = \$4/);
assert.match(tasks, /private async lockTaskForAccess/);
for (const mutation of ['reparseTaskWithAi', 'confirmTask', 'claimForBrowser', 'recordExecutionResult', 'cancelTask']) {
const start = tasks.indexOf(`async ${mutation}(`);
assert.notEqual(start, -1, `${mutation} exists`);
const body = tasks.slice(start, start + 20_000);
assert.match(body, /lockTaskForAccess\(/, `${mutation} uses the task access lock`);
}
assert.match(tasks, /async getTaskArtifact[\s\S]+assigned_user_id = \$4/);
assert.match(tasks, /async eventsSince[\s\S]+t\.assigned_user_id = \$3/);
assert.match(tasks, /async getTaskInputHistory[\s\S]+actor_user_id/);
assert.match(tasks, /connection\.organization_id = \$1[\s\S]+connection\.user_id = \$2[\s\S]+connection\.connection_id = \$3/);
assert.match(tasks, /WHERE browser_connections\.user_id = EXCLUDED\.user_id/);
assert.match(tasks, /browser_worker_conflict/);
assert.match(tasks, /identity_mismatch/);
assert.match(tasks, /erp_account_mismatch/);
assert.match(tasks, /task_execution_assignee_mismatch/);
assert.match(tasks, /assignee: publicActor\(row\.assignee_id, row\.assignee_username\)/);
const confirmation = tasks.slice(tasks.indexOf('async confirmTask('), tasks.indexOf('async claimForBrowser('));
assert.match(confirmation, /assigned_user_id/);
assert.match(confirmation, /task_execution_assignee_mismatch/);
assert.match(tasks, /i\.actor_user_id IS NOT DISTINCT FROM \$3::uuid/);
assert.match(tasks, /private async lockIdempotencyKey/);
assert.match(tasks, /pg_advisory_xact_lock/);
assert.match(server, /tasks\.listTasksPage[\s\S]+access: contextFor\(session, request\)/);
assert.match(server, /tasks\.getTaskArtifact[\s\S]+contextFor\(session, request\)/);
assert.match(server, /tasks\.eventsSince\(session\.user\.organizationId, session\.user\.id, since\)/);
assert.match(server, /app\.addHook\('preHandler'[\s\S]+isTaskDataPlaneRoute\(request\.routeOptions\.url\)[\s\S]+requireTaskDataPlane\(await getSession\(request\)\)/);
assert.match(server, /task_access_forbidden/);
});
test('administrators are excluded from task events and plugin result routing', async () => {
const [tasks, server, app] = await Promise.all([
source('../src/task-service.ts'),
source('../src/server.ts'),
source('../../LianSyn-platform/app.js')
]);
const eventHistory = tasks.slice(tasks.indexOf('async eventsSince('));
assert.match(eventHistory, /assignedUserId: string/);
assert.match(eventHistory, /AND t\.assigned_user_id = \$3/);
assert.doesNotMatch(eventHistory, /isTaskOwnerRestricted\(access\?\.role\)/);
const eventRoute = server.slice(
server.indexOf("app.get('/api/events'"),
server.indexOf('app.setErrorHandler')
);
assert.match(eventRoute, /event\.owner_user_id !== session\.user\.id/);
assert.match(eventRoute, /command\.assigned_user_id !== session\.user\.id/);
assert.match(eventRoute, /event: browser-command/);
assert.match(eventRoute, /tasks\.eventsSince\(session\.user\.organizationId, session\.user\.id, since\)/);
assert.match(eventRoute, /requireTaskSession\(request\)/);
const eventStream = app.slice(
app.indexOf('function startRemoteEventStream()'),
app.indexOf('function cacheRuntimeTask(')
);
assert.match(eventStream, /filter\(taskAssignedToCurrentAccount\)/);
assert.match(eventStream, /executable_by=me/);
assert.match(eventStream, /addEventListener\('browser-command'/);
const bridgeListener = app.slice(
app.indexOf("window.addEventListener('message'"),
app.indexOf('async function parseRawInstruction')
);
assert.match(bridgeListener, /TASK_RESULT_CHANGED/);
assert.match(bridgeListener, /extensionTaskBelongsToCurrentAccount\(taskId\)/);
const resultQueue = app.slice(
app.indexOf('async function persistExtensionTaskResult('),
app.indexOf('async function reconcileTaskReceipt(')
);
assert.match(resultQueue, /if \(!extensionTaskBelongsToCurrentAccount\(taskId\)\) return false/);
assert.match(resultQueue, /!extensionTaskBelongsToCurrentAccount\(normalizedTaskId\)/);
assert.match(resultQueue, /!knownTask \|\| !taskAssignedToCurrentAccount\(knownTask\)/);
});
test('ERP browser claims serialize only the assigned account queue', async () => {
const tasks = await source('../src/task-service.ts');
const claim = tasks.slice(tasks.indexOf('async claimForBrowser('), tasks.indexOf('async recordExecutionResult('));
assert.match(claim, /pg_advisory_xact_lock\([\s\S]+erp-account-queue:[\s\S]+context\.organizationId, context\.userId/);
assert.doesNotMatch(claim, /SELECT id FROM organizations WHERE id = \$1 FOR UPDATE/);
assert.match(claim, /WHERE t\.organization_id = \$1\s+AND t\.assigned_user_id = \$2[\s\S]+a\.status IN \('accepted', 'running'\)/);
assert.match(claim, /WHERE organization_id = \$1\s+AND assigned_user_id = \$2\s+AND status = 'confirmed'/);
assert.equal((claim.match(/assigned_user_id = \$2/g) || []).length, 2);
assert.match(claim, /\[context\.organizationId, context\.userId\]/);
});
test('force delete physically removes accessible tasks without the archive state gate', async () => {
const [tasks, server] = await Promise.all([
source('../src/task-service.ts'),
source('../src/server.ts')
]);
const hardDelete = tasks.slice(tasks.indexOf('async hardDeleteTask('), tasks.indexOf('async cancelTask('));
assert.match(hardDelete, /async hardDeleteTasks\(/);
assert.match(hardDelete, /AND \(\$3::boolean = false OR assigned_user_id = \$4\)/);
assert.match(hardDelete, /DELETE FROM outbox_events[\s\S]+aggregate_id = ANY\(\$2::text\[\]\)/);
assert.match(hardDelete, /DELETE FROM tasks[\s\S]+WHERE id = ANY\(\$1::uuid\[\]\)/);
assert.match(hardDelete, /task\.hard_deleted/);
assert.match(hardDelete, /assigned_user_id: assignedUserId/);
assert.match(hardDelete, /this\.notifyBrowserCommand\(command\)/);
assert.match(hardDelete, /this\.artifactStore\.cleanup\(outcome\.artifacts\)/);
assert.doesNotMatch(hardDelete, /task_archive_blocked|正在处理或等待 ERP 执行,不能归档/);
const bulkDeleteRoute = server.slice(
server.indexOf("app.post('/api/tasks/bulk-delete'"),
server.indexOf("app.post('/api/tasks/bulk-archive'")
);
const singleDeleteRoute = server.slice(
server.indexOf("app.delete('/api/tasks/:taskId'"),
server.indexOf("app.post('/api/tasks/:taskId/archive'")
);
assert.match(bulkDeleteRoute, /tasks\.hardDeleteTasks/);
assert.match(singleDeleteRoute, /tasks\.hardDeleteTask/);
assert.doesNotMatch(`${bulkDeleteRoute}\n${singleDeleteRoute}`, /tasks\.archiveTask|tasks\.archiveTasks/);
});
test('operator UI exposes role-aware accounts, executive drill-through, archive, and explicit permanent deletion', async () => {
const [app, index, retention] = await Promise.all([
source('../../LianSyn-platform/app.js'),
source('../../LianSyn-platform/index.html'),
source('../src/retention.ts')
]);
assert.match(index, /href="\/accounts"/);
assert.match(index, /href="\/audit"/);
assert.match(index, /href="\/operations-dashboard"/);
assert.match(index, /id="passwordChangeForm"/);
assert.match(index, /id="accountForm"/);
assert.match(index, /id="accountErpAccount"/);
assert.match(index, /id="channelOwnerUserId"/);
assert.doesNotMatch(index, /accountMustChangePassword|首次登录必须修改密码|minlength="12"|12—512/);
assert.match(index, /id="accountAuthorizationPanel"/);
assert.match(index, /id="accountAuthorizationTypes"/);
assert.match(index, /id="accountAuthorizationSave"/);
assert.match(index, /value="team_lead">组长/);
assert.match(index, /id="operationsDashboardFilters"/);
assert.match(index, /id="operationsDashboardBusiness"/);
assert.match(index, /id="operationsDashboardBusinesses"/);
assert.match(index, /任务类型排名/);
assert.match(index, /员工操作任务排名/);
assert.match(index, /员工名称/);
assert.match(index, /class="operations-dashboard-table"/);
assert.match(index, /员工名称、原始输入或最终输出/);
assert.match(index, /id="operationsDashboardRangePresets"/);
assert.match(index, /id="operationsDashboardRefresh"/);
assert.match(index, /任务操作明细/);
assert.match(index, /id="operationsDashboardDetail"/);
assert.doesNotMatch(index, /平台运行全景|operations-dashboard-hero|OPERATIONS OVERVIEW|BUSINESS TRACE|指令操作历史/);
assert.match(index, /id="historyArchiveInput"/);
assert.match(index, /id="historyArchiveSelectedButton"/);
assert.match(index, /id="historyDeleteSelectedButton"[^>]*>强制删除所选</);
assert.match(index, /id="archiveTaskButton"/);
assert.match(index, /id="deleteTaskButton"[^>]*>强制删除任务</);
assert.match(app, /authUser\?\.role === 'admin'/);
assert.doesNotMatch(app, /passwordChangeForced|must_change_password/);
assert.match(app, /crypto\.randomUUID/);
assert.match(app, /\/api\/tasks\/\$\{encodeURIComponent\(taskId\)\}\/input-history/);
assert.match(app, /创建人与原始输入审计/);
assert.match(app, /function renderAccountAuthorizationPanel/);
assert.match(app, /function renderChannelOwnerOptions/);
assert.match(app, /function taskAssignedToCurrentAccount/);
assert.match(app, /expected_erp_account/);
assert.match(app, /executable_by=me/);
assert.match(app, /const candidates = Array\.isArray\(result\.tasks\) \? result\.tasks : \[\]/);
assert.match(app, /\/business-authorizations/);
assert.match(app, /当前默认不能执行任何业务/);
assert.match(app, /function canViewOperationsDashboard/);
assert.match(app, /function canUseTaskDataPlane/);
assert.match(app, /isAdministrator\(\) && \(IS_TASK_PAGE \|\| IS_OPERATIONS_DASHBOARD_PAGE\)[\s\S]+window\.location\.replace\('\/accounts'\)/);
assert.match(app, /browserConnectionId = canUseTaskDataPlane\(\) \? connectionIdForUser\(user\) : ''/);
assert.match(app, /if \(bridgeState\) bridgeState\.hidden = !canUseTaskDataPlane\(\)/);
assert.match(app, /if \(!canUseTaskDataPlane\(\)\) return false;[\s\S]+sendToExtension\('PING'/);
assert.match(app, /account\.role === 'admin' \? '不参与任务' : '任务权限'/);
assert.match(app, /\/api\/operations-dashboard\?/);
assert.match(app, /\/api\/operations-dashboard\/tasks\/\$\{encodeURIComponent\(taskId\)\}/);
assert.match(app, /business_route_id/);
assert.doesNotMatch(app, /data-operations-status|dataset\.operationsStatus/);
assert.match(app, /\$\('#operationsDashboardStatus'\)\.value = 'all'/);
assert.match(app, /data-operations-business|dataset\.operationsBusiness/);
assert.match(app, /function operationsDashboardRankScale/);
assert.match(app, /operationsDashboardRankBar/);
assert.match(app, /operations-dashboard-rank-bar/);
assert.match(app, /sort\(\(left, right\) => Number\(right\.total/);
assert.doesNotMatch(app, /完成率|operationsDashboardCompletionRate|operationsDashboardDays/);
const rankScaleSource = app.slice(
app.indexOf('function operationsDashboardRankScale('),
app.indexOf('function operationsDashboardRankBar(')
);
const rankScale = Function(`${rankScaleSource}\nreturn operationsDashboardRankScale;`)() as (maximum: number) => number;
assert.equal(rankScale(0), 1);
assert.equal(rankScale(1), 2);
assert.equal(rankScale(43), 50);
assert.equal(rankScale(100), 120);
assert.equal(rankScale(359), 400);
assert.match(app, /function operationsDashboardReadableResult/);
assert.match(app, /function operationsDashboardReadableInput/);
assert.match(app, /原始输入/);
assert.match(app, /最终输出/);
const dashboardTaskRenderer = app.slice(
app.indexOf('function renderOperationsDashboardTasks()'),
app.indexOf('function renderOperationsDashboard()')
);
assert.match(dashboardTaskRenderer, /task\.creator\.username/);
assert.match(dashboardTaskRenderer, /task\.created_at/);
assert.match(dashboardTaskRenderer, /task\.business_route_label/);
assert.match(dashboardTaskRenderer, /task\.instruction_preview/);
assert.match(dashboardTaskRenderer, /operationsDashboardReadableInput\(task\.instruction_preview/);
assert.match(dashboardTaskRenderer, /operationsDashboardReadableResult\(task\.result_summary/);
assert.doesNotMatch(dashboardTaskRenderer, /记录号|task\.task_id\}`/);
const dashboardDetailRenderer = app.slice(
app.indexOf('function renderOperationsDashboardDetail()'),
app.indexOf('async function syncOperationsDashboard()')
);
assert.match(dashboardDetailRenderer, /人员/);
assert.match(dashboardDetailRenderer, /原始输入/);
assert.match(dashboardDetailRenderer, /最终输出/);
assert.match(dashboardDetailRenderer, /operationsDashboardReadableResult/);
assert.doesNotMatch(dashboardDetailRenderer, /任务生命周期|处理结果与技术上下文|renderTaskLifecycle|JSON\.stringify|task\.stage|parse_response|operation:/);
assert.match(app, /\/api\/tasks\/\$\{encodeURIComponent\(taskId\)\}\/archive/);
assert.match(app, /\/api\/tasks\/\$\{encodeURIComponent\(taskId\)\}\/restore/);
assert.match(app, /method: 'DELETE'/);
assert.match(app, /sendToExtension\('DELETE_TASK'/);
assert.match(app, /selectedTasks\.filter\(taskAssignedToCurrentAccount\)/);
assert.match(app, /command\?\.target_user_id !== authUser\?\.id/);
assert.match(app, /此操作不受“正在处理”或“等待 ERP 执行”状态限制/);
assert.match(retention, /SET archived_at = now\(\)/);
assert.doesNotMatch(retention, /DELETE FROM tasks/);
assert.doesNotMatch(retention, /DELETE FROM audit_events/);
});
test('account authorization editor uses a scroll-safe open layout without overriding the closed account list', async () => {
const [app, index, styles] = await Promise.all([
source('../../LianSyn-platform/app.js'),
source('../../LianSyn-platform/index.html'),
source('../../LianSyn-platform/styles.css')
]);
const renderStart = app.indexOf('function renderAccountAuthorizationPanel()');
const renderEnd = app.indexOf('function openAccountAuthorizationEditor(', renderStart);
const renderSource = app.slice(renderStart, renderEnd);
assert.match(renderSource, /const isOpen = Boolean\(account && account\.role !== 'admin'\)/);
assert.match(renderSource, /classList\.toggle\('is-authorizing', isOpen\)/);
assert.match(renderSource, /if \(!isOpen\)/);
const genericLayout = styles.indexOf('.channels-page .channel-panel {');
const openLayout = styles.indexOf('.channels-page .channel-panel.account-panel.is-authorizing {');
assert.ok(genericLayout >= 0, 'generic management-page layout is present');
assert.ok(openLayout > genericLayout, 'account open-state layout follows the generic layout in the cascade');
const openLayoutSource = styles.slice(openLayout, styles.indexOf('}', openLayout) + 1);
assert.match(openLayoutSource, /height:\s*max-content/);
assert.match(openLayoutSource, /grid-template-rows:\s*auto auto auto auto auto/);
assert.match(openLayoutSource, /overflow:\s*visible/);
assert.doesNotMatch(styles, /\.account-panel\s*\{\s*grid-template-rows:/);
assert.match(index, /styles\.css\?v=20260908-leader-webhook-1/);
assert.match(index, /app\.js\?v=20260908-leader-webhook-1/);
});