agent-messenger 2.37.0 → 2.37.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/dist/package.json +1 -1
- package/dist/src/platforms/kakaotalk/chat-classifier.d.ts +4 -4
- package/dist/src/platforms/kakaotalk/chat-classifier.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/chat-classifier.js +7 -3
- package/dist/src/platforms/kakaotalk/chat-classifier.js.map +1 -1
- package/dist/src/platforms/kakaotalk/client.d.ts +12 -0
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +107 -12
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/index.d.ts +1 -0
- 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/connection.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/connection.js +13 -2
- package/dist/src/platforms/kakaotalk/protocol/connection.js.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/login-response.d.ts +1 -0
- package/dist/src/platforms/kakaotalk/protocol/login-response.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/login-response.js +1 -1
- package/dist/src/platforms/kakaotalk/protocol/login-response.js.map +1 -1
- package/dist/src/platforms/kakaotalk/types.d.ts +4 -2
- package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/types.js +6 -1
- package/dist/src/platforms/kakaotalk/types.js.map +1 -1
- 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 +1 -1
- package/skills/agent-imessage/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +1 -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-webexbot/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 +22 -0
- package/src/platforms/kakaotalk/chat-classifier.ts +7 -4
- package/src/platforms/kakaotalk/client.test.ts +572 -4
- package/src/platforms/kakaotalk/client.ts +142 -13
- package/src/platforms/kakaotalk/index.test.ts +27 -0
- package/src/platforms/kakaotalk/index.ts +5 -0
- package/src/platforms/kakaotalk/protocol/connection.test.ts +87 -0
- package/src/platforms/kakaotalk/protocol/connection.ts +15 -2
- package/src/platforms/kakaotalk/protocol/login-response.test.ts +13 -2
- package/src/platforms/kakaotalk/protocol/login-response.ts +1 -1
- package/src/platforms/kakaotalk/types.ts +10 -2
|
@@ -242,6 +242,42 @@ describe('KakaoTalkClient', () => {
|
|
|
242
242
|
client.close()
|
|
243
243
|
})
|
|
244
244
|
|
|
245
|
+
it('preserves an unknown non-empty string type from the login snapshot', async () => {
|
|
246
|
+
mockLogin.mockResolvedValue({
|
|
247
|
+
...DEFAULT_LOGIN_RESULT,
|
|
248
|
+
chatDatas: [{ ...DEFAULT_LOGIN_RESULT.chatDatas[0], t: 'FutureChat' }],
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
const client = await new KakaoTalkClient().login({
|
|
252
|
+
oauthToken: 'token',
|
|
253
|
+
userId: 'user1',
|
|
254
|
+
deviceUuid: 'device1',
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
await expect(client.getChats()).resolves.toMatchObject([{ type: 'FutureChat' }])
|
|
258
|
+
|
|
259
|
+
client.close()
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
for (const invalidType of ['', true] as const) {
|
|
263
|
+
it(`rejects invalid ${JSON.stringify(invalidType)} type from the login snapshot`, async () => {
|
|
264
|
+
mockLogin.mockResolvedValue({
|
|
265
|
+
...DEFAULT_LOGIN_RESULT,
|
|
266
|
+
chatDatas: [{ ...DEFAULT_LOGIN_RESULT.chatDatas[0], t: invalidType }],
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
const client = await new KakaoTalkClient().login({
|
|
270
|
+
oauthToken: 'token',
|
|
271
|
+
userId: 'user1',
|
|
272
|
+
deviceUuid: 'device1',
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
await expect(client.getChats()).rejects.toMatchObject({ code: 'get_chats_failed' })
|
|
276
|
+
|
|
277
|
+
client.close()
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
|
|
245
281
|
it('populates last_message.author_name from paired chat.i / chat.k arrays', async () => {
|
|
246
282
|
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
247
283
|
const chats = await client.getChats()
|
|
@@ -500,7 +536,9 @@ describe('KakaoTalkClient', () => {
|
|
|
500
536
|
mockLogin.mockResolvedValue(emptyLoginResult)
|
|
501
537
|
|
|
502
538
|
mockGetChatList.mockResolvedValueOnce({
|
|
539
|
+
statusCode: 0,
|
|
503
540
|
body: {
|
|
541
|
+
status: 0,
|
|
504
542
|
chatDatas: [
|
|
505
543
|
{
|
|
506
544
|
c: 100,
|
|
@@ -559,7 +597,9 @@ describe('KakaoTalkClient', () => {
|
|
|
559
597
|
mockLogin.mockResolvedValue(loginResult)
|
|
560
598
|
|
|
561
599
|
mockGetChatList.mockResolvedValueOnce({
|
|
600
|
+
statusCode: 0,
|
|
562
601
|
body: {
|
|
602
|
+
status: 0,
|
|
563
603
|
chatDatas: [
|
|
564
604
|
{
|
|
565
605
|
c: 300,
|
|
@@ -596,7 +636,9 @@ describe('KakaoTalkClient', () => {
|
|
|
596
636
|
|
|
597
637
|
// Return a chat with same ID as login result
|
|
598
638
|
mockGetChatList.mockResolvedValueOnce({
|
|
639
|
+
statusCode: 0,
|
|
599
640
|
body: {
|
|
641
|
+
status: 0,
|
|
600
642
|
chatDatas: [
|
|
601
643
|
{
|
|
602
644
|
c: 100, // Same as first login chat
|
|
@@ -630,7 +672,9 @@ describe('KakaoTalkClient', () => {
|
|
|
630
672
|
mockLogin.mockResolvedValue(loginResult)
|
|
631
673
|
|
|
632
674
|
mockGetChatList.mockResolvedValueOnce({
|
|
675
|
+
statusCode: 0,
|
|
633
676
|
body: {
|
|
677
|
+
status: 0,
|
|
634
678
|
chatDatas: [
|
|
635
679
|
{
|
|
636
680
|
c: makeLong(300),
|
|
@@ -662,6 +706,148 @@ describe('KakaoTalkClient', () => {
|
|
|
662
706
|
client.close()
|
|
663
707
|
})
|
|
664
708
|
|
|
709
|
+
it('collects every LCHATLIST page until an explicit EOF', async () => {
|
|
710
|
+
mockLogin.mockResolvedValue({ ...DEFAULT_LOGIN_RESULT, eof: false })
|
|
711
|
+
mockGetChatList
|
|
712
|
+
.mockResolvedValueOnce({
|
|
713
|
+
statusCode: 0,
|
|
714
|
+
body: {
|
|
715
|
+
status: 0,
|
|
716
|
+
chatDatas: [
|
|
717
|
+
{
|
|
718
|
+
c: 300,
|
|
719
|
+
t: 1,
|
|
720
|
+
k: ['Dave'],
|
|
721
|
+
i: [4],
|
|
722
|
+
a: 1,
|
|
723
|
+
n: 0,
|
|
724
|
+
o: 1698000000,
|
|
725
|
+
l: null,
|
|
726
|
+
ll: makeLong(100),
|
|
727
|
+
},
|
|
728
|
+
],
|
|
729
|
+
lastTokenId: makeLong(1),
|
|
730
|
+
lastChatId: makeLong(300),
|
|
731
|
+
eof: false,
|
|
732
|
+
},
|
|
733
|
+
})
|
|
734
|
+
.mockResolvedValueOnce({
|
|
735
|
+
statusCode: 0,
|
|
736
|
+
body: {
|
|
737
|
+
status: 0,
|
|
738
|
+
chatDatas: [
|
|
739
|
+
{
|
|
740
|
+
c: 400,
|
|
741
|
+
t: 1,
|
|
742
|
+
k: ['Erin'],
|
|
743
|
+
i: [5],
|
|
744
|
+
a: 1,
|
|
745
|
+
n: 0,
|
|
746
|
+
o: 1697000000,
|
|
747
|
+
l: null,
|
|
748
|
+
ll: makeLong(90),
|
|
749
|
+
},
|
|
750
|
+
],
|
|
751
|
+
lastTokenId: makeLong(2),
|
|
752
|
+
lastChatId: makeLong(400),
|
|
753
|
+
eof: true,
|
|
754
|
+
},
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
758
|
+
const chats = await client.getChats({ all: true })
|
|
759
|
+
|
|
760
|
+
expect(chats.map((chat) => chat.chat_id)).toEqual(['100', '200', '300', '400'])
|
|
761
|
+
expect(mockGetChatList).toHaveBeenCalledTimes(2)
|
|
762
|
+
|
|
763
|
+
client.close()
|
|
764
|
+
})
|
|
765
|
+
|
|
766
|
+
it('fails closed when LCHATLIST has a nonzero response status', async () => {
|
|
767
|
+
mockLogin.mockResolvedValue({ ...DEFAULT_LOGIN_RESULT, eof: false })
|
|
768
|
+
mockGetChatList.mockResolvedValueOnce({
|
|
769
|
+
statusCode: 503,
|
|
770
|
+
body: { status: 0, chatDatas: [], lastTokenId: makeLong(1), lastChatId: makeLong(300), eof: true },
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
774
|
+
|
|
775
|
+
await expect(client.getChats({ all: true })).rejects.toMatchObject({
|
|
776
|
+
code: 'get_chats_failed',
|
|
777
|
+
responseFailureKind: 'provider_rejection',
|
|
778
|
+
serverStatus: 503,
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
client.close()
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
it('fails closed on an empty non-EOF LCHATLIST page', async () => {
|
|
785
|
+
mockLogin.mockResolvedValue({ ...DEFAULT_LOGIN_RESULT, eof: false })
|
|
786
|
+
mockGetChatList.mockResolvedValueOnce({
|
|
787
|
+
statusCode: 0,
|
|
788
|
+
body: { status: 0, chatDatas: [], lastTokenId: makeLong(1), lastChatId: makeLong(300), eof: false },
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
792
|
+
|
|
793
|
+
await expect(client.getChats({ all: true })).rejects.toMatchObject({ code: 'get_chats_failed' })
|
|
794
|
+
expect(mockGetChatList).toHaveBeenCalledTimes(1)
|
|
795
|
+
|
|
796
|
+
client.close()
|
|
797
|
+
})
|
|
798
|
+
|
|
799
|
+
it('fails closed when LCHATLIST reaches the page cap without EOF', async () => {
|
|
800
|
+
mockLogin.mockResolvedValue({ ...DEFAULT_LOGIN_RESULT, eof: false })
|
|
801
|
+
let page = 0
|
|
802
|
+
mockGetChatList.mockImplementation(async () => {
|
|
803
|
+
page++
|
|
804
|
+
return {
|
|
805
|
+
statusCode: 0,
|
|
806
|
+
body: {
|
|
807
|
+
status: 0,
|
|
808
|
+
chatDatas: [
|
|
809
|
+
{
|
|
810
|
+
c: 1000 + page,
|
|
811
|
+
t: 1,
|
|
812
|
+
k: [`Room ${page}`],
|
|
813
|
+
i: [1000 + page],
|
|
814
|
+
a: 1,
|
|
815
|
+
n: 0,
|
|
816
|
+
o: 1600000000 - page,
|
|
817
|
+
l: null,
|
|
818
|
+
ll: makeLong(page),
|
|
819
|
+
},
|
|
820
|
+
],
|
|
821
|
+
lastTokenId: makeLong(page),
|
|
822
|
+
lastChatId: makeLong(1000 + page),
|
|
823
|
+
eof: false,
|
|
824
|
+
},
|
|
825
|
+
}
|
|
826
|
+
})
|
|
827
|
+
|
|
828
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
829
|
+
|
|
830
|
+
await expect(client.getChats({ all: true })).rejects.toMatchObject({ code: 'get_chats_failed' })
|
|
831
|
+
expect(mockGetChatList).toHaveBeenCalledTimes(50)
|
|
832
|
+
|
|
833
|
+
client.close()
|
|
834
|
+
})
|
|
835
|
+
|
|
836
|
+
it('preserves non-all search compatibility on an empty non-EOF page', async () => {
|
|
837
|
+
mockLogin.mockResolvedValue({ ...DEFAULT_LOGIN_RESULT, eof: false })
|
|
838
|
+
mockGetChatList.mockResolvedValueOnce({
|
|
839
|
+
body: { chatDatas: [], lastTokenId: makeLong(1), lastChatId: makeLong(300), eof: false },
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
843
|
+
const chats = await client.getChats({ search: 'Alice' })
|
|
844
|
+
|
|
845
|
+
expect(chats.map((chat) => chat.chat_id)).toEqual(['100'])
|
|
846
|
+
expect(mockGetChatList).toHaveBeenCalledTimes(1)
|
|
847
|
+
|
|
848
|
+
client.close()
|
|
849
|
+
})
|
|
850
|
+
|
|
665
851
|
it('wraps errors as KakaoTalkError', async () => {
|
|
666
852
|
mockLogin.mockRejectedValue(new Error('Connection refused'))
|
|
667
853
|
|
|
@@ -884,6 +1070,7 @@ describe('KakaoTalkClient', () => {
|
|
|
884
1070
|
chatInfo: {
|
|
885
1071
|
chatId: makeLong(300),
|
|
886
1072
|
type: 11,
|
|
1073
|
+
left: false,
|
|
887
1074
|
activeMembersCount: 2,
|
|
888
1075
|
newMessageCount: 4,
|
|
889
1076
|
invalidNewMessageCount: false,
|
|
@@ -927,6 +1114,303 @@ describe('KakaoTalkClient', () => {
|
|
|
927
1114
|
client.close()
|
|
928
1115
|
})
|
|
929
1116
|
|
|
1117
|
+
it('classifies an explicit CHATINFO header rejection without exposing response contents', async () => {
|
|
1118
|
+
mockGetChannelInfo.mockResolvedValueOnce({ statusCode: 403, body: {} })
|
|
1119
|
+
|
|
1120
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1121
|
+
|
|
1122
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1123
|
+
code: 'get_chat_failed',
|
|
1124
|
+
getChatFailureReason: 'provider_rejection',
|
|
1125
|
+
responseFailureKind: 'provider_rejection',
|
|
1126
|
+
responseStatusSource: 'packet',
|
|
1127
|
+
serverStatus: 403,
|
|
1128
|
+
})
|
|
1129
|
+
|
|
1130
|
+
client.close()
|
|
1131
|
+
})
|
|
1132
|
+
|
|
1133
|
+
it('classifies an explicit CHATINFO body rejection and preserves its numeric status', async () => {
|
|
1134
|
+
mockGetChannelInfo.mockResolvedValueOnce({ statusCode: 0, body: { status: -404 } })
|
|
1135
|
+
|
|
1136
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1137
|
+
|
|
1138
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1139
|
+
code: 'get_chat_failed',
|
|
1140
|
+
getChatFailureReason: 'provider_rejection',
|
|
1141
|
+
responseFailureKind: 'provider_rejection',
|
|
1142
|
+
responseStatusSource: 'body',
|
|
1143
|
+
serverStatus: -404,
|
|
1144
|
+
})
|
|
1145
|
+
|
|
1146
|
+
client.close()
|
|
1147
|
+
})
|
|
1148
|
+
|
|
1149
|
+
it('does not expose rejected CHATINFO body contents through public error metadata', async () => {
|
|
1150
|
+
const secretSentinel = 'secret-sentinel-do-not-emit'
|
|
1151
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1152
|
+
statusCode: 0,
|
|
1153
|
+
body: { status: -404, secret: secretSentinel },
|
|
1154
|
+
})
|
|
1155
|
+
|
|
1156
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1157
|
+
let error: unknown
|
|
1158
|
+
try {
|
|
1159
|
+
await client.getChat('300')
|
|
1160
|
+
} catch (cause) {
|
|
1161
|
+
error = cause
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
expect(error).toBeInstanceOf(KakaoTalkError)
|
|
1165
|
+
const publicError = error as KakaoTalkError
|
|
1166
|
+
expect(publicError).toMatchObject({
|
|
1167
|
+
code: 'get_chat_failed',
|
|
1168
|
+
getChatFailureReason: 'provider_rejection',
|
|
1169
|
+
responseFailureKind: 'provider_rejection',
|
|
1170
|
+
responseStatusSource: 'body',
|
|
1171
|
+
serverStatus: -404,
|
|
1172
|
+
})
|
|
1173
|
+
expect(publicError.message).not.toContain(secretSentinel)
|
|
1174
|
+
expect(JSON.stringify(publicError)).not.toContain(secretSentinel)
|
|
1175
|
+
expect(JSON.stringify(publicError.cause)).not.toContain(secretSentinel)
|
|
1176
|
+
|
|
1177
|
+
client.close()
|
|
1178
|
+
})
|
|
1179
|
+
|
|
1180
|
+
it('classifies synthetic connection-close status -1 as transient rather than provider rejection', async () => {
|
|
1181
|
+
mockGetChannelInfo.mockResolvedValueOnce({ statusCode: -1, body: { error: 'connection closed' } })
|
|
1182
|
+
|
|
1183
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1184
|
+
|
|
1185
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1186
|
+
code: 'get_chat_failed',
|
|
1187
|
+
getChatFailureReason: 'synthetic_connection_close',
|
|
1188
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1189
|
+
responseStatusSource: 'packet',
|
|
1190
|
+
serverStatus: -1,
|
|
1191
|
+
})
|
|
1192
|
+
|
|
1193
|
+
client.close()
|
|
1194
|
+
})
|
|
1195
|
+
|
|
1196
|
+
it('does not classify status -1 with a nonmatching body as a synthetic connection close', async () => {
|
|
1197
|
+
mockGetChannelInfo.mockResolvedValueOnce({ statusCode: -1, body: { error: 'provider rejected' } })
|
|
1198
|
+
|
|
1199
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1200
|
+
|
|
1201
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1202
|
+
code: 'get_chat_failed',
|
|
1203
|
+
getChatFailureReason: 'provider_rejection',
|
|
1204
|
+
responseFailureKind: 'provider_rejection',
|
|
1205
|
+
responseStatusSource: 'packet',
|
|
1206
|
+
serverStatus: -1,
|
|
1207
|
+
})
|
|
1208
|
+
|
|
1209
|
+
client.close()
|
|
1210
|
+
})
|
|
1211
|
+
|
|
1212
|
+
it('classifies a thrown CHATINFO transport failure as transient or unknown', async () => {
|
|
1213
|
+
const secretSentinel = 'secret-transport-body-do-not-emit'
|
|
1214
|
+
mockGetChannelInfo.mockRejectedValueOnce(
|
|
1215
|
+
Object.assign(new Error('socket unavailable'), { body: { secret: secretSentinel } }),
|
|
1216
|
+
)
|
|
1217
|
+
|
|
1218
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1219
|
+
|
|
1220
|
+
let error: unknown
|
|
1221
|
+
try {
|
|
1222
|
+
await client.getChat('300')
|
|
1223
|
+
} catch (cause) {
|
|
1224
|
+
error = cause
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
expect(error).toBeInstanceOf(KakaoTalkError)
|
|
1228
|
+
expect(error).toMatchObject({
|
|
1229
|
+
code: 'get_chat_failed',
|
|
1230
|
+
getChatFailureReason: 'transport_or_unknown',
|
|
1231
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1232
|
+
})
|
|
1233
|
+
const serialized = JSON.stringify(error)
|
|
1234
|
+
expect(serialized).not.toContain(secretSentinel)
|
|
1235
|
+
expect(serialized).not.toContain('body')
|
|
1236
|
+
expect(serialized).not.toContain('message')
|
|
1237
|
+
expect(serialized).not.toContain('cause')
|
|
1238
|
+
|
|
1239
|
+
client.close()
|
|
1240
|
+
})
|
|
1241
|
+
|
|
1242
|
+
it('classifies a successful CHATINFO response without chatInfo as structurally absent', async () => {
|
|
1243
|
+
const secretSentinel = 'secret-chat-body-do-not-emit'
|
|
1244
|
+
mockGetChannelInfo.mockResolvedValueOnce({ statusCode: 0, body: { secret: secretSentinel } })
|
|
1245
|
+
|
|
1246
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1247
|
+
|
|
1248
|
+
let error: unknown
|
|
1249
|
+
try {
|
|
1250
|
+
await client.getChat('300')
|
|
1251
|
+
} catch (cause) {
|
|
1252
|
+
error = cause
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
expect(error).toBeInstanceOf(KakaoTalkError)
|
|
1256
|
+
expect(error).toMatchObject({
|
|
1257
|
+
code: 'get_chat_failed',
|
|
1258
|
+
getChatFailureReason: 'chat_info_absent',
|
|
1259
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1260
|
+
})
|
|
1261
|
+
const serialized = JSON.stringify(error)
|
|
1262
|
+
expect(serialized).not.toContain(secretSentinel)
|
|
1263
|
+
expect(serialized).not.toContain('body')
|
|
1264
|
+
expect(serialized).not.toContain('message')
|
|
1265
|
+
expect(serialized).not.toContain('cause')
|
|
1266
|
+
|
|
1267
|
+
client.close()
|
|
1268
|
+
})
|
|
1269
|
+
|
|
1270
|
+
it('classifies a successful CHATINFO response with null chatInfo as structurally absent', async () => {
|
|
1271
|
+
const secretSentinel = 'secret-null-chat-body-do-not-emit'
|
|
1272
|
+
const chatIdSentinel = '987654321012345678'
|
|
1273
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1274
|
+
statusCode: 0,
|
|
1275
|
+
body: { chatInfo: null, secret: secretSentinel },
|
|
1276
|
+
})
|
|
1277
|
+
|
|
1278
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1279
|
+
|
|
1280
|
+
let error: unknown
|
|
1281
|
+
try {
|
|
1282
|
+
await client.getChat(chatIdSentinel)
|
|
1283
|
+
} catch (cause) {
|
|
1284
|
+
error = cause
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
expect(error).toBeInstanceOf(KakaoTalkError)
|
|
1288
|
+
expect(error).toMatchObject({
|
|
1289
|
+
code: 'get_chat_failed',
|
|
1290
|
+
getChatFailureReason: 'chat_info_absent',
|
|
1291
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1292
|
+
})
|
|
1293
|
+
const serialized = JSON.stringify(error)
|
|
1294
|
+
expect(serialized).not.toContain(secretSentinel)
|
|
1295
|
+
expect(serialized).not.toContain('body')
|
|
1296
|
+
expect(serialized).not.toContain('message')
|
|
1297
|
+
expect(serialized).not.toContain('cause')
|
|
1298
|
+
expect(serialized).not.toContain('chatId')
|
|
1299
|
+
expect(serialized).not.toContain(chatIdSentinel)
|
|
1300
|
+
|
|
1301
|
+
client.close()
|
|
1302
|
+
})
|
|
1303
|
+
|
|
1304
|
+
it('classifies exact CHATINFO left=true as structurally absent without exposing response contents', async () => {
|
|
1305
|
+
const secretSentinel = 'secret-left-chat-body-do-not-emit'
|
|
1306
|
+
const nativeChatIdSentinel = '987654321'
|
|
1307
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1308
|
+
statusCode: 0,
|
|
1309
|
+
body: {
|
|
1310
|
+
chatInfo: {
|
|
1311
|
+
left: true,
|
|
1312
|
+
chatId: makeLong(Number(nativeChatIdSentinel)),
|
|
1313
|
+
secret: secretSentinel,
|
|
1314
|
+
},
|
|
1315
|
+
},
|
|
1316
|
+
})
|
|
1317
|
+
|
|
1318
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1319
|
+
|
|
1320
|
+
let error: unknown
|
|
1321
|
+
try {
|
|
1322
|
+
await client.getChat(nativeChatIdSentinel)
|
|
1323
|
+
} catch (cause) {
|
|
1324
|
+
error = cause
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
expect(error).toBeInstanceOf(KakaoTalkError)
|
|
1328
|
+
expect(error).toMatchObject({
|
|
1329
|
+
code: 'get_chat_failed',
|
|
1330
|
+
getChatFailureReason: 'chat_info_absent',
|
|
1331
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1332
|
+
})
|
|
1333
|
+
const publicError = error as KakaoTalkError
|
|
1334
|
+
const serialized = JSON.stringify(publicError)
|
|
1335
|
+
expect(publicError.message).not.toContain(secretSentinel)
|
|
1336
|
+
expect(publicError.message).not.toContain(nativeChatIdSentinel)
|
|
1337
|
+
expect(serialized).not.toContain(secretSentinel)
|
|
1338
|
+
expect(serialized).not.toContain(nativeChatIdSentinel)
|
|
1339
|
+
|
|
1340
|
+
client.close()
|
|
1341
|
+
})
|
|
1342
|
+
|
|
1343
|
+
for (const [label, chatInfo] of [
|
|
1344
|
+
['mismatched chatId', { left: true, chatId: makeLong(301) }],
|
|
1345
|
+
['missing chatId', { left: true }],
|
|
1346
|
+
['invalid chatId', { left: true, chatId: { invalid: true } }],
|
|
1347
|
+
] as const) {
|
|
1348
|
+
it(`does not classify CHATINFO left=true with ${label} as structurally absent`, async () => {
|
|
1349
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1350
|
+
statusCode: 0,
|
|
1351
|
+
body: { chatInfo },
|
|
1352
|
+
})
|
|
1353
|
+
|
|
1354
|
+
const client = await new KakaoTalkClient().login({
|
|
1355
|
+
oauthToken: 'token',
|
|
1356
|
+
userId: 'user1',
|
|
1357
|
+
deviceUuid: 'device1',
|
|
1358
|
+
})
|
|
1359
|
+
|
|
1360
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1361
|
+
code: 'get_chat_failed',
|
|
1362
|
+
getChatFailureReason: 'transport_or_unknown',
|
|
1363
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1364
|
+
})
|
|
1365
|
+
|
|
1366
|
+
client.close()
|
|
1367
|
+
})
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
for (const [label, chatInfo] of [
|
|
1371
|
+
['left=false', { left: false }],
|
|
1372
|
+
['left missing', {}],
|
|
1373
|
+
['left nonboolean', { left: 'true' }],
|
|
1374
|
+
] as const) {
|
|
1375
|
+
it(`does not classify malformed successful CHATINFO with ${label} as structurally absent`, async () => {
|
|
1376
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1377
|
+
statusCode: 0,
|
|
1378
|
+
body: { chatInfo },
|
|
1379
|
+
})
|
|
1380
|
+
|
|
1381
|
+
const client = await new KakaoTalkClient().login({
|
|
1382
|
+
oauthToken: 'token',
|
|
1383
|
+
userId: 'user1',
|
|
1384
|
+
deviceUuid: 'device1',
|
|
1385
|
+
})
|
|
1386
|
+
|
|
1387
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1388
|
+
code: 'get_chat_failed',
|
|
1389
|
+
getChatFailureReason: 'transport_or_unknown',
|
|
1390
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1391
|
+
})
|
|
1392
|
+
|
|
1393
|
+
client.close()
|
|
1394
|
+
})
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
it('classifies malformed successful CHATINFO chatInfo as transport or unknown', async () => {
|
|
1398
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1399
|
+
statusCode: 0,
|
|
1400
|
+
body: { chatInfo: { chatId: makeLong(300) } },
|
|
1401
|
+
})
|
|
1402
|
+
|
|
1403
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1404
|
+
|
|
1405
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1406
|
+
code: 'get_chat_failed',
|
|
1407
|
+
getChatFailureReason: 'transport_or_unknown',
|
|
1408
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1409
|
+
})
|
|
1410
|
+
|
|
1411
|
+
client.close()
|
|
1412
|
+
})
|
|
1413
|
+
|
|
930
1414
|
for (const openChatType of ['OM', 'OD'] as const) {
|
|
931
1415
|
it(`preserves ${openChatType} and falls back to INFOLINK when CHATINFO has no title`, async () => {
|
|
932
1416
|
mockGetChannelInfo.mockResolvedValueOnce({
|
|
@@ -1011,13 +1495,55 @@ describe('KakaoTalkClient', () => {
|
|
|
1011
1495
|
client.close()
|
|
1012
1496
|
})
|
|
1013
1497
|
|
|
1014
|
-
|
|
1498
|
+
for (const normalChatType of ['DirectChat', 'MultiChat', 'PlusChat', 'MemoChat'] as const) {
|
|
1499
|
+
it(`preserves donor-defined ${normalChatType} without contacting INFOLINK`, async () => {
|
|
1500
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1501
|
+
statusCode: 0,
|
|
1502
|
+
body: {
|
|
1503
|
+
chatInfo: {
|
|
1504
|
+
chatId: makeLong(300),
|
|
1505
|
+
type: normalChatType,
|
|
1506
|
+
activeMembersCount: 2,
|
|
1507
|
+
newMessageCount: 0,
|
|
1508
|
+
invalidNewMessageCount: false,
|
|
1509
|
+
lastLogId: makeLong(1),
|
|
1510
|
+
lastSeenLogId: makeLong(0),
|
|
1511
|
+
displayMembers: [],
|
|
1512
|
+
chatMetas: [],
|
|
1513
|
+
li: makeLong(7777),
|
|
1514
|
+
pushAlert: true,
|
|
1515
|
+
},
|
|
1516
|
+
},
|
|
1517
|
+
})
|
|
1518
|
+
|
|
1519
|
+
const client = await new KakaoTalkClient().login({
|
|
1520
|
+
oauthToken: 'token',
|
|
1521
|
+
userId: 'user1',
|
|
1522
|
+
deviceUuid: 'device1',
|
|
1523
|
+
})
|
|
1524
|
+
|
|
1525
|
+
await expect(client.getChat('300')).resolves.toEqual({
|
|
1526
|
+
chat_id: '300',
|
|
1527
|
+
type: normalChatType,
|
|
1528
|
+
display_name: null,
|
|
1529
|
+
title: null,
|
|
1530
|
+
active_members: 2,
|
|
1531
|
+
unread_count: 0,
|
|
1532
|
+
last_message: null,
|
|
1533
|
+
})
|
|
1534
|
+
expect(mockGetOpenLinkInfo).not.toHaveBeenCalled()
|
|
1535
|
+
|
|
1536
|
+
client.close()
|
|
1537
|
+
})
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
it('rejects an empty string CHATINFO type without contacting INFOLINK', async () => {
|
|
1015
1541
|
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1016
1542
|
statusCode: 0,
|
|
1017
1543
|
body: {
|
|
1018
1544
|
chatInfo: {
|
|
1019
1545
|
chatId: makeLong(300),
|
|
1020
|
-
type: '
|
|
1546
|
+
type: '',
|
|
1021
1547
|
activeMembersCount: 2,
|
|
1022
1548
|
newMessageCount: 0,
|
|
1023
1549
|
invalidNewMessageCount: false,
|
|
@@ -1025,7 +1551,6 @@ describe('KakaoTalkClient', () => {
|
|
|
1025
1551
|
lastSeenLogId: makeLong(0),
|
|
1026
1552
|
displayMembers: [],
|
|
1027
1553
|
chatMetas: [],
|
|
1028
|
-
li: makeLong(7777),
|
|
1029
1554
|
pushAlert: true,
|
|
1030
1555
|
},
|
|
1031
1556
|
},
|
|
@@ -1037,7 +1562,50 @@ describe('KakaoTalkClient', () => {
|
|
|
1037
1562
|
deviceUuid: 'device1',
|
|
1038
1563
|
})
|
|
1039
1564
|
|
|
1040
|
-
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1565
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1566
|
+
code: 'get_chat_failed',
|
|
1567
|
+
getChatFailureReason: 'transport_or_unknown',
|
|
1568
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1569
|
+
})
|
|
1570
|
+
expect(mockGetOpenLinkInfo).not.toHaveBeenCalled()
|
|
1571
|
+
|
|
1572
|
+
client.close()
|
|
1573
|
+
})
|
|
1574
|
+
|
|
1575
|
+
it('preserves an unknown non-empty string CHATINFO type without contacting INFOLINK', async () => {
|
|
1576
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1577
|
+
statusCode: 0,
|
|
1578
|
+
body: {
|
|
1579
|
+
chatInfo: {
|
|
1580
|
+
chatId: makeLong(300),
|
|
1581
|
+
type: 'UnknownChat',
|
|
1582
|
+
activeMembersCount: 2,
|
|
1583
|
+
newMessageCount: 0,
|
|
1584
|
+
invalidNewMessageCount: false,
|
|
1585
|
+
lastLogId: makeLong(1),
|
|
1586
|
+
lastSeenLogId: makeLong(0),
|
|
1587
|
+
displayMembers: [],
|
|
1588
|
+
chatMetas: [],
|
|
1589
|
+
pushAlert: true,
|
|
1590
|
+
},
|
|
1591
|
+
},
|
|
1592
|
+
})
|
|
1593
|
+
|
|
1594
|
+
const client = await new KakaoTalkClient().login({
|
|
1595
|
+
oauthToken: 'token',
|
|
1596
|
+
userId: 'user1',
|
|
1597
|
+
deviceUuid: 'device1',
|
|
1598
|
+
})
|
|
1599
|
+
|
|
1600
|
+
await expect(client.getChat('300')).resolves.toEqual({
|
|
1601
|
+
chat_id: '300',
|
|
1602
|
+
type: 'UnknownChat',
|
|
1603
|
+
display_name: null,
|
|
1604
|
+
title: null,
|
|
1605
|
+
active_members: 2,
|
|
1606
|
+
unread_count: 0,
|
|
1607
|
+
last_message: null,
|
|
1608
|
+
})
|
|
1041
1609
|
expect(mockGetOpenLinkInfo).not.toHaveBeenCalled()
|
|
1042
1610
|
|
|
1043
1611
|
client.close()
|