dsh-im-ops/test/channels/qq/runtime.test.mjs

134 lines
4.6 KiB
JavaScript

import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import test from 'node:test';
import { QqRuntime } from '../../../src/channels/qq/qq-runtime.mjs';
function deferred() {
let resolve;
const promise = new Promise((done) => { resolve = done; });
return { promise, resolve };
}
class FakeBot extends EventEmitter {
middlewares = [];
stopped = false;
use(value) { this.middlewares.push(value); }
async start(signal) {
queueMicrotask(() => this.emit('ready', {}));
await new Promise((resolve) => signal.addEventListener('abort', resolve, { once: true }));
}
stop() { this.stopped = true; }
async sendText() {}
}
test('QQ runtime waits for gateway ready, installs typing, and stops its client', async () => {
const bot = new FakeBot();
let botOptions;
const sdkLogs = [];
const runtime = new QqRuntime({
config: { botId: 'qq_bot', appId: 'app', ownerUserOpenid: 'owner' },
appSecret: 'secret',
harness: { ensureRunning: async () => true },
state: {},
createBot: (options) => {
botOptions = options;
return bot;
},
logger: {
debug: (...args) => sdkLogs.push(['debug', ...args]),
info: (...args) => sdkLogs.push(['info', ...args]),
},
typingMiddleware: (options) => ({ name: 'typing-middleware', options }),
connectTimeoutMs: 100,
});
const status = await runtime.start();
assert.equal(status.ready, true);
assert.equal(status.qqConnectionState, 'connected');
assert.equal(bot.middlewares[0].name, 'typing-middleware');
assert.equal(bot.middlewares[0].options.keepAlive, true);
assert.equal(bot.middlewares[0].options.predicate({ message: { senderId: 'owner' } }), true);
assert.equal(bot.middlewares[0].options.predicate({ message: { senderId: 'other' } }), false);
botOptions.logger.debug('raw gateway payload');
botOptions.logger.info('gateway ready');
assert.deepEqual(sdkLogs, [['info', 'gateway ready']]);
bot.emit('error', new Error('temporary disconnect'));
bot.emit('resumed');
assert.equal(runtime.status.ready, true);
assert.equal(runtime.status.lastError, null);
await runtime.stop();
assert.equal(bot.stopped, true);
assert.equal(runtime.status.ready, false);
});
test('QQ runtime never reports ready when the gateway does not emit ready', async () => {
const bot = new FakeBot();
bot.start = async (signal) => new Promise((resolve) => signal.addEventListener('abort', resolve, { once: true }));
const runtime = new QqRuntime({
config: { botId: 'qq_bot', appId: 'app', ownerUserOpenid: 'owner' },
appSecret: 'secret',
harness: { ensureRunning: async () => true },
state: {},
createBot: () => bot,
typingMiddleware: () => 'typing',
connectTimeoutMs: 5,
});
await assert.rejects(() => runtime.start(), /did not become ready/);
assert.equal(runtime.status.ready, false);
});
test('QQ runtime aborts an in-flight Harness interaction when stopped', async () => {
const bot = new FakeBot();
const askStarted = deferred();
let askSignal;
const runtime = new QqRuntime({
config: { botId: 'qq_bot', appId: 'app', ownerUserOpenid: 'owner' },
appSecret: 'secret',
harness: {
ensureRunning: async () => true,
sessionExists: async () => true,
ask: async (_sessionId, _text, options) => {
askSignal = options.signal;
askStarted.resolve();
await new Promise((_resolve, reject) => options.signal.addEventListener('abort', () => {
reject(options.signal.reason);
}, { once: true }));
},
},
state: {
hasSeen: () => false,
markSeen: async () => {},
sessionFor: () => 'session-existing',
setSession: async () => {},
clearSession: async () => {},
},
createBot: () => bot,
typingMiddleware: () => 'typing',
connectTimeoutMs: 100,
logger: { error() {}, warn() {}, info() {} },
});
await runtime.start();
bot.emit('message', {}, {
kind: 'c2c',
rawEventType: 'C2C_MESSAGE_CREATE',
senderId: 'owner',
senderIsBot: false,
content: '需要交互',
messageId: 'interaction-message',
replyTarget: { scope: 'c2c', targetId: 'owner', msgId: 'interaction-message' },
});
await askStarted.promise;
assert.equal(askSignal.aborted, false);
bot.emit('message', {}, {
kind: 'c2c',
rawEventType: 'C2C_MESSAGE_CREATE',
senderId: 'owner',
senderIsBot: false,
content: '排队中的下一条消息',
messageId: 'queued-message',
replyTarget: { scope: 'c2c', targetId: 'owner', msgId: 'queued-message' },
});
await runtime.stop();
assert.equal(askSignal.aborted, true);
});