fix: remember direct recipients for connection tests

This commit is contained in:
xmanrui 2026-08-19 11:24:50 +08:00
parent fb61c4235f
commit 3ac70a7c65
34 changed files with 397 additions and 178 deletions

View file

@ -354,7 +354,7 @@ function normalizeSnapshot(value) {
function connectionTestFeedback(result) {
if (result?.sent === true) return "\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002";
if (result?.code === "test-target-unavailable") {
return "\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002";
return "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u673A\u5668\u4EBA\u5C1A\u672A\u6536\u5230\u53EF\u7528\u4E8E\u6D4B\u8BD5\u7684\u79C1\u804A\u6D88\u606F\u3002";
}
return result ? "\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002" : null;
}
@ -453,20 +453,16 @@ var EN = Object.freeze({
"\u68C0\u67E5\u4E2D\u2026": "Checking\u2026",
"\u8FDE\u63A5\u68C0\u67E5\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002": "Connection check failed. Try again later.",
"\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230\u5BF9\u5E94\u673A\u5668\u4EBA\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002": "Test message sent. Check the matching bot conversation.",
"\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002": "Connection check completed. Send /status to the bot in a direct message, then check again.",
"\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u673A\u5668\u4EBA\u5C1A\u672A\u6536\u5230\u53EF\u7528\u4E8E\u6D4B\u8BD5\u7684\u79C1\u804A\u6D88\u606F\u3002": "Connection check completed. The bot has not received a direct message it can use for testing.",
"\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002": "Connection check completed, but the test message could not be sent.",
"\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230\u98DE\u4E66\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002": "Test message sent. Check the Feishu conversation.",
"\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u5F53\u524D\u6CA1\u6709\u53EF\u7528\u7684\u6D4B\u8BD5\u6D88\u606F\u63A5\u6536\u4EBA\u3002": "Connection check completed, but no test-message recipient is available.",
"\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230 WhatsApp \u81EA\u804A\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002": "Test message sent. Check the WhatsApp self-chat.",
"\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u5F53\u524D\u6CA1\u6709\u53EF\u7528\u7684 WhatsApp \u81EA\u804A\u76EE\u6807\u3002": "Connection check completed, but no WhatsApp self-chat target is available.",
"\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002": "DingTalk connection check completed and the test message was sent.",
"\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002": "DingTalk connection check completed. Send /status to the bot in a direct message, then check again.",
"\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002": "DingTalk connection check completed, but the test message could not be sent.",
"\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002": "WeChat connection check completed and the test message was sent.",
"\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002": "WeChat connection check completed. Send /status to the bot in a direct message, then check again.",
"\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002": "WeChat connection check completed, but the test message could not be sent.",
"\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002": "WeCom connection check completed and the test message was sent.",
"\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002": "WeCom connection check completed. Send /status to the bot in a direct message, then check again.",
"\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002": "WeCom connection check completed, but the test message could not be sent.",
"\u91CD\u8BD5\u8FDE\u63A5": "Reconnect",
"\u91CD\u8BD5\u4E2D\u2026": "Retrying\u2026",
@ -2501,7 +2497,7 @@ function checkedTime2(value) {
function connectionTestNotice(value) {
if (value?.testMessage?.sent === true) return "\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230\u5BF9\u5E94\u673A\u5668\u4EBA\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002";
if (value?.testMessage?.code === "test-target-unavailable") {
return "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002";
return "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u673A\u5668\u4EBA\u5C1A\u672A\u6536\u5230\u53EF\u7528\u4E8E\u6D4B\u8BD5\u7684\u79C1\u804A\u6D88\u606F\u3002";
}
return value?.testMessage ? "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002" : null;
}
@ -4063,7 +4059,7 @@ function connectionTestNotice2(value) {
return "\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230\u98DE\u4E66\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002";
}
if (value?.testMessage?.code === "test-target-unavailable") {
return "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u5F53\u524D\u6CA1\u6709\u53EF\u7528\u7684\u6D4B\u8BD5\u6D88\u606F\u63A5\u6536\u4EBA\u3002";
return "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u673A\u5668\u4EBA\u5C1A\u672A\u6536\u5230\u53EF\u7528\u4E8E\u6D4B\u8BD5\u7684\u79C1\u804A\u6D88\u606F\u3002";
}
return value?.testMessage ? "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002" : null;
}
@ -4892,7 +4888,7 @@ function normalizeSnapshot3(value) {
function connectionTestFeedback2(result) {
if (result?.sent === true) return "\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230\u5BF9\u5E94\u673A\u5668\u4EBA\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002";
if (result?.code === "test-target-unavailable") {
return "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002";
return "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u673A\u5668\u4EBA\u5C1A\u672A\u6536\u5230\u53EF\u7528\u4E8E\u6D4B\u8BD5\u7684\u79C1\u804A\u6D88\u606F\u3002";
}
return result ? "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002" : null;
}
@ -6397,7 +6393,7 @@ function WecomSettingsTab({ rpcCall }) {
} else if (snapshot.testMessage?.sent) {
feedback = "\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002";
} else if (snapshot.testMessage?.code === "test-target-unavailable") {
feedback = "\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002";
feedback = "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u673A\u5668\u4EBA\u5C1A\u672A\u6536\u5230\u53EF\u7528\u4E8E\u6D4B\u8BD5\u7684\u79C1\u804A\u6D88\u606F\u3002";
} else if (snapshot.testMessage) {
feedback = "\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002";
} else {
@ -7371,7 +7367,7 @@ function WeixinSettingsTab({ rpcCall }) {
} else if (snapshot.testMessage?.sent) {
feedback = "\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002";
} else if (snapshot.testMessage?.code === "test-target-unavailable") {
feedback = "\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002";
feedback = "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u673A\u5668\u4EBA\u5C1A\u672A\u6536\u5230\u53EF\u7528\u4E8E\u6D4B\u8BD5\u7684\u79C1\u804A\u6D88\u606F\u3002";
} else if (snapshot.testMessage) {
feedback = "\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002";
} else {

File diff suppressed because one or more lines are too long

View file

@ -206,7 +206,7 @@ export function normalizeSnapshot(value) {
export function connectionTestFeedback(result) {
if (result?.sent === true) return '钉钉连接检查完成,测试消息已发送。';
if (result?.code === 'test-target-unavailable') {
return '钉钉连接检查完成。请先私聊机器人发送 /status,再重新检查。';
return '连接检查完成。机器人尚未收到可用于测试的私聊消息。';
}
return result ? '钉钉连接检查完成,但测试消息发送失败。' : null;
}

View file

@ -335,7 +335,7 @@ function connectionTestNotice(value) {
return '测试消息已发送,请到飞书会话中确认。';
}
if (value?.testMessage?.code === 'test-target-unavailable') {
return '连接检查完成,但当前没有可用的测试消息接收人。';
return '连接检查完成。机器人尚未收到可用于测试的私聊消息。';
}
return value?.testMessage ? '连接检查完成,但测试消息发送失败。' : null;
}

View file

@ -124,7 +124,7 @@ export function normalizeSnapshot(value) {
export function connectionTestFeedback(result) {
if (result?.sent === true) return '测试消息已发送,请到对应机器人会话中确认。';
if (result?.code === 'test-target-unavailable') {
return '连接检查完成。请先私聊机器人发送 /status,再重新检查。';
return '连接检查完成。机器人尚未收到可用于测试的私聊消息。';
}
return result ? '连接检查完成,但测试消息发送失败。' : null;
}

View file

@ -33,7 +33,7 @@ function checkedTime(value) {
function connectionTestNotice(value) {
if (value?.testMessage?.sent === true) return '测试消息已发送,请到对应机器人会话中确认。';
if (value?.testMessage?.code === 'test-target-unavailable') {
return '连接检查完成。请先私聊机器人发送 /status,再重新检查。';
return '连接检查完成。机器人尚未收到可用于测试的私聊消息。';
}
return value?.testMessage ? '连接检查完成,但测试消息发送失败。' : null;
}

View file

@ -410,7 +410,7 @@ export function WecomSettingsTab({ rpcCall }) {
} else if (snapshot.testMessage?.sent) {
feedback = '企业微信连接检查完成,测试消息已发送。';
} else if (snapshot.testMessage?.code === 'test-target-unavailable') {
feedback = '企业微信连接检查完成。请先私聊机器人发送 /status,再重新检查。';
feedback = '连接检查完成。机器人尚未收到可用于测试的私聊消息。';
} else if (snapshot.testMessage) {
feedback = '企业微信连接检查完成,但测试消息发送失败。';
} else {

View file

@ -524,7 +524,7 @@ export function WeixinSettingsTab({ rpcCall }) {
} else if (snapshot.testMessage?.sent) {
feedback = '微信连接检查完成,测试消息已发送。';
} else if (snapshot.testMessage?.code === 'test-target-unavailable') {
feedback = '微信连接检查完成。请先私聊机器人发送 /status,再重新检查。';
feedback = '连接检查完成。机器人尚未收到可用于测试的私聊消息。';
} else if (snapshot.testMessage) {
feedback = '微信连接检查完成,但测试消息发送失败。';
} else {

View file

@ -77,20 +77,16 @@ const EN = Object.freeze({
'检查中…': 'Checking…',
'连接检查失败,请稍后重试。': 'Connection check failed. Try again later.',
'测试消息已发送,请到对应机器人会话中确认。': 'Test message sent. Check the matching bot conversation.',
'连接检查完成。请先私聊机器人发送 /status,再重新检查。': 'Connection check completed. Send /status to the bot in a direct message, then check again.',
'连接检查完成。机器人尚未收到可用于测试的私聊消息。': 'Connection check completed. The bot has not received a direct message it can use for testing.',
'连接检查完成,但测试消息发送失败。': 'Connection check completed, but the test message could not be sent.',
'测试消息已发送,请到飞书会话中确认。': 'Test message sent. Check the Feishu conversation.',
'连接检查完成,但当前没有可用的测试消息接收人。': 'Connection check completed, but no test-message recipient is available.',
'测试消息已发送,请到 WhatsApp 自聊会话中确认。': 'Test message sent. Check the WhatsApp self-chat.',
'连接检查完成,但当前没有可用的 WhatsApp 自聊目标。': 'Connection check completed, but no WhatsApp self-chat target is available.',
'钉钉连接检查完成,测试消息已发送。': 'DingTalk connection check completed and the test message was sent.',
'钉钉连接检查完成。请先私聊机器人发送 /status,再重新检查。': 'DingTalk connection check completed. Send /status to the bot in a direct message, then check again.',
'钉钉连接检查完成,但测试消息发送失败。': 'DingTalk connection check completed, but the test message could not be sent.',
'微信连接检查完成,测试消息已发送。': 'WeChat connection check completed and the test message was sent.',
'微信连接检查完成。请先私聊机器人发送 /status,再重新检查。': 'WeChat connection check completed. Send /status to the bot in a direct message, then check again.',
'微信连接检查完成,但测试消息发送失败。': 'WeChat connection check completed, but the test message could not be sent.',
'企业微信连接检查完成,测试消息已发送。': 'WeCom connection check completed and the test message was sent.',
'企业微信连接检查完成。请先私聊机器人发送 /status,再重新检查。': 'WeCom connection check completed. Send /status to the bot in a direct message, then check again.',
'企业微信连接检查完成,但测试消息发送失败。': 'WeCom connection check completed, but the test message could not be sent.',
'重试连接': 'Reconnect',
'重试中…': 'Retrying…',

View file

@ -310,6 +310,9 @@ export class DingtalkHarnessBridge {
} catch {
// An unsafe reply route must never be able to submit an approval.
}
if (sessionWebhook && String(message.conversationType) !== '2') {
rememberConnectionTestTarget(this.#state, { sessionWebhook });
}
const pending = this.#pendingInteractions.get(key);
const approvalReply = this.#approvals.claimReply({
key,
@ -460,9 +463,6 @@ export class DingtalkHarnessBridge {
if (isPlainText && !hasImages && command === '/status') {
await this.#harness.ensureRunning({ signal: this.#signal });
await this.#send(sessionWebhook, '钉钉机器人与 DeepSeek Harness 连接正常。');
if (String(message.conversationType) !== '2') {
rememberConnectionTestTarget(this.#state, { sessionWebhook });
}
return;
}
if (isPlainText && !hasImages && command === '/new') {

View file

@ -18,6 +18,7 @@ import {
} from '../shared/harness-question.mjs';
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
import { runCompactCommand } from '../shared/compact-command.mjs';
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
@ -133,6 +134,11 @@ export class FeishuHarnessBridge {
return Promise.resolve();
}
if (event.message.chat_type === 'p2p') {
const chatId = nonEmptyString(event.message.chat_id);
if (chatId) rememberConnectionTestTarget(this.#state, { chatId });
}
this.#acceptedMessageIds.add(messageId);
const processingReaction = this.#addReaction(messageId, 'OnIt');
if (this.#isResolvedQuestionReply(event, key)) {

View file

@ -1,5 +1,9 @@
import { FeishuHarnessBridge } from './bridge.mjs';
import { VerifiedFeishuChannel } from './feishu-channel.mjs';
import {
connectionTestTargetUnavailable,
sendRememberedConnectionTest,
} from '../shared/connection-test.mjs';
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
@ -246,27 +250,39 @@ export class FeishuRuntime {
error.code = 'test-target-unavailable';
throw error;
}
const ownerOpenId = this.#ownerOpenIds[0];
if (!ownerOpenId || ownerOpenId === '*') {
const error = new Error('飞书机器人没有可用的测试消息接收人');
error.code = 'test-target-unavailable';
throw error;
}
if (typeof text !== 'string' || !text.trim()) {
throw new TypeError('Feishu connection test text is required');
}
const response = await this.#client.im.v1.message.create({
params: { receive_id_type: 'open_id' },
data: {
receive_id: ownerOpenId,
msg_type: 'text',
content: JSON.stringify({ text }),
const send = async (receiveIdType, receiveId, content) => {
const response = await this.#client.im.v1.message.create({
params: { receive_id_type: receiveIdType },
data: {
receive_id: receiveId,
msg_type: 'text',
content: JSON.stringify({ text: content }),
},
});
if (response?.code && response.code !== 0) {
throw new Error(`Feishu connection test failed: ${response.msg || response.code}`);
}
};
const ownerOpenId = this.#ownerOpenIds.find((value) => value !== '*');
if (ownerOpenId) {
await send('open_id', ownerOpenId, text);
return { sent: true };
}
return sendRememberedConnectionTest({
state: this.#state,
text,
channelLabel: '飞书机器人',
send: async (target, content) => {
const chatId = typeof target?.chatId === 'string' ? target.chatId.trim() : '';
if (!chatId) throw connectionTestTargetUnavailable('飞书机器人');
await send('chat_id', chatId, content);
},
});
if (response?.code && response.code !== 0) {
throw new Error(`Feishu connection test failed: ${response.msg || response.code}`);
}
return { sent: true };
}
async stop({ preserveError = false } = {}) {

View file

@ -177,6 +177,12 @@ export class QqHarnessBridge {
|| this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
const key = conversationKey(message);
this.#acceptedMessageIds.add(messageId);
if (message.kind === 'c2c'
&& (this.#ownerUserOpenid === '*' || sender === this.#ownerUserOpenid)
&& message.replyTarget?.scope === 'c2c'
&& nonEmptyString(message.replyTarget.targetId) === sender) {
rememberConnectionTestTarget(this.#state, message.replyTarget);
}
const pending = this.#pendingInteractions.get(key);
const approval = this.#approvals.claimReply({
key,
@ -296,11 +302,6 @@ export class QqHarnessBridge {
}
if (!hasImages && command === '/status') {
await this.#harness.ensureRunning({ signal: this.#signal });
if (message.kind === 'c2c'
&& target?.scope === 'c2c'
&& nonEmptyString(target.targetId) === sender) {
rememberConnectionTestTarget(this.#state, target);
}
await this.#bot.sendText(target, 'QQ 机器人与 DeepSeek Harness 连接正常。');
await this.#state.markSeen(messageId);
return;

View file

@ -31,7 +31,7 @@ export function connectionTestMessage(botName, channelLabel = '机器人') {
}
export function connectionTestTargetUnavailable(channelLabel = '机器人') {
const error = new Error(`请先私聊${channelLabel}发送 /status,再点击“检查连接”。`);
const error = new Error(`${channelLabel}尚未收到可用于测试的私聊消息。`);
error.code = 'test-target-unavailable';
return error;
}

View file

@ -101,6 +101,12 @@ export class TextHarnessBridge {
return Promise.resolve();
}
this.#acceptedMessageIds.add(messageId);
if (normalized.kind === 'direct') {
rememberConnectionTestTarget(
this.#state,
normalized.connectionTestTarget ?? normalized.replyTarget,
);
}
const key = `${kind}:${conversationId}`;
const pending = this.#pendingInteractions.get(key);
@ -252,9 +258,6 @@ export class TextHarnessBridge {
if (!hasImages && command === '/status') {
await this.#harness.ensureRunning({ signal: this.#signal });
await this.#bot.sendText(target, `${this.#descriptor.label}机器人与 DeepSeek Harness 连接正常。`);
if (message.kind === 'direct') {
rememberConnectionTestTarget(this.#state, message.connectionTestTarget ?? target);
}
return;
}
const workspaceCommand = !hasImages

View file

@ -270,6 +270,9 @@ export class WecomHarnessBridge {
const key = conversationKey(frame);
this.#acceptedMessageIds.add(messageId);
if (body.chattype === 'single') {
rememberConnectionTestTarget(this.#state, { chatId });
}
const pending = this.#pendingInteractions.get(key);
const approval = this.#approvals.claimReply({
key,
@ -431,9 +434,6 @@ export class WecomHarnessBridge {
if (!hasImages && command === '/status') {
await this.#harness.ensureRunning({ signal: this.#signal });
await this.#sendImmediate(frame, chatId, '企业微信机器人与 DeepSeek Harness 连接正常。');
if (body.chattype === 'single') {
rememberConnectionTestTarget(this.#state, { chatId });
}
await this.#state.markSeen(messageId);
return;
}

View file

@ -129,6 +129,9 @@ export class WeixinHarnessBridge {
if (!messageId || !sender || this.#state.hasSeen(messageId)
|| this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
this.#acceptedMessageIds.add(messageId);
if (sender === this.#ownerUserId) {
rememberConnectionTestTarget(this.#state, { toUserId: sender });
}
const key = conversationKey(sender);
const contextToken = nonEmptyString(message?.context_token) ?? undefined;
const runId = nonEmptyString(message?.run_id) ?? undefined;
@ -251,7 +254,6 @@ export class WeixinHarnessBridge {
if (!hasImages && command === '/status') {
await this.#harness.ensureRunning({ signal: this.#signal });
await this.#send(sender, '微信与 DeepSeek Harness 连接正常。', contextToken, runId);
rememberConnectionTestTarget(this.#state, { toUserId: sender });
await this.#state.markSeen(messageId);
return;
}

View file

@ -126,7 +126,7 @@ test('connection-test feedback uses fixed client-owned messages', () => {
);
assert.equal(
connectionTestFeedback({ sent: false, code: 'test-target-unavailable' }),
'钉钉连接检查完成。请先私聊机器人发送 /status,再重新检查。',
'连接检查完成。机器人尚未收到可用于测试的私聊消息。',
);
assert.equal(
connectionTestFeedback({ sent: false, code: 'test-message-failed' }),

View file

@ -82,7 +82,7 @@ const PNG_BYTES = Buffer.from([
0x01, 0x02, 0x03,
]);
test('DingTalk remembers only a private /status session webhook for connection tests', async () => {
test('DingTalk remembers any private inbound session webhook for connection tests', async () => {
const privateFixture = stateFixture();
const privateSent = [];
const privateBridge = new DingtalkHarnessBridge({
@ -92,11 +92,11 @@ test('DingTalk remembers only a private /status session webhook for connection t
harness: { ensureRunning: async () => true },
state: privateFixture.state,
});
await privateBridge.accept(message('status-private', '/status'));
await privateBridge.accept(message('help-private', '/help'));
assert.deepEqual(connectionTestTarget(privateFixture.state), {
sessionWebhook: 'https://oapi.dingtalk.com/robot/reply?ticket=status-private',
sessionWebhook: 'https://oapi.dingtalk.com/robot/reply?ticket=help-private',
});
assert.equal(privateSent.at(-1).text, '钉钉机器人与 DeepSeek Harness 连接正常。');
assert.match(privateSent.at(-1).text, /\/help/);
const groupFixture = stateFixture();
const groupBridge = new DingtalkHarnessBridge({
@ -106,9 +106,9 @@ test('DingTalk remembers only a private /status session webhook for connection t
harness: { ensureRunning: async () => true },
state: groupFixture.state,
});
await groupBridge.accept(message('status-group', '/status', {
await groupBridge.accept(message('help-group', '/help', {
conversationType: '2',
conversationId: 'group-status',
conversationId: 'group-help',
isInAtList: true,
}));
assert.equal(connectionTestTarget(groupFixture.state), null);

View file

@ -45,7 +45,7 @@ function stateFixture() {
};
}
test('runtime sends a DingTalk connection test only through the remembered private status webhook', async () => {
test('runtime sends a DingTalk connection test only through the remembered private webhook', async () => {
const state = stateFixture();
const sends = [];
const client = {
@ -70,13 +70,13 @@ test('runtime sends a DingTalk connection test only through the remembered priva
code: 'test-target-unavailable',
});
rememberConnectionTestTarget(state, {
sessionWebhook: 'https://oapi.dingtalk.com/robot/reply?ticket=status-private',
sessionWebhook: 'https://oapi.dingtalk.com/robot/reply?ticket=inbound-private',
});
assert.deepEqual(await runtime.sendConnectionTest('连接测试'), { sent: true });
assert.equal(sends.length, 1);
assert.equal(sends[0].clientId, 'ding-client');
assert.equal(sends[0].clientSecret, 'host-secret');
assert.equal(sends[0].sessionWebhook, 'https://oapi.dingtalk.com/robot/reply?ticket=status-private');
assert.equal(sends[0].sessionWebhook, 'https://oapi.dingtalk.com/robot/reply?ticket=inbound-private');
assert.equal(sends[0].text, '连接测试');
await runtime.stop();
});

View file

@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
import { Readable } from 'node:stream';
import { FeishuHarnessBridge } from '../../../src/channels/feishu/bridge.mjs';
import { DEFAULT_IMAGE_PROMPT } from '../../../src/channels/shared/image-prompt.mjs';
import { connectionTestTarget } from '../../../src/channels/shared/connection-test.mjs';
const PNG_1X1 = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
@ -86,6 +87,54 @@ function textClient(sendText) {
};
}
test('Feishu remembers any authorized private inbound message as a connection-test target', async () => {
const groupFixture = stateFixture();
const groupBridge = new FeishuHarnessBridge({
client: textClient(async () => {}),
channel: {},
harness: { ensureRunning: async () => true },
state: groupFixture.state,
status: bridgeStatus(),
allowedSenderOpenIds: new Set(['ou_user']),
});
await groupBridge.accept(event('target-group', '/help', {
chat_type: 'group',
chat_id: 'oc_group',
}));
await groupBridge.waitForIdle();
assert.equal(connectionTestTarget(groupFixture.state), null);
const rejectedFixture = stateFixture();
const rejectedBridge = new FeishuHarnessBridge({
client: textClient(async () => {}),
channel: {},
harness: { ensureRunning: async () => true },
state: rejectedFixture.state,
status: bridgeStatus(),
allowedSenderOpenIds: new Set(['ou_owner']),
});
await rejectedBridge.accept(event('target-rejected', '/help', {
senderOpenId: 'ou_other',
}));
await rejectedBridge.waitForIdle();
assert.equal(connectionTestTarget(rejectedFixture.state), null);
const privateFixture = stateFixture();
const privateBridge = new FeishuHarnessBridge({
client: textClient(async () => {}),
channel: {},
harness: { ensureRunning: async () => true },
state: privateFixture.state,
status: bridgeStatus(),
allowedSenderOpenIds: new Set(['*']),
});
await privateBridge.accept(event('target-private', '/help', {
chat_id: 'oc_private',
}));
await privateBridge.waitForIdle();
assert.deepEqual(connectionTestTarget(privateFixture.state), { chatId: 'oc_private' });
});
test('Feishu executes /compact for the bound Session without prompting the model', async () => {
const fixture = stateFixture([['p2p:ou_user', 'session-compact']]);
const sent = [];

View file

@ -31,6 +31,8 @@ test('Feishu connection check requests and displays test-message feedback', asyn
import.meta.url,
), 'utf8');
assert.match(source, /FEISHU_ENDPOINTS\.reconnectBot, \{ botId, sendTest: true \}/);
assert.match(source, /机器人尚未收到可用于测试的私聊消息/);
assert.doesNotMatch(source, /请先私聊机器人发送 \/status/);
const markup = renderToStaticMarkup(React.createElement(BotCard, {
connection: {

View file

@ -1,6 +1,7 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { FeishuRuntime } from '../../../src/channels/feishu/feishu-runtime.mjs';
import { rememberConnectionTestTarget } from '../../../src/channels/shared/connection-test.mjs';
class FakeClient {
static instances = [];
@ -86,7 +87,7 @@ test('FeishuRuntime becomes chat-ready only after Harness and Feishu are connect
lark: fakeLark(),
appId: 'cli_test',
appSecret: 'secret',
ownerOpenId: 'ou_owner',
ownerOpenIds: ['*', 'ou_owner'],
harness: {
async ensureRunning(options) {
harnessChecks += 1;
@ -133,6 +134,40 @@ test('FeishuRuntime becomes chat-ready only after Harness and Feishu are connect
assert.equal(harnessSignal.aborted, true);
});
test('FeishuRuntime uses a remembered private target for wildcard-only manual bots', async () => {
const state = { hasSeen: () => false };
const runtime = new FeishuRuntime({
lark: fakeLark(),
appId: 'cli_manual',
appSecret: 'secret',
ownerOpenIds: ['*'],
harness: { async ensureRunning() {} },
state,
});
const starting = runtime.start();
await new Promise((resolve) => setImmediate(resolve));
FakeWSClient.instances[0].becomeReady();
await starting;
await assert.rejects(
runtime.sendConnectionTest('连接测试'),
(error) => error?.code === 'test-target-unavailable',
);
rememberConnectionTestTarget(state, { chatId: 'oc_manual_private' });
assert.deepEqual(await runtime.sendConnectionTest('连接测试'), { sent: true });
assert.deepEqual(FakeClient.sent, [{
params: { receive_id_type: 'chat_id' },
data: {
receive_id: 'oc_manual_private',
msg_type: 'text',
content: JSON.stringify({ text: '连接测试' }),
},
}]);
await runtime.stop();
});
test('FeishuRuntime fails closed when the initial WebSocket handshake times out', async () => {
const runtime = new FeishuRuntime({
lark: fakeLark(),

View file

@ -259,7 +259,7 @@ test('QQ executes /compact for the bound Session without prompting the model', a
assert.equal(fixture.seen.has('compact-qq'), true);
});
test('QQ remembers a connection-test target only for an authorized private /status', async () => {
test('QQ remembers any authorized private inbound as a connection-test target', async () => {
const fixture = stateFixture();
const sent = [];
const bridge = new QqHarnessBridge({
@ -270,32 +270,32 @@ test('QQ remembers a connection-test target only for an authorized private /stat
});
await bridge.accept(message({
messageId: 'status-rejected',
messageId: 'help-rejected',
senderId: 'other-openid',
content: '/status',
replyTarget: { scope: 'c2c', targetId: 'other-openid', msgId: 'status-rejected' },
content: '/help',
replyTarget: { scope: 'c2c', targetId: 'other-openid', msgId: 'help-rejected' },
}));
await bridge.accept(message({
kind: 'group',
rawEventType: 'GROUP_AT_MESSAGE_CREATE',
groupOpenid: 'group-1',
messageId: 'status-group',
content: '/status',
replyTarget: { scope: 'group', targetId: 'group-1', msgId: 'status-group' },
messageId: 'help-group',
content: '/help',
replyTarget: { scope: 'group', targetId: 'group-1', msgId: 'help-group' },
}));
assert.equal(connectionTestTarget(fixture.state), null);
const privateTarget = {
scope: 'c2c', targetId: 'owner-openid', msgId: 'status-private',
scope: 'c2c', targetId: 'owner-openid', msgId: 'help-private',
};
await bridge.accept(message({
messageId: 'status-private',
content: '/status',
messageId: 'help-private',
content: '/help',
replyTarget: privateTarget,
}));
assert.deepEqual(connectionTestTarget(fixture.state), privateTarget);
assert.equal(sent.filter(({ text }) => text.includes('连接正常')).length, 2);
assert.equal(sent.length, 2);
});
test('QQ private messages stream Harness snapshots and finalize once', async () => {

View file

@ -40,7 +40,10 @@ test('QQ client normalizes and presents connection-test outcomes', () => {
assert.deepEqual(unavailable.testMessage, {
sent: false, code: 'test-target-unavailable',
});
assert.match(connectionTestFeedback(unavailable.testMessage), /\/status/);
assert.equal(
connectionTestFeedback(unavailable.testMessage),
'连接检查完成。机器人尚未收到可用于测试的私聊消息。',
);
const invalid = normalizeSnapshot({
bots: [], testMessage: { sent: false, code: 'private-provider-error' },

View file

@ -100,7 +100,7 @@ test('QQ runtime sends a proactive connection test to the explicit owner fallbac
await runtime.stop();
});
test('QQ runtime requires /status for wildcard owners and strips the remembered reply id', async () => {
test('QQ runtime requires a remembered private target for wildcard owners and strips the reply id', async () => {
const bot = new FakeBot();
const state = {};
const runtime = new QqRuntime({
@ -119,7 +119,7 @@ test('QQ runtime requires /status for wildcard owners and strips the remembered
(error) => error?.code === 'test-target-unavailable',
);
rememberConnectionTestTarget(state, {
scope: 'c2c', targetId: 'recent-user', msgId: 'old-status-message',
scope: 'c2c', targetId: 'recent-user', msgId: 'old-inbound-message',
});
await runtime.sendConnectionTest('remembered-target');
assert.deepEqual(bot.sent, [{

View file

@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
import test from 'node:test';
import { DiscordHarnessBridge } from '../../../src/channels/discord/discord-bridge.mjs';
import { connectionTestTarget } from '../../../src/channels/shared/connection-test.mjs';
import { TextHarnessBridge } from '../../../src/channels/shared/text-harness-bridge.mjs';
import { SlackHarnessBridge } from '../../../src/channels/slack/slack-bridge.mjs';
import { TelegramHarnessBridge } from '../../../src/channels/telegram/telegram-bridge.mjs';
@ -149,6 +150,78 @@ test('all four shared text channels execute /compact outside the model prompt pa
}
});
test('Slack, Telegram, and Discord remember any valid direct message per bot', async () => {
for (const [name, Bridge] of [
['slack', SlackHarnessBridge],
['telegram', TelegramHarnessBridge],
['discord', DiscordHarnessBridge],
]) {
let sessionSequence = 0;
const harness = {
createSession: async () => `${name}-session-${++sessionSequence}`,
ask: async () => `${name} reply`,
};
const first = stateFixture();
const second = stateFixture();
const firstSent = [];
const secondSent = [];
const firstBot = {
sendText: async (target, text) => firstSent.push({ target, text }),
};
const secondBot = {
sendText: async (target, text) => secondSent.push({ target, text }),
};
const firstBridge = new Bridge({ bot: firstBot, harness, state: first.state });
const secondBridge = new Bridge({ bot: secondBot, harness, state: second.state });
const firstTarget = { channelId: `${name}-private-a` };
const secondTarget = { channelId: `${name}-private-b` };
await firstBridge.accept(message(`${name}-direct-a`, 'hello', {
conversationId: `${name}-private-a`,
replyTarget: { ...firstTarget, replyToMessageId: 'reply-a' },
connectionTestTarget: firstTarget,
}));
assert.deepEqual(connectionTestTarget(first.state), firstTarget, name);
await firstBridge.accept(message(`${name}-group`, 'hello group', {
kind: 'group',
conversationId: `${name}-group`,
addressed: true,
replyTarget: { channelId: `${name}-group` },
connectionTestTarget: { channelId: `${name}-group` },
}));
assert.deepEqual(connectionTestTarget(first.state), firstTarget, `${name} group`);
await firstBridge.accept(message(`${name}-direct-a`, 'duplicate', {
conversationId: `${name}-private-replay`,
replyTarget: { channelId: `${name}-private-replay` },
connectionTestTarget: { channelId: `${name}-private-replay` },
}));
assert.deepEqual(connectionTestTarget(first.state), firstTarget, `${name} duplicate`);
await secondBridge.accept(message(`${name}-direct-b`, 'hello', {
conversationId: `${name}-private-b`,
replyTarget: { ...secondTarget, replyToMessageId: 'reply-b' },
connectionTestTarget: secondTarget,
}));
assert.deepEqual(connectionTestTarget(second.state), secondTarget, `${name} second bot`);
const reconnectedFirstBridge = new Bridge({ bot: firstBot, harness, state: first.state });
await reconnectedFirstBridge.sendConnectionTest(`${name} first test`);
await secondBridge.sendConnectionTest(`${name} second test`);
assert.deepEqual(
firstSent.find(({ text }) => text === `${name} first test`)?.target,
firstTarget,
`${name} reconnect`,
);
assert.deepEqual(
secondSent.find(({ text }) => text === `${name} second test`)?.target,
secondTarget,
`${name} bot isolation`,
);
}
});
test('answers a multi-question interaction on the fast lane with canonical values', async () => {
const fixture = stateFixture();
const sent = [];

View file

@ -454,26 +454,15 @@ test('Telegram bridge ignores unaddressed groups and streams direct replies', as
assert.equal(askCount, 0);
await bridge.accept({
messageId: '2', senderId: 'u1', kind: 'direct', conversationId: 'u1', content: 'hello',
addressed: true, replyTarget: {},
addressed: true,
replyTarget: { chatId: 88, replyToMessageId: 7 },
connectionTestTarget: { chatId: 88 },
});
assert.equal(askCount, 1);
assert.deepEqual(updates, ['正在使用搜索…', '处理中']);
assert.deepEqual(sent, ['完成']);
await assert.rejects(
() => bridge.sendConnectionTest('card test'),
(error) => error?.code === 'test-target-unavailable',
);
const statusTarget = { chatId: 88, replyToMessageId: 7 };
await bridge.accept({
messageId: '3', senderId: 'u1', kind: 'direct', conversationId: 'u1', content: '/status',
addressed: true, replyTarget: statusTarget, connectionTestTarget: { chatId: 88 },
});
await bridge.sendConnectionTest('card test');
assert.deepEqual(sent.slice(-2), [
'Telegram机器人与 DeepSeek Harness 连接正常。',
'card test',
]);
assert.equal(sent.at(-1), 'card test');
assert.deepEqual(sentTargets.at(-1), { chatId: 88 });
const reconnectedBridge = new TelegramHarnessBridge({ bot, harness, state });
await reconnectedBridge.sendConnectionTest('after reconnect');

View file

@ -101,7 +101,7 @@ function questionInteraction({
};
}
test('Enterprise WeChat remembers only a private /status chat as a connection-test target', async () => {
test('Enterprise WeChat remembers any private inbound as a connection-test target', async () => {
const privateState = state();
const privateTransport = testClient();
const privateBridge = new WecomHarnessBridge({
@ -109,7 +109,7 @@ test('Enterprise WeChat remembers only a private /status chat as a connection-te
harness: { ensureRunning: async () => true },
state: privateState,
});
await privateBridge.accept(frame({ msgid: 'status-private', text: { content: '/status' } }));
await privateBridge.accept(frame({ msgid: 'help-private', text: { content: '/help' } }));
assert.deepEqual(connectionTestTarget(privateState), { chatId: 'member-1' });
const groupState = state();
@ -120,10 +120,10 @@ test('Enterprise WeChat remembers only a private /status chat as a connection-te
state: groupState,
});
await groupBridge.accept(frame({
msgid: 'status-group',
msgid: 'help-group',
chattype: 'group',
chatid: 'group-1',
text: { content: '@机器人 /status' },
text: { content: '@机器人 /help' },
}));
assert.equal(connectionTestTarget(groupState), null);
});

View file

@ -134,5 +134,7 @@ test('Enterprise WeChat connection feedback is scoped to the checked bot', async
test('Enterprise WeChat reconnect failure uses fixed translatable copy', async () => {
const source = await readFile(CLIENT_URL, 'utf8');
assert.match(source, /'连接检查失败,请稍后重试。'/);
assert.match(source, /'连接检查完成。机器人尚未收到可用于测试的私聊消息。'/);
assert.doesNotMatch(source, /请先私聊机器人发送 \/status/);
assert.doesNotMatch(source, /连接检查失败:\$\{presentError\(error\)\.message\}/);
});

View file

@ -21,7 +21,7 @@ class FakeClient extends EventEmitter {
async sendMessage(chatId, body) { this.sent.push({ chatId, body }); }
}
test('Enterprise WeChat runtime sends a connection test only to the remembered private status target', async () => {
test('Enterprise WeChat runtime sends a connection test only to the remembered private target', async () => {
const client = new FakeClient();
const state = {};
const runtime = new WecomRuntime({

View file

@ -127,5 +127,7 @@ test('Weixin connection feedback is scoped to the checked bot', async (t) => {
test('Weixin reconnect failure uses fixed translatable copy', async () => {
const source = await readFile(CLIENT_URL, 'utf8');
assert.match(source, /'连接检查失败,请稍后重试。'/);
assert.match(source, /'连接检查完成。机器人尚未收到可用于测试的私聊消息。'/);
assert.doesNotMatch(source, /请先私聊机器人发送 \/status/);
assert.doesNotMatch(source, /连接检查失败:\$\{presentError\(error\)\.message\}/);
});

View file

@ -69,7 +69,7 @@ const PNG_BYTES = Buffer.from([
0x01, 0x02, 0x03,
]);
test('Weixin remembers only the authorized owner /status as a connection-test target', async () => {
test('Weixin remembers any authorized private inbound as a connection-test target', async () => {
const fixture = stateFixture();
const sent = [];
const bridge = new WeixinHarnessBridge({
@ -81,20 +81,20 @@ test('Weixin remembers only the authorized owner /status as a connection-test ta
state: fixture.state,
});
await bridge.accept(message('status-owner', '/status'));
await bridge.accept(message('help-owner', '/help'));
assert.deepEqual(connectionTestTarget(fixture.state), { toUserId: 'owner-user' });
assert.equal(sent.at(-1).text, '微信与 DeepSeek Harness 连接正常。');
assert.match(sent.at(-1).text, /\/help/);
const rejectedFixture = stateFixture();
const rejectedBridge = new WeixinHarnessBridge({
api: { sendText: async () => assert.fail('unauthorized status must not be answered') },
api: { sendText: async () => assert.fail('unauthorized message must not be answered') },
baseUrl: 'https://ilinkai.weixin.qq.com/',
token: 'host-token',
ownerUserId: 'owner-user',
harness: { ensureRunning: async () => true },
state: rejectedFixture.state,
});
await rejectedBridge.accept(message('status-other', '/status', { from_user_id: 'other-user' }));
await rejectedBridge.accept(message('help-other', '/help', { from_user_id: 'other-user' }));
assert.equal(connectionTestTarget(rejectedFixture.state), null);
});

View file

@ -532,6 +532,50 @@ test('connection check requests a test message and shows its delivery result', a
await act(async () => { renderer.unmount(); });
});
test('shared token target-unavailable feedback asks for any direct message', async (t) => {
const previousWindow = globalThis.window;
globalThis.window = { setInterval() { return 1; }, clearInterval() {} };
setImTranslator((key) => en[key] ?? key);
t.after(() => {
setImTranslator(null);
if (previousWindow === undefined) delete globalThis.window;
else globalThis.window = previousWindow;
});
const snapshot = discordSnapshot('/workspace/current');
const rpcCall = async (endpoint) => {
if (endpoint === 'connection.status') return { ok: true, value: snapshot };
if (endpoint === 'bot.reconnect') {
return {
ok: true,
value: {
...snapshot,
testMessage: { sent: false, code: 'test-target-unavailable' },
},
};
}
throw new Error(`Unexpected endpoint: ${endpoint}`);
};
let renderer;
await act(async () => {
renderer = create(React.createElement(DiscordSettingsTab, { rpcCall }));
await flushMicrotasks();
});
const card = renderer.root.findByProps({ 'data-bot-id': 'discord_test' });
await act(async () => {
buttonNamed(card, 'Check connection').props.onClick();
await flushMicrotasks();
});
const notice = textOf(renderer.root.findByProps({ role: 'status' }));
assert.equal(
notice,
'Connection check completed. The bot has not received a direct message it can use for testing.',
);
assert.doesNotMatch(notice, /\/status|[\p{Script=Han}]/u);
await act(async () => { renderer.unmount(); });
});
test('shared token connection failures render a fixed English-safe notice', async (t) => {
const previousWindow = globalThis.window;
globalThis.window = { setInterval() { return 1; }, clearInterval() {} };