mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 04:13:17 +08:00
207 lines
8.6 KiB
JavaScript
207 lines
8.6 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
|
|
import {
|
|
BATCH_INPUT_LIMIT,
|
|
BatchInputManager,
|
|
batchInputBusyMessage,
|
|
batchInputGroupUnsupportedMessage,
|
|
isBatchInputCommand,
|
|
} from '../src/channels/shared/batch-input.mjs';
|
|
|
|
test('batch commands are reserved case-insensitively, including malformed argument forms', () => {
|
|
for (const text of [
|
|
'/batch', ' /BATCH ', '/batch now', '/send', '/SEND later', '/cancel', '/cancel all',
|
|
]) {
|
|
assert.equal(isBatchInputCommand(text), true, text);
|
|
}
|
|
for (const text of [null, '', 'batch', '/batching', '/sender', '/cancellation', 'hello /batch']) {
|
|
assert.equal(isBatchInputCommand(text), false, String(text));
|
|
}
|
|
});
|
|
|
|
test('shared boundary messages cover unsupported groups and busy conversations', () => {
|
|
assert.match(batchInputGroupUnsupportedMessage(), /仅支持私聊/);
|
|
assert.match(batchInputBusyMessage(), /\/stop.*\/batch/s);
|
|
});
|
|
|
|
test('idle conversations pass through ordinary messages and handle the three commands', () => {
|
|
const batches = new BatchInputManager();
|
|
|
|
assert.deepEqual(batches.handle('direct:one', 'normal message'), { handled: false });
|
|
assert.equal(batches.status('direct:one').phase, 'idle');
|
|
|
|
const missingSend = batches.handle('direct:one', '/send');
|
|
assert.equal(missingSend.handled, true);
|
|
assert.equal(missingSend.kind, 'no-batch');
|
|
assert.match(missingSend.message, /没有待提交/);
|
|
|
|
const missingCancel = batches.handle('direct:one', '/cancel');
|
|
assert.equal(missingCancel.kind, 'no-batch');
|
|
assert.match(missingCancel.message, /没有正在进行/);
|
|
|
|
const started = batches.handle('direct:one', ' /BATCH ');
|
|
assert.equal(started.kind, 'started');
|
|
assert.equal(started.count, 0);
|
|
assert.equal(started.limit, BATCH_INPUT_LIMIT);
|
|
assert.match(started.message, /最多可发送 10 条文字/);
|
|
assert.deepEqual(batches.status('direct:one'), {
|
|
phase: 'collecting', count: 0, limit: 10, full: false,
|
|
});
|
|
});
|
|
|
|
test('malformed batch commands are handled without changing state', () => {
|
|
const batches = new BatchInputManager();
|
|
|
|
for (const command of ['/batch now', '/send now', '/cancel now']) {
|
|
const response = batches.handle('direct:one', command);
|
|
assert.equal(response.kind, 'invalid-command');
|
|
assert.match(response.message, /不带参数/);
|
|
assert.equal(batches.status('direct:one').phase, 'idle');
|
|
}
|
|
|
|
batches.handle('direct:one', '/batch');
|
|
batches.handle('direct:one', 'first');
|
|
const invalid = batches.handle('direct:one', '/send now');
|
|
assert.equal(invalid.kind, 'invalid-command');
|
|
assert.equal(batches.status('direct:one').count, 1);
|
|
});
|
|
|
|
test('collecting is isolated by conversation and repeated /batch only reports progress', () => {
|
|
const batches = new BatchInputManager();
|
|
batches.handle('direct:one', '/batch');
|
|
|
|
assert.deepEqual(batches.handle('direct:two', 'ordinary elsewhere'), { handled: false });
|
|
assert.equal(batches.handle('direct:one', 'first').kind, 'collected');
|
|
assert.equal(batches.handle('direct:one', 'second').kind, 'collected');
|
|
|
|
const progress = batches.handle('direct:one', '/batch');
|
|
assert.equal(progress.kind, 'status');
|
|
assert.equal(progress.count, 2);
|
|
assert.match(progress.message, /已收集 2\/10 条/);
|
|
assert.equal(batches.status('direct:two').phase, 'idle');
|
|
});
|
|
|
|
test('collecting rejects non-text and other commands without counting them', () => {
|
|
const batches = new BatchInputManager();
|
|
batches.handle('direct:one', '/batch');
|
|
|
|
const media = batches.handle('direct:one', 'photo caption', { plainText: false });
|
|
assert.equal(media.kind, 'unsupported-content');
|
|
assert.match(media.message, /未收录/);
|
|
|
|
const mediaCommand = batches.handle('direct:one', '/cancel', { plainText: false });
|
|
assert.equal(mediaCommand.kind, 'unsupported-content');
|
|
assert.equal(batches.status('direct:one').phase, 'collecting');
|
|
|
|
const command = batches.handle('direct:one', '/stop');
|
|
assert.equal(command.kind, 'blocked-command');
|
|
assert.match(command.message, /\/send.*\/cancel/);
|
|
assert.equal(batches.status('direct:one').count, 0);
|
|
});
|
|
|
|
test('the tenth message is accepted and later messages are rejected without auto-submit', () => {
|
|
const batches = new BatchInputManager();
|
|
batches.handle('direct:one', '/batch');
|
|
|
|
for (let index = 1; index < BATCH_INPUT_LIMIT; index += 1) {
|
|
const response = batches.handle('direct:one', `message ${index}`);
|
|
assert.equal(response.kind, 'collected');
|
|
assert.equal(response.count, index);
|
|
assert.equal(response.message, undefined);
|
|
}
|
|
|
|
const tenth = batches.handle('direct:one', 'message 10');
|
|
assert.equal(tenth.kind, 'collected');
|
|
assert.equal(tenth.count, 10);
|
|
assert.match(tenth.message, /10\/10.*已满/);
|
|
assert.deepEqual(batches.status('direct:one'), {
|
|
phase: 'collecting', count: 10, limit: 10, full: true,
|
|
});
|
|
|
|
const eleventh = batches.handle('direct:one', 'message 11');
|
|
assert.equal(eleventh.kind, 'full');
|
|
assert.equal(eleventh.count, 10);
|
|
assert.match(eleventh.message, /这条消息未收录/);
|
|
assert.equal(batches.status('direct:one').phase, 'collecting');
|
|
assert.equal(batches.status('direct:one').count, 10);
|
|
});
|
|
|
|
test('/send keeps an empty batch collecting and creates one immutable submission snapshot', () => {
|
|
const batches = new BatchInputManager();
|
|
batches.handle('direct:one', '/batch');
|
|
|
|
const empty = batches.handle('direct:one', '/send');
|
|
assert.equal(empty.kind, 'empty');
|
|
assert.equal(batches.status('direct:one').phase, 'collecting');
|
|
|
|
batches.handle('direct:one', 'first line\ncontinued');
|
|
batches.handle('direct:one', 'second');
|
|
const submission = batches.handle('direct:one', '/send');
|
|
assert.equal(submission.kind, 'submit');
|
|
assert.equal(submission.count, 2);
|
|
assert.deepEqual(submission.messages, ['first line\ncontinued', 'second']);
|
|
assert.equal(Object.isFrozen(submission.messages), true);
|
|
assert.throws(() => submission.messages.push('third'), TypeError);
|
|
assert.equal(submission.prompt, [
|
|
'以下是用户通过批量输入模式发送的多条内容,请按顺序作为同一次输入统一处理。',
|
|
'[消息 1]\nfirst line\ncontinued',
|
|
'[消息 2]\nsecond',
|
|
].join('\n\n'));
|
|
assert.equal(batches.status('direct:one').phase, 'submitting');
|
|
|
|
const duplicate = batches.handle('direct:one', '/send');
|
|
assert.equal(duplicate.kind, 'submitting');
|
|
assert.equal(duplicate.token, undefined);
|
|
assert.equal(duplicate.messages, undefined);
|
|
assert.equal(batches.handle('direct:one', '/batch').kind, 'submitting');
|
|
assert.match(batches.handle('direct:one', '/cancel').message, /无法取消/);
|
|
assert.deepEqual(batches.handle('direct:one', 'next normal message'), { handled: false });
|
|
});
|
|
|
|
test('submission failure retains messages for retry while stale tokens cannot change state', () => {
|
|
const batches = new BatchInputManager();
|
|
batches.handle('direct:one', '/batch');
|
|
batches.handle('direct:one', 'first');
|
|
const first = batches.handle('direct:one', '/send');
|
|
|
|
assert.deepEqual(batches.fail('direct:one', Object.freeze({})), { retained: false });
|
|
assert.equal(batches.status('direct:one').phase, 'submitting');
|
|
|
|
const failed = batches.fail('direct:one', first.token);
|
|
assert.equal(failed.retained, true);
|
|
assert.equal(failed.count, 1);
|
|
assert.match(failed.message, /已保留 1 条消息/);
|
|
assert.equal(batches.status('direct:one').phase, 'collecting');
|
|
|
|
batches.handle('direct:one', 'second');
|
|
const retry = batches.handle('direct:one', '/send');
|
|
assert.notEqual(retry.token, first.token);
|
|
assert.deepEqual(retry.messages, ['first', 'second']);
|
|
assert.deepEqual(batches.complete('direct:one', first.token), { completed: false });
|
|
assert.equal(batches.status('direct:one').phase, 'submitting');
|
|
|
|
assert.deepEqual(batches.complete('direct:one', retry.token), { completed: true, count: 2 });
|
|
assert.equal(batches.status('direct:one').phase, 'idle');
|
|
assert.deepEqual(batches.fail('direct:one', retry.token), { retained: false });
|
|
});
|
|
|
|
test('/cancel discards collecting batches, including an empty batch', () => {
|
|
const batches = new BatchInputManager();
|
|
|
|
batches.handle('direct:one', '/batch');
|
|
const empty = batches.handle('direct:one', '/cancel');
|
|
assert.equal(empty.kind, 'cancelled');
|
|
assert.equal(empty.count, 0);
|
|
assert.match(empty.message, /已取消批量输入/);
|
|
assert.equal(batches.status('direct:one').phase, 'idle');
|
|
|
|
batches.handle('direct:one', '/batch');
|
|
batches.handle('direct:one', 'one');
|
|
batches.handle('direct:one', 'two');
|
|
const populated = batches.handle('direct:one', '/cancel');
|
|
assert.equal(populated.kind, 'cancelled');
|
|
assert.equal(populated.count, 2);
|
|
assert.match(populated.message, /丢弃 2 条消息/);
|
|
assert.equal(batches.status('direct:one').phase, 'idle');
|
|
});
|