feat: send test messages during bot connection checks

This commit is contained in:
xmanrui 2026-08-19 03:52:52 +08:00
parent 94da24538f
commit fb61c4235f
78 changed files with 3460 additions and 210 deletions

View file

@ -0,0 +1,124 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { act, create } from 'react-test-renderer';
import { FEISHU_ENDPOINTS } from '../../../plugin-src/client/channels/feishu/api.js';
import {
BotCard,
FeishuSettingsTab,
} from '../../../plugin-src/client/channels/feishu/index.js';
import {
en,
setImTranslator,
} from '../../../plugin-src/client/i18n.js';
const flushMicrotasks = () => new Promise((resolve) => setImmediate(resolve));
function textOf(node) {
if (typeof node === 'string' || typeof node === 'number') return String(node);
if (!node) return '';
const children = node.children ?? node.props?.children ?? [];
return (Array.isArray(children) ? children : [children]).map(textOf).join('');
}
test('Feishu connection check requests and displays test-message feedback', async () => {
const source = await readFile(new URL(
'../../../plugin-src/client/channels/feishu/index.js',
import.meta.url,
), 'utf8');
assert.match(source, /FEISHU_ENDPOINTS\.reconnectBot, \{ botId, sendTest: true \}/);
const markup = renderToStaticMarkup(React.createElement(BotCard, {
connection: {
botId: 'bot-feishu-test',
state: 'connected',
connected: true,
bot: { name: '飞书测试机器人', appIdMasked: 'cli_test••••1234' },
health: { summary: '长连接运行正常', lastCheckedAt: Date.now() },
},
testNotice: '测试消息已发送,请到飞书会话中确认。',
onReconnect() {},
onRequestRemove() {},
onConfirmRemove() {},
onCancelRemove() {},
}));
assert.match(markup, /role="status"[^>]*>测试消息已发送/);
});
test('Feishu reconnect failures render fixed English-safe feedback', async (t) => {
const previousWindow = globalThis.window;
let nextFrame = 0;
const frames = new Map();
globalThis.window = {
setInterval() { return 1; },
clearInterval() {},
requestAnimationFrame(callback) {
const id = ++nextFrame;
frames.set(id, callback);
queueMicrotask(() => {
const pending = frames.get(id);
if (!pending) return;
frames.delete(id);
pending();
});
return id;
},
cancelAnimationFrame(id) { frames.delete(id); },
};
setImTranslator((key) => en[key] ?? key);
t.after(() => {
setImTranslator(null);
if (previousWindow === undefined) delete globalThis.window;
else globalThis.window = previousWindow;
});
const snapshot = {
schemaVersion: 2,
revision: 1,
state: 'connected',
bots: [{
botId: 'bot_feishu_test',
state: 'connected',
connected: true,
configured: true,
workspace: '/workspace/current',
bot: { name: '今天是牢梁', appIdMasked: 'cli_test••••1234' },
health: { status: 'healthy', summary: 'Long connection is healthy' },
}],
};
const rpcCall = async (endpoint) => {
if (endpoint === FEISHU_ENDPOINTS.status) return { ok: true, value: snapshot };
if (endpoint === FEISHU_ENDPOINTS.reconnectBot) {
return {
ok: false,
error: { code: 'FEISHU_UPSTREAM_FAILED', message: '飞书上游操作失败' },
};
}
throw new Error(`Unexpected endpoint: ${endpoint}`);
};
let renderer;
await act(async () => {
renderer = create(React.createElement(FeishuSettingsTab, { rpcCall }));
await flushMicrotasks();
});
const card = renderer.root.findByProps({ 'data-bot-id': 'bot_feishu_test' });
await act(async () => {
card.findAllByType('button')
.find((button) => textOf(button) === 'Check connection').props.onClick();
await flushMicrotasks();
});
const announcement = renderer.root.find(
(node) => node.props.role === 'status' && node.props['aria-live'] === 'polite',
);
assert.equal(textOf(announcement), 'Connection check failed. Try again later.');
assert.doesNotMatch(textOf(announcement), /[\p{Script=Han}]/u);
assert.match(textOf(card), /Connection check failed\. Try again later\./);
assert.doesNotMatch(textOf(card), /飞书上游操作失败/);
await act(async () => { renderer.unmount(); });
});

View file

@ -4,9 +4,20 @@ import { FeishuRuntime } from '../../../src/channels/feishu/feishu-runtime.mjs';
class FakeClient {
static instances = [];
static sent = [];
constructor(options) {
this.options = options;
this.im = {
v1: {
message: {
create: async (payload) => {
FakeClient.sent.push(payload);
return { code: 0, data: { message_id: `message-${FakeClient.sent.length}` } };
},
},
},
};
FakeClient.instances.push(this);
}
}
@ -48,6 +59,7 @@ class FakeWSClient {
function fakeLark() {
FakeWSClient.instances.length = 0;
FakeClient.instances.length = 0;
FakeClient.sent.length = 0;
return {
Domain: { Feishu: 'feishu-domain', Lark: 'lark-domain' },
LoggerLevel: { info: 'info' },
@ -104,6 +116,16 @@ test('FeishuRuntime becomes chat-ready only after Harness and Feishu are connect
url: 'https://open.feishu.cn/test',
})).timeout, 15_000);
assert.deepEqual(await runtime.sendConnectionTest('连接测试'), { sent: true });
assert.deepEqual(FakeClient.sent, [{
params: { receive_id_type: 'open_id' },
data: {
receive_id: 'ou_owner',
msg_type: 'text',
content: JSON.stringify({ text: '连接测试' }),
},
}]);
const stopped = await runtime.stop();
assert.equal(stopped.ready, false);
assert.equal(stopped.feishuLongConnectionState, 'idle');

View file

@ -100,6 +100,7 @@ function fixture({
appSecret,
starts: 0,
stops: 0,
sentTests: [],
get status() { return structuredClone(status); },
async start() {
runtime.starts += 1;
@ -113,6 +114,10 @@ function fixture({
status.ready = false;
status.feishuLongConnectionState = 'idle';
},
async sendConnectionTest(text) {
runtime.sentTests.push(text);
return { sent: true };
},
};
const history = runtimes.get(botId) ?? [];
history.push(runtime);
@ -197,6 +202,22 @@ test('repeated initialization never restarts an already healthy bot', async () =
assert.equal(fx.runtimes.get(healthy.id)[0].stops, 0);
});
test('connection test uses the selected bot runtime and shared message copy', async () => {
const healthy = bot('bot_healthy', 'healthy');
healthy.appId = 'cli_healthy_1234567890';
const fx = fixture({
bots: [healthy],
secrets: { [healthy.secretRef]: 'healthy-secret' },
});
await fx.controller.initialize();
assert.deepEqual(await fx.controller.sendConnectionTest(healthy.id), { sent: true });
assert.deepEqual(fx.runtimes.get(healthy.id)[0].sentTests, [
'✅ DeepSeek Harness 连接测试成功\n这条消息由插件页面中的“机器人 healthy(cli_heal••••7890)”机器人卡片发出。',
]);
await fx.controller.close();
});
test('multiple scans create independent bots, credential refs and runtimes', async () => {
const fx = fixture({ createBotIds: ['bot_alpha', 'bot_beta'] });
const alpha = completeScan(fx, {

View file

@ -398,22 +398,27 @@ test('multi-bot RPC operations require a botId and never expose host-only fields
cancelRegistration: async () => multi,
disconnect: async () => status(),
reconnectBot: async (botId) => { calls.push(['reconnect', botId]); return multi; },
sendConnectionTest: async (botId) => { calls.push(['test-message', botId]); },
disconnectBot: async (botId) => { calls.push(['disconnect', botId]); return multi; },
deleteBot: async (botId) => { calls.push(['delete', botId]); return status({ schemaVersion: 2, bots: [] }); },
};
const fx = await rpcFixture(controller);
for (const [endpoint, payload] of [
[FEISHU_MULTI_ENDPOINTS.reconnectBot, { botId: 'bot_safe' }],
[FEISHU_MULTI_ENDPOINTS.reconnectBot, { botId: 'bot_safe', sendTest: true }],
[FEISHU_MULTI_ENDPOINTS.disconnectBot, { botId: 'bot_safe' }],
[FEISHU_MULTI_ENDPOINTS.deleteBot, { botId: 'bot_safe', confirm: true }],
]) {
const result = await fx.registration.handler(endpoint, payload, signal());
assert.equal(result.ok, true);
assert.doesNotMatch(JSON.stringify(result), /DSH_FEISHU_APP_SECRET|ou_private|cli_raw_private|private-token/);
if (endpoint === FEISHU_MULTI_ENDPOINTS.reconnectBot) {
assert.deepEqual(result.value.testMessage, { sent: true });
}
}
assert.deepEqual(calls, [
['reconnect', 'bot_safe'],
['test-message', 'bot_safe'],
['disconnect', 'bot_safe'],
['delete', 'bot_safe'],
]);
@ -428,6 +433,105 @@ test('multi-bot RPC operations require a botId and never expose host-only fields
assert.doesNotMatch(JSON.stringify(invalid), /\.\.\/private/);
});
test('Feishu reconnect test delivery is best-effort and requires the target bot to be connected', async () => {
let connected = true;
let sendCalls = 0;
const snapshot = () => status({
schemaVersion: 2,
revision: 1,
configured: true,
bots: [{
botId: 'bot_safe',
phase: connected ? 'connected' : 'disconnected',
connected,
configured: true,
bot: { name: '安全机器人', appIdMasked: 'cli_safe••••1234', domain: 'feishu' },
connection: {
ready: connected,
feishuLongConnectionState: connected ? 'connected' : 'idle',
harnessReachable: connected,
},
}],
});
const controller = {
status: async () => snapshot(),
startRegistration: async () => snapshot(),
cancelRegistration: async () => snapshot(),
disconnect: async () => status(),
reconnectBot: async () => snapshot(),
sendConnectionTest: async () => {
sendCalls += 1;
throw new Error('private Feishu provider failure');
},
};
const fx = await rpcFixture(controller);
const failedSend = await fx.registration.handler(
FEISHU_MULTI_ENDPOINTS.reconnectBot,
{ botId: 'bot_safe', sendTest: true },
signal(),
);
assert.equal(failedSend.ok, true);
assert.deepEqual(failedSend.value.testMessage, {
sent: false,
code: 'test-message-failed',
});
assert.doesNotMatch(JSON.stringify(failedSend), /private Feishu provider failure/);
assert.equal(sendCalls, 1);
connected = false;
const unavailable = await fx.registration.handler(
FEISHU_MULTI_ENDPOINTS.reconnectBot,
{ botId: 'bot_safe', sendTest: true },
signal(),
);
assert.equal(unavailable.ok, true);
assert.deepEqual(unavailable.value.testMessage, {
sent: false,
code: 'test-target-unavailable',
});
assert.equal(sendCalls, 1);
const invalid = await fx.registration.handler(
FEISHU_MULTI_ENDPOINTS.reconnectBot,
{ botId: 'bot_safe', sendTest: 'yes' },
signal(),
);
assert.equal(invalid.ok, false);
assert.equal(invalid.error.code, 'bad-request');
await fx.dispose();
});
test('Feishu RPC never sends a connection test after reconnect is cancelled', async () => {
let resolveReconnect;
let sendCalls = 0;
const reconnect = new Promise((resolve) => { resolveReconnect = resolve; });
const controller = {
status: async () => status(),
startRegistration: async () => status(),
cancelRegistration: async () => status(),
disconnect: async () => status(),
reconnectBot: async () => reconnect,
sendConnectionTest: async () => { sendCalls += 1; },
};
const fx = await rpcFixture(controller);
const abort = new AbortController();
const result = fx.registration.handler(FEISHU_MULTI_ENDPOINTS.reconnectBot, {
botId: 'bot_safe',
sendTest: true,
}, abort.signal);
abort.abort();
resolveReconnect(status({
schemaVersion: 2,
bots: [{ botId: 'bot_safe', connected: true }],
}));
assert.equal((await result).error.code, 'cancelled');
assert.equal(sendCalls, 0);
await fx.dispose();
});
test('dependency failures and AbortSignal use valid RpcResult error branches', async () => {
const secret = 'should-never-be-reflected';
const controller = {