feat: add account roles audit and task authorization

This commit is contained in:
inman committed 2026-09-01 19:14:06 +08:00
1 parent 337aaf7c88
commit 191c1a1aad
15 files changed
+4961 -457

No files matched your search

@@ -0,0 +1,242 @@
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('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, /account\.password_reset/);
assert.match(auth, /account\.password_changed/);
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.match(server, /password_change_required/);
const publicUser = server.slice(server.indexOf('function publicUser'), server.indexOf('async function loadExternalParser'));
assert.match(publicUser, /must_change_password/);
assert.doesNotMatch(publicUser, /organization/);
});
test('administrators manage task-type grants and manual intake enforces them before parsing or ERP dispatch', 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(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, business-facing, searchable, 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]+role === 'admin' \|\| role === 'team_lead'/);
assert.match(tasks, /isTaskOwnerRestricted[\s\S]+role === 'team_lead' \|\| role === 'user'/);
assert.match(tasks, /async listOperationsDashboard[\s\S]+t\.source = 'manual'/);
assert.match(tasks, /operations_dashboard_range_too_large/);
assert.match(tasks, /businessRouteId\?: string/);
assert.match(tasks, /operations_dashboard_business_invalid/);
assert.match(tasks, /LIMIT 2001/);
assert.match(tasks, /operations_dashboard_search_scope_too_large/);
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, /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, /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, /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, /created_by = \$4 AND source = 'manual'/);
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]+created_by = \$4 AND source = 'manual'/);
assert.match(tasks, /async eventsSince[\s\S]+t\.created_by = \$4 AND t\.source = 'manual'/);
assert.match(tasks, /async getTaskInputHistory[\s\S]+actor_user_id/);
assert.match(tasks, /WHERE organization_id = \$1 AND user_id = \$2 AND connection_id = \$3/);
assert.match(tasks, /WHERE browser_connections\.user_id = EXCLUDED\.user_id/);
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, since, contextFor\(session, request\)\)/);
});
test('operator UI exposes role-aware accounts, business drill-through, original input history, and reversible archive', 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="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="operationsDashboardDays"/);
assert.match(index, /id="operationsDashboardBusinesses"/);
assert.match(index, /姓名、指令内容、完成结果、团号或订单号/);
assert.match(index, /id="operationsDashboardDetail"/);
assert.match(index, /id="historyArchiveInput"/);
assert.match(app, /authUser\?\.role === 'admin'/);
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, /\/business-authorizations/);
assert.match(app, /当前默认不能执行任何业务/);
assert.match(app, /function canViewOperationsDashboard/);
assert.match(app, /\/api\/operations-dashboard\?/);
assert.match(app, /\/api\/operations-dashboard\/tasks\/\$\{encodeURIComponent\(taskId\)\}/);
assert.match(app, /business_route_id/);
assert.match(app, /data-operations-status|dataset\.operationsStatus/);
assert.match(app, /data-operations-day|dataset\.operationsDay/);
assert.match(app, /data-operations-business|dataset\.operationsBusiness/);
assert.match(app, /`指令:\$\{task\.instruction_preview/);
assert.match(app, /`结果:\$\{task\.result_summary/);
const dashboardDetailRenderer = app.slice(
app.indexOf('function renderOperationsDashboardDetail()'),
app.indexOf('async function syncOperationsDashboard()')
);
assert.match(dashboardDetailRenderer, /操作人/);
assert.match(dashboardDetailRenderer, /指令内容/);
assert.match(dashboardDetailRenderer, /完成结果/);
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.doesNotMatch(app, /sendToExtension\('DELETE_TASK'/);
assert.match(retention, /SET archived_at = now\(\)/);
assert.doesNotMatch(retention, /DELETE FROM tasks/);
assert.doesNotMatch(retention, /DELETE FROM audit_events/);
});
+69 -20
View File
@@ -13,12 +13,16 @@ import {
buildImmediateParserPromotionGates,
classifyExpiredExecutionResult,
classifyExecutionResult,
canAccessTask,
canExecuteBusinessRoute,
canViewOperationsDashboard,
executionLifecycleFacts,
failureSummary,
executionStatusImmutable,
executionUpdateAllowed,
hasPrewriteNoErpEvidence,
hasLifecycleTestContext,
isTaskOwnerRestricted,
isLifecycleOperation,
parseLifecycleFacts,
prepareParsedOperationForConfirmation,
@@ -41,6 +45,51 @@ test('message routing starts a new session for a business directive, not for a s
assert.equal(isNewDirectiveMessage('数量改为 2'), false);
});
test('task access contract isolates users and team leads while preserving administrator and worker access', () => {
const cases = [
{ name: 'administrator sees another user manual task', access: { userId: 'admin', role: 'admin' as const }, createdBy: 'user-a', source: 'manual' as const, allowed: true },
{ name: 'trusted worker sees AgentBus task', access: { userId: '', role: undefined }, createdBy: null, source: 'agentbus' as const, allowed: true },
{ name: 'team lead sees own manual task', access: { userId: 'lead-a', role: 'team_lead' as const }, createdBy: 'lead-a', source: 'manual' as const, allowed: true },
{ name: 'team lead cannot use the normal task path for another manual task', access: { userId: 'lead-a', role: 'team_lead' as const }, createdBy: 'user-b', source: 'manual' as const, allowed: false },
{ name: 'team lead cannot use the normal task path for AgentBus work', access: { userId: 'lead-a', role: 'team_lead' as const }, createdBy: 'lead-a', source: 'agentbus' as const, allowed: false },
{ name: 'ordinary user sees own manual task', access: { userId: 'user-a', role: 'user' as const }, createdBy: 'user-a', source: 'manual' as const, allowed: true },
{ name: 'ordinary user cannot see another manual task', access: { userId: 'user-a', role: 'user' as const }, createdBy: 'user-b', source: 'manual' as const, allowed: false },
{ name: 'ordinary user cannot see AgentBus task', access: { userId: 'user-a', role: 'user' as const }, createdBy: 'user-a', source: 'agentbus' as const, allowed: false },
{ name: 'ordinary user without an actor cannot see a task', access: { userId: '', role: 'user' as const }, createdBy: '', source: 'manual' as const, allowed: false }
];
for (const item of cases) {
assert.equal(canAccessTask(item.access, { createdBy: item.createdBy, source: item.source }), item.allowed, item.name);
}
assert.equal(isTaskOwnerRestricted('admin'), false);
assert.equal(isTaskOwnerRestricted('team_lead'), true);
assert.equal(isTaskOwnerRestricted('user'), true);
assert.equal(canViewOperationsDashboard('admin'), true);
assert.equal(canViewOperationsDashboard('team_lead'), true);
assert.equal(canViewOperationsDashboard('user'), false);
});
test('business route authorization is an explicit allowlist for team leads and ordinary users', () => {
const routeId = 'arrangement_hotel_create' as const;
assert.equal(canExecuteBusinessRoute({
role: 'admin', source: 'manual', routeId: null, authorizedRouteIds: []
}), true, 'administrators retain all registered and unclassified manual intake');
assert.equal(canExecuteBusinessRoute({
role: 'team_lead', source: 'manual', routeId, authorizedRouteIds: [routeId]
}), true, 'team lead can use a granted route');
assert.equal(canExecuteBusinessRoute({
role: 'team_lead', source: 'manual', routeId, authorizedRouteIds: []
}), false, 'team lead cannot use an ungranted route');
assert.equal(canExecuteBusinessRoute({
role: 'user', source: 'manual', routeId, authorizedRouteIds: [routeId]
}), true, 'ordinary user can use a granted route');
assert.equal(canExecuteBusinessRoute({
role: 'user', source: 'manual', routeId: null, authorizedRouteIds: [routeId]
}), false, 'unclassified manual input fails closed for non-administrators');
assert.equal(canExecuteBusinessRoute({
role: undefined, source: 'agentbus', routeId: null, authorizedRouteIds: []
}), true, 'trusted AgentBus intake retains its separate administrator-controlled boundary');
});
test('field encryption round-trips without storing plaintext', () => {
const config = loadConfig({
NODE_ENV: 'test',
@@ -262,7 +311,7 @@ test('control plane requires the latest durable task-outcome migration before re
const { readFile } = await import('node:fs/promises');
const db = await readFile(new URL('../src/db.ts', import.meta.url), 'utf8');
const server = await readFile(new URL('../src/server.ts', import.meta.url), 'utf8');
assert.equal(REQUIRED_SCHEMA_VERSION, '014_task_input_attachments');
assert.equal(REQUIRED_SCHEMA_VERSION, '017_user_business_route_authorizations');
assert.match(db, /schema_migrations/);
assert.match(db, /databaseReadiness/);
assert.match(db, /assertDatabaseSchema/);
@@ -1176,8 +1225,8 @@ test('operator page has a login gate and uses the durable task API', async () =>
const inpage = await readFile(new URL('../../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8');
assert.match(index, /id="loginPanel"/);
assert.match(index, /id="workbench"[^>]*hidden/);
assert.match(index, /styles\.css\?v=20260827-roster-attachment-1/);
assert.match(index, /app\.js\?v=20260830-team-order-validation-scope-1/);
assert.match(index, /styles\.css\?v=20260901-business-authorization-1/);
assert.match(index, /app\.js\?v=20260901-business-authorization-1/);
assert.match(index, /id="statusDetailsPopover"/);
assert.match(index, /id="statusDetailsRefresh"/);
assert.match(app, /apiRequest\(`\/api\/tasks\?\$\{params\.toString\(\)\}`/);
@@ -1289,7 +1338,7 @@ test('operator page has a login gate and uses the durable task API', async () =>
assert.match(app, /data-history-task-delete/);
assert.match(app, /const historySelectedTaskIds = new Set\(\)/);
assert.match(app, /async function deleteHistorySelectedTasks\(\)/);
assert.match(app, /apiRequest\('\/api\/tasks\/bulk-delete'/);
assert.match(app, /apiRequest\('\/api\/tasks\/bulk-archive'/);
assert.match(app, /function renderHistoryBatchActions\(visibleTasks/);
assert.match(app, /historySelectedTaskIds\.clear\(\)/);
assert.match(app, /recorded_at/);
@@ -1335,30 +1384,30 @@ test('operator page has a login gate and uses the durable task API', async () =>
assert.match(styles, /\.task-operation-review-card/);
assert.match(styles, /\.task-important-message-card\.state-ok/);
assert.match(app, /operation_contract_validation/);
assert.match(app, /api\/tasks\/\$\{encodeURIComponent\(taskId\)\}.*method: 'DELETE'/s);
assert.match(app, /api\/tasks\/\$\{encodeURIComponent\(taskId\)\}\/archive/);
assert.match(app, /api\/tasks\/\$\{encodeURIComponent\(taskId\)\}\/restore/);
assert.match(app, /const taskDeleteStates = new Map\(\)/);
assert.match(app, /taskDeleteStates\.get\(task\.task_id\)/);
assert.match(app, /taskDeleteStates\.set\(taskId, 'deleting'\)/);
assert.match(app, /deleteButton\.textContent = '删除中…'/);
assert.match(app, /void sendToExtension\('DELETE_TASK'/);
assert.doesNotMatch(app, /await sendToExtension\('DELETE_TASK'/);
assert.match(taskService, /async hardDeleteTask\(/);
assert.match(taskService, /async hardDeleteTasks\(/);
assert.match(taskService, /task_id = ANY\(\$2::text\[\]\)/);
assert.match(app, /deleteButton\.textContent = task\?\.archived_at \? '恢复中…' : '归档中…'/);
assert.doesNotMatch(app, /sendToExtension\('DELETE_TASK'/);
assert.match(taskService, /async archiveTask\(/);
assert.match(taskService, /async archiveTasks\(/);
assert.match(taskService, /async restoreTask\(/);
assert.match(taskService, /archived_at = now\(\), archived_by = \$1/);
assert.match(taskService, /SET archived_at = NULL, archived_by = NULL, archive_reason = NULL/);
assert.match(taskService, /const missingTaskIds = normalizedTaskIds\.filter/);
assert.match(taskService, /DELETE FROM audit_events[\s\S]+entity_id = ANY\(\$2::text\[\]\)/);
assert.match(taskService, /DELETE FROM audit_events/);
const hardDeleteTasksSource = taskService.slice(
taskService.indexOf('async hardDeleteTasks('),
taskService.indexOf('async cancelTask(')
);
assert.match(hardDeleteTasksSource, /FROM task_artifacts a/);
assert.match(hardDeleteTasksSource, /FOR UPDATE OF a/);
assert.match(hardDeleteTasksSource, /this\.artifactStore\.cleanup\(outcome\.artifacts\)/);
assert.doesNotMatch(taskService, /async hardDeleteTask\(/);
assert.doesNotMatch(taskService, /DELETE FROM audit_events/);
assert.doesNotMatch(taskService, /DELETE FROM tasks/);
assert.match(server, /taskBulkDeleteSchema/);
assert.match(server, /\.max\(100\)/);
assert.match(server, /app\.post\('\/api\/tasks\/bulk-delete'/);
assert.match(server, /app\.post\('\/api\/tasks\/bulk-archive'/);
assert.match(server, /app\.delete\('\/api\/tasks\/:taskId'/);
assert.match(server, /app\.post\('\/api\/tasks\/:taskId\/archive'/);
assert.match(server, /app\.post\('\/api\/tasks\/:taskId\/restore'/);
assert.doesNotMatch(server, /tasks\.hardDelete/);
assert.match(bridge, /status: 'deleted'/);
assert.match(background, /LTJT_HARD_DELETE_TASK/);
assert.doesNotMatch(app, /task-json-output|taskResponseJson/);