Files
LWLT-AIBOT/control-plane/test/leadership-task-summary.test.ts
2026-09-10 15:58:22 +08:00

215 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import assert from 'node:assert/strict';
import test from 'node:test';
import {
buildLeaderTaskSummary,
formatShanghaiTimestamp,
isLeaderTaskSummaryStatus,
LEADER_SUMMARY_MAX_MESSAGE_BYTES
} from '../src/leadership-task-summary.js';
import { loadConfig } from '../src/config.js';
import { encryptText } from '../src/crypto.js';
import { readLeaderSummaryOriginalText } from '../src/leader-notification-service.js';
const base = {
taskId: 'TASK-20260907-001',
businessRouteId: 'arrangement_hotel_create',
assigneeUsername: 'employee-a',
createdAt: '2026-09-07T00:05:06.000Z'
};
const originalHotelInput = [
'安排酒店',
'团号:LW-260915A-B',
'入住日期:2026-09-16',
'离店日期:2026-09-18',
'酒店搜索:示例酒店 twn',
'间数:9',
'备注:请保留原话、全角标点及 两个空格。'
].join('\n');
test('notification shows the original multiline input and omits the generated task-number field', () => {
const summary = buildLeaderTaskSummary({
...base,
status: 'completed',
originalText: originalHotelInput
});
assert.equal(summary?.messageText, [
'【员工任务摘要】',
'员工:employee-a',
'业务:安排酒店',
'状态:已完成',
'提交:2026-09-07 08:05:06',
'',
'原始输入:',
originalHotelInput,
'',
'结果:业务处理已完成,结果已记录。'
].join('\n'));
assert.doesNotMatch(summary?.messageText || '', /TASK-20260907-001|^任务:/mu);
});
test('original input is available even when parsing failed or the business route is unknown', () => {
for (const status of ['parse_failed', 'blocked', 'cancelled', 'saved_unverified', 'dry_run']) {
const summary = buildLeaderTaskSummary({
...base,
businessRouteId: null,
status,
originalText: '这段输入未能解析:日期稍后补充。\n第二行仍须展示。'
});
assert.match(summary?.messageText || '', /业务:其他任务/);
assert.match(summary?.messageText || '', /原始输入:\n这段输入未能解析:日期稍后补充。\n第二行仍须展示。/);
assert.doesNotMatch(summary?.messageText || '', /TASK-20260907-001|^任务:/mu);
}
});
test('result-update messages retain the original input and full result', () => {
const summary = buildLeaderTaskSummary({
...base,
status: 'completed',
hadNeedsReview: true,
originalText: originalHotelInput,
successReceipt: { group_number: 'LW-260915A-B' }
});
assert.equal(summary?.milestone, 'resolved');
assert.ok(summary?.messageText.startsWith('【员工任务摘要·结果更新】'));
assert.ok(summary?.messageText.includes(originalHotelInput));
assert.ok(summary?.messageText.endsWith('结果:业务处理已完成。团号:LW-260915A-B'));
});
test('input formatting preserves punctuation and tabs, normalizes newlines and removes nonprinting controls', () => {
const summary = buildLeaderTaskSummary({
...base,
status: 'completed',
originalText: ' 安排酒店\r\n酒店:A酒店\tTWN\r间数:9\u0000\u0007 '
});
assert.ok(summary?.messageText.includes('原始输入:\n安排酒店\n酒店:A酒店\tTWN\n间数:9\n\n结果:'));
assert.doesNotMatch(summary?.messageText || '', /\r|\u0000|\u0007/);
});
test('missing input has an explicit fallback without inventing input from a receipt', () => {
for (const originalText of [undefined, null, '', ' \n\t ']) {
const summary = buildLeaderTaskSummary({
...base,
status: 'failed',
originalText,
successReceipt: { original_text: '不能作为原始输入的回执内容' }
});
assert.match(summary?.messageText || '', /原始输入:\n原始输入暂不可用,请在平台查看。/);
assert.doesNotMatch(summary?.messageText || '', /不能作为原始输入的回执内容/);
}
});
test('UTF-8 budget retains fitting inputs and visibly truncates only overflowing input', () => {
const options = { ...base, status: 'completed', originalText: 'x' };
const fixedBytes = Buffer.byteLength(buildLeaderTaskSummary(options)!.messageText, 'utf8') - 1;
const available = LEADER_SUMMARY_MAX_MESSAGE_BYTES - fixedBytes;
const fittingInput = 'a'.repeat(available);
const fitting = buildLeaderTaskSummary({ ...options, originalText: fittingInput })!;
assert.equal(Buffer.byteLength(fitting.messageText, 'utf8'), LEADER_SUMMARY_MAX_MESSAGE_BYTES);
assert.ok(fitting.messageText.includes(fittingInput));
assert.doesNotMatch(fitting.messageText, /原文过长/);
const overflowing = buildLeaderTaskSummary({ ...options, originalText: fittingInput + 'a' })!;
assert.ok(Buffer.byteLength(overflowing.messageText, 'utf8') <= LEADER_SUMMARY_MAX_MESSAGE_BYTES);
assert.match(overflowing.messageText, /原文过长,剩余内容请在平台查看/);
assert.ok(overflowing.messageText.endsWith('结果:业务处理已完成,结果已记录。'));
});
test('very long Chinese and emoji input cannot break Unicode or displace the outcome', () => {
const summary = buildLeaderTaskSummary({
...base,
assigneeUsername: '员'.repeat(200),
status: 'completed',
hadNeedsReview: true,
originalText: '酒店🏨中文\n'.repeat(20_000),
successReceipt: { group_numbers: Array.from({ length: 10 }, (_, index) => `GROUP-${index}-${'A'.repeat(60)}`) }
})!;
assert.ok(Buffer.byteLength(summary.messageText, 'utf8') <= LEADER_SUMMARY_MAX_MESSAGE_BYTES);
assert.equal(Buffer.from(summary.messageText).toString('utf8'), summary.messageText);
assert.doesNotMatch(summary.messageText, /\uFFFD/);
assert.match(summary.messageText, /原文过长,剩余内容请在平台查看/);
assert.ok(summary.messageText.endsWith(`结果:${summary.resultText}`));
});
test('worker reads the same encrypted original input for manual and AgentBus tasks', () => {
const config = loadConfig({ NODE_ENV: 'test', FIELD_ENCRYPTION_KEY: Buffer.alloc(32, 17).toString('base64') });
const ciphertext = encryptText(config, originalHotelInput);
const originalText = readLeaderSummaryOriginalText(config, ciphertext);
assert.equal(originalText, originalHotelInput);
const summary = buildLeaderTaskSummary({ ...base, status: 'completed', originalText });
assert.ok(summary?.messageText.includes(originalHotelInput));
assert.ok(!summary?.messageText.includes(ciphertext));
});
test('missing, invalid or unreadable ciphertext does not prevent a notification', () => {
const config = loadConfig({ NODE_ENV: 'test', FIELD_ENCRYPTION_KEY: Buffer.alloc(32, 17).toString('base64') });
const otherConfig = loadConfig({ NODE_ENV: 'test', FIELD_ENCRYPTION_KEY: Buffer.alloc(32, 18).toString('base64') });
for (const ciphertext of [null, undefined, '', {}, 'not-encrypted', encryptText(otherConfig, originalHotelInput)]) {
const originalText = readLeaderSummaryOriginalText(config, ciphertext);
assert.equal(originalText, null);
const summary = buildLeaderTaskSummary({ ...base, status: 'completed', originalText });
assert.match(summary?.messageText || '', /原始输入暂不可用/);
assert.ok(summary?.messageText.endsWith('结果:业务处理已完成,结果已记录。'));
}
});
test('leader summary projects only stable outcomes', () => {
assert.equal(buildLeaderTaskSummary({ ...base, status: 'running' }), null);
assert.equal(buildLeaderTaskSummary({ ...base, status: 'awaiting_confirmation' }), null);
assert.equal(isLeaderTaskSummaryStatus('completed'), true);
assert.equal(isLeaderTaskSummaryStatus('execution_uncertain'), true);
assert.equal(isLeaderTaskSummaryStatus('running'), false);
});
test('completed leader summary exposes only whitelisted business identifiers', () => {
const result = buildLeaderTaskSummary({
...base,
status: 'completed',
successReceipt: {
group_numbers: ['LW-260907A-B', '张三', '13800138000'],
order_number: 'D12345',
customer_name: '绝密客户',
passenger_names: ['游客甲', '游客乙'],
phone: '13900139000',
url: 'https://secret.example/token',
technical_error: 'stack trace'
}
});
assert.ok(result);
assert.equal(result.milestone, 'final');
assert.equal(result.deliveryStatus, 'completed');
assert.match(result.messageText, /employee-a/);
assert.match(result.messageText, /安排酒店/);
assert.match(result.messageText, /LW-260907A-B/);
assert.match(result.messageText, /D12345/);
assert.match(result.messageText, /2026-09-07 08:05:06/);
assert.doesNotMatch(result.messageText, /绝密客户|游客甲|13800138000|13900139000|secret\.example|stack trace|张三/);
});
test('failure, uncertainty, cancellation and dry-run use stable safe wording', () => {
const failed = buildLeaderTaskSummary({ ...base, status: 'operation_blocked' });
const uncertain = buildLeaderTaskSummary({ ...base, status: 'saved_unverified' });
const cancelled = buildLeaderTaskSummary({ ...base, status: 'cancelled' });
const dryRun = buildLeaderTaskSummary({ ...base, status: 'dry_run' });
assert.equal(failed?.resultText, '本次工作未完成,请在平台查看业务结果。');
assert.equal(uncertain?.milestone, 'needs_review');
assert.match(uncertain?.messageText || '', /需人工核验|请勿重复提交/);
assert.equal(cancelled?.deliveryStatus, 'cancelled');
assert.match(dryRun?.resultText || '', /尚未正式提交/);
});
test('a stable result after needs-review produces one result-update milestone', () => {
const result = buildLeaderTaskSummary({
...base,
status: 'completed',
hadNeedsReview: true,
successReceipt: { group_number: 'LW-260907A-B' }
});
assert.equal(result?.milestone, 'resolved');
assert.match(result?.messageText || '', /员工任务摘要·结果更新/);
});
test('Shanghai timestamp formatting is deterministic and invalid-safe', () => {
assert.equal(formatShanghaiTimestamp(base.createdAt), '2026-09-07 08:05:06');
assert.equal(formatShanghaiTimestamp('not-a-date'), '时间未记录');
});