fix: surface actionable channel message errors

This commit is contained in:
xmanrui 2026-08-25 14:54:50 +08:00
parent b42fc411c2
commit 7cbed3aefb
59 changed files with 2405 additions and 607 deletions

View file

@ -0,0 +1,149 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { normalizeLastMessageError } from '../plugin-src/client/last-message-error.js';
import { normalizeBotsSnapshot as normalizeFeishu } from '../plugin-src/client/channels/feishu/api.js';
import { normalizeSnapshot as normalizeDingtalk } from '../plugin-src/client/channels/dingtalk/api.js';
import { normalizeSnapshot as normalizeWeixin } from '../plugin-src/client/channels/weixin/api.js';
import { normalizeSnapshot as normalizeWecom } from '../plugin-src/client/channels/wecom/api.js';
import { normalizeSnapshot as normalizeQq } from '../plugin-src/client/channels/qq/api.js';
import { normalizeSnapshot as normalizeSlack } from '../plugin-src/client/channels/slack/api.js';
import { normalizeSnapshot as normalizeTelegram } from '../plugin-src/client/channels/telegram/api.js';
import { normalizeSnapshot as normalizeDiscord } from '../plugin-src/client/channels/discord/api.js';
import { normalizeSnapshot as normalizeWhatsapp } from '../plugin-src/client/channels/whatsapp/api.js';
import { BotCard as FeishuBotCard } from '../plugin-src/client/channels/feishu/index.js';
import { AccountCard as DingtalkAccountCard } from '../plugin-src/client/channels/dingtalk/index.js';
import { AccountCard as WeixinAccountCard } from '../plugin-src/client/channels/weixin/index.js';
import { AccountCard as WecomAccountCard } from '../plugin-src/client/channels/wecom/index.js';
import { AccountCard as QqAccountCard } from '../plugin-src/client/channels/qq/index.js';
import { SlackAccountCard } from '../plugin-src/client/channels/slack/index.js';
import { TelegramAccountCard } from '../plugin-src/client/channels/telegram/index.js';
import { DiscordAccountCard } from '../plugin-src/client/channels/discord/index.js';
import { WhatsappAccountCard } from '../plugin-src/client/channels/whatsapp/index.js';
const publicFailure = Object.freeze({
code: 'MODEL_RATE_LIMIT',
reason: 'MODEL_RATE_LIMIT',
message: '模型服务正在限流,本次任务未完成。请稍后重试。',
referenceId: 'MF-12AB34CD',
at: Date.UTC(2026, 7, 25, 7, 30),
});
function rawBot() {
return {
botId: 'bot_safe',
connected: true,
configured: true,
state: 'connected',
workspace: '/workspace/current',
groupResponseMode: 'mention',
bot: {
name: 'Harness Bot',
username: 'harness_bot',
appIdMasked: 'app••••01',
clientIdMasked: 'client••••01',
accountIdMasked: 'account••••01',
idMasked: 'bot••••01',
},
health: {
status: 'healthy',
summary: '连接运行正常',
lastCheckedAt: publicFailure.at,
},
lastMessageError: {
...publicFailure,
providerDetail: 'must-not-cross-client-normalization',
stack: '/private/path/that/must/not/render',
},
};
}
test('last-message error normalizer keeps only the bounded public contract', () => {
assert.deepEqual(normalizeLastMessageError({
...publicFailure,
providerDetail: 'secret provider response',
token: 'secret token',
}), publicFailure);
const bounded = normalizeLastMessageError({
code: 'C'.repeat(80),
reason: 'R'.repeat(80),
message: 'M'.repeat(600),
referenceId: 'F'.repeat(60),
at: 1,
});
assert.deepEqual({
code: bounded.code.length,
reason: bounded.reason.length,
message: bounded.message.length,
referenceId: bounded.referenceId.length,
}, { code: 64, reason: 64, message: 500, referenceId: 40 });
for (const invalid of [
null,
[],
{ ...publicFailure, referenceId: '' },
{ ...publicFailure, at: '2026-08-25T07:30:00Z' },
{ ...publicFailure, at: Number.POSITIVE_INFINITY },
]) assert.equal(normalizeLastMessageError(invalid), null);
});
test('all channel snapshot normalizers retain the same safe message failure', () => {
const normalizers = [
['Feishu', normalizeFeishu],
['DingTalk', normalizeDingtalk],
['WeChat', normalizeWeixin],
['WeCom', normalizeWecom],
['QQ', normalizeQq],
['Slack', normalizeSlack],
['Telegram', normalizeTelegram],
['Discord', normalizeDiscord],
['WhatsApp', normalizeWhatsapp],
];
for (const [channel, normalize] of normalizers) {
const snapshot = normalize({ bots: [rawBot()] });
assert.deepEqual(snapshot.bots[0].lastMessageError, publicFailure, channel);
assert.doesNotMatch(
JSON.stringify(snapshot.bots[0].lastMessageError),
/providerDetail|private|token/i,
channel,
);
}
});
test('all channel cards show message failures independently from healthy connections', () => {
const account = rawBot();
const callbacks = {
onReconnect() {},
onRequestRemove() {},
onConfirmRemove() {},
onCancelRemove() {},
};
const cards = [
['Feishu', FeishuBotCard, { connection: account }],
['DingTalk', DingtalkAccountCard, { account }],
['WeChat', WeixinAccountCard, { account }],
['WeCom', WecomAccountCard, { account }],
['QQ', QqAccountCard, { account }],
['Slack', SlackAccountCard, { account }],
['Telegram', TelegramAccountCard, { account }],
['Discord', DiscordAccountCard, { account }],
['WhatsApp', WhatsappAccountCard, { account }],
];
for (const [channel, Card, props] of cards) {
const markup = renderToStaticMarkup(React.createElement(Card, { ...callbacks, ...props }));
assert.match(markup, /运行正常/, channel);
assert.match(markup, /最近一条消息处理失败/, channel);
assert.match(markup, /模型服务正在限流/, channel);
assert.match(markup, /MODEL_RATE_LIMIT/, channel);
assert.match(markup, /MF-12AB34CD/, channel);
assert.match(markup, /role="status"/, channel);
assert.match(markup, /<time dateTime="2026-08-25T07:30:00.000Z">/, channel);
assert.doesNotMatch(markup, /providerDetail|must-not-cross|private\/path/, channel);
}
});