agent-messenger 2.37.0 → 2.37.2
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 +122 -14
- 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 +638 -4
- package/src/platforms/kakaotalk/client.ts +156 -15
- 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,214 @@ 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('advances LCHATLIST pagination when cursor fields are plain numbers', async () => {
|
|
767
|
+
// LOCO sends lastTokenId/lastChatId as plain numbers when they fit in a safe
|
|
768
|
+
// integer (bson promotes them). The mocked session ignores its arguments, so
|
|
769
|
+
// asserting only on the collected chats would pass even while every request
|
|
770
|
+
// re-sent cursor 0. Assert on the cursor that actually reaches getChatList.
|
|
771
|
+
mockLogin.mockResolvedValue({ ...DEFAULT_LOGIN_RESULT, lastTokenId: 7, lastChatId: 200, eof: false })
|
|
772
|
+
mockGetChatList
|
|
773
|
+
.mockResolvedValueOnce({
|
|
774
|
+
statusCode: 0,
|
|
775
|
+
body: {
|
|
776
|
+
status: 0,
|
|
777
|
+
chatDatas: [
|
|
778
|
+
{
|
|
779
|
+
c: 300,
|
|
780
|
+
t: 1,
|
|
781
|
+
k: ['Dave'],
|
|
782
|
+
i: [4],
|
|
783
|
+
a: 1,
|
|
784
|
+
n: 0,
|
|
785
|
+
o: 1698000000,
|
|
786
|
+
l: null,
|
|
787
|
+
ll: makeLong(100),
|
|
788
|
+
},
|
|
789
|
+
],
|
|
790
|
+
lastTokenId: 8,
|
|
791
|
+
lastChatId: 300,
|
|
792
|
+
eof: false,
|
|
793
|
+
},
|
|
794
|
+
})
|
|
795
|
+
.mockResolvedValueOnce({
|
|
796
|
+
statusCode: 0,
|
|
797
|
+
body: {
|
|
798
|
+
status: 0,
|
|
799
|
+
chatDatas: [
|
|
800
|
+
{
|
|
801
|
+
c: 400,
|
|
802
|
+
t: 1,
|
|
803
|
+
k: ['Erin'],
|
|
804
|
+
i: [5],
|
|
805
|
+
a: 1,
|
|
806
|
+
n: 0,
|
|
807
|
+
o: 1697000000,
|
|
808
|
+
l: null,
|
|
809
|
+
ll: makeLong(90),
|
|
810
|
+
},
|
|
811
|
+
],
|
|
812
|
+
lastTokenId: 9,
|
|
813
|
+
lastChatId: 400,
|
|
814
|
+
eof: true,
|
|
815
|
+
},
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
819
|
+
const chats = await client.getChats({ all: true })
|
|
820
|
+
|
|
821
|
+
expect(chats.map((chat) => chat.chat_id)).toEqual(['100', '200', '300', '400'])
|
|
822
|
+
expect(mockGetChatList).toHaveBeenCalledTimes(2)
|
|
823
|
+
const cursors = mockGetChatList.mock.calls.map((call) => (call as unknown[]).map((v) => String(v)))
|
|
824
|
+
expect(cursors).toEqual([
|
|
825
|
+
['7', '200'],
|
|
826
|
+
['8', '300'],
|
|
827
|
+
])
|
|
828
|
+
|
|
829
|
+
client.close()
|
|
830
|
+
})
|
|
831
|
+
|
|
832
|
+
it('fails closed when LCHATLIST has a nonzero response status', async () => {
|
|
833
|
+
mockLogin.mockResolvedValue({ ...DEFAULT_LOGIN_RESULT, eof: false })
|
|
834
|
+
mockGetChatList.mockResolvedValueOnce({
|
|
835
|
+
statusCode: 503,
|
|
836
|
+
body: { status: 0, chatDatas: [], lastTokenId: makeLong(1), lastChatId: makeLong(300), eof: true },
|
|
837
|
+
})
|
|
838
|
+
|
|
839
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
840
|
+
|
|
841
|
+
await expect(client.getChats({ all: true })).rejects.toMatchObject({
|
|
842
|
+
code: 'get_chats_failed',
|
|
843
|
+
responseFailureKind: 'provider_rejection',
|
|
844
|
+
serverStatus: 503,
|
|
845
|
+
})
|
|
846
|
+
|
|
847
|
+
client.close()
|
|
848
|
+
})
|
|
849
|
+
|
|
850
|
+
it('fails closed on an empty non-EOF LCHATLIST page', async () => {
|
|
851
|
+
mockLogin.mockResolvedValue({ ...DEFAULT_LOGIN_RESULT, eof: false })
|
|
852
|
+
mockGetChatList.mockResolvedValueOnce({
|
|
853
|
+
statusCode: 0,
|
|
854
|
+
body: { status: 0, chatDatas: [], lastTokenId: makeLong(1), lastChatId: makeLong(300), eof: false },
|
|
855
|
+
})
|
|
856
|
+
|
|
857
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
858
|
+
|
|
859
|
+
await expect(client.getChats({ all: true })).rejects.toMatchObject({ code: 'get_chats_failed' })
|
|
860
|
+
expect(mockGetChatList).toHaveBeenCalledTimes(1)
|
|
861
|
+
|
|
862
|
+
client.close()
|
|
863
|
+
})
|
|
864
|
+
|
|
865
|
+
it('fails closed when LCHATLIST reaches the page cap without EOF', async () => {
|
|
866
|
+
mockLogin.mockResolvedValue({ ...DEFAULT_LOGIN_RESULT, eof: false })
|
|
867
|
+
let page = 0
|
|
868
|
+
mockGetChatList.mockImplementation(async () => {
|
|
869
|
+
page++
|
|
870
|
+
return {
|
|
871
|
+
statusCode: 0,
|
|
872
|
+
body: {
|
|
873
|
+
status: 0,
|
|
874
|
+
chatDatas: [
|
|
875
|
+
{
|
|
876
|
+
c: 1000 + page,
|
|
877
|
+
t: 1,
|
|
878
|
+
k: [`Room ${page}`],
|
|
879
|
+
i: [1000 + page],
|
|
880
|
+
a: 1,
|
|
881
|
+
n: 0,
|
|
882
|
+
o: 1600000000 - page,
|
|
883
|
+
l: null,
|
|
884
|
+
ll: makeLong(page),
|
|
885
|
+
},
|
|
886
|
+
],
|
|
887
|
+
lastTokenId: makeLong(page),
|
|
888
|
+
lastChatId: makeLong(1000 + page),
|
|
889
|
+
eof: false,
|
|
890
|
+
},
|
|
891
|
+
}
|
|
892
|
+
})
|
|
893
|
+
|
|
894
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
895
|
+
|
|
896
|
+
await expect(client.getChats({ all: true })).rejects.toMatchObject({ code: 'get_chats_failed' })
|
|
897
|
+
expect(mockGetChatList).toHaveBeenCalledTimes(50)
|
|
898
|
+
|
|
899
|
+
client.close()
|
|
900
|
+
})
|
|
901
|
+
|
|
902
|
+
it('preserves non-all search compatibility on an empty non-EOF page', async () => {
|
|
903
|
+
mockLogin.mockResolvedValue({ ...DEFAULT_LOGIN_RESULT, eof: false })
|
|
904
|
+
mockGetChatList.mockResolvedValueOnce({
|
|
905
|
+
body: { chatDatas: [], lastTokenId: makeLong(1), lastChatId: makeLong(300), eof: false },
|
|
906
|
+
})
|
|
907
|
+
|
|
908
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
909
|
+
const chats = await client.getChats({ search: 'Alice' })
|
|
910
|
+
|
|
911
|
+
expect(chats.map((chat) => chat.chat_id)).toEqual(['100'])
|
|
912
|
+
expect(mockGetChatList).toHaveBeenCalledTimes(1)
|
|
913
|
+
|
|
914
|
+
client.close()
|
|
915
|
+
})
|
|
916
|
+
|
|
665
917
|
it('wraps errors as KakaoTalkError', async () => {
|
|
666
918
|
mockLogin.mockRejectedValue(new Error('Connection refused'))
|
|
667
919
|
|
|
@@ -884,6 +1136,7 @@ describe('KakaoTalkClient', () => {
|
|
|
884
1136
|
chatInfo: {
|
|
885
1137
|
chatId: makeLong(300),
|
|
886
1138
|
type: 11,
|
|
1139
|
+
left: false,
|
|
887
1140
|
activeMembersCount: 2,
|
|
888
1141
|
newMessageCount: 4,
|
|
889
1142
|
invalidNewMessageCount: false,
|
|
@@ -927,6 +1180,303 @@ describe('KakaoTalkClient', () => {
|
|
|
927
1180
|
client.close()
|
|
928
1181
|
})
|
|
929
1182
|
|
|
1183
|
+
it('classifies an explicit CHATINFO header rejection without exposing response contents', async () => {
|
|
1184
|
+
mockGetChannelInfo.mockResolvedValueOnce({ statusCode: 403, body: {} })
|
|
1185
|
+
|
|
1186
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1187
|
+
|
|
1188
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1189
|
+
code: 'get_chat_failed',
|
|
1190
|
+
getChatFailureReason: 'provider_rejection',
|
|
1191
|
+
responseFailureKind: 'provider_rejection',
|
|
1192
|
+
responseStatusSource: 'packet',
|
|
1193
|
+
serverStatus: 403,
|
|
1194
|
+
})
|
|
1195
|
+
|
|
1196
|
+
client.close()
|
|
1197
|
+
})
|
|
1198
|
+
|
|
1199
|
+
it('classifies an explicit CHATINFO body rejection and preserves its numeric status', async () => {
|
|
1200
|
+
mockGetChannelInfo.mockResolvedValueOnce({ statusCode: 0, body: { status: -404 } })
|
|
1201
|
+
|
|
1202
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1203
|
+
|
|
1204
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1205
|
+
code: 'get_chat_failed',
|
|
1206
|
+
getChatFailureReason: 'provider_rejection',
|
|
1207
|
+
responseFailureKind: 'provider_rejection',
|
|
1208
|
+
responseStatusSource: 'body',
|
|
1209
|
+
serverStatus: -404,
|
|
1210
|
+
})
|
|
1211
|
+
|
|
1212
|
+
client.close()
|
|
1213
|
+
})
|
|
1214
|
+
|
|
1215
|
+
it('does not expose rejected CHATINFO body contents through public error metadata', async () => {
|
|
1216
|
+
const secretSentinel = 'secret-sentinel-do-not-emit'
|
|
1217
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1218
|
+
statusCode: 0,
|
|
1219
|
+
body: { status: -404, secret: secretSentinel },
|
|
1220
|
+
})
|
|
1221
|
+
|
|
1222
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1223
|
+
let error: unknown
|
|
1224
|
+
try {
|
|
1225
|
+
await client.getChat('300')
|
|
1226
|
+
} catch (cause) {
|
|
1227
|
+
error = cause
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
expect(error).toBeInstanceOf(KakaoTalkError)
|
|
1231
|
+
const publicError = error as KakaoTalkError
|
|
1232
|
+
expect(publicError).toMatchObject({
|
|
1233
|
+
code: 'get_chat_failed',
|
|
1234
|
+
getChatFailureReason: 'provider_rejection',
|
|
1235
|
+
responseFailureKind: 'provider_rejection',
|
|
1236
|
+
responseStatusSource: 'body',
|
|
1237
|
+
serverStatus: -404,
|
|
1238
|
+
})
|
|
1239
|
+
expect(publicError.message).not.toContain(secretSentinel)
|
|
1240
|
+
expect(JSON.stringify(publicError)).not.toContain(secretSentinel)
|
|
1241
|
+
expect(JSON.stringify(publicError.cause)).not.toContain(secretSentinel)
|
|
1242
|
+
|
|
1243
|
+
client.close()
|
|
1244
|
+
})
|
|
1245
|
+
|
|
1246
|
+
it('classifies synthetic connection-close status -1 as transient rather than provider rejection', async () => {
|
|
1247
|
+
mockGetChannelInfo.mockResolvedValueOnce({ statusCode: -1, body: { error: 'connection closed' } })
|
|
1248
|
+
|
|
1249
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1250
|
+
|
|
1251
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1252
|
+
code: 'get_chat_failed',
|
|
1253
|
+
getChatFailureReason: 'synthetic_connection_close',
|
|
1254
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1255
|
+
responseStatusSource: 'packet',
|
|
1256
|
+
serverStatus: -1,
|
|
1257
|
+
})
|
|
1258
|
+
|
|
1259
|
+
client.close()
|
|
1260
|
+
})
|
|
1261
|
+
|
|
1262
|
+
it('does not classify status -1 with a nonmatching body as a synthetic connection close', async () => {
|
|
1263
|
+
mockGetChannelInfo.mockResolvedValueOnce({ statusCode: -1, body: { error: 'provider rejected' } })
|
|
1264
|
+
|
|
1265
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1266
|
+
|
|
1267
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1268
|
+
code: 'get_chat_failed',
|
|
1269
|
+
getChatFailureReason: 'provider_rejection',
|
|
1270
|
+
responseFailureKind: 'provider_rejection',
|
|
1271
|
+
responseStatusSource: 'packet',
|
|
1272
|
+
serverStatus: -1,
|
|
1273
|
+
})
|
|
1274
|
+
|
|
1275
|
+
client.close()
|
|
1276
|
+
})
|
|
1277
|
+
|
|
1278
|
+
it('classifies a thrown CHATINFO transport failure as transient or unknown', async () => {
|
|
1279
|
+
const secretSentinel = 'secret-transport-body-do-not-emit'
|
|
1280
|
+
mockGetChannelInfo.mockRejectedValueOnce(
|
|
1281
|
+
Object.assign(new Error('socket unavailable'), { body: { secret: secretSentinel } }),
|
|
1282
|
+
)
|
|
1283
|
+
|
|
1284
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1285
|
+
|
|
1286
|
+
let error: unknown
|
|
1287
|
+
try {
|
|
1288
|
+
await client.getChat('300')
|
|
1289
|
+
} catch (cause) {
|
|
1290
|
+
error = cause
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
expect(error).toBeInstanceOf(KakaoTalkError)
|
|
1294
|
+
expect(error).toMatchObject({
|
|
1295
|
+
code: 'get_chat_failed',
|
|
1296
|
+
getChatFailureReason: 'transport_or_unknown',
|
|
1297
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1298
|
+
})
|
|
1299
|
+
const serialized = JSON.stringify(error)
|
|
1300
|
+
expect(serialized).not.toContain(secretSentinel)
|
|
1301
|
+
expect(serialized).not.toContain('body')
|
|
1302
|
+
expect(serialized).not.toContain('message')
|
|
1303
|
+
expect(serialized).not.toContain('cause')
|
|
1304
|
+
|
|
1305
|
+
client.close()
|
|
1306
|
+
})
|
|
1307
|
+
|
|
1308
|
+
it('classifies a successful CHATINFO response without chatInfo as structurally absent', async () => {
|
|
1309
|
+
const secretSentinel = 'secret-chat-body-do-not-emit'
|
|
1310
|
+
mockGetChannelInfo.mockResolvedValueOnce({ statusCode: 0, body: { secret: secretSentinel } })
|
|
1311
|
+
|
|
1312
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1313
|
+
|
|
1314
|
+
let error: unknown
|
|
1315
|
+
try {
|
|
1316
|
+
await client.getChat('300')
|
|
1317
|
+
} catch (cause) {
|
|
1318
|
+
error = cause
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
expect(error).toBeInstanceOf(KakaoTalkError)
|
|
1322
|
+
expect(error).toMatchObject({
|
|
1323
|
+
code: 'get_chat_failed',
|
|
1324
|
+
getChatFailureReason: 'chat_info_absent',
|
|
1325
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1326
|
+
})
|
|
1327
|
+
const serialized = JSON.stringify(error)
|
|
1328
|
+
expect(serialized).not.toContain(secretSentinel)
|
|
1329
|
+
expect(serialized).not.toContain('body')
|
|
1330
|
+
expect(serialized).not.toContain('message')
|
|
1331
|
+
expect(serialized).not.toContain('cause')
|
|
1332
|
+
|
|
1333
|
+
client.close()
|
|
1334
|
+
})
|
|
1335
|
+
|
|
1336
|
+
it('classifies a successful CHATINFO response with null chatInfo as structurally absent', async () => {
|
|
1337
|
+
const secretSentinel = 'secret-null-chat-body-do-not-emit'
|
|
1338
|
+
const chatIdSentinel = '987654321012345678'
|
|
1339
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1340
|
+
statusCode: 0,
|
|
1341
|
+
body: { chatInfo: null, secret: secretSentinel },
|
|
1342
|
+
})
|
|
1343
|
+
|
|
1344
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1345
|
+
|
|
1346
|
+
let error: unknown
|
|
1347
|
+
try {
|
|
1348
|
+
await client.getChat(chatIdSentinel)
|
|
1349
|
+
} catch (cause) {
|
|
1350
|
+
error = cause
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
expect(error).toBeInstanceOf(KakaoTalkError)
|
|
1354
|
+
expect(error).toMatchObject({
|
|
1355
|
+
code: 'get_chat_failed',
|
|
1356
|
+
getChatFailureReason: 'chat_info_absent',
|
|
1357
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1358
|
+
})
|
|
1359
|
+
const serialized = JSON.stringify(error)
|
|
1360
|
+
expect(serialized).not.toContain(secretSentinel)
|
|
1361
|
+
expect(serialized).not.toContain('body')
|
|
1362
|
+
expect(serialized).not.toContain('message')
|
|
1363
|
+
expect(serialized).not.toContain('cause')
|
|
1364
|
+
expect(serialized).not.toContain('chatId')
|
|
1365
|
+
expect(serialized).not.toContain(chatIdSentinel)
|
|
1366
|
+
|
|
1367
|
+
client.close()
|
|
1368
|
+
})
|
|
1369
|
+
|
|
1370
|
+
it('classifies exact CHATINFO left=true as structurally absent without exposing response contents', async () => {
|
|
1371
|
+
const secretSentinel = 'secret-left-chat-body-do-not-emit'
|
|
1372
|
+
const nativeChatIdSentinel = '987654321'
|
|
1373
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1374
|
+
statusCode: 0,
|
|
1375
|
+
body: {
|
|
1376
|
+
chatInfo: {
|
|
1377
|
+
left: true,
|
|
1378
|
+
chatId: makeLong(Number(nativeChatIdSentinel)),
|
|
1379
|
+
secret: secretSentinel,
|
|
1380
|
+
},
|
|
1381
|
+
},
|
|
1382
|
+
})
|
|
1383
|
+
|
|
1384
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1385
|
+
|
|
1386
|
+
let error: unknown
|
|
1387
|
+
try {
|
|
1388
|
+
await client.getChat(nativeChatIdSentinel)
|
|
1389
|
+
} catch (cause) {
|
|
1390
|
+
error = cause
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
expect(error).toBeInstanceOf(KakaoTalkError)
|
|
1394
|
+
expect(error).toMatchObject({
|
|
1395
|
+
code: 'get_chat_failed',
|
|
1396
|
+
getChatFailureReason: 'chat_info_absent',
|
|
1397
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1398
|
+
})
|
|
1399
|
+
const publicError = error as KakaoTalkError
|
|
1400
|
+
const serialized = JSON.stringify(publicError)
|
|
1401
|
+
expect(publicError.message).not.toContain(secretSentinel)
|
|
1402
|
+
expect(publicError.message).not.toContain(nativeChatIdSentinel)
|
|
1403
|
+
expect(serialized).not.toContain(secretSentinel)
|
|
1404
|
+
expect(serialized).not.toContain(nativeChatIdSentinel)
|
|
1405
|
+
|
|
1406
|
+
client.close()
|
|
1407
|
+
})
|
|
1408
|
+
|
|
1409
|
+
for (const [label, chatInfo] of [
|
|
1410
|
+
['mismatched chatId', { left: true, chatId: makeLong(301) }],
|
|
1411
|
+
['missing chatId', { left: true }],
|
|
1412
|
+
['invalid chatId', { left: true, chatId: { invalid: true } }],
|
|
1413
|
+
] as const) {
|
|
1414
|
+
it(`does not classify CHATINFO left=true with ${label} as structurally absent`, async () => {
|
|
1415
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1416
|
+
statusCode: 0,
|
|
1417
|
+
body: { chatInfo },
|
|
1418
|
+
})
|
|
1419
|
+
|
|
1420
|
+
const client = await new KakaoTalkClient().login({
|
|
1421
|
+
oauthToken: 'token',
|
|
1422
|
+
userId: 'user1',
|
|
1423
|
+
deviceUuid: 'device1',
|
|
1424
|
+
})
|
|
1425
|
+
|
|
1426
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1427
|
+
code: 'get_chat_failed',
|
|
1428
|
+
getChatFailureReason: 'transport_or_unknown',
|
|
1429
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1430
|
+
})
|
|
1431
|
+
|
|
1432
|
+
client.close()
|
|
1433
|
+
})
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
for (const [label, chatInfo] of [
|
|
1437
|
+
['left=false', { left: false }],
|
|
1438
|
+
['left missing', {}],
|
|
1439
|
+
['left nonboolean', { left: 'true' }],
|
|
1440
|
+
] as const) {
|
|
1441
|
+
it(`does not classify malformed successful CHATINFO with ${label} as structurally absent`, async () => {
|
|
1442
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1443
|
+
statusCode: 0,
|
|
1444
|
+
body: { chatInfo },
|
|
1445
|
+
})
|
|
1446
|
+
|
|
1447
|
+
const client = await new KakaoTalkClient().login({
|
|
1448
|
+
oauthToken: 'token',
|
|
1449
|
+
userId: 'user1',
|
|
1450
|
+
deviceUuid: 'device1',
|
|
1451
|
+
})
|
|
1452
|
+
|
|
1453
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1454
|
+
code: 'get_chat_failed',
|
|
1455
|
+
getChatFailureReason: 'transport_or_unknown',
|
|
1456
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1457
|
+
})
|
|
1458
|
+
|
|
1459
|
+
client.close()
|
|
1460
|
+
})
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
it('classifies malformed successful CHATINFO chatInfo as transport or unknown', async () => {
|
|
1464
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1465
|
+
statusCode: 0,
|
|
1466
|
+
body: { chatInfo: { chatId: makeLong(300) } },
|
|
1467
|
+
})
|
|
1468
|
+
|
|
1469
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1470
|
+
|
|
1471
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1472
|
+
code: 'get_chat_failed',
|
|
1473
|
+
getChatFailureReason: 'transport_or_unknown',
|
|
1474
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1475
|
+
})
|
|
1476
|
+
|
|
1477
|
+
client.close()
|
|
1478
|
+
})
|
|
1479
|
+
|
|
930
1480
|
for (const openChatType of ['OM', 'OD'] as const) {
|
|
931
1481
|
it(`preserves ${openChatType} and falls back to INFOLINK when CHATINFO has no title`, async () => {
|
|
932
1482
|
mockGetChannelInfo.mockResolvedValueOnce({
|
|
@@ -1011,13 +1561,90 @@ describe('KakaoTalkClient', () => {
|
|
|
1011
1561
|
client.close()
|
|
1012
1562
|
})
|
|
1013
1563
|
|
|
1014
|
-
|
|
1564
|
+
for (const normalChatType of ['DirectChat', 'MultiChat', 'PlusChat', 'MemoChat'] as const) {
|
|
1565
|
+
it(`preserves donor-defined ${normalChatType} without contacting INFOLINK`, async () => {
|
|
1566
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1567
|
+
statusCode: 0,
|
|
1568
|
+
body: {
|
|
1569
|
+
chatInfo: {
|
|
1570
|
+
chatId: makeLong(300),
|
|
1571
|
+
type: normalChatType,
|
|
1572
|
+
activeMembersCount: 2,
|
|
1573
|
+
newMessageCount: 0,
|
|
1574
|
+
invalidNewMessageCount: false,
|
|
1575
|
+
lastLogId: makeLong(1),
|
|
1576
|
+
lastSeenLogId: makeLong(0),
|
|
1577
|
+
displayMembers: [],
|
|
1578
|
+
chatMetas: [],
|
|
1579
|
+
li: makeLong(7777),
|
|
1580
|
+
pushAlert: true,
|
|
1581
|
+
},
|
|
1582
|
+
},
|
|
1583
|
+
})
|
|
1584
|
+
|
|
1585
|
+
const client = await new KakaoTalkClient().login({
|
|
1586
|
+
oauthToken: 'token',
|
|
1587
|
+
userId: 'user1',
|
|
1588
|
+
deviceUuid: 'device1',
|
|
1589
|
+
})
|
|
1590
|
+
|
|
1591
|
+
await expect(client.getChat('300')).resolves.toEqual({
|
|
1592
|
+
chat_id: '300',
|
|
1593
|
+
type: normalChatType,
|
|
1594
|
+
display_name: null,
|
|
1595
|
+
title: null,
|
|
1596
|
+
active_members: 2,
|
|
1597
|
+
unread_count: 0,
|
|
1598
|
+
last_message: null,
|
|
1599
|
+
})
|
|
1600
|
+
expect(mockGetOpenLinkInfo).not.toHaveBeenCalled()
|
|
1601
|
+
|
|
1602
|
+
client.close()
|
|
1603
|
+
})
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
it('rejects an empty string CHATINFO type without contacting INFOLINK', async () => {
|
|
1607
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1608
|
+
statusCode: 0,
|
|
1609
|
+
body: {
|
|
1610
|
+
chatInfo: {
|
|
1611
|
+
chatId: makeLong(300),
|
|
1612
|
+
type: '',
|
|
1613
|
+
activeMembersCount: 2,
|
|
1614
|
+
newMessageCount: 0,
|
|
1615
|
+
invalidNewMessageCount: false,
|
|
1616
|
+
lastLogId: makeLong(1),
|
|
1617
|
+
lastSeenLogId: makeLong(0),
|
|
1618
|
+
displayMembers: [],
|
|
1619
|
+
chatMetas: [],
|
|
1620
|
+
pushAlert: true,
|
|
1621
|
+
},
|
|
1622
|
+
},
|
|
1623
|
+
})
|
|
1624
|
+
|
|
1625
|
+
const client = await new KakaoTalkClient().login({
|
|
1626
|
+
oauthToken: 'token',
|
|
1627
|
+
userId: 'user1',
|
|
1628
|
+
deviceUuid: 'device1',
|
|
1629
|
+
})
|
|
1630
|
+
|
|
1631
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1632
|
+
code: 'get_chat_failed',
|
|
1633
|
+
getChatFailureReason: 'transport_or_unknown',
|
|
1634
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1635
|
+
})
|
|
1636
|
+
expect(mockGetOpenLinkInfo).not.toHaveBeenCalled()
|
|
1637
|
+
|
|
1638
|
+
client.close()
|
|
1639
|
+
})
|
|
1640
|
+
|
|
1641
|
+
it('preserves an unknown non-empty string CHATINFO type without contacting INFOLINK', async () => {
|
|
1015
1642
|
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1016
1643
|
statusCode: 0,
|
|
1017
1644
|
body: {
|
|
1018
1645
|
chatInfo: {
|
|
1019
1646
|
chatId: makeLong(300),
|
|
1020
|
-
type: '
|
|
1647
|
+
type: 'UnknownChat',
|
|
1021
1648
|
activeMembersCount: 2,
|
|
1022
1649
|
newMessageCount: 0,
|
|
1023
1650
|
invalidNewMessageCount: false,
|
|
@@ -1025,7 +1652,6 @@ describe('KakaoTalkClient', () => {
|
|
|
1025
1652
|
lastSeenLogId: makeLong(0),
|
|
1026
1653
|
displayMembers: [],
|
|
1027
1654
|
chatMetas: [],
|
|
1028
|
-
li: makeLong(7777),
|
|
1029
1655
|
pushAlert: true,
|
|
1030
1656
|
},
|
|
1031
1657
|
},
|
|
@@ -1037,7 +1663,15 @@ describe('KakaoTalkClient', () => {
|
|
|
1037
1663
|
deviceUuid: 'device1',
|
|
1038
1664
|
})
|
|
1039
1665
|
|
|
1040
|
-
await expect(client.getChat('300')).
|
|
1666
|
+
await expect(client.getChat('300')).resolves.toEqual({
|
|
1667
|
+
chat_id: '300',
|
|
1668
|
+
type: 'UnknownChat',
|
|
1669
|
+
display_name: null,
|
|
1670
|
+
title: null,
|
|
1671
|
+
active_members: 2,
|
|
1672
|
+
unread_count: 0,
|
|
1673
|
+
last_message: null,
|
|
1674
|
+
})
|
|
1041
1675
|
expect(mockGetOpenLinkInfo).not.toHaveBeenCalled()
|
|
1042
1676
|
|
|
1043
1677
|
client.close()
|