mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-16 22:20:51 +08:00
feat: send test messages during bot connection checks
This commit is contained in:
parent
94da24538f
commit
fb61c4235f
78 changed files with 3460 additions and 210 deletions
|
|
@ -1,15 +1,30 @@
|
|||
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 {
|
||||
EmptyView,
|
||||
ProvisionView,
|
||||
QrPanel,
|
||||
WhatsappAccountCard,
|
||||
WhatsappSettingsTab,
|
||||
} from '../../../plugin-src/client/channels/whatsapp/index.js';
|
||||
import { en, setImTranslator } from '../../../plugin-src/client/i18n.js';
|
||||
|
||||
const { act, create } = TestRenderer;
|
||||
|
||||
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('') ?? '';
|
||||
}
|
||||
|
||||
test('WhatsApp onboarding is QR-only with no Cloud API credential form', () => {
|
||||
const empty = renderToStaticMarkup(React.createElement(EmptyView, {}));
|
||||
|
|
@ -48,10 +63,78 @@ test('WhatsApp account card uses the unified compact channel layout', () => {
|
|||
health: { summary: 'WhatsApp Web 关联设备运行正常', lastCheckedAt: Date.now() },
|
||||
error: null,
|
||||
},
|
||||
testNotice: '测试消息已发送,请到 WhatsApp 自聊会话中确认。',
|
||||
}));
|
||||
assert.match(markup, /data-im-channel-logo="whatsapp"/);
|
||||
assert.match(markup, /WhatsApp Web/);
|
||||
assert.match(markup, /检查连接/);
|
||||
assert.match(markup, /移除接入/);
|
||||
assert.match(markup, /role="status"[^>]*>测试消息已发送/);
|
||||
assert.equal((markup.match(/class="ddt-metric dim-botMetric"/g) ?? []).length, 2);
|
||||
});
|
||||
|
||||
test('WhatsApp connection check requests a test message from the existing reconnect endpoint', async () => {
|
||||
const source = await readFile(new URL(
|
||||
'../../../plugin-src/client/channels/whatsapp/index.js',
|
||||
import.meta.url,
|
||||
), 'utf8');
|
||||
assert.match(source, /WHATSAPP_ENDPOINTS\.reconnectBot,[\s\S]*\{ botId: account\.botId, sendTest: true \}/);
|
||||
assert.match(source, /\[account\.botId\]: '连接检查失败,请稍后重试。'/);
|
||||
assert.doesNotMatch(source, /连接检查失败:\$\{presentError\(error\)\.message\}/);
|
||||
});
|
||||
|
||||
test('WhatsApp reconnect failures render a fixed English-safe notice', async (t) => {
|
||||
const previousWindow = globalThis.window;
|
||||
globalThis.window = {
|
||||
setInterval() { return 1; },
|
||||
clearInterval() {},
|
||||
setTimeout() { return 1; },
|
||||
clearTimeout() {},
|
||||
};
|
||||
setImTranslator((key) => en[key] ?? key);
|
||||
t.after(() => {
|
||||
setImTranslator(null);
|
||||
if (previousWindow === undefined) delete globalThis.window;
|
||||
else globalThis.window = previousWindow;
|
||||
});
|
||||
const snapshot = {
|
||||
schemaVersion: 1,
|
||||
bots: [{
|
||||
botId: 'whatsapp_test',
|
||||
state: 'connected',
|
||||
connected: true,
|
||||
workspace: '/workspace/current',
|
||||
bot: { name: 'Harness WhatsApp', idMasked: '1650••••0123' },
|
||||
health: { summary: 'WhatsApp Web is healthy', lastCheckedAt: Date.now() },
|
||||
error: null,
|
||||
}],
|
||||
totals: { configured: 1, connected: 1 },
|
||||
};
|
||||
const rpcCall = async (endpoint) => {
|
||||
if (endpoint === 'connection.status') return { ok: true, value: snapshot };
|
||||
if (endpoint === 'bot.reconnect') {
|
||||
return {
|
||||
ok: false,
|
||||
error: { code: 'whatsapp-operation-failed', message: 'WhatsApp 操作失败,请稍后重试。' },
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected endpoint: ${endpoint}`);
|
||||
};
|
||||
|
||||
let renderer;
|
||||
await act(async () => {
|
||||
renderer = create(React.createElement(WhatsappSettingsTab, { rpcCall }));
|
||||
await flushMicrotasks();
|
||||
});
|
||||
const card = renderer.root.findByProps({ 'data-bot-id': 'whatsapp_test' });
|
||||
await act(async () => {
|
||||
card.findAllByType('button')
|
||||
.find((button) => textOf(button) === 'Check connection').props.onClick();
|
||||
await flushMicrotasks();
|
||||
});
|
||||
|
||||
const notice = textOf(renderer.root.findByProps({ role: 'status' }));
|
||||
assert.equal(notice, 'Connection check failed. Try again later.');
|
||||
assert.doesNotMatch(notice, /[\p{Script=Han}]/u);
|
||||
await act(async () => { renderer.unmount(); });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -426,6 +426,193 @@ test('WhatsApp runtime answers self-chat without processing its own reply echo',
|
|||
await runtime.stop();
|
||||
});
|
||||
|
||||
test('WhatsApp runtime sends a connection test to self and suppresses its outbound echo', async () => {
|
||||
let callbacks;
|
||||
let askCount = 0;
|
||||
const sent = [];
|
||||
const socket = {
|
||||
sendPresenceUpdate: async () => {},
|
||||
readMessages: async () => {},
|
||||
sendMessage: async (jid, content) => {
|
||||
sent.push([jid, content]);
|
||||
return { key: { id: 'connection-test-1' } };
|
||||
},
|
||||
};
|
||||
const runtime = new WhatsappRuntime({
|
||||
config: linkedConfig(),
|
||||
authDir: '/tmp/test-whatsapp-connection-test',
|
||||
harness: {
|
||||
ensureRunning: async () => {},
|
||||
sessionExists: async () => true,
|
||||
ask: async () => { askCount += 1; return 'unexpected'; },
|
||||
},
|
||||
state: {
|
||||
hasSeen: () => false,
|
||||
markSeen: async () => {},
|
||||
sessionFor: () => 'session-connection-test',
|
||||
sessionExists: async () => true,
|
||||
},
|
||||
createSession: async (options) => {
|
||||
callbacks = options;
|
||||
return {
|
||||
socket,
|
||||
ready: Promise.resolve({ accountJid: ACCOUNT_JID, name: 'Harness WhatsApp' }),
|
||||
close: async () => {},
|
||||
logout: async () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
await runtime.start();
|
||||
assert.deepEqual(await runtime.sendConnectionTest('连接测试'), { sent: true });
|
||||
assert.deepEqual(sent, [[ACCOUNT_JID, { text: '连接测试' }]]);
|
||||
await callbacks.onMessage({
|
||||
key: { remoteJid: ACCOUNT_JID, id: 'connection-test-1', fromMe: true },
|
||||
message: { conversation: '连接测试' },
|
||||
});
|
||||
assert.equal(askCount, 0);
|
||||
await runtime.stop();
|
||||
});
|
||||
|
||||
test('WhatsApp controller delegates connection test copy to the current runtime', async (t) => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-im-whatsapp-test-message-'));
|
||||
const configStore = await new WhatsappConfigStore(join(root, 'config.json')).load();
|
||||
const config = await configStore.save(linkedConfig());
|
||||
const sent = [];
|
||||
const controller = new WhatsappController({
|
||||
configStore,
|
||||
authPath: (name) => join(root, 'auth', name),
|
||||
createSession: async () => { throw new Error('not used'); },
|
||||
createRuntime: async () => ({
|
||||
status: {
|
||||
ready: true,
|
||||
connectionState: 'connected',
|
||||
harnessReachable: true,
|
||||
},
|
||||
start: async () => {},
|
||||
stop: async () => {},
|
||||
sendConnectionTest: async (text) => {
|
||||
sent.push(text);
|
||||
return { sent: true };
|
||||
},
|
||||
}),
|
||||
});
|
||||
t.after(() => controller.close());
|
||||
|
||||
await controller.initialize();
|
||||
assert.deepEqual(await controller.sendConnectionTest(config.botId), { sent: true });
|
||||
assert.deepEqual(sent, [
|
||||
'✅ DeepSeek Harness 连接测试成功\n这条消息由插件页面中的“Harness WhatsApp(1650••••0123)”机器人卡片发出。',
|
||||
]);
|
||||
});
|
||||
|
||||
test('WhatsApp reconnect RPC sends tests only for the connected target and keeps failures non-fatal', async () => {
|
||||
const botId = deriveWhatsappBotId(ACCOUNT_JID);
|
||||
let connected = true;
|
||||
let sendFailure = false;
|
||||
let sendCalls = 0;
|
||||
const snapshot = () => ({
|
||||
schemaVersion: 1,
|
||||
revision: 1,
|
||||
bots: [{
|
||||
botId,
|
||||
state: connected ? 'connected' : 'offline',
|
||||
connected,
|
||||
configured: true,
|
||||
bot: { name: 'Harness WhatsApp', idMasked: '1650••••0123' },
|
||||
health: { summary: 'status', lastCheckedAt: Date.now() },
|
||||
}],
|
||||
totals: { configured: 1, connected: connected ? 1 : 0 },
|
||||
});
|
||||
const controller = {
|
||||
status: async () => snapshot(),
|
||||
startProvisioning: async () => null,
|
||||
registrationStatus: async () => null,
|
||||
cancelProvisioning: async () => null,
|
||||
reconnectBot: async () => snapshot(),
|
||||
deleteBot: async () => snapshot(),
|
||||
sendConnectionTest: async () => {
|
||||
sendCalls += 1;
|
||||
if (sendFailure) throw new Error('private provider failure');
|
||||
return { sent: true };
|
||||
},
|
||||
};
|
||||
const handler = createWhatsappRpcHandler(controller);
|
||||
|
||||
const legacy = await handler(WHATSAPP_ENDPOINTS.reconnectBot, { botId });
|
||||
assert.equal(legacy.ok, true);
|
||||
assert.equal('testMessage' in legacy.value, false);
|
||||
assert.equal(sendCalls, 0);
|
||||
|
||||
const success = await handler(
|
||||
WHATSAPP_ENDPOINTS.reconnectBot,
|
||||
{ botId, sendTest: true },
|
||||
);
|
||||
assert.deepEqual(success.value.testMessage, { sent: true });
|
||||
assert.equal(sendCalls, 1);
|
||||
|
||||
sendFailure = true;
|
||||
const failedSend = await handler(
|
||||
WHATSAPP_ENDPOINTS.reconnectBot,
|
||||
{ botId, sendTest: true },
|
||||
);
|
||||
assert.equal(failedSend.ok, true);
|
||||
assert.deepEqual(failedSend.value.testMessage, {
|
||||
sent: false,
|
||||
code: 'test-message-failed',
|
||||
});
|
||||
assert.doesNotMatch(JSON.stringify(failedSend), /private provider failure/);
|
||||
|
||||
connected = false;
|
||||
const unavailable = await handler(
|
||||
WHATSAPP_ENDPOINTS.reconnectBot,
|
||||
{ botId, sendTest: true },
|
||||
);
|
||||
assert.equal(unavailable.ok, true);
|
||||
assert.deepEqual(unavailable.value.testMessage, {
|
||||
sent: false,
|
||||
code: 'test-target-unavailable',
|
||||
});
|
||||
assert.equal(sendCalls, 2);
|
||||
|
||||
const invalid = await handler(
|
||||
WHATSAPP_ENDPOINTS.reconnectBot,
|
||||
{ botId, sendTest: 'yes' },
|
||||
);
|
||||
assert.equal(invalid.ok, false);
|
||||
assert.equal(invalid.error.code, 'bad-request');
|
||||
});
|
||||
|
||||
test('WhatsApp RPC never sends a connection test after reconnect is cancelled', async () => {
|
||||
let resolveReconnect;
|
||||
let sendCalls = 0;
|
||||
const reconnect = new Promise((resolve) => { resolveReconnect = resolve; });
|
||||
const botId = deriveWhatsappBotId(ACCOUNT_JID);
|
||||
const controller = {
|
||||
status: async () => ({ bots: [] }),
|
||||
startProvisioning: async () => null,
|
||||
registrationStatus: async () => null,
|
||||
cancelProvisioning: async () => null,
|
||||
reconnectBot: async () => reconnect,
|
||||
sendConnectionTest: async () => { sendCalls += 1; },
|
||||
deleteBot: async () => ({ bots: [] }),
|
||||
};
|
||||
const abort = new AbortController();
|
||||
const result = createWhatsappRpcHandler(controller)(WHATSAPP_ENDPOINTS.reconnectBot, {
|
||||
botId,
|
||||
sendTest: true,
|
||||
}, abort.signal);
|
||||
|
||||
abort.abort();
|
||||
resolveReconnect({ bots: [{ botId, connected: true }] });
|
||||
|
||||
assert.deepEqual(await result, {
|
||||
ok: false,
|
||||
error: { code: 'cancelled', message: 'The request was cancelled.' },
|
||||
});
|
||||
assert.equal(sendCalls, 0);
|
||||
});
|
||||
|
||||
test('WhatsApp QR controller and RPC keep the raw QR and linked identity host-only', async (t) => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-im-whatsapp-controller-'));
|
||||
const configStore = await new WhatsappConfigStore(join(root, 'config.json')).load();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue