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

@ -6,6 +6,7 @@ import {
wecomInboundMessage,
} from '../../../src/channels/wecom/wecom-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=',
@ -100,6 +101,33 @@ function questionInteraction({
};
}
test('Enterprise WeChat remembers only a private /status chat as a connection-test target', async () => {
const privateState = state();
const privateTransport = testClient();
const privateBridge = new WecomHarnessBridge({
client: privateTransport.client,
harness: { ensureRunning: async () => true },
state: privateState,
});
await privateBridge.accept(frame({ msgid: 'status-private', text: { content: '/status' } }));
assert.deepEqual(connectionTestTarget(privateState), { chatId: 'member-1' });
const groupState = state();
const groupTransport = testClient();
const groupBridge = new WecomHarnessBridge({
client: groupTransport.client,
harness: { ensureRunning: async () => true },
state: groupState,
});
await groupBridge.accept(frame({
msgid: 'status-group',
chattype: 'group',
chatid: 'group-1',
text: { content: '@机器人 /status' },
}));
assert.equal(connectionTestTarget(groupState), null);
});
test('Enterprise WeChat executes /compact for the bound Session without prompting the model', async () => {
const store = state();
const transport = testClient();

View file

@ -15,6 +15,7 @@ test('Enterprise WeChat client keeps only redacted bot and Host-rendered QR stat
});
assert.equal(provision.qrCodeDataUrl, qr);
const snapshot = normalizeSnapshot({
testMessage: { sent: false, code: 'test-target-unavailable', ignored: 'secret' },
bots: [{
botId: 'wecom_abc', connected: true, state: 'connected',
bot: { name: '企业微信机器人', appIdMasked: 'bot••••001' },
@ -23,4 +24,7 @@ test('Enterprise WeChat client keeps only redacted bot and Host-rendered QR stat
});
assert.equal(snapshot.totals.connected, 1);
assert.equal(snapshot.bots[0].bot.appIdMasked, 'bot••••001');
assert.deepEqual(snapshot.testMessage, {
sent: false, code: 'test-target-unavailable',
});
});

View file

@ -1,14 +1,44 @@
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 TestRenderer from 'react-test-renderer';
import {
AccountCard,
WecomSettingsTab,
} from '../../../plugin-src/client/channels/wecom/index.js';
const { act, create } = TestRenderer;
const CLIENT_URL = new URL('../../../plugin-src/client/channels/wecom/index.js', import.meta.url);
async function flushMicrotasks() {
for (let index = 0; index < 6; index += 1) await Promise.resolve();
}
function textOf(node) {
if (typeof node === 'string' || typeof node === 'number') return String(node);
return node?.children?.map(textOf).join('') ?? '';
}
function buttonNamed(root, name) {
return root.findAllByType('button').find((button) => textOf(button) === name);
}
function account(botId, name) {
return {
botId,
connected: true,
state: 'connected',
workspace: '/workspace/current',
bot: { name, appIdMasked: `${botId}•••` },
health: { summary: '企业微信 WebSocket 长连接运行正常', lastCheckedAt: Date.now() },
error: null,
};
}
test('Enterprise WeChat settings uses the shared compact channel toolbar', () => {
const markup = renderToStaticMarkup(React.createElement(WecomSettingsTab, {
rpcCall: async () => ({ ok: true, value: {} }),
@ -42,3 +72,67 @@ test('Enterprise WeChat bot cards match the shared two-metric card treatment', (
assert.match(markup, />检查连接<[^]*>移除接入</);
assert.doesNotMatch(markup, /收到\s*\/\s*回复|dim-cardSummary|企业微信 WebSocket 长连接运行正常/);
});
test('Enterprise WeChat card feedback stays visible without hiding connection errors', () => {
const markup = renderToStaticMarkup(React.createElement(AccountCard, {
account: {
...account('wecom_bot', '企业微信机器人'),
connected: false,
state: 'error',
error: { code: 'offline', message: '连接凭据已失效' },
},
feedback: '企业微信连接检查完成,测试消息已发送。',
onReconnect() {}, onRequestRemove() {}, onConfirmRemove() {}, onCancelRemove() {},
}));
assert.match(markup, />连接凭据已失效</);
assert.match(markup, /role="status"[^>]*>企业微信连接检查完成,测试消息已发送。</);
});
test('Enterprise WeChat connection feedback is scoped to the checked bot', async (t) => {
const previousWindow = globalThis.window;
globalThis.window = {
setInterval() { return 1; }, clearInterval() {},
setTimeout() { return 1; }, clearTimeout() {},
requestAnimationFrame(callback) { callback(); return 1; }, cancelAnimationFrame() {},
};
t.after(() => {
if (previousWindow === undefined) delete globalThis.window;
else globalThis.window = previousWindow;
});
const bots = [account('wecom_first', 'First Bot'), account('wecom_second', 'Second Bot')];
const calls = [];
const rpcCall = async (endpoint, payload) => {
if (endpoint === 'connection.status') return { ok: true, value: { bots } };
if (endpoint === 'bot.reconnect') {
calls.push(payload);
return { ok: true, value: { bots, testMessage: { sent: true } } };
}
throw new Error(`Unexpected endpoint: ${endpoint}`);
};
let renderer;
await act(async () => {
renderer = create(React.createElement(WecomSettingsTab, { rpcCall }));
await flushMicrotasks();
});
const first = renderer.root.findByProps({ 'data-bot-id': 'wecom_first' });
await act(async () => {
buttonNamed(first, '检查连接').props.onClick();
await flushMicrotasks();
});
const firstAfter = renderer.root.findByProps({ 'data-bot-id': 'wecom_first' });
const secondAfter = renderer.root.findByProps({ 'data-bot-id': 'wecom_second' });
assert.match(textOf(firstAfter), /测试消息已发送/);
assert.doesNotMatch(textOf(secondAfter), /测试消息已发送/);
assert.deepEqual(calls, [{ botId: 'wecom_first', sendTest: true }]);
await act(async () => { renderer.unmount(); });
});
test('Enterprise WeChat reconnect failure uses fixed translatable copy', async () => {
const source = await readFile(CLIENT_URL, 'utf8');
assert.match(source, /'连接检查失败,请稍后重试。'/);
assert.doesNotMatch(source, /连接检查失败:\$\{presentError\(error\)\.message\}/);
});

View file

@ -56,6 +56,7 @@ test('Enterprise WeChat Bot ID and Secret binding stores credentials and starts
const values = new Map();
const configs = [];
let runtimeArgs;
let connectionTestText;
const controller = new WecomController({
qrAuth: { start: async () => ({}), poll: async () => ({ status: 'waiting' }) },
credentials: {
@ -75,6 +76,7 @@ test('Enterprise WeChat Bot ID and Secret binding stores credentials and starts
return {
status: { ready: true, wecomConnectionState: 'connected', harnessReachable: true },
start: async () => {}, stop: async () => {},
sendConnectionTest: async (text) => { connectionTestText = text; },
};
},
});
@ -83,6 +85,9 @@ test('Enterprise WeChat Bot ID and Secret binding stores credentials and starts
assert.equal(values.get(configs[0].secretRef), 'manual-secret');
assert.equal(runtimeArgs.secret, 'manual-secret');
assert.doesNotMatch(JSON.stringify(status), /manual-secret|remote-manual|secretRef/);
await controller.sendConnectionTest(status.bots[0].botId);
assert.match(connectionTestText, /DeepSeek Harness 连接测试成功/);
assert.match(connectionTestText, /企业微信机器人(remote••••nual)/);
await controller.close();
});
@ -102,6 +107,7 @@ test('Enterprise WeChat RPC encodes the QR on Host and strips every authorizatio
cancelProvisioning: async () => ({}),
bindCredentials: async () => ({ bots: [], totals: { configured: 0, connected: 0 } }),
reconnectBot: async () => ({}),
sendConnectionTest: async () => ({ sent: true }),
deleteBot: async () => ({}),
}, { encodeQr: async () => 'data:image/png;base64,YWJjZA==' });
const result = await handler(WECOM_ENDPOINTS.beginProvisioning, { locale: 'zh-CN' });
@ -115,7 +121,8 @@ test('Enterprise WeChat credential RPC accepts official Bot ID fields and redact
const handler = createWecomRpcHandler({
status: () => ({ bots: [], totals: { configured: 0, connected: 0 } }),
startProvisioning: async () => ({}), registrationStatus: async () => null,
cancelProvisioning: async () => ({}), reconnectBot: async () => ({}), deleteBot: async () => ({}),
cancelProvisioning: async () => ({}), reconnectBot: async () => ({}),
sendConnectionTest: async () => ({ sent: true }), deleteBot: async () => ({}),
bindCredentials: async (payload) => {
received = payload;
return { bots: [], totals: { configured: 0, connected: 0 }, secret: payload.secret };
@ -129,3 +136,63 @@ test('Enterprise WeChat credential RPC accepts official Bot ID fields and redact
assert.doesNotMatch(JSON.stringify(result), /manual-secret|"secret"/);
assert.equal((await handler(WECOM_ENDPOINTS.bindCredentials, { botId: 'remote-manual' })).ok, false);
});
test('Enterprise WeChat reconnect optionally sends a test message without changing connection success', async () => {
const sent = [];
const base = {
status: () => ({ bots: [] }),
startProvisioning: async () => ({}), registrationStatus: async () => null,
cancelProvisioning: async () => ({}), bindCredentials: async () => ({}),
deleteBot: async () => ({}),
};
const connected = {
bots: [{ botId: 'wecom_bot', connected: true }],
totals: { configured: 1, connected: 1 },
};
const handler = createWecomRpcHandler({
...base,
reconnectBot: async () => connected,
sendConnectionTest: async (botId) => { sent.push(botId); },
});
const success = await handler(WECOM_ENDPOINTS.reconnectBot, {
botId: 'wecom_bot', sendTest: true,
});
assert.equal(success.ok, true);
assert.deepEqual(success.value.testMessage, { sent: true });
assert.deepEqual(sent, ['wecom_bot']);
const failedSend = await createWecomRpcHandler({
...base,
reconnectBot: async () => connected,
sendConnectionTest: async () => { throw new Error('provider rejected'); },
})(WECOM_ENDPOINTS.reconnectBot, { botId: 'wecom_bot', sendTest: true });
assert.equal(failedSend.ok, true);
assert.deepEqual(failedSend.value.testMessage, { sent: false, code: 'test-message-failed' });
let offlineSendCalled = false;
const offline = await createWecomRpcHandler({
...base,
reconnectBot: async () => ({
bots: [{ botId: 'wecom_bot', connected: false }],
totals: { configured: 1, connected: 0 },
}),
sendConnectionTest: async () => { offlineSendCalled = true; },
})(WECOM_ENDPOINTS.reconnectBot, { botId: 'wecom_bot', sendTest: true });
assert.equal(offline.ok, true);
assert.deepEqual(offline.value.testMessage, {
sent: false, code: 'test-target-unavailable',
});
assert.equal(offlineSendCalled, false);
const missingMethod = await createWecomRpcHandler({
...base,
reconnectBot: async () => connected,
})(WECOM_ENDPOINTS.reconnectBot, { botId: 'wecom_bot', sendTest: true });
assert.equal(missingMethod.ok, true);
assert.deepEqual(missingMethod.value.testMessage, {
sent: false, code: 'test-target-unavailable',
});
assert.equal((await handler(WECOM_ENDPOINTS.reconnectBot, {
botId: 'wecom_bot', sendTest: false,
})).ok, false);
});

View file

@ -3,6 +3,7 @@ import { EventEmitter } from 'node:events';
import test from 'node:test';
import { WecomRuntime } from '../../../src/channels/wecom/wecom-runtime.mjs';
import { rememberConnectionTestTarget } from '../../../src/channels/shared/connection-test.mjs';
function deferred() {
let resolve;
@ -12,13 +13,38 @@ function deferred() {
class FakeClient extends EventEmitter {
disconnected = false;
sent = [];
connect() { queueMicrotask(() => this.emit('authenticated')); }
disconnect() { this.disconnected = true; }
async replyStream() {}
async replyStreamNonBlocking() {}
async sendMessage() {}
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 () => {
const client = new FakeClient();
const state = {};
const runtime = new WecomRuntime({
config: { botId: 'wecom_bot', remoteBotId: 'remote-bot' },
secret: 'private-secret',
harness: { ensureRunning: async () => true },
state,
createClient: () => client,
connectTimeoutMs: 100,
});
await runtime.start();
await assert.rejects(() => runtime.sendConnectionTest('测试'), {
code: 'test-target-unavailable',
});
rememberConnectionTestTarget(state, { chatId: 'member-private' });
assert.deepEqual(await runtime.sendConnectionTest('测试'), { sent: true });
assert.deepEqual(client.sent, [{
chatId: 'member-private',
body: { msgtype: 'markdown', markdown: { content: '测试' } },
}]);
await runtime.stop();
});
test('Enterprise WeChat runtime waits for authentication, suppresses SDK payload logs, and reconnects', async () => {
const client = new FakeClient();
let options;