agent-messenger 2.12.2 → 2.13.0
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/dist/package.json +1 -1
- package/dist/src/platforms/kakaotalk/chat-classifier.d.ts +18 -0
- package/dist/src/platforms/kakaotalk/chat-classifier.d.ts.map +1 -0
- package/dist/src/platforms/kakaotalk/chat-classifier.js +29 -0
- package/dist/src/platforms/kakaotalk/chat-classifier.js.map +1 -0
- package/dist/src/platforms/kakaotalk/cli.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/cli.js +2 -1
- package/dist/src/platforms/kakaotalk/cli.js.map +1 -1
- package/dist/src/platforms/kakaotalk/client.d.ts +13 -1
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +225 -8
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/chat.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/chat.js +2 -1
- package/dist/src/platforms/kakaotalk/commands/chat.js.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/index.d.ts +1 -0
- package/dist/src/platforms/kakaotalk/commands/index.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/index.js +1 -0
- package/dist/src/platforms/kakaotalk/commands/index.js.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/member.d.ts +3 -0
- package/dist/src/platforms/kakaotalk/commands/member.d.ts.map +1 -0
- package/dist/src/platforms/kakaotalk/commands/member.js +22 -0
- package/dist/src/platforms/kakaotalk/commands/member.js.map +1 -0
- package/dist/src/platforms/kakaotalk/index.d.ts +4 -2
- package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/index.js +2 -1
- package/dist/src/platforms/kakaotalk/index.js.map +1 -1
- package/dist/src/platforms/kakaotalk/listener.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/listener.js +5 -2
- package/dist/src/platforms/kakaotalk/listener.js.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/session.d.ts +28 -0
- package/dist/src/platforms/kakaotalk/protocol/session.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/session.js +44 -0
- package/dist/src/platforms/kakaotalk/protocol/session.js.map +1 -1
- package/dist/src/platforms/kakaotalk/types.d.ts +37 -0
- package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/types.js +17 -0
- package/dist/src/platforms/kakaotalk/types.js.map +1 -1
- package/dist/src/platforms/slackbot/client.d.ts +5 -0
- package/dist/src/platforms/slackbot/client.d.ts.map +1 -1
- package/dist/src/platforms/slackbot/client.js +5 -0
- package/dist/src/platforms/slackbot/client.js.map +1 -1
- package/dist/src/tui/adapters/kakaotalk-adapter.js +3 -3
- package/dist/src/tui/adapters/kakaotalk-adapter.js.map +1 -1
- package/docs/content/docs/cli/kakaotalk.mdx +26 -1
- package/docs/content/docs/sdk/kakaotalk.mdx +45 -13
- package/docs/content/docs/sdk/slackbot.mdx +11 -0
- package/package.json +1 -1
- package/scripts/kakao-loco-capture.ts +466 -0
- 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 +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +30 -3
- package/skills/agent-kakaotalk/references/common-patterns.md +49 -1
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -2
- 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/kakaotalk/chat-classifier.test.ts +33 -0
- package/src/platforms/kakaotalk/chat-classifier.ts +31 -0
- package/src/platforms/kakaotalk/cli.ts +2 -1
- package/src/platforms/kakaotalk/client.test.ts +782 -1
- package/src/platforms/kakaotalk/client.ts +262 -10
- package/src/platforms/kakaotalk/commands/chat.ts +3 -1
- package/src/platforms/kakaotalk/commands/index.ts +1 -0
- package/src/platforms/kakaotalk/commands/member.test.ts +102 -0
- package/src/platforms/kakaotalk/commands/member.ts +32 -0
- package/src/platforms/kakaotalk/index.test.ts +5 -0
- package/src/platforms/kakaotalk/index.ts +4 -0
- package/src/platforms/kakaotalk/listener.test.ts +29 -0
- package/src/platforms/kakaotalk/listener.ts +5 -2
- package/src/platforms/kakaotalk/protocol/session.ts +44 -0
- package/src/platforms/kakaotalk/types.ts +39 -0
- package/src/platforms/slackbot/client.test.ts +67 -0
- package/src/platforms/slackbot/client.ts +17 -1
- package/src/tui/adapters/kakaotalk-adapter.ts +3 -3
|
@@ -7,6 +7,10 @@ const mockLogin = mock(() => Promise.resolve({}))
|
|
|
7
7
|
const mockGetChatList = mock(() => Promise.resolve({}))
|
|
8
8
|
const mockGetChatLogs = mock(() => Promise.resolve({}))
|
|
9
9
|
const mockGetChatInfo = mock(() => Promise.resolve({}))
|
|
10
|
+
const mockGetChannelInfo = mock(() => Promise.resolve({}))
|
|
11
|
+
const mockGetOpenLinkInfo = mock(() => Promise.resolve({}))
|
|
12
|
+
const mockGetAllMembers = mock(() => Promise.resolve({}))
|
|
13
|
+
const mockGetMembersByIds = mock(() => Promise.resolve({}))
|
|
10
14
|
const mockSyncMessages = mock(() => Promise.resolve({}))
|
|
11
15
|
const mockSendMessage = mock(() => Promise.resolve({}))
|
|
12
16
|
const mockClose = mock(() => {})
|
|
@@ -19,6 +23,10 @@ mock.module('./protocol/session', () => ({
|
|
|
19
23
|
getChatList = mockGetChatList
|
|
20
24
|
getChatLogs = mockGetChatLogs
|
|
21
25
|
getChatInfo = mockGetChatInfo
|
|
26
|
+
getChannelInfo = mockGetChannelInfo
|
|
27
|
+
getOpenLinkInfo = mockGetOpenLinkInfo
|
|
28
|
+
getAllMembers = mockGetAllMembers
|
|
29
|
+
getMembersByIds = mockGetMembersByIds
|
|
22
30
|
syncMessages = mockSyncMessages
|
|
23
31
|
sendMessage = mockSendMessage
|
|
24
32
|
close = mockClose
|
|
@@ -36,6 +44,10 @@ function resetAllMocks() {
|
|
|
36
44
|
mockGetChatList.mockReset()
|
|
37
45
|
mockGetChatLogs.mockReset()
|
|
38
46
|
mockGetChatInfo.mockReset()
|
|
47
|
+
mockGetChannelInfo.mockReset()
|
|
48
|
+
mockGetOpenLinkInfo.mockReset()
|
|
49
|
+
mockGetAllMembers.mockReset()
|
|
50
|
+
mockGetMembersByIds.mockReset()
|
|
39
51
|
mockSyncMessages.mockReset()
|
|
40
52
|
mockSendMessage.mockReset()
|
|
41
53
|
mockClose.mockReset()
|
|
@@ -43,13 +55,15 @@ function resetAllMocks() {
|
|
|
43
55
|
mockOnPush.mockReset()
|
|
44
56
|
}
|
|
45
57
|
|
|
46
|
-
// LOCO protocol uses plain numbers for chat.c, but Long-like objects for logIds/cursors
|
|
58
|
+
// LOCO protocol uses plain numbers for chat.c, but Long-like objects for logIds/cursors.
|
|
59
|
+
// `i` and `k` are paired arrays — i[n] is the user_id, k[n] is the nickname.
|
|
47
60
|
const DEFAULT_LOGIN_RESULT = {
|
|
48
61
|
chatDatas: [
|
|
49
62
|
{
|
|
50
63
|
c: 100,
|
|
51
64
|
t: 1,
|
|
52
65
|
k: ['Alice', 'Bob'],
|
|
66
|
+
i: [1, 2],
|
|
53
67
|
a: 2,
|
|
54
68
|
n: 3,
|
|
55
69
|
o: 1700000000,
|
|
@@ -60,6 +74,7 @@ const DEFAULT_LOGIN_RESULT = {
|
|
|
60
74
|
c: 200,
|
|
61
75
|
t: 2,
|
|
62
76
|
k: ['Charlie'],
|
|
77
|
+
i: [3],
|
|
63
78
|
a: 1,
|
|
64
79
|
n: 0,
|
|
65
80
|
o: 1699999000,
|
|
@@ -123,19 +138,248 @@ describe('KakaoTalkClient', () => {
|
|
|
123
138
|
|
|
124
139
|
expect(chats).toHaveLength(2)
|
|
125
140
|
expect(chats[0].display_name).toBe('Alice, Bob')
|
|
141
|
+
expect(chats[0].title).toBeNull()
|
|
126
142
|
expect(chats[0].active_members).toBe(2)
|
|
127
143
|
expect(chats[0].unread_count).toBe(3)
|
|
128
144
|
expect(chats[0].last_message).toEqual({
|
|
129
145
|
author_id: 1,
|
|
146
|
+
author_name: 'Alice',
|
|
130
147
|
message: 'hi',
|
|
131
148
|
sent_at: 1700000000,
|
|
132
149
|
})
|
|
133
150
|
expect(chats[1].display_name).toBe('Charlie')
|
|
151
|
+
expect(chats[1].title).toBeNull()
|
|
134
152
|
expect(chats[1].last_message).toBeNull()
|
|
135
153
|
|
|
136
154
|
client.close()
|
|
137
155
|
})
|
|
138
156
|
|
|
157
|
+
it('populates last_message.author_name from paired chat.i / chat.k arrays', async () => {
|
|
158
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
159
|
+
const chats = await client.getChats()
|
|
160
|
+
|
|
161
|
+
expect(chats[0].last_message?.author_name).toBe('Alice')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('returns null author_name when authorId is not in the paired arrays', async () => {
|
|
165
|
+
const customLogin = {
|
|
166
|
+
...DEFAULT_LOGIN_RESULT,
|
|
167
|
+
chatDatas: [
|
|
168
|
+
{
|
|
169
|
+
c: 100,
|
|
170
|
+
t: 1,
|
|
171
|
+
k: ['Alice'],
|
|
172
|
+
i: [1],
|
|
173
|
+
a: 1,
|
|
174
|
+
n: 0,
|
|
175
|
+
o: 1700000000,
|
|
176
|
+
l: { authorId: 99, message: 'from a stranger', sendAt: 1700000000 },
|
|
177
|
+
ll: makeLong(999),
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
}
|
|
181
|
+
mockLogin.mockResolvedValue(customLogin)
|
|
182
|
+
|
|
183
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
184
|
+
const chats = await client.getChats()
|
|
185
|
+
|
|
186
|
+
expect(chats[0].last_message?.author_id).toBe(99)
|
|
187
|
+
expect(chats[0].last_message?.author_name).toBeNull()
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('returns null author_name when chat.i is missing (no paired data)', async () => {
|
|
191
|
+
const customLogin = {
|
|
192
|
+
...DEFAULT_LOGIN_RESULT,
|
|
193
|
+
chatDatas: [
|
|
194
|
+
{
|
|
195
|
+
c: 100,
|
|
196
|
+
t: 1,
|
|
197
|
+
k: ['Alice'],
|
|
198
|
+
a: 1,
|
|
199
|
+
n: 0,
|
|
200
|
+
o: 1700000000,
|
|
201
|
+
l: { authorId: 1, message: 'hi', sendAt: 1700000000 },
|
|
202
|
+
ll: makeLong(999),
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
}
|
|
206
|
+
mockLogin.mockResolvedValue(customLogin)
|
|
207
|
+
|
|
208
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
209
|
+
const chats = await client.getChats()
|
|
210
|
+
|
|
211
|
+
expect(chats[0].last_message?.author_name).toBeNull()
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('does not call CHATINFO when resolveTitles is not set (default behavior preserved)', async () => {
|
|
215
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
216
|
+
await client.getChats()
|
|
217
|
+
|
|
218
|
+
expect(mockGetChannelInfo).not.toHaveBeenCalled()
|
|
219
|
+
|
|
220
|
+
client.close()
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('resolves user-set titles via CHATINFO when resolveTitles=true', async () => {
|
|
224
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
225
|
+
body: {
|
|
226
|
+
chatInfo: {
|
|
227
|
+
chatMetas: [
|
|
228
|
+
{ type: 1, content: '{"notice":"hello"}', revision: makeLong(1), updatedAt: 0 },
|
|
229
|
+
{ type: 3, content: 'Renamed Chat', revision: makeLong(2), updatedAt: 0 },
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
})
|
|
234
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
235
|
+
body: {
|
|
236
|
+
chatInfo: {
|
|
237
|
+
chatMetas: [],
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
243
|
+
const chats = await client.getChats({ resolveTitles: true })
|
|
244
|
+
|
|
245
|
+
expect(mockGetChannelInfo).toHaveBeenCalledTimes(2)
|
|
246
|
+
expect(chats[0].title).toBe('Renamed Chat')
|
|
247
|
+
expect(chats[0].display_name).toBe('Alice, Bob')
|
|
248
|
+
expect(chats[1].title).toBeNull()
|
|
249
|
+
expect(chats[1].display_name).toBe('Charlie')
|
|
250
|
+
|
|
251
|
+
client.close()
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('returns null title when chatInfo or chatMetas is missing', async () => {
|
|
255
|
+
mockGetChannelInfo.mockResolvedValue({ body: {} })
|
|
256
|
+
|
|
257
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
258
|
+
const chats = await client.getChats({ resolveTitles: true })
|
|
259
|
+
|
|
260
|
+
for (const chat of chats) {
|
|
261
|
+
expect(chat.title).toBeNull()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
client.close()
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('swallows CHATINFO failures and returns null title (does not poison list)', async () => {
|
|
268
|
+
mockGetChannelInfo.mockRejectedValueOnce(new Error('CHATINFO timed out'))
|
|
269
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
270
|
+
body: { chatInfo: { chatMetas: [{ type: 3, content: 'OK Title' }] } },
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
274
|
+
const chats = await client.getChats({ resolveTitles: true })
|
|
275
|
+
|
|
276
|
+
expect(chats).toHaveLength(2)
|
|
277
|
+
expect(chats[0].title).toBeNull()
|
|
278
|
+
expect(chats[1].title).toBe('OK Title')
|
|
279
|
+
|
|
280
|
+
client.close()
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('ignores empty-string title content and returns null', async () => {
|
|
284
|
+
mockGetChannelInfo.mockResolvedValue({
|
|
285
|
+
body: { chatInfo: { chatMetas: [{ type: 3, content: '' }] } },
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
289
|
+
const chats = await client.getChats({ resolveTitles: true })
|
|
290
|
+
|
|
291
|
+
for (const chat of chats) {
|
|
292
|
+
expect(chat.title).toBeNull()
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
client.close()
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('resolves open-chat titles via INFOLINK fallback in batch resolveTitles=true path', async () => {
|
|
299
|
+
// Mixed login snapshot: regular chat (uses CHATINFO TITLE meta), open chat
|
|
300
|
+
// with TITLE meta (CHATINFO only), open chat without TITLE meta (CHATINFO
|
|
301
|
+
// empty + INFOLINK fallback). Verifies the batch path wires open-chat
|
|
302
|
+
// fallback correctly — previously only the single-chat getChatTitle()
|
|
303
|
+
// path was covered.
|
|
304
|
+
const mixedLogin = {
|
|
305
|
+
chatDatas: [
|
|
306
|
+
{
|
|
307
|
+
c: 100,
|
|
308
|
+
t: 1,
|
|
309
|
+
k: ['Alice'],
|
|
310
|
+
i: [1],
|
|
311
|
+
a: 1,
|
|
312
|
+
n: 0,
|
|
313
|
+
o: 1700000003,
|
|
314
|
+
l: null,
|
|
315
|
+
ll: makeLong(1),
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
c: 500,
|
|
319
|
+
t: 'OM',
|
|
320
|
+
li: makeLong(7777),
|
|
321
|
+
k: ['User1'],
|
|
322
|
+
i: [10],
|
|
323
|
+
a: 1,
|
|
324
|
+
n: 0,
|
|
325
|
+
o: 1700000002,
|
|
326
|
+
l: null,
|
|
327
|
+
ll: makeLong(2),
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
c: 600,
|
|
331
|
+
t: 'OD',
|
|
332
|
+
li: makeLong(8888),
|
|
333
|
+
k: ['User2'],
|
|
334
|
+
i: [20],
|
|
335
|
+
a: 1,
|
|
336
|
+
n: 0,
|
|
337
|
+
o: 1700000001,
|
|
338
|
+
l: null,
|
|
339
|
+
ll: makeLong(3),
|
|
340
|
+
},
|
|
341
|
+
],
|
|
342
|
+
lastTokenId: makeLong(0),
|
|
343
|
+
lastChatId: makeLong(0),
|
|
344
|
+
eof: true,
|
|
345
|
+
}
|
|
346
|
+
mockLogin.mockResolvedValue(mixedLogin)
|
|
347
|
+
|
|
348
|
+
// CHATINFO is fired in parallel for all 3 chats; mock by chatId so order
|
|
349
|
+
// doesn't matter (Promise.all resolves in fire order, not array order).
|
|
350
|
+
mockGetChannelInfo.mockImplementation((chatId: { low: number; high: number }) => {
|
|
351
|
+
switch (chatId.low) {
|
|
352
|
+
case 100:
|
|
353
|
+
return Promise.resolve({ body: { chatInfo: { chatMetas: [{ type: 3, content: 'Regular Title' }] } } })
|
|
354
|
+
case 500:
|
|
355
|
+
return Promise.resolve({ body: { chatInfo: { chatMetas: [{ type: 3, content: 'Open With Title' }] } } })
|
|
356
|
+
case 600:
|
|
357
|
+
return Promise.resolve({ body: { chatInfo: { chatMetas: [] } } })
|
|
358
|
+
default:
|
|
359
|
+
return Promise.resolve({ body: {} })
|
|
360
|
+
}
|
|
361
|
+
})
|
|
362
|
+
mockGetOpenLinkInfo.mockResolvedValueOnce({ body: { ols: [{ ln: 'Open Link Name' }] } })
|
|
363
|
+
|
|
364
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
365
|
+
const chats = await client.getChats({ resolveTitles: true })
|
|
366
|
+
|
|
367
|
+
const byId = Object.fromEntries(chats.map((c) => [c.chat_id, c]))
|
|
368
|
+
expect(byId['100'].title).toBe('Regular Title')
|
|
369
|
+
expect(byId['500'].title).toBe('Open With Title')
|
|
370
|
+
expect(byId['600'].title).toBe('Open Link Name')
|
|
371
|
+
|
|
372
|
+
// INFOLINK fires only for the open chat that lacked a TITLE meta — not
|
|
373
|
+
// for the regular chat (skipped by isOpenChat) and not for the open
|
|
374
|
+
// chat that already had a TITLE (skipped by short-circuit).
|
|
375
|
+
expect(mockGetOpenLinkInfo).toHaveBeenCalledTimes(1)
|
|
376
|
+
const [linkIds] = mockGetOpenLinkInfo.mock.calls[0] as [Array<{ low: number; high: number }>]
|
|
377
|
+
expect(linkIds).toHaveLength(1)
|
|
378
|
+
expect(linkIds[0]).toMatchObject({ low: 8888, high: 0 })
|
|
379
|
+
|
|
380
|
+
client.close()
|
|
381
|
+
})
|
|
382
|
+
|
|
139
383
|
it('sorts chats by recency (o field descending)', async () => {
|
|
140
384
|
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
141
385
|
const chats = await client.getChats()
|
|
@@ -307,6 +551,185 @@ describe('KakaoTalkClient', () => {
|
|
|
307
551
|
client.close()
|
|
308
552
|
})
|
|
309
553
|
|
|
554
|
+
it('exposes getChatTitle() for single-chat title resolution', async () => {
|
|
555
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
556
|
+
body: { chatInfo: { chatMetas: [{ type: 3, content: 'Direct Lookup' }] } },
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
560
|
+
const title = await client.getChatTitle('100')
|
|
561
|
+
|
|
562
|
+
expect(title).toBe('Direct Lookup')
|
|
563
|
+
expect(mockGetChannelInfo).toHaveBeenCalledTimes(1)
|
|
564
|
+
|
|
565
|
+
client.close()
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
it('getChatTitle() returns null on CHATINFO failure', async () => {
|
|
569
|
+
mockGetChannelInfo.mockRejectedValueOnce(new Error('Network error'))
|
|
570
|
+
|
|
571
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
572
|
+
const title = await client.getChatTitle('100')
|
|
573
|
+
|
|
574
|
+
expect(title).toBeNull()
|
|
575
|
+
|
|
576
|
+
client.close()
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
it('getChatTitle() returns null on invalid chatId without contacting LOCO', async () => {
|
|
580
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
581
|
+
const title = await client.getChatTitle('not-a-number')
|
|
582
|
+
|
|
583
|
+
expect(title).toBeNull()
|
|
584
|
+
expect(mockGetChannelInfo).not.toHaveBeenCalled()
|
|
585
|
+
expect(mockLogin).not.toHaveBeenCalled()
|
|
586
|
+
|
|
587
|
+
client.close()
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
it('falls back to INFOLINK link name for open chats with no TITLE meta', async () => {
|
|
591
|
+
const openChatLogin = {
|
|
592
|
+
chatDatas: [
|
|
593
|
+
{
|
|
594
|
+
c: 500,
|
|
595
|
+
t: 'OM',
|
|
596
|
+
li: makeLong(7777),
|
|
597
|
+
k: ['User1', 'User2'],
|
|
598
|
+
i: [10, 20],
|
|
599
|
+
a: 2,
|
|
600
|
+
n: 0,
|
|
601
|
+
o: 1700000000,
|
|
602
|
+
l: null,
|
|
603
|
+
ll: makeLong(1),
|
|
604
|
+
},
|
|
605
|
+
],
|
|
606
|
+
lastTokenId: makeLong(0),
|
|
607
|
+
lastChatId: makeLong(0),
|
|
608
|
+
eof: true,
|
|
609
|
+
}
|
|
610
|
+
mockLogin.mockResolvedValue(openChatLogin)
|
|
611
|
+
mockGetChannelInfo.mockResolvedValueOnce({ body: { chatInfo: { chatMetas: [] } } })
|
|
612
|
+
mockGetOpenLinkInfo.mockResolvedValueOnce({ body: { ols: [{ ln: 'Open Group Title' }] } })
|
|
613
|
+
|
|
614
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
615
|
+
const title = await client.getChatTitle('500')
|
|
616
|
+
|
|
617
|
+
expect(title).toBe('Open Group Title')
|
|
618
|
+
expect(mockGetChannelInfo).toHaveBeenCalledTimes(1)
|
|
619
|
+
expect(mockGetOpenLinkInfo).toHaveBeenCalledTimes(1)
|
|
620
|
+
|
|
621
|
+
client.close()
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
it('does not call INFOLINK for non-open chats (regular MultiChat)', async () => {
|
|
625
|
+
mockGetChannelInfo.mockResolvedValueOnce({ body: { chatInfo: { chatMetas: [] } } })
|
|
626
|
+
|
|
627
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
628
|
+
const title = await client.getChatTitle('100')
|
|
629
|
+
|
|
630
|
+
expect(title).toBeNull()
|
|
631
|
+
expect(mockGetOpenLinkInfo).not.toHaveBeenCalled()
|
|
632
|
+
|
|
633
|
+
client.close()
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
it('does not call INFOLINK when CHATINFO already returned a TITLE meta', async () => {
|
|
637
|
+
const openChatLogin = {
|
|
638
|
+
chatDatas: [
|
|
639
|
+
{
|
|
640
|
+
c: 500,
|
|
641
|
+
t: 'OM',
|
|
642
|
+
li: makeLong(7777),
|
|
643
|
+
k: [],
|
|
644
|
+
i: [],
|
|
645
|
+
a: 1,
|
|
646
|
+
n: 0,
|
|
647
|
+
o: 1700000000,
|
|
648
|
+
l: null,
|
|
649
|
+
ll: makeLong(1),
|
|
650
|
+
},
|
|
651
|
+
],
|
|
652
|
+
lastTokenId: makeLong(0),
|
|
653
|
+
lastChatId: makeLong(0),
|
|
654
|
+
eof: true,
|
|
655
|
+
}
|
|
656
|
+
mockLogin.mockResolvedValue(openChatLogin)
|
|
657
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
658
|
+
body: { chatInfo: { chatMetas: [{ type: 3, content: 'User Set Title' }] } },
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
662
|
+
const title = await client.getChatTitle('500')
|
|
663
|
+
|
|
664
|
+
expect(title).toBe('User Set Title')
|
|
665
|
+
expect(mockGetOpenLinkInfo).not.toHaveBeenCalled()
|
|
666
|
+
|
|
667
|
+
client.close()
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
it('returns null when INFOLINK fails for open chats', async () => {
|
|
671
|
+
const openChatLogin = {
|
|
672
|
+
chatDatas: [
|
|
673
|
+
{
|
|
674
|
+
c: 500,
|
|
675
|
+
t: 'OD',
|
|
676
|
+
li: makeLong(7777),
|
|
677
|
+
k: [],
|
|
678
|
+
i: [],
|
|
679
|
+
a: 1,
|
|
680
|
+
n: 0,
|
|
681
|
+
o: 1700000000,
|
|
682
|
+
l: null,
|
|
683
|
+
ll: makeLong(1),
|
|
684
|
+
},
|
|
685
|
+
],
|
|
686
|
+
lastTokenId: makeLong(0),
|
|
687
|
+
lastChatId: makeLong(0),
|
|
688
|
+
eof: true,
|
|
689
|
+
}
|
|
690
|
+
mockLogin.mockResolvedValue(openChatLogin)
|
|
691
|
+
mockGetChannelInfo.mockResolvedValueOnce({ body: { chatInfo: { chatMetas: [] } } })
|
|
692
|
+
mockGetOpenLinkInfo.mockRejectedValueOnce(new Error('INFOLINK timeout'))
|
|
693
|
+
|
|
694
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
695
|
+
const title = await client.getChatTitle('500')
|
|
696
|
+
|
|
697
|
+
expect(title).toBeNull()
|
|
698
|
+
|
|
699
|
+
client.close()
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
it('returns null when open chat has no link id (li missing)', async () => {
|
|
703
|
+
const openChatLogin = {
|
|
704
|
+
chatDatas: [
|
|
705
|
+
{
|
|
706
|
+
c: 500,
|
|
707
|
+
t: 'OM',
|
|
708
|
+
k: [],
|
|
709
|
+
i: [],
|
|
710
|
+
a: 1,
|
|
711
|
+
n: 0,
|
|
712
|
+
o: 1700000000,
|
|
713
|
+
l: null,
|
|
714
|
+
ll: makeLong(1),
|
|
715
|
+
},
|
|
716
|
+
],
|
|
717
|
+
lastTokenId: makeLong(0),
|
|
718
|
+
lastChatId: makeLong(0),
|
|
719
|
+
eof: true,
|
|
720
|
+
}
|
|
721
|
+
mockLogin.mockResolvedValue(openChatLogin)
|
|
722
|
+
mockGetChannelInfo.mockResolvedValueOnce({ body: { chatInfo: { chatMetas: [] } } })
|
|
723
|
+
|
|
724
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
725
|
+
const title = await client.getChatTitle('500')
|
|
726
|
+
|
|
727
|
+
expect(title).toBeNull()
|
|
728
|
+
expect(mockGetOpenLinkInfo).not.toHaveBeenCalled()
|
|
729
|
+
|
|
730
|
+
client.close()
|
|
731
|
+
})
|
|
732
|
+
|
|
310
733
|
it('wraps getChatList failure as KakaoTalkError with code get_chats_failed', async () => {
|
|
311
734
|
const loginResult = { ...DEFAULT_LOGIN_RESULT, eof: false }
|
|
312
735
|
mockLogin.mockResolvedValue(loginResult)
|
|
@@ -346,6 +769,7 @@ describe('KakaoTalkClient', () => {
|
|
|
346
769
|
log_id: '10',
|
|
347
770
|
type: 1,
|
|
348
771
|
author_id: 42,
|
|
772
|
+
author_name: null,
|
|
349
773
|
message: 'hello',
|
|
350
774
|
sent_at: 1700000001,
|
|
351
775
|
})
|
|
@@ -353,6 +777,7 @@ describe('KakaoTalkClient', () => {
|
|
|
353
777
|
log_id: '11',
|
|
354
778
|
type: 1,
|
|
355
779
|
author_id: 43,
|
|
780
|
+
author_name: null,
|
|
356
781
|
message: 'world',
|
|
357
782
|
sent_at: 1700000002,
|
|
358
783
|
})
|
|
@@ -360,6 +785,31 @@ describe('KakaoTalkClient', () => {
|
|
|
360
785
|
client.close()
|
|
361
786
|
})
|
|
362
787
|
|
|
788
|
+
it('resolves author_name for known members from the chat list cache', async () => {
|
|
789
|
+
mockGetChatLogs.mockResolvedValueOnce({
|
|
790
|
+
body: {
|
|
791
|
+
status: 0,
|
|
792
|
+
chatLogs: [
|
|
793
|
+
{ logId: makeLong(1), chatId: 100, type: 1, authorId: 1, message: 'from Alice', sendAt: 100 },
|
|
794
|
+
{ logId: makeLong(2), chatId: 100, type: 1, authorId: 2, message: 'from Bob', sendAt: 200 },
|
|
795
|
+
{ logId: makeLong(3), chatId: 100, type: 1, authorId: 99, message: 'from a stranger', sendAt: 300 },
|
|
796
|
+
],
|
|
797
|
+
eof: true,
|
|
798
|
+
},
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
802
|
+
// Trigger login so the cache is populated from DEFAULT_LOGIN_RESULT
|
|
803
|
+
await client.getChats()
|
|
804
|
+
const messages = await client.getMessages('100')
|
|
805
|
+
|
|
806
|
+
expect(messages[0].author_name).toBe('Alice')
|
|
807
|
+
expect(messages[1].author_name).toBe('Bob')
|
|
808
|
+
expect(messages[2].author_name).toBeNull()
|
|
809
|
+
|
|
810
|
+
client.close()
|
|
811
|
+
})
|
|
812
|
+
|
|
363
813
|
it('respects count option', async () => {
|
|
364
814
|
const logs = Array.from({ length: 50 }, (_, i) => ({
|
|
365
815
|
logId: makeLong(i + 1),
|
|
@@ -460,6 +910,337 @@ describe('KakaoTalkClient', () => {
|
|
|
460
910
|
})
|
|
461
911
|
})
|
|
462
912
|
|
|
913
|
+
describe('getMembers / getMembersByIds', () => {
|
|
914
|
+
it('returns formatted members from GETMEM with normalized fields', async () => {
|
|
915
|
+
mockGetAllMembers.mockResolvedValueOnce({
|
|
916
|
+
statusCode: 0,
|
|
917
|
+
body: {
|
|
918
|
+
members: [
|
|
919
|
+
{
|
|
920
|
+
userId: makeLong(42),
|
|
921
|
+
nickName: 'Alice',
|
|
922
|
+
type: 100,
|
|
923
|
+
profileImageUrl: 'https://kakao.com/p/alice.jpg',
|
|
924
|
+
fullProfileImageUrl: 'https://kakao.com/p/alice-full.jpg',
|
|
925
|
+
originalProfileImageUrl: 'https://kakao.com/p/alice-orig.jpg',
|
|
926
|
+
statusMessage: 'hi',
|
|
927
|
+
countryIso: 'KR',
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
userId: makeLong(43),
|
|
931
|
+
nickName: 'Bob',
|
|
932
|
+
type: 1000,
|
|
933
|
+
pi: 'https://kakao.com/p/bob.jpg',
|
|
934
|
+
fpi: 'https://kakao.com/p/bob-full.jpg',
|
|
935
|
+
opi: 'https://kakao.com/p/bob-orig.jpg',
|
|
936
|
+
opt: 12345,
|
|
937
|
+
pli: makeLong(99),
|
|
938
|
+
mt: 4,
|
|
939
|
+
},
|
|
940
|
+
],
|
|
941
|
+
token: 0,
|
|
942
|
+
},
|
|
943
|
+
})
|
|
944
|
+
|
|
945
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
946
|
+
const members = await client.getMembers('100')
|
|
947
|
+
|
|
948
|
+
expect(members).toHaveLength(2)
|
|
949
|
+
expect(members[0]).toEqual({
|
|
950
|
+
user_id: '42',
|
|
951
|
+
nickname: 'Alice',
|
|
952
|
+
profile_image_url: 'https://kakao.com/p/alice.jpg',
|
|
953
|
+
full_profile_image_url: 'https://kakao.com/p/alice-full.jpg',
|
|
954
|
+
original_profile_image_url: 'https://kakao.com/p/alice-orig.jpg',
|
|
955
|
+
status_message: 'hi',
|
|
956
|
+
country_iso: 'KR',
|
|
957
|
+
user_type: 100,
|
|
958
|
+
open_token: null,
|
|
959
|
+
open_profile_link_id: null,
|
|
960
|
+
open_permission: null,
|
|
961
|
+
})
|
|
962
|
+
expect(members[1]).toEqual({
|
|
963
|
+
user_id: '43',
|
|
964
|
+
nickname: 'Bob',
|
|
965
|
+
profile_image_url: 'https://kakao.com/p/bob.jpg',
|
|
966
|
+
full_profile_image_url: 'https://kakao.com/p/bob-full.jpg',
|
|
967
|
+
original_profile_image_url: 'https://kakao.com/p/bob-orig.jpg',
|
|
968
|
+
status_message: null,
|
|
969
|
+
country_iso: null,
|
|
970
|
+
user_type: 1000,
|
|
971
|
+
open_token: 12345,
|
|
972
|
+
open_profile_link_id: '99',
|
|
973
|
+
open_permission: 4,
|
|
974
|
+
})
|
|
975
|
+
|
|
976
|
+
client.close()
|
|
977
|
+
})
|
|
978
|
+
|
|
979
|
+
it('returns empty array when GETMEM returns no members', async () => {
|
|
980
|
+
mockGetAllMembers.mockResolvedValueOnce({ statusCode: 0, body: {} })
|
|
981
|
+
|
|
982
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
983
|
+
const members = await client.getMembers('100')
|
|
984
|
+
|
|
985
|
+
expect(members).toEqual([])
|
|
986
|
+
|
|
987
|
+
client.close()
|
|
988
|
+
})
|
|
989
|
+
|
|
990
|
+
it('normalizes missing user_type to null and treats pli=0 as absent', async () => {
|
|
991
|
+
mockGetAllMembers.mockResolvedValueOnce({
|
|
992
|
+
statusCode: 0,
|
|
993
|
+
body: {
|
|
994
|
+
members: [
|
|
995
|
+
{ userId: makeLong(1), nickName: 'NoType' },
|
|
996
|
+
{ userId: makeLong(2), nickName: 'ZeroPli', type: 1000, pli: makeLong(0) },
|
|
997
|
+
{ userId: makeLong(3), nickName: 'NumericZeroPli', type: 1000, pli: 0 },
|
|
998
|
+
],
|
|
999
|
+
},
|
|
1000
|
+
})
|
|
1001
|
+
|
|
1002
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1003
|
+
const members = await client.getMembers('100')
|
|
1004
|
+
|
|
1005
|
+
expect(members[0].user_type).toBeNull()
|
|
1006
|
+
expect(members[0].open_profile_link_id).toBeNull()
|
|
1007
|
+
expect(members[1].user_type).toBe(1000)
|
|
1008
|
+
expect(members[1].open_profile_link_id).toBeNull()
|
|
1009
|
+
expect(members[2].open_profile_link_id).toBeNull()
|
|
1010
|
+
|
|
1011
|
+
client.close()
|
|
1012
|
+
})
|
|
1013
|
+
|
|
1014
|
+
it('wraps GETMEM failures as KakaoTalkError get_members_failed', async () => {
|
|
1015
|
+
mockGetAllMembers.mockRejectedValueOnce(new Error('Network error'))
|
|
1016
|
+
|
|
1017
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1018
|
+
try {
|
|
1019
|
+
await client.getMembers('100')
|
|
1020
|
+
expect.unreachable('should have thrown')
|
|
1021
|
+
} catch (e) {
|
|
1022
|
+
expect(e).toBeInstanceOf(KakaoTalkError)
|
|
1023
|
+
expect((e as KakaoTalkError).code).toBe('get_members_failed')
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
client.close()
|
|
1027
|
+
})
|
|
1028
|
+
|
|
1029
|
+
it('throws on synthetic disconnect packet from GETMEM (statusCode != 0)', async () => {
|
|
1030
|
+
mockGetAllMembers.mockResolvedValueOnce({
|
|
1031
|
+
statusCode: -1,
|
|
1032
|
+
body: { error: 'connection closed' },
|
|
1033
|
+
})
|
|
1034
|
+
|
|
1035
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1036
|
+
try {
|
|
1037
|
+
await client.getMembers('100')
|
|
1038
|
+
expect.unreachable('should have thrown')
|
|
1039
|
+
} catch (e) {
|
|
1040
|
+
expect(e).toBeInstanceOf(KakaoTalkError)
|
|
1041
|
+
expect((e as KakaoTalkError).code).toBe('get_members_failed')
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
client.close()
|
|
1045
|
+
})
|
|
1046
|
+
|
|
1047
|
+
it('throws on GETMEM body.status nonzero', async () => {
|
|
1048
|
+
mockGetAllMembers.mockResolvedValueOnce({
|
|
1049
|
+
statusCode: 0,
|
|
1050
|
+
body: { status: -500 },
|
|
1051
|
+
})
|
|
1052
|
+
|
|
1053
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1054
|
+
try {
|
|
1055
|
+
await client.getMembers('100')
|
|
1056
|
+
expect.unreachable('should have thrown')
|
|
1057
|
+
} catch (e) {
|
|
1058
|
+
expect(e).toBeInstanceOf(KakaoTalkError)
|
|
1059
|
+
expect((e as KakaoTalkError).code).toBe('get_members_failed')
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
client.close()
|
|
1063
|
+
})
|
|
1064
|
+
|
|
1065
|
+
it('getMembersByIds passes parsed Long IDs to MEMBER request', async () => {
|
|
1066
|
+
mockGetMembersByIds.mockResolvedValueOnce({
|
|
1067
|
+
statusCode: 0,
|
|
1068
|
+
body: {
|
|
1069
|
+
chatId: makeLong(100),
|
|
1070
|
+
members: [{ userId: makeLong(42), nickName: 'Alice', type: 100 }],
|
|
1071
|
+
},
|
|
1072
|
+
})
|
|
1073
|
+
|
|
1074
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1075
|
+
const members = await client.getMembersByIds('100', ['42', '43', '99'])
|
|
1076
|
+
|
|
1077
|
+
expect(members).toHaveLength(1)
|
|
1078
|
+
expect(members[0].nickname).toBe('Alice')
|
|
1079
|
+
expect(mockGetMembersByIds).toHaveBeenCalledTimes(1)
|
|
1080
|
+
|
|
1081
|
+
// Verify the actual LOCO call args, not just the call count: chatId is a
|
|
1082
|
+
// Long-shaped { low, high } and memberIds is a list of Longs in the order
|
|
1083
|
+
// the SDK consumer passed them. Guards against accidentally sending raw
|
|
1084
|
+
// strings or losing IDs — both fail silently server-side.
|
|
1085
|
+
const [chatIdArg, memberIdsArg] = mockGetMembersByIds.mock.calls[0] as [
|
|
1086
|
+
{ low: number; high: number },
|
|
1087
|
+
Array<{ low: number; high: number }>,
|
|
1088
|
+
]
|
|
1089
|
+
expect(chatIdArg).toMatchObject({ low: 100, high: 0 })
|
|
1090
|
+
expect(memberIdsArg).toHaveLength(3)
|
|
1091
|
+
expect(memberIdsArg[0]).toMatchObject({ low: 42, high: 0 })
|
|
1092
|
+
expect(memberIdsArg[1]).toMatchObject({ low: 43, high: 0 })
|
|
1093
|
+
expect(memberIdsArg[2]).toMatchObject({ low: 99, high: 0 })
|
|
1094
|
+
|
|
1095
|
+
client.close()
|
|
1096
|
+
})
|
|
1097
|
+
|
|
1098
|
+
it('wraps MEMBER failures as KakaoTalkError get_members_failed', async () => {
|
|
1099
|
+
mockGetMembersByIds.mockRejectedValueOnce(new Error('Network error'))
|
|
1100
|
+
|
|
1101
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1102
|
+
try {
|
|
1103
|
+
await client.getMembersByIds('100', ['42'])
|
|
1104
|
+
expect.unreachable('should have thrown')
|
|
1105
|
+
} catch (e) {
|
|
1106
|
+
expect(e).toBeInstanceOf(KakaoTalkError)
|
|
1107
|
+
expect((e as KakaoTalkError).code).toBe('get_members_failed')
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
client.close()
|
|
1111
|
+
})
|
|
1112
|
+
|
|
1113
|
+
it('throws on synthetic disconnect packet from MEMBER (statusCode != 0)', async () => {
|
|
1114
|
+
mockGetMembersByIds.mockResolvedValueOnce({
|
|
1115
|
+
statusCode: -1,
|
|
1116
|
+
body: { error: 'connection closed' },
|
|
1117
|
+
})
|
|
1118
|
+
|
|
1119
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1120
|
+
try {
|
|
1121
|
+
await client.getMembersByIds('100', ['42'])
|
|
1122
|
+
expect.unreachable('should have thrown')
|
|
1123
|
+
} catch (e) {
|
|
1124
|
+
expect(e).toBeInstanceOf(KakaoTalkError)
|
|
1125
|
+
expect((e as KakaoTalkError).code).toBe('get_members_failed')
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
client.close()
|
|
1129
|
+
})
|
|
1130
|
+
|
|
1131
|
+
it('throws on MEMBER body.status nonzero', async () => {
|
|
1132
|
+
mockGetMembersByIds.mockResolvedValueOnce({
|
|
1133
|
+
statusCode: 0,
|
|
1134
|
+
body: { status: -500 },
|
|
1135
|
+
})
|
|
1136
|
+
|
|
1137
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1138
|
+
try {
|
|
1139
|
+
await client.getMembersByIds('100', ['42'])
|
|
1140
|
+
expect.unreachable('should have thrown')
|
|
1141
|
+
} catch (e) {
|
|
1142
|
+
expect(e).toBeInstanceOf(KakaoTalkError)
|
|
1143
|
+
expect((e as KakaoTalkError).code).toBe('get_members_failed')
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
client.close()
|
|
1147
|
+
})
|
|
1148
|
+
|
|
1149
|
+
it('getMembers throws KakaoTalkError invalid_chat_id without contacting LOCO', async () => {
|
|
1150
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1151
|
+
try {
|
|
1152
|
+
await client.getMembers('not-a-number')
|
|
1153
|
+
expect.unreachable('should have thrown')
|
|
1154
|
+
} catch (e) {
|
|
1155
|
+
expect(e).toBeInstanceOf(KakaoTalkError)
|
|
1156
|
+
expect((e as KakaoTalkError).code).toBe('invalid_chat_id')
|
|
1157
|
+
}
|
|
1158
|
+
expect(mockGetAllMembers).not.toHaveBeenCalled()
|
|
1159
|
+
expect(mockLogin).not.toHaveBeenCalled()
|
|
1160
|
+
|
|
1161
|
+
client.close()
|
|
1162
|
+
})
|
|
1163
|
+
|
|
1164
|
+
it('getMembersByIds throws invalid_chat_id for bad chatId without contacting LOCO', async () => {
|
|
1165
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1166
|
+
try {
|
|
1167
|
+
await client.getMembersByIds('not-a-number', ['42'])
|
|
1168
|
+
expect.unreachable('should have thrown')
|
|
1169
|
+
} catch (e) {
|
|
1170
|
+
expect(e).toBeInstanceOf(KakaoTalkError)
|
|
1171
|
+
expect((e as KakaoTalkError).code).toBe('invalid_chat_id')
|
|
1172
|
+
}
|
|
1173
|
+
expect(mockGetMembersByIds).not.toHaveBeenCalled()
|
|
1174
|
+
expect(mockLogin).not.toHaveBeenCalled()
|
|
1175
|
+
|
|
1176
|
+
client.close()
|
|
1177
|
+
})
|
|
1178
|
+
|
|
1179
|
+
it('getMembersByIds throws invalid_user_id for bad userId without contacting LOCO', async () => {
|
|
1180
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1181
|
+
try {
|
|
1182
|
+
await client.getMembersByIds('100', ['42', 'bogus', '99'])
|
|
1183
|
+
expect.unreachable('should have thrown')
|
|
1184
|
+
} catch (e) {
|
|
1185
|
+
expect(e).toBeInstanceOf(KakaoTalkError)
|
|
1186
|
+
expect((e as KakaoTalkError).code).toBe('invalid_user_id')
|
|
1187
|
+
}
|
|
1188
|
+
expect(mockGetMembersByIds).not.toHaveBeenCalled()
|
|
1189
|
+
expect(mockLogin).not.toHaveBeenCalled()
|
|
1190
|
+
|
|
1191
|
+
client.close()
|
|
1192
|
+
})
|
|
1193
|
+
|
|
1194
|
+
it('getMembersByIds returns [] without contacting LOCO when userIds is empty', async () => {
|
|
1195
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1196
|
+
const result = await client.getMembersByIds('100', [])
|
|
1197
|
+
|
|
1198
|
+
expect(result).toEqual([])
|
|
1199
|
+
expect(mockGetMembersByIds).not.toHaveBeenCalled()
|
|
1200
|
+
expect(mockLogin).not.toHaveBeenCalled()
|
|
1201
|
+
|
|
1202
|
+
client.close()
|
|
1203
|
+
})
|
|
1204
|
+
|
|
1205
|
+
it('reconnects and retries getMembers after silent disconnect (full executeWithReconnect path)', async () => {
|
|
1206
|
+
// Earlier tests verify assertLocoOk throws on a synthetic-disconnect packet,
|
|
1207
|
+
// but they don't exercise the reconnect path: executeWithReconnect only
|
|
1208
|
+
// retries when the underlying socket also closed (state.session !== this.state.session).
|
|
1209
|
+
// This test fires the captured onClose callback before the rejection so the
|
|
1210
|
+
// client's state is nulled, then verifies the operation is retried against
|
|
1211
|
+
// a fresh session and the second response is returned to the caller.
|
|
1212
|
+
const closeHandlers: Array<() => void> = []
|
|
1213
|
+
mockOnClose.mockImplementation((handler: () => void) => {
|
|
1214
|
+
closeHandlers.push(handler)
|
|
1215
|
+
})
|
|
1216
|
+
|
|
1217
|
+
mockGetAllMembers.mockImplementationOnce(() => {
|
|
1218
|
+
// Fire the close handler captured during the first session's setup —
|
|
1219
|
+
// simulates the LOCO TCP socket closing while the GETMEM is in flight.
|
|
1220
|
+
// executeWithReconnect sees state.session !== this.state.session and retries.
|
|
1221
|
+
const handler = closeHandlers[0]
|
|
1222
|
+
if (handler) handler()
|
|
1223
|
+
return Promise.resolve({ statusCode: -1, body: { error: 'connection closed' } })
|
|
1224
|
+
})
|
|
1225
|
+
mockGetAllMembers.mockResolvedValueOnce({
|
|
1226
|
+
statusCode: 0,
|
|
1227
|
+
body: { members: [{ userId: makeLong(42), nickName: 'Alice', type: 100 }] },
|
|
1228
|
+
})
|
|
1229
|
+
|
|
1230
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1231
|
+
const members = await client.getMembers('100')
|
|
1232
|
+
|
|
1233
|
+
expect(members).toHaveLength(1)
|
|
1234
|
+
expect(members[0].nickname).toBe('Alice')
|
|
1235
|
+
expect(mockGetAllMembers).toHaveBeenCalledTimes(2)
|
|
1236
|
+
// Login fires twice: once for the initial connect, once for the reconnect
|
|
1237
|
+
// after the captured onClose handler invalidated this.state.
|
|
1238
|
+
expect(mockLogin).toHaveBeenCalledTimes(2)
|
|
1239
|
+
|
|
1240
|
+
client.close()
|
|
1241
|
+
})
|
|
1242
|
+
})
|
|
1243
|
+
|
|
463
1244
|
describe('getProfile', () => {
|
|
464
1245
|
const mockFetch = mock(() => Promise.resolve(new Response()))
|
|
465
1246
|
|