mirror of
https://github.com/hansjone/dsh-ops-cron.git
synced 2026-10-08 23:20:45 +08:00
Mirror scheduled-run summaries into the origin effective session.
Pin origin at create time, keep isolated fire sessions for audit, and after IM/DSH delivery resume or open the origin chat to append the same summary for follow-ups. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2e690c30c8
commit
29d946f3dd
12 changed files with 527 additions and 26 deletions
|
|
@ -3,8 +3,13 @@ import test from 'node:test'
|
|||
import {
|
||||
matchTargetForPeer,
|
||||
normalizeDelivery,
|
||||
normalizeOrigin,
|
||||
resolveCreateDelivery,
|
||||
resolveCreateOrigin,
|
||||
resolveMirrorSession,
|
||||
formatRunResultBody,
|
||||
deliverRunToIm,
|
||||
mirrorRunToSession,
|
||||
} from '../lib/delivery.js'
|
||||
|
||||
test('normalizeDelivery defaults to dsh', () => {
|
||||
|
|
@ -19,6 +24,26 @@ test('normalizeDelivery requires botId+targetId for im', () => {
|
|||
)
|
||||
})
|
||||
|
||||
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 = [
|
||||
|
|
@ -55,6 +80,37 @@ test('resolveCreateDelivery auto-matches peer target', async () => {
|
|||
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 = {
|
||||
|
|
@ -151,7 +207,6 @@ test('resolveCreateDelivery pins group creator mention from peer', async () => {
|
|||
kind: 'im',
|
||||
botId: 'bot_wa',
|
||||
targetId: 'ops-group',
|
||||
// Prefer phone JID over LID so WhatsApp can show nickname + notify.
|
||||
mentionJid: '8613800000000@s.whatsapp.net',
|
||||
mentionName: 'Alice',
|
||||
})
|
||||
|
|
@ -196,3 +251,123 @@ test('deliverRunToIm sends via dshIm', async () => {
|
|||
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.equal(appended[0][1].source.kind, 'plugin')
|
||||
assert.deepEqual(appended[0][2], { surfaceOp: 'append' })
|
||||
})
|
||||
|
||||
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$/)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ test('installable bundle declares host apply, client half, unique id, and no @de
|
|||
const host = await readFile(join(root, 'lib/host.js'), 'utf8')
|
||||
assert.match(host, /agentDefaultModel/)
|
||||
assert.match(host, /deliverRunToIm/)
|
||||
assert.match(host, /mirrorRunToSession/)
|
||||
assert.match(host, /getAgents/)
|
||||
assert.match(host, /PLUGIN_NAME = 'dsh-ops-cron'/)
|
||||
assert.match(host, /API_PREFIX = '\/dsh-ops-cron'/)
|
||||
assert.match(host, /resolveSessionPlacement/)
|
||||
|
|
@ -68,7 +70,10 @@ test('installable bundle declares host apply, client half, unique id, and no @de
|
|||
assert.match(preset, /resolveCreateAgentPreset/)
|
||||
const delivery = await readFile(join(root, 'lib/delivery.js'), 'utf8')
|
||||
assert.match(delivery, /resolveCreateDelivery/)
|
||||
assert.match(delivery, /resolveCreateOrigin/)
|
||||
assert.match(delivery, /mirrorRunToSession/)
|
||||
assert.match(delivery, /deliverRunToIm/)
|
||||
assert.match(client, /origin/)
|
||||
const prd = await readFile(join(root, 'docs/PRD.md'), 'utf8')
|
||||
assert.match(prd, /定时任务/)
|
||||
assert.match(prd, /dsh-schedule/)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue