agent-messenger 2.19.5 → 2.20.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 (63) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/package.json +1 -1
  3. package/dist/src/platforms/line/client.d.ts +4 -0
  4. package/dist/src/platforms/line/client.d.ts.map +1 -1
  5. package/dist/src/platforms/line/client.js +124 -24
  6. package/dist/src/platforms/line/client.js.map +1 -1
  7. package/dist/src/platforms/line/e2ee-storage.d.ts +16 -0
  8. package/dist/src/platforms/line/e2ee-storage.d.ts.map +1 -0
  9. package/dist/src/platforms/line/e2ee-storage.js +93 -0
  10. package/dist/src/platforms/line/e2ee-storage.js.map +1 -0
  11. package/dist/src/platforms/teams/cli.d.ts.map +1 -1
  12. package/dist/src/platforms/teams/cli.js +2 -1
  13. package/dist/src/platforms/teams/cli.js.map +1 -1
  14. package/dist/src/platforms/teams/client.d.ts +4 -1
  15. package/dist/src/platforms/teams/client.d.ts.map +1 -1
  16. package/dist/src/platforms/teams/client.js +84 -0
  17. package/dist/src/platforms/teams/client.js.map +1 -1
  18. package/dist/src/platforms/teams/commands/chat.d.ts +13 -0
  19. package/dist/src/platforms/teams/commands/chat.d.ts.map +1 -0
  20. package/dist/src/platforms/teams/commands/chat.js +111 -0
  21. package/dist/src/platforms/teams/commands/chat.js.map +1 -0
  22. package/dist/src/platforms/teams/commands/index.d.ts +1 -0
  23. package/dist/src/platforms/teams/commands/index.d.ts.map +1 -1
  24. package/dist/src/platforms/teams/commands/index.js +1 -0
  25. package/dist/src/platforms/teams/commands/index.js.map +1 -1
  26. package/dist/src/platforms/teams/types.d.ts +24 -0
  27. package/dist/src/platforms/teams/types.d.ts.map +1 -1
  28. package/dist/src/platforms/teams/types.js +8 -0
  29. package/dist/src/platforms/teams/types.js.map +1 -1
  30. package/dist/src/tui/adapters/line-adapter.js +1 -1
  31. package/dist/src/tui/adapters/line-adapter.js.map +1 -1
  32. package/docs/content/docs/cli/line.mdx +13 -11
  33. package/package.json +1 -1
  34. package/skills/agent-channeltalk/SKILL.md +1 -1
  35. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  36. package/skills/agent-discord/SKILL.md +1 -1
  37. package/skills/agent-discordbot/SKILL.md +1 -1
  38. package/skills/agent-instagram/SKILL.md +1 -1
  39. package/skills/agent-kakaotalk/SKILL.md +1 -1
  40. package/skills/agent-line/SKILL.md +7 -5
  41. package/skills/agent-line/references/common-patterns.md +5 -2
  42. package/skills/agent-slack/SKILL.md +1 -1
  43. package/skills/agent-slackbot/SKILL.md +1 -1
  44. package/skills/agent-teams/SKILL.md +20 -2
  45. package/skills/agent-teams/references/common-patterns.md +28 -0
  46. package/skills/agent-telegram/SKILL.md +1 -1
  47. package/skills/agent-telegrambot/SKILL.md +1 -1
  48. package/skills/agent-webex/SKILL.md +1 -1
  49. package/skills/agent-wechatbot/SKILL.md +1 -1
  50. package/skills/agent-whatsapp/SKILL.md +1 -1
  51. package/skills/agent-whatsappbot/SKILL.md +1 -1
  52. package/src/platforms/line/client.test.ts +223 -20
  53. package/src/platforms/line/client.ts +141 -29
  54. package/src/platforms/line/e2ee-storage.test.ts +154 -0
  55. package/src/platforms/line/e2ee-storage.ts +119 -0
  56. package/src/platforms/teams/cli.ts +2 -0
  57. package/src/platforms/teams/client.test.ts +96 -0
  58. package/src/platforms/teams/client.ts +133 -0
  59. package/src/platforms/teams/commands/chat.test.ts +100 -0
  60. package/src/platforms/teams/commands/chat.ts +131 -0
  61. package/src/platforms/teams/commands/index.ts +1 -0
  62. package/src/platforms/teams/types.ts +20 -0
  63. package/src/tui/adapters/line-adapter.ts +1 -1
@@ -71,12 +71,15 @@ MESSAGES=$(agent-line message list "$CHAT_ID" -n 50)
71
71
  MSG_COUNT=$(echo "$MESSAGES" | jq 'length')
72
72
  echo "Found $MSG_COUNT messages"
73
73
 
74
- # Show messages; encrypted Letter Sealing messages may include decryption_error.
75
- echo "$MESSAGES" | jq -r '.[] | "\(.author_id): \(.text // .decryption_error.message // "[non-text]")"'
74
+ # Show messages by display name; Letter Sealing messages are decrypted when E2EE
75
+ # key material is available, otherwise decryption_error explains why text is null.
76
+ echo "$MESSAGES" | jq -r '.[] | "\(.author_name // .author_id): \(.text // .decryption_error.message // "[non-text]")"'
76
77
  ```
77
78
 
78
79
  **When to use**: Context gathering, summarizing conversations, catching up on missed messages.
79
80
 
81
+ **E2EE note**: `message list` decrypts Letter Sealing (E2EE) messages when key material is available — restored from a prior QR/email login. When keys are missing, `text` is `null` and `decryption_error.code` is `missing_e2ee_key`; re-run `agent-line auth login` (QR) to provision keys.
82
+
80
83
  ## Pattern 4: Monitor for New Messages
81
84
 
82
85
  **Use case**: Watch a chat room and respond to new messages using the SDK
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-slack
3
3
  description: Interact with Slack workspaces - send messages, read channels, manage reactions
4
- version: 2.19.5
4
+ version: 2.20.1
5
5
  allowed-tools: Bash(agent-slack:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-slackbot
3
3
  description: Interact with Slack workspaces using bot tokens - send messages, read channels, manage reactions
4
- version: 2.19.5
4
+ version: 2.20.1
5
5
  allowed-tools: Bash(agent-slackbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-teams
3
3
  description: Interact with Microsoft Teams - send messages, read channels, manage reactions
4
- version: 2.19.5
4
+ version: 2.20.1
5
5
  allowed-tools: Bash(agent-teams:*)
6
6
  metadata:
7
7
  openclaw:
@@ -207,6 +207,23 @@ agent-teams channel info <team-id> 19:abc123@thread.tacv2
207
207
  agent-teams channel history <team-id> <channel-id> --limit 100
208
208
  ```
209
209
 
210
+ ### Chat Commands
211
+
212
+ For personal Microsoft accounts (`@outlook.com` / `@live.com`) that have no teams or channels — only 1:1, group, and self chat threads. Work accounts can use these too for their 1:1 and group chats.
213
+
214
+ ```bash
215
+ # List 1:1, group, and self chats
216
+ agent-teams chat list
217
+
218
+ # Get chat message history
219
+ agent-teams chat history <chat-id> --limit 100
220
+
221
+ # Send a message to a chat
222
+ agent-teams chat send <chat-id> "Hello"
223
+ ```
224
+
225
+ Chat IDs look like `19:guid1_guid2@unq.gbl.spaces` (1:1) or `19:guid@thread.tacv2` (group). Get them from `chat list`. The `48:notes` chat (`type: self`) is your personal "to me" notes thread.
226
+
210
227
  ### Team Commands
211
228
 
212
229
  ```bash
@@ -357,7 +374,7 @@ Common errors:
357
374
 
358
375
  - `Not authenticated`: No valid token (auto-extraction failed — see Troubleshooting)
359
376
  - `Token expired`: Token has expired and auto-refresh failed — see Troubleshooting
360
- - `No current team set`: Run `team switch <id>` first
377
+ - `No current team set`: Run `team switch <id>` first. Personal accounts have no teams — use `chat list` / `chat history` / `chat send` instead
361
378
  - `Message not found`: Invalid message ID
362
379
  - `Channel not found`: Invalid channel ID
363
380
  - `401 Unauthorized`: Token expired and auto-refresh failed — see Troubleshooting
@@ -419,6 +436,7 @@ See the [Teams SDK documentation](https://agent-messenger.dev/docs/sdk/teams) fo
419
436
  - No real-time events / WebSocket connection
420
437
  - No voice/video channel support
421
438
  - No team management (create/delete channels, roles)
439
+ - Personal accounts: chats only (no teams/channels); use the `chat` commands
422
440
  - No meeting support
423
441
  - No webhook support
424
442
  - Plain text messages only (no adaptive cards in v1)
@@ -438,6 +438,34 @@ SNAPSHOT=$(teams_cmd agent-teams snapshot)
438
438
 
439
439
  **When to use**: Any script that runs for more than a few minutes.
440
440
 
441
+ ## Pattern 12: Personal Account Chats (No Teams)
442
+
443
+ **Use case**: Message from a personal Microsoft account (`@outlook.com` / `@live.com`), which has no teams or channels — only 1:1, group, and self ("to me") chats
444
+
445
+ ```bash
446
+ #!/bin/bash
447
+
448
+ # Personal accounts have no teams, so `team list` is empty and team/channel
449
+ # commands fail with "No current team set". Use the `chat` commands instead.
450
+
451
+ agent-teams auth extract 2>/dev/null || true
452
+
453
+ # List chats (type is oneOnOne, group, or self)
454
+ CHATS=$(agent-teams chat list)
455
+ echo "$CHATS" | jq -r '.[] | " \(.type): \(.id) — \(.topic // .last_message // "")"'
456
+
457
+ # Pick a chat ID (here: the self "to me" notes thread)
458
+ CHAT_ID=$(echo "$CHATS" | jq -r '.[] | select(.type=="self") | .id')
459
+
460
+ # Read history and send a message
461
+ agent-teams chat history "$CHAT_ID" --limit 20
462
+ agent-teams chat send "$CHAT_ID" "Reminder: stand-up at 10am"
463
+ ```
464
+
465
+ **When to use**: Any workflow on a personal/consumer Teams account. Get chat IDs from `chat list` — they look like `19:guid1_guid2@unq.gbl.spaces` (1:1), `19:guid@thread.tacv2` (group), or `48:notes` (self).
466
+
467
+ **Note**: Work accounts also have 1:1 and group chats and can use these same `chat` commands.
468
+
441
469
  ## Best Practices
442
470
 
443
471
  ### 1. Always Handle Token Expiry
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-telegram
3
3
  description: Interact with Telegram through TDLib - authenticate, inspect chats, and send messages
4
- version: 2.19.5
4
+ version: 2.20.1
5
5
  allowed-tools: Bash(agent-telegram:*)
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-telegrambot
3
3
  description: Interact with Telegram using bot tokens - send messages, read chats, manage reactions
4
- version: 2.19.5
4
+ version: 2.20.1
5
5
  allowed-tools: Bash(agent-telegrambot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-webex
3
3
  description: Interact with Cisco Webex - send messages, read spaces, manage memberships
4
- version: 2.19.5
4
+ version: 2.20.1
5
5
  allowed-tools: Bash(agent-webex:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-wechatbot
3
3
  description: Interact with WeChat Official Account using API credentials - send messages, manage templates, list followers
4
- version: 2.19.5
4
+ version: 2.20.1
5
5
  allowed-tools: Bash(agent-wechatbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-whatsapp
3
3
  description: Interact with WhatsApp - send messages, read chats, manage conversations
4
- version: 2.19.5
4
+ version: 2.20.1
5
5
  allowed-tools: Bash(agent-whatsapp:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-whatsappbot
3
3
  description: Interact with WhatsApp using Cloud API credentials - send messages, manage templates
4
- version: 2.19.5
4
+ version: 2.20.1
5
5
  allowed-tools: Bash(agent-whatsappbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -56,9 +56,10 @@ describe('LineClient', () => {
56
56
  })
57
57
 
58
58
  describe('getMessages()', () => {
59
- function clientWithTalk(talk: Record<string, unknown>): LineClient {
59
+ function clientWithTalk(talk: Record<string, unknown>, e2ee?: Record<string, unknown>): LineClient {
60
60
  const client = new LineClient()
61
- ;(client as any).client = { base: { talk } }
61
+ const { profile, ...talkMethods } = talk
62
+ ;(client as any).client = { base: { talk: talkMethods, profile, e2ee: e2ee ?? {} } }
62
63
  return client
63
64
  }
64
65
 
@@ -106,28 +107,179 @@ describe('LineClient', () => {
106
107
  expect(result.map((m) => m.text)).toEqual(['c', 'b'])
107
108
  })
108
109
 
109
- it('marks encrypted chunk messages without text as missing E2EE keys', async () => {
110
+ it('resolves author MIDs to display names via getContacts', async () => {
110
111
  const client = clientWithTalk({
112
+ profile: { mid: 'me' },
111
113
  getServerTime: async () => 1700000000000,
112
114
  getPreviousMessagesV2WithRequest: async () => [
113
- {
114
- id: '40',
115
- from: 'u1',
116
- text: null,
117
- contentType: 'NONE',
118
- createdTime: 1700000004000,
119
- chunks: ['a', 'b'],
120
- metadata: { e2eeMark: '2', e2eeVersion: '2' },
121
- },
115
+ { id: '10', from: 'u1', text: 'hi', contentType: 'NONE', createdTime: 1700000001000 },
116
+ { id: '11', from: 'u2', text: 'yo', contentType: 'NONE', createdTime: 1700000002000 },
117
+ ],
118
+ getContacts: async ({ mids }: { mids: string[] }) =>
119
+ mids.map((mid) => ({ mid, displayName: mid === 'u1' ? 'Alice' : 'Bob' })),
120
+ })
121
+
122
+ const result = await client.getMessages('chat1', { count: 2 })
123
+ expect(result.map((m) => m.author_name)).toEqual(['Alice', 'Bob'])
124
+ })
125
+
126
+ it('batch-resolves unique authors in a single getContacts call', async () => {
127
+ let contactCalls = 0
128
+ const client = clientWithTalk({
129
+ profile: { mid: 'me' },
130
+ getServerTime: async () => 1700000000000,
131
+ getPreviousMessagesV2WithRequest: async () => [
132
+ { id: '10', from: 'u1', text: 'a', contentType: 'NONE', createdTime: 1700000001000 },
133
+ { id: '11', from: 'u1', text: 'b', contentType: 'NONE', createdTime: 1700000002000 },
134
+ { id: '12', from: 'u2', text: 'c', contentType: 'NONE', createdTime: 1700000003000 },
135
+ ],
136
+ getContacts: async ({ mids }: { mids: string[] }) => {
137
+ contactCalls++
138
+ expect([...mids].sort()).toEqual(['u1', 'u2'])
139
+ return mids.map((mid) => ({ mid, displayName: mid.toUpperCase() }))
140
+ },
141
+ })
142
+
143
+ const result = await client.getMessages('chat1', { count: 3 })
144
+ expect(contactCalls).toBe(1)
145
+ expect(result.map((m) => m.author_name)).toEqual(['U1', 'U1', 'U2'])
146
+ })
147
+
148
+ it('resolves the current user via getProfile since getContacts omits self', async () => {
149
+ const client = clientWithTalk({
150
+ profile: { mid: 'me' },
151
+ getServerTime: async () => 1700000000000,
152
+ getPreviousMessagesV2WithRequest: async () => [
153
+ { id: '10', from: 'me', text: 'mine', contentType: 'NONE', createdTime: 1700000001000 },
122
154
  ],
155
+ getContacts: async () => [],
156
+ getProfile: async () => ({ mid: 'me', displayName: 'My Name' }),
123
157
  })
124
158
 
125
159
  const result = await client.getMessages('chat1', { count: 1 })
126
- expect(result[0].text).toBeNull()
127
- expect(result[0].decryption_error).toEqual({
128
- code: 'missing_e2ee_key',
129
- message: 'LINE message is encrypted with Letter Sealing, but this session has no saved E2EE key material.',
160
+ expect(result[0].author_name).toBe('My Name')
161
+ })
162
+
163
+ it('falls back to bare author_id when name resolution fails', async () => {
164
+ const client = clientWithTalk({
165
+ profile: { mid: 'me' },
166
+ getServerTime: async () => 1700000000000,
167
+ getPreviousMessagesV2WithRequest: async () => [
168
+ { id: '10', from: 'u1', text: 'hi', contentType: 'NONE', createdTime: 1700000001000 },
169
+ ],
170
+ getContacts: async () => {
171
+ throw new Error('network down')
172
+ },
130
173
  })
174
+
175
+ const result = await client.getMessages('chat1', { count: 1 })
176
+ expect(result[0].author_name).toBeUndefined()
177
+ expect(result[0].author_id).toBe('u1')
178
+ })
179
+
180
+ it('decrypts Letter-Sealing chunk messages via decryptE2EEMessage', async () => {
181
+ const encrypted = {
182
+ id: '40',
183
+ from: 'u1',
184
+ text: null,
185
+ contentType: 'NONE',
186
+ createdTime: 1700000004000,
187
+ chunks: ['a', 'b'],
188
+ metadata: { e2eeMark: '2', e2eeVersion: '2' },
189
+ }
190
+ const client = clientWithTalk(
191
+ {
192
+ getServerTime: async () => 1700000000000,
193
+ getPreviousMessagesV2WithRequest: async () => [encrypted],
194
+ },
195
+ { decryptE2EEMessage: async (m: { id: string }) => ({ ...m, text: 'decrypted secret' }) },
196
+ )
197
+
198
+ const result = await client.getMessages('chat1', { count: 1 })
199
+ expect(result[0].text).toBe('decrypted secret')
200
+ expect(result[0].decryption_error).toBeUndefined()
201
+ })
202
+
203
+ it('normalizes metadata-shaped history messages to contentMetadata before decrypting', async () => {
204
+ let received: { contentMetadata?: unknown } | undefined
205
+ const client = clientWithTalk(
206
+ {
207
+ getServerTime: async () => 1700000000000,
208
+ getPreviousMessagesV2WithRequest: async () => [
209
+ {
210
+ id: '40',
211
+ from: 'u1',
212
+ text: null,
213
+ contentType: 'NONE',
214
+ createdTime: 1700000004000,
215
+ chunks: ['a', 'b'],
216
+ metadata: { e2eeMark: '2', e2eeVersion: '2' },
217
+ },
218
+ ],
219
+ },
220
+ {
221
+ decryptE2EEMessage: async (m: { contentMetadata?: unknown }) => {
222
+ received = m
223
+ // mirror the vendor decryptor: it reads contentMetadata.e2eeVersion
224
+ const meta = m.contentMetadata as { e2eeVersion?: string }
225
+ return { text: `v${meta.e2eeVersion}` }
226
+ },
227
+ },
228
+ )
229
+
230
+ const result = await client.getMessages('chat1', { count: 1 })
231
+ expect((received?.contentMetadata as { e2eeVersion?: string })?.e2eeVersion).toBe('2')
232
+ expect(result[0].text).toBe('v2')
233
+ })
234
+
235
+ it('surfaces missing_e2ee_key when decryption fails for lack of keys', async () => {
236
+ const client = clientWithTalk(
237
+ {
238
+ getServerTime: async () => 1700000000000,
239
+ getPreviousMessagesV2WithRequest: async () => [
240
+ {
241
+ id: '40',
242
+ from: 'u1',
243
+ text: null,
244
+ contentType: 'NONE',
245
+ createdTime: 1700000004000,
246
+ chunks: ['a', 'b'],
247
+ metadata: { e2eeMark: '2', e2eeVersion: '2' },
248
+ },
249
+ ],
250
+ },
251
+ {
252
+ decryptE2EEMessage: async () => {
253
+ throw new Error('NoE2EEKey: E2EE Key has not been saved')
254
+ },
255
+ },
256
+ )
257
+
258
+ const result = await client.getMessages('chat1', { count: 1 })
259
+ expect(result[0].text).toBeNull()
260
+ expect(result[0].decryption_error?.code).toBe('missing_e2ee_key')
261
+ })
262
+
263
+ it('does not decrypt plain messages that already carry text', async () => {
264
+ let decryptCalls = 0
265
+ const client = clientWithTalk(
266
+ {
267
+ getServerTime: async () => 1700000000000,
268
+ getPreviousMessagesV2WithRequest: async () => [
269
+ { id: '50', from: 'u1', text: 'plain hello', contentType: 'NONE', createdTime: 1700000005000 },
270
+ ],
271
+ },
272
+ {
273
+ decryptE2EEMessage: async () => {
274
+ decryptCalls++
275
+ return { text: 'should-not-run' }
276
+ },
277
+ },
278
+ )
279
+
280
+ const result = await client.getMessages('chat1', { count: 1 })
281
+ expect(result[0].text).toBe('plain hello')
282
+ expect(decryptCalls).toBe(0)
131
283
  })
132
284
  })
133
285
 
@@ -206,8 +358,17 @@ describe('LineClient', () => {
206
358
  return out
207
359
  }
208
360
 
209
- it('decrypts messages and emits event + message', async () => {
210
- const op = { type: 'RECEIVE_MESSAGE', message: { id: '1', from: 'u1', to: 'me' } }
361
+ it('decrypts Letter-Sealing chunk messages and emits event + message', async () => {
362
+ const op = {
363
+ type: 'RECEIVE_MESSAGE',
364
+ message: {
365
+ id: '1',
366
+ from: 'u1',
367
+ to: 'me',
368
+ chunks: ['a', 'b'],
369
+ contentMetadata: { e2eeMark: '2', e2eeVersion: '2' },
370
+ },
371
+ }
211
372
  const client = clientWithStream([op], async () => ({ id: '1', from: 'u1', to: 'me', text: 'hello' }))
212
373
 
213
374
  const events = await collect(client)
@@ -216,9 +377,48 @@ describe('LineClient', () => {
216
377
  expect(msg.message.text).toBe('hello')
217
378
  })
218
379
 
380
+ it('normalizes metadata-only E2EE messages to contentMetadata before decrypting', async () => {
381
+ // given: an encrypted message whose E2EE metadata lives under `metadata`
382
+ // (not `contentMetadata`); the vendor decryptor reads contentMetadata
383
+ // unconditionally, so it must be normalized first — same as the history path
384
+ const op = {
385
+ type: 'RECEIVE_MESSAGE',
386
+ message: { id: '1', from: 'u1', to: 'me', chunks: ['a', 'b'], metadata: { e2eeMark: '2', e2eeVersion: '2' } },
387
+ }
388
+ const client = clientWithStream([op], async (m) => {
389
+ const meta = (m as { contentMetadata?: { e2eeVersion?: string } }).contentMetadata
390
+ return { id: '1', from: 'u1', to: 'me', text: `v${meta?.e2eeVersion}` }
391
+ })
392
+
393
+ const events = await collect(client)
394
+ const msg = events[1] as Extract<LineRawEvent, { kind: 'message' }>
395
+ expect(msg.message.text).toBe('v2')
396
+ expect(msg.message.decryption_error).toBeUndefined()
397
+ })
398
+
399
+ it('keeps a plain message text without decrypting it', async () => {
400
+ // given: a plain (non-Letter-Sealing) message that already carries text and
401
+ // has no E2EE chunks — decrypt MUST NOT run, mirroring the history path
402
+ const op = { type: 'RECEIVE_MESSAGE', message: { id: '1', from: 'u1', to: 'me', text: 'plain hi' } }
403
+ let decryptCalls = 0
404
+ const client = clientWithStream([op], async (m) => {
405
+ decryptCalls++
406
+ return m
407
+ })
408
+
409
+ const events = await collect(client)
410
+ const msg = events[1] as Extract<LineRawEvent, { kind: 'message' }>
411
+ expect(decryptCalls).toBe(0)
412
+ expect(msg.message.text).toBe('plain hi')
413
+ expect(msg.message.decryption_error).toBeUndefined()
414
+ })
415
+
219
416
  it('keeps streaming when E2EE decryption fails (no reconnect loop)', async () => {
220
417
  const ops = [
221
- { type: 'RECEIVE_MESSAGE', message: { id: '1', from: 'u1', to: 'me', text: 'plain-on-raw' } },
418
+ {
419
+ type: 'RECEIVE_MESSAGE',
420
+ message: { id: '1', from: 'u1', to: 'me', chunks: ['a', 'b'], metadata: { e2eeMark: '2', e2eeVersion: '2' } },
421
+ },
222
422
  { type: 'NOTIFIED_READ_MESSAGE' },
223
423
  ]
224
424
  const client = clientWithStream(ops, async () => {
@@ -234,7 +434,10 @@ describe('LineClient', () => {
234
434
  })
235
435
 
236
436
  it('marks missing E2EE key failures explicitly', async () => {
237
- const op = { type: 'RECEIVE_MESSAGE', message: { id: '1', from: 'u1', to: 'me' } }
437
+ const op = {
438
+ type: 'RECEIVE_MESSAGE',
439
+ message: { id: '1', from: 'u1', to: 'me', chunks: ['a', 'b'], metadata: { e2eeMark: '2', e2eeVersion: '2' } },
440
+ }
238
441
  const client = clientWithStream([op], async () => {
239
442
  throw new Error('NoE2EEKey: E2EE Key has not been saved')
240
443
  })