215 lines
9.8 KiB
TypeScript
215 lines
9.8 KiB
TypeScript
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'), '时间未记录');
|
||
});
|