dsh-im-ops/test/delivery-service.test.mjs
oliver 356538ac18 Improve WhatsApp delivery suggestions and peer labels (ops.30).
Surface chat/access-list contacts as selectable targets with clearer nicknames, and keep conversation peers usable for ops schedulers.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-06 02:31:42 +08:00

242 lines
8.4 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { createDeliveryAdapter } from '../plugin-src/host/delivery-adapter.mjs';
import { createDeliveryService } from '../plugin-src/host/delivery-service.mjs';
function memoryAdapter({ channel = 'telegram', botId = 'bot_one' } = {}) {
const targets = new Map();
const sends = [];
return {
channel,
sends,
ownsBot: (candidate) => candidate === botId,
listTargets: () => [...targets.values()].map((target) => structuredClone(target)),
listSuggestions: () => [{ kind: 'chat', route: { chatId: '123' } }],
async createTarget(_botId, target) {
if (targets.has(target.targetId)) {
const error = new Error('duplicate');
error.code = 'target-conflict';
throw error;
}
targets.set(target.targetId, structuredClone(target));
return structuredClone(target);
},
async updateTarget(_botId, targetId, replacement) {
if (!targets.has(targetId)) {
const error = new Error('missing');
error.code = 'unknown-target';
throw error;
}
const target = { targetId, ...structuredClone(replacement) };
targets.set(targetId, target);
return structuredClone(target);
},
async deleteTarget(_botId, targetId) {
if (!targets.delete(targetId)) {
const error = new Error('missing');
error.code = 'unknown-target';
throw error;
}
},
async sendText(...args) { sends.push(args); },
};
}
test('DeliveryService shares target CRUD and sending through one adapter', async () => {
const service = createDeliveryService();
const adapter = memoryAdapter();
service.registerAdapter(adapter);
const target = {
targetId: 'daily-report',
name: 'Daily report',
kind: 'chat',
route: { chatId: 123 },
};
assert.deepEqual(await service.createTarget('bot_one', target), target);
assert.deepEqual(await service.listTargets('bot_one'), {
botId: 'bot_one',
channel: 'telegram',
targets: [target],
});
assert.deepEqual(await service.listSuggestions('bot_one'), {
botId: 'bot_one',
channel: 'telegram',
suggestions: [{ kind: 'chat', route: { chatId: '123' } }],
});
assert.deepEqual(await service.updateTarget('bot_one', 'daily-report', {
name: 'New target',
kind: 'chat',
route: { chatId: 456 },
}), {
targetId: 'daily-report',
name: 'New target',
kind: 'chat',
route: { chatId: 456 },
});
const signal = new AbortController().signal;
assert.deepEqual(
await service.send('bot_one', 'daily-report', 'keep original whitespace\n', { signal }),
{ sent: true },
);
assert.deepEqual(adapter.sends, [[
'bot_one',
{
targetId: 'daily-report',
name: 'New target',
kind: 'chat',
route: { chatId: 456 },
},
'keep original whitespace\n',
{ signal },
]]);
assert.deepEqual(await service.deleteTarget('bot_one', 'daily-report'), { deleted: true });
await assert.rejects(service.send('bot_one', 'daily-report', 'missing'), { code: 'unknown-target' });
});
test('DeliveryService forwards WhatsApp mentions to the adapter', async () => {
const service = createDeliveryService();
const adapter = memoryAdapter({ channel: 'whatsapp' });
service.registerAdapter(adapter);
await service.createTarget('bot_one', {
targetId: 'ops-group',
name: 'Ops',
kind: 'group',
route: { jid: '120363@g.us' },
});
const mentions = ['8613800000000@s.whatsapp.net'];
await service.send('bot_one', 'ops-group', '@8613800000000 hello', { mentions });
assert.equal(adapter.sends.length, 1);
assert.deepEqual(adapter.sends[0][3], { signal: undefined, mentions });
});
test('DeliveryService validates public ids, text, cancellation, and unknown bots', async () => {
const service = createDeliveryService();
service.registerAdapter(memoryAdapter());
await assert.rejects(service.listTargets('../bot'), { code: 'bad-request' });
await assert.rejects(service.listSuggestions('../bot'), { code: 'bad-request' });
await assert.rejects(service.listTargets('bot_missing'), { code: 'unknown-bot' });
await assert.rejects(service.listSuggestions('bot_missing'), { code: 'unknown-bot' });
await assert.rejects(service.send('bot_one', 'bad target', 'hello'), { code: 'bad-request' });
await assert.rejects(service.send('bot_one', 'target', ' '), { code: 'bad-request' });
const abort = new AbortController();
abort.abort();
await assert.rejects(service.send('bot_one', 'target', 'hello', { signal: abort.signal }), {
code: 'cancelled',
});
await assert.rejects(
service.updateTarget('bot_one', 'target', {
targetId: 'renamed', kind: 'chat', route: { chatId: 1 },
}),
{ code: 'bad-request' },
);
});
test('DeliveryService tests a validated draft route without reading or persisting targets', async () => {
const service = createDeliveryService();
const workspaceCalls = [];
const sends = [];
service.registerAdapter(createDeliveryAdapter({
channel: 'telegram',
workspaces: {
has: (botId) => botId === 'bot_one',
listDeliveryTargets: (...args) => workspaceCalls.push(['list', ...args]),
createDeliveryTarget: (...args) => workspaceCalls.push(['create', ...args]),
updateDeliveryTarget: (...args) => workspaceCalls.push(['update', ...args]),
deleteDeliveryTarget: (...args) => workspaceCalls.push(['delete', ...args]),
},
coreController: {
async sendProactiveText(...args) { sends.push(args); },
},
stateFor: async () => ({ snapshot: () => ({ sessions: {} }) }),
}));
const signal = new AbortController().signal;
const draft = {
kind: 'topic',
route: { chatId: '-1001234567890', messageThreadId: 42 },
};
assert.deepEqual(
await service.send('bot_one', draft, 'draft test', { signal }),
{ sent: true },
);
assert.deepEqual(workspaceCalls, []);
assert.deepEqual(sends, [[
'bot_one',
{ targetId: '__test__', ...draft },
'draft test',
{ signal },
]]);
assert.deepEqual(draft, {
kind: 'topic',
route: { chatId: '-1001234567890', messageThreadId: 42 },
});
await assert.rejects(
service.send('bot_one', { ...draft, targetId: 'must-not-participate' }, 'draft test'),
{ code: 'bad-request' },
);
await assert.rejects(
service.send('bot_one', { ...draft, name: 'must-not-participate' }, 'draft test'),
{ code: 'bad-request' },
);
await assert.rejects(
service.send('bot_one', {
kind: 'topic', route: { chatId: '-1001234567890', messageThreadId: '42' },
}, 'draft test'),
{ code: 'invalid-target' },
);
assert.deepEqual(workspaceCalls, []);
assert.equal(sends.length, 1);
});
test('DeliveryService revalidates suggestions before exposing adapter output', async () => {
const service = createDeliveryService();
const adapter = memoryAdapter();
service.registerAdapter(adapter);
adapter.listSuggestions = () => [{
kind: 'chat',
route: { chatId: '123' },
sessionId: 'secret-session',
}];
await assert.rejects(service.listSuggestions('bot_one'), { code: 'invalid-target' });
adapter.listSuggestions = () => [{
kind: 'chat',
route: { chatId: '123', replyTarget: 'secret-reply' },
}];
await assert.rejects(service.listSuggestions('bot_one'), { code: 'invalid-target' });
});
test('DeliveryService maps an adapter abort to cancelled', async () => {
const service = createDeliveryService();
const adapter = memoryAdapter();
adapter.sendText = async () => {
const error = new Error('aborted downstream');
error.name = 'AbortError';
throw error;
};
service.registerAdapter(adapter);
await service.createTarget('bot_one', {
targetId: 'target', kind: 'chat', route: { chatId: 1 },
});
await assert.rejects(service.send('bot_one', 'target', 'hello'), { code: 'cancelled' });
});
test('DeliveryService adapter replacement has stale-safe unregister semantics', async () => {
const service = createDeliveryService();
const first = memoryAdapter();
const second = memoryAdapter();
const unregisterFirst = service.registerAdapter(first);
const unregisterSecond = service.registerAdapter(second);
assert.equal(unregisterFirst(), false);
await service.createTarget('bot_one', {
targetId: 'current', kind: 'chat', route: { chatId: 1 },
});
assert.equal((await service.listTargets('bot_one')).targets.length, 1);
assert.equal(unregisterSecond(), true);
await assert.rejects(service.listTargets('bot_one'), { code: 'unknown-bot' });
});