agent-messenger 2.19.0 → 2.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/package.json +1 -1
  3. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  4. package/dist/src/platforms/kakaotalk/client.js +51 -1
  5. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  6. package/package.json +1 -1
  7. package/skills/agent-channeltalk/SKILL.md +1 -1
  8. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  9. package/skills/agent-discord/SKILL.md +1 -1
  10. package/skills/agent-discordbot/SKILL.md +1 -1
  11. package/skills/agent-instagram/SKILL.md +1 -1
  12. package/skills/agent-kakaotalk/SKILL.md +1 -1
  13. package/skills/agent-line/SKILL.md +1 -1
  14. package/skills/agent-slack/SKILL.md +1 -1
  15. package/skills/agent-slackbot/SKILL.md +1 -1
  16. package/skills/agent-teams/SKILL.md +1 -1
  17. package/skills/agent-telegram/SKILL.md +1 -1
  18. package/skills/agent-telegrambot/SKILL.md +1 -1
  19. package/skills/agent-webex/SKILL.md +1 -1
  20. package/skills/agent-wechatbot/SKILL.md +1 -1
  21. package/skills/agent-whatsapp/SKILL.md +1 -1
  22. package/skills/agent-whatsappbot/SKILL.md +1 -1
  23. package/src/platforms/kakaotalk/client.test.ts +39 -0
  24. package/src/platforms/kakaotalk/client.ts +59 -1
  25. package/src/platforms/webex/commands/auth.test.ts +8 -2
  26. package/src/platforms/webex/commands/member.test.ts +29 -27
  27. package/src/platforms/webex/commands/message.test.ts +36 -38
  28. package/src/platforms/webex/commands/snapshot.test.ts +25 -26
  29. package/src/platforms/webex/commands/space.test.ts +31 -29
  30. package/src/platforms/webex/commands/whoami.test.ts +3 -1
  31. package/src/platforms/webex/credential-manager.test.ts +3 -0
  32. package/src/platforms/whatsapp/commands/auth.test.ts +14 -20
  33. package/src/platforms/whatsapp/commands/chat.test.ts +17 -24
  34. package/src/platforms/whatsapp/commands/message.test.ts +31 -41
@@ -17,6 +17,7 @@ describe('auth commands', () => {
17
17
  let consoleSpy: ReturnType<typeof spyOn>
18
18
  let consoleErrorSpy: ReturnType<typeof spyOn>
19
19
  let execSpy: ReturnType<typeof spyOn>
20
+ let stderrWriteSpy: ReturnType<typeof spyOn>
20
21
  const protoSpies: ReturnType<typeof spyOn>[] = []
21
22
  let originalStdinTTY: boolean | undefined
22
23
  let originalStdoutTTY: boolean | undefined
@@ -44,16 +45,23 @@ describe('auth commands', () => {
44
45
  consoleSpy = spyOn(console, 'log').mockImplementation(() => {})
45
46
  consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
46
47
  execSpy = spyOn(childProcess, 'exec').mockImplementation((() => {}) as any)
48
+ stderrWriteSpy = spyOn(process.stderr, 'write').mockImplementation(() => true)
47
49
  originalStdinTTY = process.stdin.isTTY
48
50
  originalStdoutTTY = process.stdout.isTTY
49
51
  // Default to interactive TTY for existing tests; non-interactive tests override.
50
52
  setTTY(true)
53
+ // Fail loudly instead of running the real 300s network polling loop; tests that
54
+ // exercise the Device Grant flow override this with a resolved value.
55
+ protoSpy(WebexCredentialManager.prototype, 'pollDeviceToken').mockImplementation(() => {
56
+ throw new Error('Unexpected real device polling in test')
57
+ })
51
58
  })
52
59
 
53
60
  afterEach(() => {
54
61
  consoleSpy.mockRestore()
55
62
  consoleErrorSpy.mockRestore()
56
63
  execSpy.mockRestore()
64
+ stderrWriteSpy.mockRestore()
57
65
  for (const s of protoSpies) s.mockRestore()
58
66
  protoSpies.length = 0
59
67
  setTTY(originalStdinTTY)
@@ -447,7 +455,6 @@ describe('auth commands', () => {
447
455
  protoSpy(WebexCredentialManager.prototype, 'refreshToken').mockResolvedValue(null)
448
456
  protoSpy(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
449
457
  protoSpy(WebexClient.prototype, 'testAuth').mockRejectedValue(new Error('Network error'))
450
- const stderrWriteSpy = protoSpy(process.stderr, 'write').mockImplementation(() => true)
451
458
  const exitSpy = protoSpy(process, 'exit').mockImplementation((code?: string | number | null) => {
452
459
  throw new ProcessExit(code)
453
460
  })
@@ -467,7 +474,6 @@ describe('auth commands', () => {
467
474
  })
468
475
  protoSpy(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
469
476
  protoSpy(WebexClient.prototype, 'testAuth').mockRejectedValue(new Error('Network error'))
470
- const stderrWriteSpy = protoSpy(process.stderr, 'write').mockImplementation(() => true)
471
477
  const exitSpy = protoSpy(process, 'exit').mockImplementation((code?: string | number | null) => {
472
478
  throw new ProcessExit(code)
473
479
  })
@@ -1,6 +1,4 @@
1
- import { afterEach, beforeEach, describe, expect, mock, spyOn, it } from 'bun:test'
2
-
3
- import * as errorHandler from '@/shared/utils/error-handler'
1
+ import { afterEach, beforeEach, describe, expect, spyOn, it } from 'bun:test'
4
2
 
5
3
  import { WebexClient } from '../client'
6
4
  import { WebexError } from '../types'
@@ -26,31 +24,39 @@ const mockMembers = [
26
24
  },
27
25
  ]
28
26
 
29
- const mockListMemberships = mock(() => Promise.resolve(mockMembers))
30
-
31
27
  import { listAction } from './member'
32
28
 
33
29
  describe('member commands', () => {
30
+ let mockListMemberships: ReturnType<typeof spyOn>
31
+ let mockLogin: ReturnType<typeof spyOn>
34
32
  let consoleSpy: ReturnType<typeof spyOn>
35
- let loginSpy: ReturnType<typeof spyOn>
36
- let handleErrorSpy: ReturnType<typeof spyOn>
33
+ let consoleErrorSpy: ReturnType<typeof spyOn>
34
+ let processExitSpy: ReturnType<typeof spyOn>
35
+ const protoSpies: ReturnType<typeof spyOn>[] = []
36
+
37
+ function protoSpy(method: keyof WebexClient) {
38
+ const s = spyOn(WebexClient.prototype, method as never)
39
+ protoSpies.push(s)
40
+ return s
41
+ }
37
42
 
38
43
  beforeEach(() => {
39
- mockListMemberships.mockReset().mockImplementation(() => Promise.resolve(mockMembers))
40
- handleErrorSpy = spyOn(errorHandler, 'handleError').mockImplementation((err: Error) => {
41
- throw err
44
+ mockLogin = protoSpy('login').mockImplementation(async function (this: WebexClient) {
45
+ return this
42
46
  })
47
+ mockListMemberships = protoSpy('listMemberships').mockResolvedValue(mockMembers)
43
48
 
44
- loginSpy = spyOn(WebexClient.prototype, 'login').mockResolvedValue(
45
- Object.assign(new WebexClient(), { listMemberships: mockListMemberships }),
46
- )
47
49
  consoleSpy = spyOn(console, 'log').mockImplementation(() => {})
50
+ consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
51
+ processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => undefined as never)
48
52
  })
49
53
 
50
54
  afterEach(() => {
51
- loginSpy.mockRestore()
52
- handleErrorSpy.mockRestore()
53
55
  consoleSpy.mockRestore()
56
+ consoleErrorSpy.mockRestore()
57
+ processExitSpy.mockRestore()
58
+ for (const s of protoSpies) s.mockRestore()
59
+ protoSpies.length = 0
54
60
  })
55
61
 
56
62
  it('calls listMemberships with spaceId and outputs mapped members', async () => {
@@ -85,23 +91,19 @@ describe('member commands', () => {
85
91
  expect(mockListMemberships).toHaveBeenCalledWith('room-1', { max: 25 })
86
92
  })
87
93
 
88
- it('throws when not authenticated', async () => {
89
- loginSpy.mockImplementation(async () => {
90
- throw new WebexError('No Webex credentials found.', 'no_credentials')
91
- })
94
+ it('exits with code 1 when not authenticated', async () => {
95
+ mockLogin.mockRejectedValue(new WebexError('No Webex credentials found.', 'no_credentials'))
92
96
 
93
- await expect(listAction('room-1', {})).rejects.toThrow('No Webex credentials found.')
97
+ await listAction('room-1', {})
94
98
 
95
- expect(handleErrorSpy).toHaveBeenCalledWith(expect.any(WebexError))
99
+ expect(processExitSpy).toHaveBeenCalledWith(1)
96
100
  })
97
101
 
98
- it('throws on API error', async () => {
99
- mockListMemberships.mockImplementation(async () => {
100
- throw new Error('API failure')
101
- })
102
+ it('exits with code 1 on API error', async () => {
103
+ mockListMemberships.mockRejectedValue(new Error('API failure'))
102
104
 
103
- await expect(listAction('room-1', {})).rejects.toThrow('API failure')
105
+ await listAction('room-1', {})
104
106
 
105
- expect(handleErrorSpy).toHaveBeenCalledWith(expect.any(Error))
107
+ expect(processExitSpy).toHaveBeenCalledWith(1)
106
108
  })
107
109
  })
@@ -1,6 +1,4 @@
1
- import { afterEach, beforeEach, expect, mock, spyOn, it } from 'bun:test'
2
-
3
- import * as errorHandler from '@/shared/utils/error-handler'
1
+ import { afterEach, beforeEach, expect, spyOn, it } from 'bun:test'
4
2
 
5
3
  import { WebexClient } from '../client'
6
4
  import { WebexError } from '../types'
@@ -25,47 +23,48 @@ const mockMessage2 = {
25
23
  created: '2025-01-29T10:01:00.000Z',
26
24
  }
27
25
 
28
- const mockSendMessage = mock(() => Promise.resolve(mockMessage))
29
- const mockSendDirectMessage = mock(() => Promise.resolve(mockMessage))
30
- const mockListMessages = mock(() => Promise.resolve([mockMessage, mockMessage2]))
31
- const mockGetMessage = mock(() => Promise.resolve(mockMessage))
32
- const mockDeleteMessage = mock(() => Promise.resolve(undefined))
33
- const mockEditMessage = mock(() => Promise.resolve({ ...mockMessage, text: 'Updated message' }))
34
-
35
- const mockClient = {
36
- sendMessage: mockSendMessage,
37
- sendDirectMessage: mockSendDirectMessage,
38
- listMessages: mockListMessages,
39
- getMessage: mockGetMessage,
40
- deleteMessage: mockDeleteMessage,
41
- editMessage: mockEditMessage,
42
- }
43
-
44
26
  import { deleteAction, dmAction, editAction, getAction, listAction, sendAction } from './message'
45
27
 
28
+ let mockSendMessage: ReturnType<typeof spyOn>
29
+ let mockSendDirectMessage: ReturnType<typeof spyOn>
30
+ let mockListMessages: ReturnType<typeof spyOn>
31
+ let mockGetMessage: ReturnType<typeof spyOn>
32
+ let mockDeleteMessage: ReturnType<typeof spyOn>
33
+ let mockEditMessage: ReturnType<typeof spyOn>
34
+ let mockLogin: ReturnType<typeof spyOn>
46
35
  let consoleLogSpy: ReturnType<typeof spyOn>
47
- let loginSpy: ReturnType<typeof spyOn>
48
- let handleErrorSpy: ReturnType<typeof spyOn>
36
+ let consoleErrorSpy: ReturnType<typeof spyOn>
37
+ let processExitSpy: ReturnType<typeof spyOn>
38
+ const protoSpies: ReturnType<typeof spyOn>[] = []
39
+
40
+ function protoSpy(method: keyof WebexClient) {
41
+ const s = spyOn(WebexClient.prototype, method as never)
42
+ protoSpies.push(s)
43
+ return s
44
+ }
49
45
 
50
46
  beforeEach(() => {
51
- mockSendMessage.mockReset().mockImplementation(() => Promise.resolve(mockMessage))
52
- mockSendDirectMessage.mockReset().mockImplementation(() => Promise.resolve(mockMessage))
53
- mockListMessages.mockReset().mockImplementation(() => Promise.resolve([mockMessage, mockMessage2]))
54
- mockGetMessage.mockReset().mockImplementation(() => Promise.resolve(mockMessage))
55
- mockDeleteMessage.mockReset().mockImplementation(() => Promise.resolve(undefined))
56
- mockEditMessage.mockReset().mockImplementation(() => Promise.resolve({ ...mockMessage, text: 'Updated message' }))
57
- handleErrorSpy = spyOn(errorHandler, 'handleError').mockImplementation((err: Error) => {
58
- throw err
47
+ mockLogin = protoSpy('login').mockImplementation(async function (this: WebexClient) {
48
+ return this
59
49
  })
50
+ mockSendMessage = protoSpy('sendMessage').mockResolvedValue(mockMessage)
51
+ mockSendDirectMessage = protoSpy('sendDirectMessage').mockResolvedValue(mockMessage)
52
+ mockListMessages = protoSpy('listMessages').mockResolvedValue([mockMessage, mockMessage2])
53
+ mockGetMessage = protoSpy('getMessage').mockResolvedValue(mockMessage)
54
+ mockDeleteMessage = protoSpy('deleteMessage').mockResolvedValue(undefined)
55
+ mockEditMessage = protoSpy('editMessage').mockResolvedValue({ ...mockMessage, text: 'Updated message' })
60
56
 
61
- loginSpy = spyOn(WebexClient.prototype, 'login').mockResolvedValue(Object.assign(new WebexClient(), mockClient))
62
57
  consoleLogSpy = spyOn(console, 'log').mockImplementation(() => {})
58
+ consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
59
+ processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => undefined as never)
63
60
  })
64
61
 
65
62
  afterEach(() => {
66
- loginSpy.mockRestore()
67
- handleErrorSpy.mockRestore()
68
63
  consoleLogSpy.mockRestore()
64
+ consoleErrorSpy.mockRestore()
65
+ processExitSpy.mockRestore()
66
+ for (const s of protoSpies) s.mockRestore()
67
+ protoSpies.length = 0
69
68
  })
70
69
 
71
70
  it('calls sendMessage with correct args and outputs result', async () => {
@@ -87,14 +86,13 @@ it('passes markdown option when --markdown flag is set on send', async () => {
87
86
  expect(mockSendMessage).toHaveBeenCalledWith('space_456', '**bold**', { markdown: true })
88
87
  })
89
88
 
90
- it('throws when not authenticated on send', async () => {
91
- loginSpy.mockImplementation(async () => {
92
- throw new WebexError('No Webex credentials found.', 'no_credentials')
93
- })
89
+ it('exits with code 1 when not authenticated on send', async () => {
90
+ mockLogin.mockRejectedValue(new WebexError('No Webex credentials found.', 'no_credentials'))
94
91
 
95
- await expect(sendAction('space_456', 'Hello', { pretty: false })).rejects.toThrow('No Webex credentials found.')
92
+ await sendAction('space_456', 'Hello', { pretty: false })
96
93
 
97
- expect(handleErrorSpy).toHaveBeenCalledWith(expect.any(WebexError))
94
+ expect(mockSendMessage).not.toHaveBeenCalled()
95
+ expect(processExitSpy).toHaveBeenCalledWith(1)
98
96
  })
99
97
 
100
98
  it('calls sendDirectMessage with email and text', async () => {
@@ -1,6 +1,4 @@
1
- import { afterEach, beforeEach, describe, expect, mock, spyOn, it } from 'bun:test'
2
-
3
- import * as errorHandler from '@/shared/utils/error-handler'
1
+ import { afterEach, beforeEach, describe, expect, spyOn, it } from 'bun:test'
4
2
 
5
3
  import { WebexClient } from '../client'
6
4
  import { WebexError } from '../types'
@@ -38,36 +36,39 @@ const mockMyMemberships = [
38
36
  },
39
37
  ]
40
38
 
41
- const mockListSpaces = mock(() => Promise.resolve(mockSpaces as any))
42
- const mockListMyMemberships = mock(() => Promise.resolve(mockMyMemberships as any))
43
-
44
- const mockClient = {
45
- listSpaces: mockListSpaces,
46
- listMyMemberships: mockListMyMemberships,
47
- }
48
-
49
39
  import { snapshotAction } from './snapshot'
50
40
 
51
41
  describe('snapshot command', () => {
42
+ let mockLogin: ReturnType<typeof spyOn>
52
43
  let consoleSpy: ReturnType<typeof spyOn>
53
- let loginSpy: ReturnType<typeof spyOn>
54
- let handleErrorSpy: ReturnType<typeof spyOn>
44
+ let consoleErrorSpy: ReturnType<typeof spyOn>
45
+ let processExitSpy: ReturnType<typeof spyOn>
46
+ const protoSpies: ReturnType<typeof spyOn>[] = []
47
+
48
+ function protoSpy(method: keyof WebexClient) {
49
+ const s = spyOn(WebexClient.prototype, method as never)
50
+ protoSpies.push(s)
51
+ return s
52
+ }
55
53
 
56
54
  beforeEach(() => {
57
- mockListSpaces.mockReset().mockImplementation(() => Promise.resolve(mockSpaces as any))
58
- mockListMyMemberships.mockReset().mockImplementation(() => Promise.resolve(mockMyMemberships as any))
59
- handleErrorSpy = spyOn(errorHandler, 'handleError').mockImplementation((err: Error) => {
60
- throw err
55
+ mockLogin = protoSpy('login').mockImplementation(async function (this: WebexClient) {
56
+ return this
61
57
  })
58
+ protoSpy('listSpaces').mockResolvedValue(mockSpaces as any)
59
+ protoSpy('listMyMemberships').mockResolvedValue(mockMyMemberships as any)
62
60
 
63
- loginSpy = spyOn(WebexClient.prototype, 'login').mockResolvedValue(Object.assign(new WebexClient(), mockClient))
64
61
  consoleSpy = spyOn(console, 'log').mockImplementation(() => {})
62
+ consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
63
+ processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => undefined as never)
65
64
  })
66
65
 
67
66
  afterEach(() => {
68
- loginSpy.mockRestore()
69
- handleErrorSpy.mockRestore()
70
67
  consoleSpy.mockRestore()
68
+ consoleErrorSpy.mockRestore()
69
+ processExitSpy.mockRestore()
70
+ for (const s of protoSpies) s.mockRestore()
71
+ protoSpies.length = 0
71
72
  })
72
73
 
73
74
  it('returns spaces with id and title only in brief mode', async () => {
@@ -103,13 +104,11 @@ describe('snapshot command', () => {
103
104
  expect(output.spaces[0].id).toBe('space-1')
104
105
  })
105
106
 
106
- it('throws when not authenticated', async () => {
107
- loginSpy.mockImplementation(async () => {
108
- throw new WebexError('No Webex credentials found.', 'no_credentials')
109
- })
107
+ it('exits with code 1 when not authenticated', async () => {
108
+ mockLogin.mockRejectedValue(new WebexError('No Webex credentials found.', 'no_credentials'))
110
109
 
111
- await expect(snapshotAction({})).rejects.toThrow('No Webex credentials found.')
110
+ await snapshotAction({})
112
111
 
113
- expect(handleErrorSpy).toHaveBeenCalledWith(expect.any(WebexError))
112
+ expect(processExitSpy).toHaveBeenCalledWith(1)
114
113
  })
115
114
  })
@@ -1,6 +1,4 @@
1
- import { afterEach, beforeEach, describe, expect, mock, spyOn, it } from 'bun:test'
2
-
3
- import * as errorHandler from '@/shared/utils/error-handler'
1
+ import { afterEach, beforeEach, describe, expect, spyOn, it } from 'bun:test'
4
2
 
5
3
  import { WebexClient } from '../client'
6
4
  import { WebexError } from '../types'
@@ -37,32 +35,40 @@ const mockSpace = {
37
35
  creatorId: 'person-1',
38
36
  }
39
37
 
40
- const mockListSpaces = mock(() => Promise.resolve(mockSpaces))
41
- const mockGetSpace = mock(() => Promise.resolve(mockSpace))
42
-
43
38
  import { infoAction, listAction } from './space'
44
39
 
40
+ let mockListSpaces: ReturnType<typeof spyOn>
41
+ let mockGetSpace: ReturnType<typeof spyOn>
42
+ let mockLogin: ReturnType<typeof spyOn>
45
43
  let consoleLogSpy: ReturnType<typeof spyOn>
46
- let loginSpy: ReturnType<typeof spyOn>
47
- let handleErrorSpy: ReturnType<typeof spyOn>
44
+ let consoleErrorSpy: ReturnType<typeof spyOn>
45
+ let processExitSpy: ReturnType<typeof spyOn>
46
+ const protoSpies: ReturnType<typeof spyOn>[] = []
47
+
48
+ function protoSpy(method: keyof WebexClient) {
49
+ const s = spyOn(WebexClient.prototype, method as never)
50
+ protoSpies.push(s)
51
+ return s
52
+ }
48
53
 
49
54
  beforeEach(() => {
50
- mockListSpaces.mockReset().mockImplementation(() => Promise.resolve(mockSpaces))
51
- mockGetSpace.mockReset().mockImplementation(() => Promise.resolve(mockSpace))
52
- handleErrorSpy = spyOn(errorHandler, 'handleError').mockImplementation((err: Error) => {
53
- throw err
55
+ mockLogin = protoSpy('login').mockImplementation(async function (this: WebexClient) {
56
+ return this
54
57
  })
58
+ mockListSpaces = protoSpy('listSpaces').mockResolvedValue(mockSpaces)
59
+ mockGetSpace = protoSpy('getSpace').mockResolvedValue(mockSpace)
55
60
 
56
- loginSpy = spyOn(WebexClient.prototype, 'login').mockResolvedValue(
57
- Object.assign(new WebexClient(), { listSpaces: mockListSpaces, getSpace: mockGetSpace }),
58
- )
59
61
  consoleLogSpy = spyOn(console, 'log').mockImplementation(() => {})
62
+ consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
63
+ processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => undefined as never)
60
64
  })
61
65
 
62
66
  afterEach(() => {
63
- loginSpy.mockRestore()
64
- handleErrorSpy.mockRestore()
65
67
  consoleLogSpy.mockRestore()
68
+ consoleErrorSpy.mockRestore()
69
+ processExitSpy.mockRestore()
70
+ for (const s of protoSpies) s.mockRestore()
71
+ protoSpies.length = 0
66
72
  })
67
73
 
68
74
  describe('listAction', () => {
@@ -129,15 +135,13 @@ describe('listAction', () => {
129
135
  )
130
136
  })
131
137
 
132
- it('throws when not authenticated', async () => {
133
- loginSpy.mockImplementation(async () => {
134
- throw new WebexError('No Webex credentials found.', 'no_credentials')
135
- })
138
+ it('exits with code 1 when not authenticated', async () => {
139
+ mockLogin.mockRejectedValue(new WebexError('No Webex credentials found.', 'no_credentials'))
136
140
 
137
- await expect(listAction({})).rejects.toThrow('No Webex credentials found.')
141
+ await listAction({})
138
142
 
139
143
  expect(mockListSpaces).not.toHaveBeenCalled()
140
- expect(handleErrorSpy).toHaveBeenCalledWith(expect.any(WebexError))
144
+ expect(processExitSpy).toHaveBeenCalledWith(1)
141
145
  })
142
146
  })
143
147
 
@@ -192,14 +196,12 @@ describe('infoAction', () => {
192
196
  )
193
197
  })
194
198
 
195
- it('throws when not authenticated', async () => {
196
- loginSpy.mockImplementation(async () => {
197
- throw new WebexError('No Webex credentials found.', 'no_credentials')
198
- })
199
+ it('exits with code 1 when not authenticated', async () => {
200
+ mockLogin.mockRejectedValue(new WebexError('No Webex credentials found.', 'no_credentials'))
199
201
 
200
- await expect(infoAction('space-1', {})).rejects.toThrow('No Webex credentials found.')
202
+ await infoAction('space-1', {})
201
203
 
202
204
  expect(mockGetSpace).not.toHaveBeenCalled()
203
- expect(handleErrorSpy).toHaveBeenCalledWith(expect.any(WebexError))
205
+ expect(processExitSpy).toHaveBeenCalledWith(1)
204
206
  })
205
207
  })
@@ -23,7 +23,9 @@ let consoleLogSpy: ReturnType<typeof spyOn>
23
23
  let processExitSpy: ReturnType<typeof spyOn>
24
24
 
25
25
  beforeEach(() => {
26
- loginSpy = spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient())
26
+ loginSpy = spyOn(WebexClient.prototype, 'login').mockImplementation(async function (this: WebexClient) {
27
+ return this
28
+ })
27
29
  testAuthSpy = spyOn(WebexClient.prototype, 'testAuth').mockResolvedValue(mockUser)
28
30
  consoleLogSpy = spyOn(console, 'log').mockImplementation(() => {})
29
31
  processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => undefined as never)
@@ -8,6 +8,7 @@ import { WebexCredentialManager } from './credential-manager'
8
8
  describe('WebexCredentialManager', () => {
9
9
  let tempDir: string
10
10
  let credManager: WebexCredentialManager
11
+ const realFetch = globalThis.fetch
11
12
 
12
13
  beforeEach(async () => {
13
14
  tempDir = await mkdtemp(join(tmpdir(), 'webex-cred-test-'))
@@ -16,6 +17,8 @@ describe('WebexCredentialManager', () => {
16
17
 
17
18
  afterEach(async () => {
18
19
  await rm(tempDir, { recursive: true, force: true })
20
+ // Guarantee fetch restoration even if a test throws before its own restore line.
21
+ globalThis.fetch = realFetch
19
22
  })
20
23
 
21
24
  it('loadConfig returns null when no file exists', async () => {
@@ -2,12 +2,6 @@ import { afterEach, beforeEach, describe, expect, mock, spyOn, it } from 'bun:te
2
2
 
3
3
  const originalConsoleLog = console.log
4
4
 
5
- mock.module('@/shared/utils/error-handler', () => ({
6
- handleError: (err: Error) => {
7
- throw err
8
- },
9
- }))
10
-
11
5
  const mockGetAccount = mock(() => Promise.resolve(null))
12
6
  const mockListAccounts = mock(() => Promise.resolve([]))
13
7
  const mockSetCurrent = mock(() => Promise.resolve(false))
@@ -46,6 +40,7 @@ import { authCommand } from './auth'
46
40
 
47
41
  describe('auth commands', () => {
48
42
  let consoleLogSpy: ReturnType<typeof mock>
43
+ let consoleErrorSpy: ReturnType<typeof spyOn>
49
44
  let processExitSpy: ReturnType<typeof spyOn>
50
45
 
51
46
  beforeEach(() => {
@@ -73,14 +68,14 @@ describe('auth commands', () => {
73
68
 
74
69
  consoleLogSpy = mock((..._args: unknown[]) => {})
75
70
  console.log = consoleLogSpy
76
- processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => {
77
- throw new Error(`process.exit(${_code})`)
78
- })
71
+ consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
72
+ processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => undefined as never)
79
73
  processExitSpy.mockClear()
80
74
  })
81
75
 
82
76
  afterEach(() => {
83
77
  console.log = originalConsoleLog
78
+ consoleErrorSpy.mockRestore()
84
79
  processExitSpy.mockRestore()
85
80
  })
86
81
 
@@ -153,7 +148,7 @@ describe('auth commands', () => {
153
148
  it('outputs error and exits when account not found', async () => {
154
149
  mockSetCurrent.mockImplementation(() => Promise.resolve(false))
155
150
 
156
- await expect(authCommand.parseAsync(['use', 'nonexistent'], { from: 'user' })).rejects.toThrow('process.exit(1)')
151
+ await authCommand.parseAsync(['use', 'nonexistent'], { from: 'user' })
157
152
 
158
153
  expect(processExitSpy).toHaveBeenCalledWith(1)
159
154
  const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
@@ -185,7 +180,7 @@ describe('auth commands', () => {
185
180
  it('outputs error and exits when no account configured', async () => {
186
181
  mockGetAccount.mockImplementation(() => Promise.resolve(null))
187
182
 
188
- await expect(authCommand.parseAsync(['status'], { from: 'user' })).rejects.toThrow('process.exit(1)')
183
+ await authCommand.parseAsync(['status'], { from: 'user' })
189
184
 
190
185
  expect(processExitSpy).toHaveBeenCalledWith(1)
191
186
  const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
@@ -217,9 +212,7 @@ describe('auth commands', () => {
217
212
  it('outputs error for specific missing account', async () => {
218
213
  mockGetAccount.mockImplementation(() => Promise.resolve(null))
219
214
 
220
- await expect(authCommand.parseAsync(['status', '--account', 'missing-id'], { from: 'user' })).rejects.toThrow(
221
- 'process.exit(1)',
222
- )
215
+ await authCommand.parseAsync(['status', '--account', 'missing-id'], { from: 'user' })
223
216
 
224
217
  expect(processExitSpy).toHaveBeenCalledWith(1)
225
218
  const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
@@ -241,8 +234,9 @@ describe('auth commands', () => {
241
234
  )
242
235
  mockRemoveAccount.mockImplementation(() => Promise.resolve(true))
243
236
 
244
- await expect(authCommand.parseAsync(['logout'], { from: 'user' })).rejects.toThrow('process.exit(0)')
237
+ await authCommand.parseAsync(['logout'], { from: 'user' })
245
238
 
239
+ expect(processExitSpy).toHaveBeenCalledWith(0)
246
240
  expect(mockRemoveAccount).toHaveBeenCalledWith('plus-12025551234')
247
241
  const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
248
242
  expect(output.success).toBe(true)
@@ -253,7 +247,7 @@ describe('auth commands', () => {
253
247
  it('outputs error and exits when no account configured', async () => {
254
248
  mockGetAccount.mockImplementation(() => Promise.resolve(null))
255
249
 
256
- await expect(authCommand.parseAsync(['logout'], { from: 'user' })).rejects.toThrow('process.exit(1)')
250
+ await authCommand.parseAsync(['logout'], { from: 'user' })
257
251
 
258
252
  expect(processExitSpy).toHaveBeenCalledWith(1)
259
253
  const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
@@ -273,8 +267,9 @@ describe('auth commands', () => {
273
267
  mockConnect.mockImplementation(() => Promise.reject(new Error('Connection failed')))
274
268
  mockRemoveAccount.mockImplementation(() => Promise.resolve(true))
275
269
 
276
- await expect(authCommand.parseAsync(['logout'], { from: 'user' })).rejects.toThrow('process.exit(0)')
270
+ await authCommand.parseAsync(['logout'], { from: 'user' })
277
271
 
272
+ expect(processExitSpy).toHaveBeenCalledWith(0)
278
273
  expect(mockRemoveAccount).toHaveBeenCalledWith('plus-12025551234')
279
274
  const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
280
275
  expect(output.success).toBe(true)
@@ -295,10 +290,9 @@ describe('auth commands', () => {
295
290
  })
296
291
  mockRemoveAccount.mockImplementation(() => Promise.resolve(true))
297
292
 
298
- await expect(
299
- authCommand.parseAsync(['logout', '--account', 'plus-19995551234'], { from: 'user' }),
300
- ).rejects.toThrow('process.exit(0)')
293
+ await authCommand.parseAsync(['logout', '--account', 'plus-19995551234'], { from: 'user' })
301
294
 
295
+ expect(processExitSpy).toHaveBeenCalledWith(0)
302
296
  expect(mockRemoveAccount).toHaveBeenCalledWith('plus-19995551234')
303
297
  const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
304
298
  expect(output.success).toBe(true)