mirror of
https://github.com/hansjone/dsh-ops-cron.git
synced 2026-10-08 23:20:45 +08:00
456 lines
14 KiB
JavaScript
456 lines
14 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import test from 'node:test'
|
|
import {
|
|
matchTargetForPeer,
|
|
normalizeDelivery,
|
|
normalizeOrigin,
|
|
resolveCreateDelivery,
|
|
resolveCreateOrigin,
|
|
resolveMirrorSession,
|
|
formatRunResultBody,
|
|
deliverRunToIm,
|
|
mirrorRunToSession,
|
|
jobVisibleToPeer,
|
|
assertDeliveryAllowedForIdentity,
|
|
} from '../lib/delivery.js'
|
|
|
|
test('normalizeDelivery defaults to dsh', () => {
|
|
assert.deepEqual(normalizeDelivery({}), { kind: 'dsh' })
|
|
})
|
|
|
|
test('normalizeDelivery requires botId+targetId for im', () => {
|
|
assert.throws(() => normalizeDelivery({ kind: 'im', botId: 'b' }), /im delivery/)
|
|
assert.deepEqual(
|
|
normalizeDelivery({ kind: 'im', botId: 'bot_1', targetId: 'alerts' }),
|
|
{ kind: 'im', botId: 'bot_1', targetId: 'alerts' },
|
|
)
|
|
})
|
|
|
|
test('normalizeOrigin pins web and im shapes', () => {
|
|
assert.equal(normalizeOrigin(null), null)
|
|
assert.deepEqual(
|
|
normalizeOrigin({ kind: 'web', sessionId: 'sess-web' }),
|
|
{ kind: 'web', sessionId: 'sess-web' },
|
|
)
|
|
assert.deepEqual(
|
|
normalizeOrigin({
|
|
kind: 'im',
|
|
sessionId: 'sess-im',
|
|
peer: { botId: 'bot_1', conversationKey: 'direct:86138@s.whatsapp.net', kind: 'direct' },
|
|
}),
|
|
{
|
|
kind: 'im',
|
|
sessionId: 'sess-im',
|
|
peer: { botId: 'bot_1', conversationKey: 'direct:86138@s.whatsapp.net', kind: 'direct' },
|
|
},
|
|
)
|
|
})
|
|
|
|
test('matchTargetForPeer matches whatsapp jid from conversationKey', () => {
|
|
const peer = { botId: 'bot_1', conversationKey: 'direct:8613800000000@s.whatsapp.net' }
|
|
const targets = [
|
|
{ targetId: 'other', route: { jid: '1@s.whatsapp.net' } },
|
|
{ targetId: 'alerts', route: { jid: '8613800000000@s.whatsapp.net' } },
|
|
]
|
|
assert.equal(matchTargetForPeer(targets, peer).targetId, 'alerts')
|
|
})
|
|
|
|
test('resolveCreateDelivery uses explicit im args', async () => {
|
|
const delivery = await resolveCreateDelivery({
|
|
delivery: 'im',
|
|
im_bot_id: 'bot_x',
|
|
im_target_id: 'tgt_y',
|
|
}, {})
|
|
assert.deepEqual(delivery, { kind: 'im', botId: 'bot_x', targetId: 'tgt_y' })
|
|
})
|
|
|
|
test('resolveCreateDelivery auto-matches peer target', async () => {
|
|
const dshIm = {
|
|
resolveSessionPeer: async () => ({
|
|
botId: 'bot_1',
|
|
conversationKey: 'direct:86138@s.whatsapp.net',
|
|
conversationId: '86138@s.whatsapp.net',
|
|
kind: 'direct',
|
|
}),
|
|
listTargets: async () => ([
|
|
{ targetId: 'wa-dm', route: { jid: '86138@s.whatsapp.net' } },
|
|
]),
|
|
}
|
|
const delivery = await resolveCreateDelivery({}, {
|
|
agent: { session: { id: 'sess-1' } },
|
|
}, { dshIm })
|
|
assert.deepEqual(delivery, { kind: 'im', botId: 'bot_1', targetId: 'wa-dm' })
|
|
})
|
|
|
|
test('resolveCreateOrigin captures im peer from caller session', async () => {
|
|
const dshIm = {
|
|
resolveSessionPeer: async () => ({
|
|
botId: 'bot_1',
|
|
conversationKey: 'group:120363@g.us:user:86138@s.whatsapp.net',
|
|
conversationId: '120363@g.us',
|
|
kind: 'group',
|
|
}),
|
|
}
|
|
const origin = await resolveCreateOrigin({}, {
|
|
agent: { session: { id: 'sess-group' } },
|
|
}, { dshIm })
|
|
assert.deepEqual(origin, {
|
|
kind: 'im',
|
|
sessionId: 'sess-group',
|
|
peer: {
|
|
botId: 'bot_1',
|
|
conversationKey: 'group:120363@g.us:user:86138@s.whatsapp.net',
|
|
conversationId: '120363@g.us',
|
|
kind: 'group',
|
|
},
|
|
})
|
|
})
|
|
|
|
test('resolveCreateOrigin falls back to web when no peer', async () => {
|
|
const origin = await resolveCreateOrigin({}, {
|
|
agent: { session: { id: 'sess-web' } },
|
|
}, { dshIm: { resolveSessionPeer: async () => null } })
|
|
assert.deepEqual(origin, { kind: 'web', sessionId: 'sess-web' })
|
|
})
|
|
|
|
test('resolveCreateDelivery auto-creates group target when none exists', async () => {
|
|
const created = []
|
|
const dshIm = {
|
|
resolveSessionPeer: async () => ({
|
|
botId: 'bot_wa',
|
|
kind: 'group',
|
|
conversationKey: 'group:120363@g.us:user:86138@s.whatsapp.net',
|
|
conversationId: '120363@g.us',
|
|
label: '运维群',
|
|
}),
|
|
listTargets: async () => ([]),
|
|
createTarget: async (botId, target) => {
|
|
created.push({ botId, target })
|
|
return target
|
|
},
|
|
}
|
|
const delivery = await resolveCreateDelivery({}, {
|
|
agent: { session: { id: 'sess-group' } },
|
|
}, { dshIm })
|
|
assert.equal(created.length, 1)
|
|
assert.equal(created[0].botId, 'bot_wa')
|
|
assert.equal(created[0].target.kind, 'group')
|
|
assert.equal(created[0].target.route.jid, '120363@g.us')
|
|
assert.deepEqual(delivery, {
|
|
kind: 'im',
|
|
botId: 'bot_wa',
|
|
targetId: created[0].target.targetId,
|
|
})
|
|
})
|
|
|
|
test('matchTargetForPeer matches phone-mapped DM against phone JID target', () => {
|
|
const peer = {
|
|
botId: 'bot_1',
|
|
conversationKey: 'direct:910xxx@lid',
|
|
conversationId: '910xxx@lid',
|
|
phone: '8613800000000',
|
|
}
|
|
const targets = [
|
|
{ targetId: 'alerts', kind: 'user', route: { jid: '8613800000000@s.whatsapp.net' } },
|
|
]
|
|
assert.equal(matchTargetForPeer(targets, peer).targetId, 'alerts')
|
|
})
|
|
|
|
test('matchTargetForPeer does not bind group peer to a DM via sender phone', () => {
|
|
const peer = {
|
|
botId: 'bot_1',
|
|
kind: 'group',
|
|
conversationKey: 'group:120363999@g.us:user:8613800000000@s.whatsapp.net',
|
|
conversationId: '120363999@g.us',
|
|
phone: '8613800000000',
|
|
label: 'Ops Room · Alice · 8613800000000',
|
|
}
|
|
const targets = [
|
|
{ targetId: 'my-dm', kind: 'user', route: { jid: '8613800000000@s.whatsapp.net' } },
|
|
{ targetId: 'ops-group', kind: 'group', route: { jid: '120363999@g.us' } },
|
|
]
|
|
assert.equal(matchTargetForPeer(targets, peer).targetId, 'ops-group')
|
|
})
|
|
|
|
test('matchTargetForPeer ignores DM-only catalog when peer is a group', () => {
|
|
const peer = {
|
|
botId: 'bot_1',
|
|
kind: 'group',
|
|
conversationKey: 'group:120363999@g.us:user:8613800000000@s.whatsapp.net',
|
|
conversationId: '120363999@g.us',
|
|
phone: '8613800000000',
|
|
}
|
|
const targets = [
|
|
{ targetId: 'my-dm', kind: 'user', route: { jid: '8613800000000@s.whatsapp.net' } },
|
|
]
|
|
assert.equal(matchTargetForPeer(targets, peer), null)
|
|
})
|
|
|
|
test('resolveCreateDelivery pins group creator mention from peer', async () => {
|
|
const dshIm = {
|
|
resolveSessionPeer: async () => ({
|
|
botId: 'bot_wa',
|
|
kind: 'group',
|
|
conversationKey: 'group:120363@g.us:user:910xxx@lid',
|
|
conversationId: '120363@g.us',
|
|
senderId: '910xxx@lid',
|
|
phone: '8613800000000',
|
|
pushName: 'Alice',
|
|
label: 'Ops · Alice · 8613800000000',
|
|
}),
|
|
listTargets: async () => ([
|
|
{ targetId: 'ops-group', kind: 'group', route: { jid: '120363@g.us' } },
|
|
]),
|
|
}
|
|
const delivery = await resolveCreateDelivery({}, {
|
|
agent: { session: { id: 'sess-group' } },
|
|
}, { dshIm })
|
|
assert.deepEqual(delivery, {
|
|
kind: 'im',
|
|
botId: 'bot_wa',
|
|
targetId: 'ops-group',
|
|
mentionJid: '8613800000000@s.whatsapp.net',
|
|
mentionName: 'Alice',
|
|
})
|
|
})
|
|
|
|
test('deliverRunToIm @mentions the creator in group deliveries', async () => {
|
|
const sent = []
|
|
await deliverRunToIm(
|
|
{
|
|
name: 'ping',
|
|
delivery: {
|
|
kind: 'im',
|
|
botId: 'b',
|
|
targetId: 't',
|
|
mentionJid: '8613800000000@s.whatsapp.net',
|
|
mentionName: 'Alice',
|
|
},
|
|
},
|
|
'hello world',
|
|
{
|
|
dshIm: {
|
|
send: async (...args) => { sent.push(args) },
|
|
},
|
|
},
|
|
)
|
|
assert.equal(sent.length, 1)
|
|
assert.match(sent[0][2], /^@8613800000000\n/)
|
|
assert.match(sent[0][2], /定时任务/)
|
|
assert.deepEqual(sent[0][3], { mentions: ['8613800000000@s.whatsapp.net'] })
|
|
})
|
|
|
|
test('deliverRunToIm sends via dshIm', async () => {
|
|
const sent = []
|
|
await deliverRunToIm(
|
|
{ name: 'ping', delivery: { kind: 'im', botId: 'b', targetId: 't' } },
|
|
'hello world',
|
|
{ dshIm: { send: async (...args) => { sent.push(args) } } },
|
|
)
|
|
assert.equal(sent.length, 1)
|
|
assert.equal(sent[0][0], 'b')
|
|
assert.equal(sent[0][1], 't')
|
|
assert.match(sent[0][2], /定时任务/)
|
|
assert.match(sent[0][2], /hello world/)
|
|
})
|
|
|
|
test('resolveMirrorSession prefers live conversation binding over stale origin.sessionId', async () => {
|
|
const hit = await resolveMirrorSession({
|
|
origin: {
|
|
kind: 'im',
|
|
sessionId: 'stale-sess',
|
|
peer: { botId: 'bot_1', conversationKey: 'direct:86138@s.whatsapp.net' },
|
|
},
|
|
delivery: { kind: 'im', botId: 'bot_1', targetId: 'wa-dm' },
|
|
}, {
|
|
dshIm: {
|
|
resolveConversationSession: async () => ({
|
|
sessionId: 'live-sess',
|
|
botId: 'bot_1',
|
|
conversationKey: 'direct:86138@s.whatsapp.net',
|
|
}),
|
|
},
|
|
})
|
|
assert.deepEqual(hit, { sessionId: 'live-sess', via: 'conversation' })
|
|
})
|
|
|
|
test('resolveMirrorSession falls back to origin.sessionId', async () => {
|
|
const hit = await resolveMirrorSession({
|
|
origin: { kind: 'web', sessionId: 'origin-web' },
|
|
delivery: { kind: 'dsh' },
|
|
}, {})
|
|
assert.deepEqual(hit, { sessionId: 'origin-web', via: 'origin' })
|
|
})
|
|
|
|
test('mirrorRunToSession appends plugin notice without waking a turn', async () => {
|
|
const appended = []
|
|
const result = await mirrorRunToSession(
|
|
{
|
|
name: '日报',
|
|
origin: { kind: 'web', sessionId: 'origin-1' },
|
|
delivery: { kind: 'dsh' },
|
|
},
|
|
'line one',
|
|
{
|
|
getAgents: () => ({
|
|
get: () => ({
|
|
session: {
|
|
append: (...args) => { appended.push(args) },
|
|
},
|
|
}),
|
|
}),
|
|
newId: () => 'msg-1',
|
|
pluginName: 'dsh-ops-cron',
|
|
},
|
|
)
|
|
assert.equal(result.mirrored, true)
|
|
assert.equal(result.method, 'append')
|
|
assert.equal(appended.length, 1)
|
|
assert.equal(appended[0][0], 'user/message')
|
|
assert.match(appended[0][1].content[0].text, /日报/)
|
|
assert.match(appended[0][1].content[0].text, /line one/)
|
|
assert.match(appended[0][1].content[0].text, /<system-reminder>/)
|
|
assert.equal(appended[0][1].source.kind, 'plugin')
|
|
assert.deepEqual(appended[0][2], { surfaceOp: 'append' })
|
|
})
|
|
|
|
test('matchTargetForPeer requires exact JID equality', () => {
|
|
const targets = [{
|
|
targetId: 'suffix-trap',
|
|
kind: 'user',
|
|
route: { jid: '8613800138000@s.whatsapp.net' },
|
|
}]
|
|
const peer = {
|
|
kind: 'direct',
|
|
conversationId: '13800138000@s.whatsapp.net',
|
|
phone: '13800138000',
|
|
}
|
|
assert.equal(matchTargetForPeer(targets, peer), null)
|
|
})
|
|
|
|
test('jobVisibleToPeer scopes IM ownership to origin conversation', () => {
|
|
const peer = {
|
|
botId: 'bot-a',
|
|
conversationKey: 'group:120363@g.us:user:1@lid',
|
|
conversationId: '120363@g.us',
|
|
}
|
|
assert.equal(jobVisibleToPeer({
|
|
origin: {
|
|
kind: 'im',
|
|
peer: { botId: 'bot-a', conversationKey: 'group:120363@g.us:user:1@lid', conversationId: '120363@g.us' },
|
|
},
|
|
}, peer), true)
|
|
assert.equal(jobVisibleToPeer({
|
|
origin: {
|
|
kind: 'im',
|
|
peer: { botId: 'bot-a', conversationKey: 'group:other@g.us', conversationId: 'other@g.us' },
|
|
},
|
|
}, peer), false)
|
|
assert.equal(jobVisibleToPeer({
|
|
origin: { kind: 'web', sessionId: 'web-1' },
|
|
}, peer), false)
|
|
})
|
|
|
|
test('resolveCreateDelivery binds IM peers to the current chat', async () => {
|
|
const peer = {
|
|
botId: 'bot-a',
|
|
conversationKey: 'direct:86138@s.whatsapp.net',
|
|
conversationId: '86138@s.whatsapp.net',
|
|
kind: 'direct',
|
|
phone: '86138',
|
|
}
|
|
const delivery = await resolveCreateDelivery(
|
|
{ delivery: 'im', im_bot_id: 'other-bot', im_target_id: 'other-target' },
|
|
{ agent: { session: { id: 'sess-im' } } },
|
|
{
|
|
dshIm: {
|
|
resolveSessionPeer: async () => peer,
|
|
listTargets: async () => [{
|
|
targetId: 'auto-dm',
|
|
kind: 'user',
|
|
route: { jid: '86138@s.whatsapp.net' },
|
|
}],
|
|
},
|
|
},
|
|
)
|
|
assert.equal(delivery.kind, 'im')
|
|
assert.equal(delivery.botId, 'bot-a')
|
|
assert.equal(delivery.targetId, 'auto-dm')
|
|
})
|
|
|
|
test('mirrorRunToSession resumes a cold origin session instead of create', async () => {
|
|
const appended = []
|
|
const resumed = []
|
|
const result = await mirrorRunToSession(
|
|
{
|
|
name: '冷会话',
|
|
origin: { kind: 'web', sessionId: 'cold-1' },
|
|
},
|
|
'hello',
|
|
{
|
|
getAgents: () => ({
|
|
get: () => null,
|
|
create: async () => {
|
|
throw new Error('create must not be used for origin mirror')
|
|
},
|
|
resume: async (opts) => {
|
|
resumed.push(opts)
|
|
return {
|
|
agent: {
|
|
session: {
|
|
append: (...args) => { appended.push(args) },
|
|
},
|
|
},
|
|
}
|
|
},
|
|
}),
|
|
newId: () => 'msg-2',
|
|
},
|
|
)
|
|
assert.equal(result.mirrored, true)
|
|
assert.equal(result.resumed, true)
|
|
assert.deepEqual(resumed, [{ resumeSessionId: 'cold-1' }])
|
|
assert.equal(appended[0][0], 'user/message')
|
|
})
|
|
|
|
test('mirrorRunToSession skips when origin session is unavailable', async () => {
|
|
const result = await mirrorRunToSession(
|
|
{
|
|
name: 'gone',
|
|
origin: { kind: 'web', sessionId: 'missing' },
|
|
},
|
|
'x',
|
|
{
|
|
getAgents: () => ({
|
|
get: () => null,
|
|
resume: async () => {
|
|
throw new Error('not found')
|
|
},
|
|
}),
|
|
},
|
|
)
|
|
assert.equal(result.mirrored, false)
|
|
assert.equal(result.reason, 'session_unavailable')
|
|
})
|
|
|
|
test('formatRunResultBody matches IM delivery header', () => {
|
|
const body = formatRunResultBody({ name: '晨报' }, 'ok')
|
|
assert.match(body, /^【定时任务 · 晨报】\n/)
|
|
assert.match(body, /ok$/)
|
|
const en = formatRunResultBody({ name: 'Daily' }, 'ok', 'en')
|
|
assert.match(en, /^\[Scheduled tasks · Daily\]\n/)
|
|
})
|
|
|
|
test('assertDeliveryAllowedForIdentity blocks non-admin web IM delivery', () => {
|
|
const im = { kind: 'im', botId: 'b', targetId: 't' }
|
|
assert.equal(
|
|
assertDeliveryAllowedForIdentity(im, { empNo: 'a1', permissions: { canViewAllSessions: true } }),
|
|
im,
|
|
)
|
|
assert.doesNotThrow(() => assertDeliveryAllowedForIdentity(im, { empNo: '__local__', permissions: { canViewAllSessions: true } }))
|
|
assert.throws(
|
|
() => assertDeliveryAllowedForIdentity(im, { empNo: 'u1', permissions: { canViewAllSessions: false } }),
|
|
(error) => error.code === 'IM_DELIVERY_FORBIDDEN',
|
|
)
|
|
assert.doesNotThrow(() => assertDeliveryAllowedForIdentity({ kind: 'dsh' }, { empNo: 'u1', permissions: {} }))
|
|
})
|