agent-messenger 2.18.0 → 2.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/bunfig.toml +1 -0
- package/dist/package.json +1 -1
- package/dist/src/platforms/discordbot/client.d.ts +1 -0
- package/dist/src/platforms/discordbot/client.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/client.js +3 -0
- package/dist/src/platforms/discordbot/client.js.map +1 -1
- package/dist/src/platforms/discordbot/commands/message.d.ts +1 -0
- package/dist/src/platforms/discordbot/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/commands/message.js +2 -0
- package/dist/src/platforms/discordbot/commands/message.js.map +1 -1
- package/dist/src/platforms/kakaotalk/client.d.ts +4 -2
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +67 -3
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/message.js +23 -1
- package/dist/src/platforms/kakaotalk/commands/message.js.map +1 -1
- package/dist/src/platforms/kakaotalk/index.d.ts +1 -1
- package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/index.js.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/session.d.ts +2 -1
- package/dist/src/platforms/kakaotalk/protocol/session.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/session.js +15 -0
- package/dist/src/platforms/kakaotalk/protocol/session.js.map +1 -1
- package/dist/src/platforms/kakaotalk/types.d.ts +18 -0
- package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/types.js +1 -0
- package/dist/src/platforms/kakaotalk/types.js.map +1 -1
- package/dist/src/shared/chromium/browsers.js +1 -1
- package/dist/src/shared/chromium/browsers.js.map +1 -1
- package/docs/content/docs/cli/discordbot.mdx +6 -0
- package/docs/content/docs/sdk/discordbot.mdx +4 -0
- package/package.json +1 -1
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +1 -1
- package/skills/agent-discordbot/SKILL.md +9 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +24 -1
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +1 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-telegrambot/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/discord/commands/dm.test.ts +28 -20
- package/src/platforms/discord/commands/reaction.test.ts +12 -7
- package/src/platforms/discordbot/client.test.ts +17 -0
- package/src/platforms/discordbot/client.ts +9 -2
- package/src/platforms/discordbot/commands/message.test.ts +28 -11
- package/src/platforms/discordbot/commands/message.ts +4 -2
- package/src/platforms/instagram/commands/auth.test.ts +11 -9
- package/src/platforms/instagram/commands/chat.test.ts +8 -6
- package/src/platforms/instagram/commands/message.test.ts +8 -6
- package/src/platforms/kakaotalk/client.test.ts +96 -0
- package/src/platforms/kakaotalk/client.ts +82 -3
- package/src/platforms/kakaotalk/commands/message.test.ts +42 -0
- package/src/platforms/kakaotalk/commands/message.ts +33 -2
- package/src/platforms/kakaotalk/index.ts +2 -0
- package/src/platforms/kakaotalk/protocol/session.ts +15 -1
- package/src/platforms/kakaotalk/types.ts +24 -0
- package/src/platforms/webex/commands/auth.test.ts +30 -14
- package/src/platforms/webex/commands/member.test.ts +30 -34
- package/src/platforms/webex/commands/message.test.ts +37 -48
- package/src/platforms/webex/commands/snapshot.test.ts +26 -36
- package/src/platforms/webex/commands/space.test.ts +32 -38
- package/src/platforms/webex/commands/whoami.test.ts +10 -22
- package/src/platforms/webex/credential-manager.test.ts +3 -0
- package/src/platforms/whatsapp/commands/auth.test.ts +14 -20
- package/src/platforms/whatsapp/commands/chat.test.ts +17 -24
- package/src/platforms/whatsapp/commands/message.test.ts +31 -41
- package/src/shared/chromium/browsers.ts +1 -1
- package/src/test-setup.ts +5 -0
- package/tsconfig.json +1 -1
|
@@ -6,6 +6,7 @@ import { Command } from 'commander'
|
|
|
6
6
|
import { handleError } from '@/shared/utils/error-handler'
|
|
7
7
|
import { formatOutput } from '@/shared/utils/output'
|
|
8
8
|
|
|
9
|
+
import type { KakaoMessage, KakaoReplyTarget } from '../types'
|
|
9
10
|
import { withKakaoClient } from './shared'
|
|
10
11
|
|
|
11
12
|
async function listAction(
|
|
@@ -26,16 +27,45 @@ async function listAction(
|
|
|
26
27
|
async function sendAction(
|
|
27
28
|
chatId: string,
|
|
28
29
|
text: string,
|
|
29
|
-
options: { account?: string; pretty?: boolean },
|
|
30
|
+
options: { account?: string; pretty?: boolean; replyTo?: string },
|
|
30
31
|
): Promise<void> {
|
|
31
32
|
try {
|
|
32
|
-
const result = await withKakaoClient(options, (client) =>
|
|
33
|
+
const result = await withKakaoClient(options, async (client) => {
|
|
34
|
+
if (options.replyTo === undefined) {
|
|
35
|
+
return client.sendMessage(chatId, text)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const target = await resolveReplyTarget(client, chatId, options.replyTo)
|
|
39
|
+
return client.sendMessage(chatId, text, { replyTo: target })
|
|
40
|
+
})
|
|
33
41
|
console.log(formatOutput(result, options.pretty))
|
|
34
42
|
} catch (error) {
|
|
35
43
|
handleError(error as Error)
|
|
36
44
|
}
|
|
37
45
|
}
|
|
38
46
|
|
|
47
|
+
// A reply attachment needs the source message's author, text, and type — not
|
|
48
|
+
// just its log_id — so we look it up in the chat's recent history.
|
|
49
|
+
async function resolveReplyTarget(
|
|
50
|
+
client: {
|
|
51
|
+
getMessages: (chatId: string, opts: { count: number }) => Promise<KakaoMessage[]>
|
|
52
|
+
},
|
|
53
|
+
chatId: string,
|
|
54
|
+
logId: string,
|
|
55
|
+
): Promise<KakaoReplyTarget> {
|
|
56
|
+
const messages = await client.getMessages(chatId, { count: 100 })
|
|
57
|
+
const source = messages.find((m) => m.log_id === logId)
|
|
58
|
+
if (!source) {
|
|
59
|
+
throw new Error(`Reply target log-id ${logId} not found in the latest 100 messages of chat ${chatId}`)
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
log_id: source.log_id,
|
|
63
|
+
author_id: source.author_id,
|
|
64
|
+
message: source.message,
|
|
65
|
+
type: source.type,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
39
69
|
type UploadKind = 'auto' | 'photo' | 'video' | 'audio' | 'file' | 'multi'
|
|
40
70
|
|
|
41
71
|
async function uploadAction(
|
|
@@ -122,6 +152,7 @@ export const messageCommand = new Command('message')
|
|
|
122
152
|
.argument('<chat-id>', 'Chat room ID')
|
|
123
153
|
.argument('<text>', 'Message text')
|
|
124
154
|
.option('--account <id>', 'Use a specific KakaoTalk account')
|
|
155
|
+
.option('--reply-to <log-id>', 'Send as a quoted reply to this message log ID')
|
|
125
156
|
.option('--pretty', 'Pretty print JSON output')
|
|
126
157
|
.action(sendAction),
|
|
127
158
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Binary, Long } from 'bson'
|
|
2
2
|
|
|
3
|
-
import type
|
|
3
|
+
import { KAKAO_MESSAGE_TYPE, type KakaoDeviceType } from '../types'
|
|
4
4
|
import {
|
|
5
5
|
BOOKING_HOST,
|
|
6
6
|
BOOKING_PORT,
|
|
@@ -126,6 +126,20 @@ export class LocoSession {
|
|
|
126
126
|
})
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
// Quoted reply — a WRITE with message_type 26 (REPLY) whose `extra` JSON
|
|
130
|
+
// carries the source-message reference. The reply semantics ride entirely on
|
|
131
|
+
// `type` + `extra`; no extra top-level WRITE fields are needed.
|
|
132
|
+
async sendReply(chatId: Long, text: string, extra: Record<string, unknown>): Promise<LocoPacket> {
|
|
133
|
+
if (!this.connection) throw new Error('Not connected')
|
|
134
|
+
return this.connection.sendPacket('WRITE', {
|
|
135
|
+
chatId,
|
|
136
|
+
msg: text,
|
|
137
|
+
type: KAKAO_MESSAGE_TYPE.REPLY,
|
|
138
|
+
noSeen: false,
|
|
139
|
+
extra: JSON.stringify(extra),
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
129
143
|
// Sends a WRITE with non-text message_type plus the JSON-stringified `extra`
|
|
130
144
|
// payload that KakaoTalk clients render as the attachment (photo, file, etc).
|
|
131
145
|
// See types.ts → KakaoPhotoExtra / KakaoFileExtra for the per-type shape.
|
|
@@ -126,6 +126,7 @@ export const KAKAO_MESSAGE_TYPE = {
|
|
|
126
126
|
VIDEO: 3,
|
|
127
127
|
AUDIO: 5,
|
|
128
128
|
FILE: 18,
|
|
129
|
+
REPLY: 26,
|
|
129
130
|
MULTIPHOTO: 27,
|
|
130
131
|
} as const
|
|
131
132
|
|
|
@@ -176,6 +177,29 @@ export interface KakaoMultiPhotoExtra {
|
|
|
176
177
|
expire?: number
|
|
177
178
|
}
|
|
178
179
|
|
|
180
|
+
// REPLY (type 26) extra. Field names verified against storycraft/node-kakao
|
|
181
|
+
// (src/chat/attachment/reply.ts) and jhleekr/kakao.py: `Id` fields are
|
|
182
|
+
// camelCase (src_logId, NOT src_log_id), and src_mentions/mentions are
|
|
183
|
+
// required even when empty.
|
|
184
|
+
export interface KakaoReplyExtra {
|
|
185
|
+
attach_only: boolean
|
|
186
|
+
attach_type: number
|
|
187
|
+
src_logId: string
|
|
188
|
+
src_userId: number
|
|
189
|
+
src_message: string
|
|
190
|
+
src_type: number
|
|
191
|
+
src_mentions: unknown[]
|
|
192
|
+
mentions: unknown[]
|
|
193
|
+
src_linkId?: string
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface KakaoReplyTarget {
|
|
197
|
+
log_id: string
|
|
198
|
+
author_id: number
|
|
199
|
+
message: string
|
|
200
|
+
type: number
|
|
201
|
+
}
|
|
202
|
+
|
|
179
203
|
export interface KakaoMarkReadResult {
|
|
180
204
|
success: boolean
|
|
181
205
|
status_code: number
|
|
@@ -7,10 +7,17 @@ import { WebexTokenExtractor } from '../token-extractor'
|
|
|
7
7
|
import { WebexError } from '../types'
|
|
8
8
|
import { extractAction, loginAction, logoutAction, statusAction } from './auth'
|
|
9
9
|
|
|
10
|
+
class ProcessExit extends Error {
|
|
11
|
+
constructor(readonly code?: string | number | null) {
|
|
12
|
+
super(`process.exit(${code})`)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
describe('auth commands', () => {
|
|
11
17
|
let consoleSpy: ReturnType<typeof spyOn>
|
|
12
18
|
let consoleErrorSpy: ReturnType<typeof spyOn>
|
|
13
19
|
let execSpy: ReturnType<typeof spyOn>
|
|
20
|
+
let stderrWriteSpy: ReturnType<typeof spyOn>
|
|
14
21
|
const protoSpies: ReturnType<typeof spyOn>[] = []
|
|
15
22
|
let originalStdinTTY: boolean | undefined
|
|
16
23
|
let originalStdoutTTY: boolean | undefined
|
|
@@ -38,16 +45,23 @@ describe('auth commands', () => {
|
|
|
38
45
|
consoleSpy = spyOn(console, 'log').mockImplementation(() => {})
|
|
39
46
|
consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
|
|
40
47
|
execSpy = spyOn(childProcess, 'exec').mockImplementation((() => {}) as any)
|
|
48
|
+
stderrWriteSpy = spyOn(process.stderr, 'write').mockImplementation(() => true)
|
|
41
49
|
originalStdinTTY = process.stdin.isTTY
|
|
42
50
|
originalStdoutTTY = process.stdout.isTTY
|
|
43
51
|
// Default to interactive TTY for existing tests; non-interactive tests override.
|
|
44
52
|
setTTY(true)
|
|
53
|
+
// Fail loudly instead of running the real 300s network polling loop; tests that
|
|
54
|
+
// exercise the Device Grant flow override this with a resolved value.
|
|
55
|
+
protoSpy(WebexCredentialManager.prototype, 'pollDeviceToken').mockImplementation(() => {
|
|
56
|
+
throw new Error('Unexpected real device polling in test')
|
|
57
|
+
})
|
|
45
58
|
})
|
|
46
59
|
|
|
47
60
|
afterEach(() => {
|
|
48
61
|
consoleSpy.mockRestore()
|
|
49
62
|
consoleErrorSpy.mockRestore()
|
|
50
63
|
execSpy.mockRestore()
|
|
64
|
+
stderrWriteSpy.mockRestore()
|
|
51
65
|
for (const s of protoSpies) s.mockRestore()
|
|
52
66
|
protoSpies.length = 0
|
|
53
67
|
setTTY(originalStdinTTY)
|
|
@@ -441,15 +455,16 @@ describe('auth commands', () => {
|
|
|
441
455
|
protoSpy(WebexCredentialManager.prototype, 'refreshToken').mockResolvedValue(null)
|
|
442
456
|
protoSpy(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
|
|
443
457
|
protoSpy(WebexClient.prototype, 'testAuth').mockRejectedValue(new Error('Network error'))
|
|
444
|
-
protoSpy(process, 'exit').mockImplementation((
|
|
458
|
+
const exitSpy = protoSpy(process, 'exit').mockImplementation((code?: string | number | null) => {
|
|
459
|
+
throw new ProcessExit(code)
|
|
460
|
+
})
|
|
445
461
|
|
|
446
|
-
await extractAction({ pretty: false })
|
|
462
|
+
await expect(extractAction({ pretty: false })).rejects.toThrow(ProcessExit)
|
|
447
463
|
|
|
448
|
-
const lastCall =
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
464
|
+
const lastCall = stderrWriteSpy.mock.calls[stderrWriteSpy.mock.calls.length - 1][0] as string
|
|
465
|
+
const output = JSON.parse(lastCall)
|
|
466
|
+
expect(output.error).toContain('Network error')
|
|
467
|
+
expect(exitSpy).toHaveBeenCalledWith(1)
|
|
453
468
|
})
|
|
454
469
|
|
|
455
470
|
it('rethrows non-expiry auth errors', async () => {
|
|
@@ -459,15 +474,16 @@ describe('auth commands', () => {
|
|
|
459
474
|
})
|
|
460
475
|
protoSpy(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
|
|
461
476
|
protoSpy(WebexClient.prototype, 'testAuth').mockRejectedValue(new Error('Network error'))
|
|
462
|
-
protoSpy(process, 'exit').mockImplementation((
|
|
477
|
+
const exitSpy = protoSpy(process, 'exit').mockImplementation((code?: string | number | null) => {
|
|
478
|
+
throw new ProcessExit(code)
|
|
479
|
+
})
|
|
463
480
|
|
|
464
|
-
await extractAction({ pretty: false })
|
|
481
|
+
await expect(extractAction({ pretty: false })).rejects.toThrow(ProcessExit)
|
|
465
482
|
|
|
466
|
-
const lastCall =
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
483
|
+
const lastCall = stderrWriteSpy.mock.calls[stderrWriteSpy.mock.calls.length - 1][0] as string
|
|
484
|
+
const output = JSON.parse(lastCall)
|
|
485
|
+
expect(output.error).toContain('Network error')
|
|
486
|
+
expect(exitSpy).toHaveBeenCalledWith(1)
|
|
471
487
|
})
|
|
472
488
|
|
|
473
489
|
it('outputs no token found when extract returns null', async () => {
|
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect,
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, spyOn, it } from 'bun:test'
|
|
2
2
|
|
|
3
|
+
import { WebexClient } from '../client'
|
|
3
4
|
import { WebexError } from '../types'
|
|
4
5
|
|
|
5
|
-
const mockHandleError = mock((err: Error) => {
|
|
6
|
-
throw err
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
mock.module('@/shared/utils/error-handler', () => ({
|
|
10
|
-
handleError: mockHandleError,
|
|
11
|
-
}))
|
|
12
|
-
|
|
13
6
|
const mockMembers = [
|
|
14
7
|
{
|
|
15
8
|
id: 'mem-1',
|
|
@@ -31,32 +24,39 @@ const mockMembers = [
|
|
|
31
24
|
},
|
|
32
25
|
]
|
|
33
26
|
|
|
34
|
-
const mockListMemberships = mock(() => Promise.resolve(mockMembers))
|
|
35
|
-
const mockLogin = mock(() => Promise.resolve({ listMemberships: mockListMemberships }))
|
|
36
|
-
|
|
37
|
-
mock.module('../client', () => ({
|
|
38
|
-
WebexClient: class {
|
|
39
|
-
login = mockLogin
|
|
40
|
-
},
|
|
41
|
-
}))
|
|
42
|
-
|
|
43
27
|
import { listAction } from './member'
|
|
44
28
|
|
|
45
29
|
describe('member commands', () => {
|
|
30
|
+
let mockListMemberships: ReturnType<typeof spyOn>
|
|
31
|
+
let mockLogin: ReturnType<typeof spyOn>
|
|
46
32
|
let consoleSpy: ReturnType<typeof spyOn>
|
|
33
|
+
let consoleErrorSpy: ReturnType<typeof spyOn>
|
|
34
|
+
let processExitSpy: ReturnType<typeof spyOn>
|
|
35
|
+
const protoSpies: ReturnType<typeof spyOn>[] = []
|
|
36
|
+
|
|
37
|
+
function protoSpy(method: keyof WebexClient) {
|
|
38
|
+
const s = spyOn(WebexClient.prototype, method as never)
|
|
39
|
+
protoSpies.push(s)
|
|
40
|
+
return s
|
|
41
|
+
}
|
|
47
42
|
|
|
48
43
|
beforeEach(() => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
mockHandleError.mockReset().mockImplementation((err: Error) => {
|
|
52
|
-
throw err
|
|
44
|
+
mockLogin = protoSpy('login').mockImplementation(async function (this: WebexClient) {
|
|
45
|
+
return this
|
|
53
46
|
})
|
|
47
|
+
mockListMemberships = protoSpy('listMemberships').mockResolvedValue(mockMembers)
|
|
54
48
|
|
|
55
49
|
consoleSpy = spyOn(console, 'log').mockImplementation(() => {})
|
|
50
|
+
consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
|
|
51
|
+
processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => undefined as never)
|
|
56
52
|
})
|
|
57
53
|
|
|
58
54
|
afterEach(() => {
|
|
59
55
|
consoleSpy.mockRestore()
|
|
56
|
+
consoleErrorSpy.mockRestore()
|
|
57
|
+
processExitSpy.mockRestore()
|
|
58
|
+
for (const s of protoSpies) s.mockRestore()
|
|
59
|
+
protoSpies.length = 0
|
|
60
60
|
})
|
|
61
61
|
|
|
62
62
|
it('calls listMemberships with spaceId and outputs mapped members', async () => {
|
|
@@ -91,23 +91,19 @@ describe('member commands', () => {
|
|
|
91
91
|
expect(mockListMemberships).toHaveBeenCalledWith('room-1', { max: 25 })
|
|
92
92
|
})
|
|
93
93
|
|
|
94
|
-
it('
|
|
95
|
-
mockLogin.
|
|
96
|
-
throw new WebexError('No Webex credentials found.', 'no_credentials')
|
|
97
|
-
})
|
|
94
|
+
it('exits with code 1 when not authenticated', async () => {
|
|
95
|
+
mockLogin.mockRejectedValue(new WebexError('No Webex credentials found.', 'no_credentials'))
|
|
98
96
|
|
|
99
|
-
await
|
|
97
|
+
await listAction('room-1', {})
|
|
100
98
|
|
|
101
|
-
expect(
|
|
99
|
+
expect(processExitSpy).toHaveBeenCalledWith(1)
|
|
102
100
|
})
|
|
103
101
|
|
|
104
|
-
it('
|
|
105
|
-
mockListMemberships.
|
|
106
|
-
throw new Error('API failure')
|
|
107
|
-
})
|
|
102
|
+
it('exits with code 1 on API error', async () => {
|
|
103
|
+
mockListMemberships.mockRejectedValue(new Error('API failure'))
|
|
108
104
|
|
|
109
|
-
await
|
|
105
|
+
await listAction('room-1', {})
|
|
110
106
|
|
|
111
|
-
expect(
|
|
107
|
+
expect(processExitSpy).toHaveBeenCalledWith(1)
|
|
112
108
|
})
|
|
113
109
|
})
|
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
import { afterEach, beforeEach, expect,
|
|
1
|
+
import { afterEach, beforeEach, expect, spyOn, it } from 'bun:test'
|
|
2
2
|
|
|
3
|
+
import { WebexClient } from '../client'
|
|
3
4
|
import { WebexError } from '../types'
|
|
4
5
|
|
|
5
|
-
const mockHandleError = mock((err: Error) => {
|
|
6
|
-
throw err
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
mock.module('@/shared/utils/error-handler', () => ({
|
|
10
|
-
handleError: mockHandleError,
|
|
11
|
-
}))
|
|
12
|
-
|
|
13
6
|
const mockMessage = {
|
|
14
7
|
id: 'msg_123',
|
|
15
8
|
roomId: 'space_456',
|
|
@@ -30,51 +23,48 @@ const mockMessage2 = {
|
|
|
30
23
|
created: '2025-01-29T10:01:00.000Z',
|
|
31
24
|
}
|
|
32
25
|
|
|
33
|
-
const mockSendMessage = mock(() => Promise.resolve(mockMessage))
|
|
34
|
-
const mockSendDirectMessage = mock(() => Promise.resolve(mockMessage))
|
|
35
|
-
const mockListMessages = mock(() => Promise.resolve([mockMessage, mockMessage2]))
|
|
36
|
-
const mockGetMessage = mock(() => Promise.resolve(mockMessage))
|
|
37
|
-
const mockDeleteMessage = mock(() => Promise.resolve(undefined))
|
|
38
|
-
const mockEditMessage = mock(() => Promise.resolve({ ...mockMessage, text: 'Updated message' }))
|
|
39
|
-
|
|
40
|
-
const mockClient = {
|
|
41
|
-
sendMessage: mockSendMessage,
|
|
42
|
-
sendDirectMessage: mockSendDirectMessage,
|
|
43
|
-
listMessages: mockListMessages,
|
|
44
|
-
getMessage: mockGetMessage,
|
|
45
|
-
deleteMessage: mockDeleteMessage,
|
|
46
|
-
editMessage: mockEditMessage,
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const mockLogin = mock(() => Promise.resolve(mockClient))
|
|
50
|
-
|
|
51
|
-
mock.module('../client', () => ({
|
|
52
|
-
WebexClient: class {
|
|
53
|
-
login = mockLogin
|
|
54
|
-
},
|
|
55
|
-
}))
|
|
56
|
-
|
|
57
26
|
import { deleteAction, dmAction, editAction, getAction, listAction, sendAction } from './message'
|
|
58
27
|
|
|
28
|
+
let mockSendMessage: ReturnType<typeof spyOn>
|
|
29
|
+
let mockSendDirectMessage: ReturnType<typeof spyOn>
|
|
30
|
+
let mockListMessages: ReturnType<typeof spyOn>
|
|
31
|
+
let mockGetMessage: ReturnType<typeof spyOn>
|
|
32
|
+
let mockDeleteMessage: ReturnType<typeof spyOn>
|
|
33
|
+
let mockEditMessage: ReturnType<typeof spyOn>
|
|
34
|
+
let mockLogin: ReturnType<typeof spyOn>
|
|
59
35
|
let consoleLogSpy: ReturnType<typeof spyOn>
|
|
36
|
+
let consoleErrorSpy: ReturnType<typeof spyOn>
|
|
37
|
+
let processExitSpy: ReturnType<typeof spyOn>
|
|
38
|
+
const protoSpies: ReturnType<typeof spyOn>[] = []
|
|
39
|
+
|
|
40
|
+
function protoSpy(method: keyof WebexClient) {
|
|
41
|
+
const s = spyOn(WebexClient.prototype, method as never)
|
|
42
|
+
protoSpies.push(s)
|
|
43
|
+
return s
|
|
44
|
+
}
|
|
60
45
|
|
|
61
46
|
beforeEach(() => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
mockListMessages.mockReset().mockImplementation(() => Promise.resolve([mockMessage, mockMessage2]))
|
|
65
|
-
mockGetMessage.mockReset().mockImplementation(() => Promise.resolve(mockMessage))
|
|
66
|
-
mockDeleteMessage.mockReset().mockImplementation(() => Promise.resolve(undefined))
|
|
67
|
-
mockEditMessage.mockReset().mockImplementation(() => Promise.resolve({ ...mockMessage, text: 'Updated message' }))
|
|
68
|
-
mockLogin.mockReset().mockImplementation(() => Promise.resolve(mockClient))
|
|
69
|
-
mockHandleError.mockReset().mockImplementation((err: Error) => {
|
|
70
|
-
throw err
|
|
47
|
+
mockLogin = protoSpy('login').mockImplementation(async function (this: WebexClient) {
|
|
48
|
+
return this
|
|
71
49
|
})
|
|
50
|
+
mockSendMessage = protoSpy('sendMessage').mockResolvedValue(mockMessage)
|
|
51
|
+
mockSendDirectMessage = protoSpy('sendDirectMessage').mockResolvedValue(mockMessage)
|
|
52
|
+
mockListMessages = protoSpy('listMessages').mockResolvedValue([mockMessage, mockMessage2])
|
|
53
|
+
mockGetMessage = protoSpy('getMessage').mockResolvedValue(mockMessage)
|
|
54
|
+
mockDeleteMessage = protoSpy('deleteMessage').mockResolvedValue(undefined)
|
|
55
|
+
mockEditMessage = protoSpy('editMessage').mockResolvedValue({ ...mockMessage, text: 'Updated message' })
|
|
72
56
|
|
|
73
57
|
consoleLogSpy = spyOn(console, 'log').mockImplementation(() => {})
|
|
58
|
+
consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
|
|
59
|
+
processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => undefined as never)
|
|
74
60
|
})
|
|
75
61
|
|
|
76
62
|
afterEach(() => {
|
|
77
63
|
consoleLogSpy.mockRestore()
|
|
64
|
+
consoleErrorSpy.mockRestore()
|
|
65
|
+
processExitSpy.mockRestore()
|
|
66
|
+
for (const s of protoSpies) s.mockRestore()
|
|
67
|
+
protoSpies.length = 0
|
|
78
68
|
})
|
|
79
69
|
|
|
80
70
|
it('calls sendMessage with correct args and outputs result', async () => {
|
|
@@ -96,14 +86,13 @@ it('passes markdown option when --markdown flag is set on send', async () => {
|
|
|
96
86
|
expect(mockSendMessage).toHaveBeenCalledWith('space_456', '**bold**', { markdown: true })
|
|
97
87
|
})
|
|
98
88
|
|
|
99
|
-
it('
|
|
100
|
-
mockLogin.
|
|
101
|
-
throw new WebexError('No Webex credentials found.', 'no_credentials')
|
|
102
|
-
})
|
|
89
|
+
it('exits with code 1 when not authenticated on send', async () => {
|
|
90
|
+
mockLogin.mockRejectedValue(new WebexError('No Webex credentials found.', 'no_credentials'))
|
|
103
91
|
|
|
104
|
-
await
|
|
92
|
+
await sendAction('space_456', 'Hello', { pretty: false })
|
|
105
93
|
|
|
106
|
-
expect(
|
|
94
|
+
expect(mockSendMessage).not.toHaveBeenCalled()
|
|
95
|
+
expect(processExitSpy).toHaveBeenCalledWith(1)
|
|
107
96
|
})
|
|
108
97
|
|
|
109
98
|
it('calls sendDirectMessage with email and text', async () => {
|
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect,
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, spyOn, it } from 'bun:test'
|
|
2
2
|
|
|
3
|
+
import { WebexClient } from '../client'
|
|
3
4
|
import { WebexError } from '../types'
|
|
4
5
|
|
|
5
|
-
const mockHandleError = mock((err: Error) => {
|
|
6
|
-
throw err
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
mock.module('@/shared/utils/error-handler', () => ({
|
|
10
|
-
handleError: mockHandleError,
|
|
11
|
-
}))
|
|
12
|
-
|
|
13
6
|
const mockSpaces = [
|
|
14
7
|
{
|
|
15
8
|
id: 'space-1',
|
|
@@ -43,40 +36,39 @@ const mockMyMemberships = [
|
|
|
43
36
|
},
|
|
44
37
|
]
|
|
45
38
|
|
|
46
|
-
const mockListSpaces = mock(() => Promise.resolve(mockSpaces as any))
|
|
47
|
-
const mockListMyMemberships = mock(() => Promise.resolve(mockMyMemberships as any))
|
|
48
|
-
|
|
49
|
-
const mockClient = {
|
|
50
|
-
listSpaces: mockListSpaces,
|
|
51
|
-
listMyMemberships: mockListMyMemberships,
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const mockLogin = mock(() => Promise.resolve(mockClient))
|
|
55
|
-
|
|
56
|
-
mock.module('../client', () => ({
|
|
57
|
-
WebexClient: class {
|
|
58
|
-
login = mockLogin
|
|
59
|
-
},
|
|
60
|
-
}))
|
|
61
|
-
|
|
62
39
|
import { snapshotAction } from './snapshot'
|
|
63
40
|
|
|
64
41
|
describe('snapshot command', () => {
|
|
42
|
+
let mockLogin: ReturnType<typeof spyOn>
|
|
65
43
|
let consoleSpy: ReturnType<typeof spyOn>
|
|
44
|
+
let consoleErrorSpy: ReturnType<typeof spyOn>
|
|
45
|
+
let processExitSpy: ReturnType<typeof spyOn>
|
|
46
|
+
const protoSpies: ReturnType<typeof spyOn>[] = []
|
|
47
|
+
|
|
48
|
+
function protoSpy(method: keyof WebexClient) {
|
|
49
|
+
const s = spyOn(WebexClient.prototype, method as never)
|
|
50
|
+
protoSpies.push(s)
|
|
51
|
+
return s
|
|
52
|
+
}
|
|
66
53
|
|
|
67
54
|
beforeEach(() => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
mockLogin.mockReset().mockImplementation(() => Promise.resolve(mockClient))
|
|
71
|
-
mockHandleError.mockReset().mockImplementation((err: Error) => {
|
|
72
|
-
throw err
|
|
55
|
+
mockLogin = protoSpy('login').mockImplementation(async function (this: WebexClient) {
|
|
56
|
+
return this
|
|
73
57
|
})
|
|
58
|
+
protoSpy('listSpaces').mockResolvedValue(mockSpaces as any)
|
|
59
|
+
protoSpy('listMyMemberships').mockResolvedValue(mockMyMemberships as any)
|
|
74
60
|
|
|
75
61
|
consoleSpy = spyOn(console, 'log').mockImplementation(() => {})
|
|
62
|
+
consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
|
|
63
|
+
processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => undefined as never)
|
|
76
64
|
})
|
|
77
65
|
|
|
78
66
|
afterEach(() => {
|
|
79
67
|
consoleSpy.mockRestore()
|
|
68
|
+
consoleErrorSpy.mockRestore()
|
|
69
|
+
processExitSpy.mockRestore()
|
|
70
|
+
for (const s of protoSpies) s.mockRestore()
|
|
71
|
+
protoSpies.length = 0
|
|
80
72
|
})
|
|
81
73
|
|
|
82
74
|
it('returns spaces with id and title only in brief mode', async () => {
|
|
@@ -112,13 +104,11 @@ describe('snapshot command', () => {
|
|
|
112
104
|
expect(output.spaces[0].id).toBe('space-1')
|
|
113
105
|
})
|
|
114
106
|
|
|
115
|
-
it('
|
|
116
|
-
mockLogin.
|
|
117
|
-
throw new WebexError('No Webex credentials found.', 'no_credentials')
|
|
118
|
-
})
|
|
107
|
+
it('exits with code 1 when not authenticated', async () => {
|
|
108
|
+
mockLogin.mockRejectedValue(new WebexError('No Webex credentials found.', 'no_credentials'))
|
|
119
109
|
|
|
120
|
-
await
|
|
110
|
+
await snapshotAction({})
|
|
121
111
|
|
|
122
|
-
expect(
|
|
112
|
+
expect(processExitSpy).toHaveBeenCalledWith(1)
|
|
123
113
|
})
|
|
124
114
|
})
|
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect,
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, spyOn, it } from 'bun:test'
|
|
2
2
|
|
|
3
|
+
import { WebexClient } from '../client'
|
|
3
4
|
import { WebexError } from '../types'
|
|
4
5
|
|
|
5
|
-
const mockHandleError = mock((err: Error) => {
|
|
6
|
-
throw err
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
mock.module('@/shared/utils/error-handler', () => ({
|
|
10
|
-
handleError: mockHandleError,
|
|
11
|
-
}))
|
|
12
|
-
|
|
13
6
|
const mockSpaces = [
|
|
14
7
|
{
|
|
15
8
|
id: 'space-1',
|
|
@@ -42,35 +35,40 @@ const mockSpace = {
|
|
|
42
35
|
creatorId: 'person-1',
|
|
43
36
|
}
|
|
44
37
|
|
|
45
|
-
const mockListSpaces = mock(() => Promise.resolve(mockSpaces))
|
|
46
|
-
const mockGetSpace = mock(() => Promise.resolve(mockSpace))
|
|
47
|
-
const mockLogin = mock(() => Promise.resolve({ listSpaces: mockListSpaces, getSpace: mockGetSpace }))
|
|
48
|
-
|
|
49
|
-
mock.module('../client', () => ({
|
|
50
|
-
WebexClient: class {
|
|
51
|
-
login = mockLogin
|
|
52
|
-
},
|
|
53
|
-
}))
|
|
54
|
-
|
|
55
38
|
import { infoAction, listAction } from './space'
|
|
56
39
|
|
|
40
|
+
let mockListSpaces: ReturnType<typeof spyOn>
|
|
41
|
+
let mockGetSpace: ReturnType<typeof spyOn>
|
|
42
|
+
let mockLogin: ReturnType<typeof spyOn>
|
|
57
43
|
let consoleLogSpy: ReturnType<typeof spyOn>
|
|
44
|
+
let consoleErrorSpy: ReturnType<typeof spyOn>
|
|
45
|
+
let processExitSpy: ReturnType<typeof spyOn>
|
|
46
|
+
const protoSpies: ReturnType<typeof spyOn>[] = []
|
|
47
|
+
|
|
48
|
+
function protoSpy(method: keyof WebexClient) {
|
|
49
|
+
const s = spyOn(WebexClient.prototype, method as never)
|
|
50
|
+
protoSpies.push(s)
|
|
51
|
+
return s
|
|
52
|
+
}
|
|
58
53
|
|
|
59
54
|
beforeEach(() => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
mockLogin
|
|
63
|
-
.mockReset()
|
|
64
|
-
.mockImplementation(() => Promise.resolve({ listSpaces: mockListSpaces, getSpace: mockGetSpace }))
|
|
65
|
-
mockHandleError.mockReset().mockImplementation((err: Error) => {
|
|
66
|
-
throw err
|
|
55
|
+
mockLogin = protoSpy('login').mockImplementation(async function (this: WebexClient) {
|
|
56
|
+
return this
|
|
67
57
|
})
|
|
58
|
+
mockListSpaces = protoSpy('listSpaces').mockResolvedValue(mockSpaces)
|
|
59
|
+
mockGetSpace = protoSpy('getSpace').mockResolvedValue(mockSpace)
|
|
68
60
|
|
|
69
61
|
consoleLogSpy = spyOn(console, 'log').mockImplementation(() => {})
|
|
62
|
+
consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
|
|
63
|
+
processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => undefined as never)
|
|
70
64
|
})
|
|
71
65
|
|
|
72
66
|
afterEach(() => {
|
|
73
67
|
consoleLogSpy.mockRestore()
|
|
68
|
+
consoleErrorSpy.mockRestore()
|
|
69
|
+
processExitSpy.mockRestore()
|
|
70
|
+
for (const s of protoSpies) s.mockRestore()
|
|
71
|
+
protoSpies.length = 0
|
|
74
72
|
})
|
|
75
73
|
|
|
76
74
|
describe('listAction', () => {
|
|
@@ -137,15 +135,13 @@ describe('listAction', () => {
|
|
|
137
135
|
)
|
|
138
136
|
})
|
|
139
137
|
|
|
140
|
-
it('
|
|
141
|
-
mockLogin.
|
|
142
|
-
throw new WebexError('No Webex credentials found.', 'no_credentials')
|
|
143
|
-
})
|
|
138
|
+
it('exits with code 1 when not authenticated', async () => {
|
|
139
|
+
mockLogin.mockRejectedValue(new WebexError('No Webex credentials found.', 'no_credentials'))
|
|
144
140
|
|
|
145
|
-
await
|
|
141
|
+
await listAction({})
|
|
146
142
|
|
|
147
143
|
expect(mockListSpaces).not.toHaveBeenCalled()
|
|
148
|
-
expect(
|
|
144
|
+
expect(processExitSpy).toHaveBeenCalledWith(1)
|
|
149
145
|
})
|
|
150
146
|
})
|
|
151
147
|
|
|
@@ -200,14 +196,12 @@ describe('infoAction', () => {
|
|
|
200
196
|
)
|
|
201
197
|
})
|
|
202
198
|
|
|
203
|
-
it('
|
|
204
|
-
mockLogin.
|
|
205
|
-
throw new WebexError('No Webex credentials found.', 'no_credentials')
|
|
206
|
-
})
|
|
199
|
+
it('exits with code 1 when not authenticated', async () => {
|
|
200
|
+
mockLogin.mockRejectedValue(new WebexError('No Webex credentials found.', 'no_credentials'))
|
|
207
201
|
|
|
208
|
-
await
|
|
202
|
+
await infoAction('space-1', {})
|
|
209
203
|
|
|
210
204
|
expect(mockGetSpace).not.toHaveBeenCalled()
|
|
211
|
-
expect(
|
|
205
|
+
expect(processExitSpy).toHaveBeenCalledWith(1)
|
|
212
206
|
})
|
|
213
207
|
})
|