@elizaos/plugin-line 2.0.0-alpha.3 → 2.0.0-alpha.5
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/dist/accounts.d.ts +152 -0
- package/dist/accounts.d.ts.map +1 -0
- package/dist/accounts.js +260 -0
- package/dist/accounts.js.map +1 -0
- package/dist/actions/index.d.ts +7 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/{src/actions/index.ts → dist/actions/index.js} +1 -1
- package/dist/actions/index.js.map +1 -0
- package/dist/actions/sendFlexMessage.d.ts +6 -0
- package/dist/actions/sendFlexMessage.d.ts.map +1 -0
- package/dist/actions/sendFlexMessage.js +178 -0
- package/dist/actions/sendFlexMessage.js.map +1 -0
- package/dist/actions/sendLocation.d.ts +6 -0
- package/dist/actions/sendLocation.d.ts.map +1 -0
- package/dist/actions/sendLocation.js +160 -0
- package/dist/actions/sendLocation.js.map +1 -0
- package/dist/actions/sendMessage.d.ts +6 -0
- package/dist/actions/sendMessage.d.ts.map +1 -0
- package/dist/actions/sendMessage.js +146 -0
- package/dist/actions/sendMessage.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js.map +1 -0
- package/dist/messaging.d.ts +142 -0
- package/dist/messaging.d.ts.map +1 -0
- package/dist/messaging.js +351 -0
- package/dist/messaging.js.map +1 -0
- package/dist/providers/chatContext.d.ts +6 -0
- package/dist/providers/chatContext.d.ts.map +1 -0
- package/dist/providers/chatContext.js +85 -0
- package/dist/providers/chatContext.js.map +1 -0
- package/dist/providers/index.d.ts +6 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/{src/providers/index.ts → dist/providers/index.js} +1 -1
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/userContext.d.ts +6 -0
- package/dist/providers/userContext.d.ts.map +1 -0
- package/dist/providers/userContext.js +72 -0
- package/dist/providers/userContext.js.map +1 -0
- package/dist/service.d.ts +102 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +443 -0
- package/dist/service.js.map +1 -0
- package/dist/types.d.ts +279 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +106 -0
- package/dist/types.js.map +1 -0
- package/package.json +66 -5
- package/__tests__/integration.test.ts +0 -782
- package/build.ts +0 -16
- package/src/accounts.ts +0 -462
- package/src/actions/sendFlexMessage.ts +0 -243
- package/src/actions/sendLocation.ts +0 -223
- package/src/actions/sendMessage.ts +0 -202
- package/src/index.ts +0 -123
- package/src/messaging.ts +0 -507
- package/src/providers/chatContext.ts +0 -110
- package/src/providers/userContext.ts +0 -99
- package/src/service.ts +0 -578
- package/src/types.ts +0 -417
- package/tsconfig.json +0 -22
|
@@ -1,782 +0,0 @@
|
|
|
1
|
-
import { createHmac } from "node:crypto";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
|
|
4
|
-
import linePlugin, {
|
|
5
|
-
LINE_SERVICE_NAME,
|
|
6
|
-
LINE_TEXT_CHUNK_LIMIT,
|
|
7
|
-
LineApiError,
|
|
8
|
-
LineConfigurationError,
|
|
9
|
-
LineEventTypes,
|
|
10
|
-
LineService,
|
|
11
|
-
MAX_LINE_BATCH_SIZE,
|
|
12
|
-
buildLineDeepLink,
|
|
13
|
-
chatContextProvider,
|
|
14
|
-
chunkLineText,
|
|
15
|
-
extractCodeBlocks,
|
|
16
|
-
extractLinks,
|
|
17
|
-
extractMarkdownTables,
|
|
18
|
-
formatCodeBlockAsText,
|
|
19
|
-
formatLineUser,
|
|
20
|
-
formatTableAsText,
|
|
21
|
-
getChatId,
|
|
22
|
-
getChatType,
|
|
23
|
-
getChatTypeFromId,
|
|
24
|
-
hasMarkdownContent,
|
|
25
|
-
isGroupChat,
|
|
26
|
-
isValidLineId,
|
|
27
|
-
markdownToLineChunks,
|
|
28
|
-
normalizeLineTarget,
|
|
29
|
-
processLineMessage,
|
|
30
|
-
resolveLineSystemLocation,
|
|
31
|
-
sendFlexMessage,
|
|
32
|
-
sendLocation,
|
|
33
|
-
sendMessage,
|
|
34
|
-
splitMessageForLine,
|
|
35
|
-
stripMarkdown,
|
|
36
|
-
truncateText,
|
|
37
|
-
userContextProvider,
|
|
38
|
-
} from "../src/index";
|
|
39
|
-
|
|
40
|
-
import {
|
|
41
|
-
DEFAULT_ACCOUNT_ID,
|
|
42
|
-
normalizeAccountId,
|
|
43
|
-
} from "../src/accounts";
|
|
44
|
-
|
|
45
|
-
// ===========================================================================
|
|
46
|
-
// Plugin metadata
|
|
47
|
-
// ===========================================================================
|
|
48
|
-
|
|
49
|
-
describe("Plugin metadata", () => {
|
|
50
|
-
it("exports correct plugin name and description", () => {
|
|
51
|
-
expect(linePlugin.name).toBe("line");
|
|
52
|
-
expect(linePlugin.description).toContain("LINE");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("exports actions array", () => {
|
|
56
|
-
expect(Array.isArray(linePlugin.actions)).toBe(true);
|
|
57
|
-
expect(linePlugin.actions?.length).toBe(3);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("exports providers array", () => {
|
|
61
|
-
expect(Array.isArray(linePlugin.providers)).toBe(true);
|
|
62
|
-
expect(linePlugin.providers?.length).toBe(2);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("exports services array", () => {
|
|
66
|
-
expect(Array.isArray(linePlugin.services)).toBe(true);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("exports all expected components", () => {
|
|
70
|
-
expect(sendMessage).toBeDefined();
|
|
71
|
-
expect(sendFlexMessage).toBeDefined();
|
|
72
|
-
expect(sendLocation).toBeDefined();
|
|
73
|
-
expect(chatContextProvider).toBeDefined();
|
|
74
|
-
expect(userContextProvider).toBeDefined();
|
|
75
|
-
expect(LineService).toBeDefined();
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// ===========================================================================
|
|
80
|
-
// Config validation
|
|
81
|
-
// ===========================================================================
|
|
82
|
-
|
|
83
|
-
describe("Config validation", () => {
|
|
84
|
-
it("defines correct service name constant", () => {
|
|
85
|
-
expect(LINE_SERVICE_NAME).toBe("line");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("defines correct batch size constant", () => {
|
|
89
|
-
expect(MAX_LINE_BATCH_SIZE).toBe(5);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("defines text chunk limit", () => {
|
|
93
|
-
expect(LINE_TEXT_CHUNK_LIMIT).toBe(5000);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("creates LineConfigurationError with field", () => {
|
|
97
|
-
const err = new LineConfigurationError(
|
|
98
|
-
"Token required",
|
|
99
|
-
"LINE_CHANNEL_ACCESS_TOKEN",
|
|
100
|
-
);
|
|
101
|
-
expect(err.name).toBe("LineConfigurationError");
|
|
102
|
-
expect(err.message).toBe("Token required");
|
|
103
|
-
expect(err.field).toBe("LINE_CHANNEL_ACCESS_TOKEN");
|
|
104
|
-
expect(err instanceof Error).toBe(true);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("creates LineConfigurationError without field", () => {
|
|
108
|
-
const err = new LineConfigurationError("General error");
|
|
109
|
-
expect(err.field).toBeUndefined();
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("creates LineApiError with status code", () => {
|
|
113
|
-
const err = new LineApiError("Not found", 404);
|
|
114
|
-
expect(err.name).toBe("LineApiError");
|
|
115
|
-
expect(err.message).toBe("Not found");
|
|
116
|
-
expect(err.statusCode).toBe(404);
|
|
117
|
-
expect(err instanceof Error).toBe(true);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("creates LineApiError without status code", () => {
|
|
121
|
-
const err = new LineApiError("Unknown error");
|
|
122
|
-
expect(err.statusCode).toBeUndefined();
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// ===========================================================================
|
|
127
|
-
// Type utilities (types.ts)
|
|
128
|
-
// ===========================================================================
|
|
129
|
-
|
|
130
|
-
describe("Type utilities", () => {
|
|
131
|
-
describe("isValidLineId", () => {
|
|
132
|
-
it("accepts valid user IDs", () => {
|
|
133
|
-
expect(isValidLineId("U1234567890abcdef1234567890abcdef")).toBe(true);
|
|
134
|
-
expect(isValidLineId("u1234567890abcdef1234567890abcdef")).toBe(true);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("accepts valid group IDs", () => {
|
|
138
|
-
expect(isValidLineId("C1234567890abcdef1234567890abcdef")).toBe(true);
|
|
139
|
-
expect(isValidLineId("c1234567890abcdef1234567890abcdef")).toBe(true);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it("accepts valid room IDs", () => {
|
|
143
|
-
expect(isValidLineId("R1234567890abcdef1234567890abcdef")).toBe(true);
|
|
144
|
-
expect(isValidLineId("r1234567890abcdef1234567890abcdef")).toBe(true);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("rejects invalid IDs", () => {
|
|
148
|
-
expect(isValidLineId("")).toBe(false);
|
|
149
|
-
expect(isValidLineId("X12345")).toBe(false);
|
|
150
|
-
expect(isValidLineId("U123")).toBe(false);
|
|
151
|
-
expect(isValidLineId("invalid")).toBe(false);
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
describe("normalizeLineTarget", () => {
|
|
156
|
-
it("returns valid IDs unchanged", () => {
|
|
157
|
-
const id = "U1234567890abcdef1234567890abcdef";
|
|
158
|
-
expect(normalizeLineTarget(id)).toBe(id);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("trims whitespace", () => {
|
|
162
|
-
const id = "U1234567890abcdef1234567890abcdef";
|
|
163
|
-
expect(normalizeLineTarget(` ${id} `)).toBe(id);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("returns null for empty strings", () => {
|
|
167
|
-
expect(normalizeLineTarget("")).toBeNull();
|
|
168
|
-
expect(normalizeLineTarget(" ")).toBeNull();
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("returns null for invalid IDs", () => {
|
|
172
|
-
expect(normalizeLineTarget("invalid_id")).toBeNull();
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
describe("getChatTypeFromId", () => {
|
|
177
|
-
it("returns user for U-prefix IDs", () => {
|
|
178
|
-
expect(getChatTypeFromId("U123")).toBe("user");
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("returns group for C-prefix IDs", () => {
|
|
182
|
-
expect(getChatTypeFromId("C123")).toBe("group");
|
|
183
|
-
expect(getChatTypeFromId("c123")).toBe("group");
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("returns room for R-prefix IDs", () => {
|
|
187
|
-
expect(getChatTypeFromId("R123")).toBe("room");
|
|
188
|
-
expect(getChatTypeFromId("r123")).toBe("room");
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it("defaults to user for unknown prefixes", () => {
|
|
192
|
-
expect(getChatTypeFromId("X123")).toBe("user");
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
describe("splitMessageForLine", () => {
|
|
197
|
-
it("returns single chunk for short messages", () => {
|
|
198
|
-
expect(splitMessageForLine("Hello")).toEqual(["Hello"]);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("returns empty array for empty string", () => {
|
|
202
|
-
expect(splitMessageForLine("")).toEqual([]);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("returns single chunk at exactly 5000 chars", () => {
|
|
206
|
-
const text = "a".repeat(5000);
|
|
207
|
-
const chunks = splitMessageForLine(text);
|
|
208
|
-
expect(chunks).toHaveLength(1);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("splits messages over 5000 chars", () => {
|
|
212
|
-
const text = "a".repeat(6000);
|
|
213
|
-
const chunks = splitMessageForLine(text);
|
|
214
|
-
expect(chunks.length).toBeGreaterThan(1);
|
|
215
|
-
for (const chunk of chunks) {
|
|
216
|
-
expect(chunk.length).toBeLessThanOrEqual(5000);
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it("prefers splitting at newlines", () => {
|
|
221
|
-
const first = "a".repeat(3000);
|
|
222
|
-
const second = "b".repeat(3000);
|
|
223
|
-
const text = `${first}\n${second}`;
|
|
224
|
-
const chunks = splitMessageForLine(text);
|
|
225
|
-
expect(chunks).toHaveLength(2);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it("prefers splitting at spaces", () => {
|
|
229
|
-
const first = "a".repeat(3000);
|
|
230
|
-
const second = "b".repeat(3000);
|
|
231
|
-
const text = `${first} ${second}`;
|
|
232
|
-
const chunks = splitMessageForLine(text);
|
|
233
|
-
expect(chunks).toHaveLength(2);
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
// ===========================================================================
|
|
239
|
-
// Event types
|
|
240
|
-
// ===========================================================================
|
|
241
|
-
|
|
242
|
-
describe("Event types", () => {
|
|
243
|
-
it("defines all expected event type constants", () => {
|
|
244
|
-
expect(LineEventTypes.CONNECTION_READY).toBe("line:connection_ready");
|
|
245
|
-
expect(LineEventTypes.MESSAGE_RECEIVED).toBe("line:message_received");
|
|
246
|
-
expect(LineEventTypes.MESSAGE_SENT).toBe("line:message_sent");
|
|
247
|
-
expect(LineEventTypes.FOLLOW).toBe("line:follow");
|
|
248
|
-
expect(LineEventTypes.UNFOLLOW).toBe("line:unfollow");
|
|
249
|
-
expect(LineEventTypes.JOIN_GROUP).toBe("line:join_group");
|
|
250
|
-
expect(LineEventTypes.LEAVE_GROUP).toBe("line:leave_group");
|
|
251
|
-
expect(LineEventTypes.POSTBACK).toBe("line:postback");
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it("event types are readonly", () => {
|
|
255
|
-
const keys = Object.keys(LineEventTypes);
|
|
256
|
-
expect(keys.length).toBe(8);
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
// ===========================================================================
|
|
261
|
-
// Messaging utilities
|
|
262
|
-
// ===========================================================================
|
|
263
|
-
|
|
264
|
-
describe("Messaging utilities", () => {
|
|
265
|
-
describe("chunkLineText", () => {
|
|
266
|
-
it("returns empty array for empty/whitespace text", () => {
|
|
267
|
-
expect(chunkLineText("")).toEqual([]);
|
|
268
|
-
expect(chunkLineText(" ")).toEqual([]);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it("returns single chunk for short text", () => {
|
|
272
|
-
expect(chunkLineText("Hello")).toEqual(["Hello"]);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it("respects custom limit", () => {
|
|
276
|
-
const text = "Hello World, this is a test message.";
|
|
277
|
-
const chunks = chunkLineText(text, { limit: 15 });
|
|
278
|
-
expect(chunks.length).toBeGreaterThan(1);
|
|
279
|
-
for (const chunk of chunks) {
|
|
280
|
-
expect(chunk.length).toBeLessThanOrEqual(15);
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
describe("extractCodeBlocks", () => {
|
|
286
|
-
it("extracts code blocks with language", () => {
|
|
287
|
-
const text = "Before\n```python\nprint('hello')\n```\nAfter";
|
|
288
|
-
const { codeBlocks, textWithoutCode } = extractCodeBlocks(text);
|
|
289
|
-
expect(codeBlocks).toHaveLength(1);
|
|
290
|
-
expect(codeBlocks[0].language).toBe("python");
|
|
291
|
-
expect(codeBlocks[0].code).toBe("print('hello')");
|
|
292
|
-
expect(textWithoutCode).not.toContain("```");
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it("extracts code blocks without language", () => {
|
|
296
|
-
const text = "```\nsome code\n```";
|
|
297
|
-
const { codeBlocks } = extractCodeBlocks(text);
|
|
298
|
-
expect(codeBlocks).toHaveLength(1);
|
|
299
|
-
expect(codeBlocks[0].language).toBeUndefined();
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it("handles text with no code blocks", () => {
|
|
303
|
-
const text = "No code here";
|
|
304
|
-
const { codeBlocks, textWithoutCode } = extractCodeBlocks(text);
|
|
305
|
-
expect(codeBlocks).toHaveLength(0);
|
|
306
|
-
expect(textWithoutCode).toBe(text);
|
|
307
|
-
});
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
describe("extractLinks", () => {
|
|
311
|
-
it("extracts markdown links", () => {
|
|
312
|
-
const text = "Check [this link](https://example.com) out";
|
|
313
|
-
const { links, textWithLinks } = extractLinks(text);
|
|
314
|
-
expect(links).toHaveLength(1);
|
|
315
|
-
expect(links[0].text).toBe("this link");
|
|
316
|
-
expect(links[0].url).toBe("https://example.com");
|
|
317
|
-
expect(textWithLinks).toBe("Check this link out");
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
it("handles text with no links", () => {
|
|
321
|
-
const text = "No links here";
|
|
322
|
-
const { links } = extractLinks(text);
|
|
323
|
-
expect(links).toHaveLength(0);
|
|
324
|
-
});
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
describe("extractMarkdownTables", () => {
|
|
328
|
-
it("extracts simple tables", () => {
|
|
329
|
-
const text =
|
|
330
|
-
"Before\n| A | B |\n|---|---|\n| 1 | 2 |\n| 3 | 4 |\nAfter";
|
|
331
|
-
const { tables, textWithoutTables } = extractMarkdownTables(text);
|
|
332
|
-
expect(tables).toHaveLength(1);
|
|
333
|
-
expect(tables[0].headers).toEqual(["A", "B"]);
|
|
334
|
-
expect(tables[0].rows).toHaveLength(2);
|
|
335
|
-
expect(textWithoutTables).not.toContain("|");
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
it("handles text with no tables", () => {
|
|
339
|
-
const text = "No tables here";
|
|
340
|
-
const { tables } = extractMarkdownTables(text);
|
|
341
|
-
expect(tables).toHaveLength(0);
|
|
342
|
-
});
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
describe("stripMarkdown", () => {
|
|
346
|
-
it("removes bold formatting", () => {
|
|
347
|
-
expect(stripMarkdown("**bold text**")).toBe("bold text");
|
|
348
|
-
expect(stripMarkdown("__bold text__")).toBe("bold text");
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
it("removes strikethrough", () => {
|
|
352
|
-
expect(stripMarkdown("~~deleted~~")).toBe("deleted");
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
it("removes headers", () => {
|
|
356
|
-
expect(stripMarkdown("# Title")).toBe("Title");
|
|
357
|
-
expect(stripMarkdown("## Subtitle")).toBe("Subtitle");
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
it("removes blockquotes", () => {
|
|
361
|
-
expect(stripMarkdown("> quoted text")).toBe("quoted text");
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
it("removes inline code", () => {
|
|
365
|
-
expect(stripMarkdown("use `code` here")).toBe("use code here");
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
it("preserves plain text", () => {
|
|
369
|
-
expect(stripMarkdown("plain text")).toBe("plain text");
|
|
370
|
-
});
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
describe("hasMarkdownContent", () => {
|
|
374
|
-
it("detects bold", () => {
|
|
375
|
-
expect(hasMarkdownContent("**bold**")).toBe(true);
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
it("detects headers", () => {
|
|
379
|
-
expect(hasMarkdownContent("# Header")).toBe(true);
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
it("detects blockquotes", () => {
|
|
383
|
-
expect(hasMarkdownContent("> quote")).toBe(true);
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
it("returns false for plain text", () => {
|
|
387
|
-
expect(hasMarkdownContent("plain text")).toBe(false);
|
|
388
|
-
});
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
describe("processLineMessage", () => {
|
|
392
|
-
it("processes text with markdown content", () => {
|
|
393
|
-
const result = processLineMessage(
|
|
394
|
-
"**Hello** [link](https://example.com)",
|
|
395
|
-
);
|
|
396
|
-
expect(result.text).toContain("Hello");
|
|
397
|
-
expect(result.links).toHaveLength(1);
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
it("processes plain text", () => {
|
|
401
|
-
const result = processLineMessage("Just plain text");
|
|
402
|
-
expect(result.text).toBe("Just plain text");
|
|
403
|
-
expect(result.tables).toHaveLength(0);
|
|
404
|
-
expect(result.codeBlocks).toHaveLength(0);
|
|
405
|
-
});
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
describe("markdownToLineChunks", () => {
|
|
409
|
-
it("processes and chunks markdown", () => {
|
|
410
|
-
const result = markdownToLineChunks("Simple message");
|
|
411
|
-
expect(result.textChunks).toEqual(["Simple message"]);
|
|
412
|
-
});
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
describe("formatTableAsText", () => {
|
|
416
|
-
it("formats table with headers and rows", () => {
|
|
417
|
-
const result = formatTableAsText({
|
|
418
|
-
headers: ["Name", "Age"],
|
|
419
|
-
rows: [
|
|
420
|
-
["Alice", "30"],
|
|
421
|
-
["Bob", "25"],
|
|
422
|
-
],
|
|
423
|
-
});
|
|
424
|
-
expect(result).toContain("Name");
|
|
425
|
-
expect(result).toContain("Alice");
|
|
426
|
-
expect(result).toContain("Bob");
|
|
427
|
-
});
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
describe("formatCodeBlockAsText", () => {
|
|
431
|
-
it("formats with language label", () => {
|
|
432
|
-
const result = formatCodeBlockAsText({
|
|
433
|
-
language: "python",
|
|
434
|
-
code: "print(1)",
|
|
435
|
-
});
|
|
436
|
-
expect(result).toContain("[python]");
|
|
437
|
-
expect(result).toContain("print(1)");
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
it("formats without language", () => {
|
|
441
|
-
const result = formatCodeBlockAsText({ code: "hello" });
|
|
442
|
-
expect(result).toContain("[code]");
|
|
443
|
-
});
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
describe("truncateText", () => {
|
|
447
|
-
it("returns text unchanged if within limit", () => {
|
|
448
|
-
expect(truncateText("hello", 10)).toBe("hello");
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
it("truncates with ellipsis", () => {
|
|
452
|
-
expect(truncateText("hello world", 8)).toBe("hello...");
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
it("handles very short max length", () => {
|
|
456
|
-
expect(truncateText("hello", 3)).toBe("...");
|
|
457
|
-
});
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
describe("formatLineUser", () => {
|
|
461
|
-
it("returns display name if provided", () => {
|
|
462
|
-
expect(formatLineUser("Alice", "U123456")).toBe("Alice");
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
it("returns fallback with user ID if no display name", () => {
|
|
466
|
-
expect(formatLineUser("", "U1234567890abcdef")).toContain("User(");
|
|
467
|
-
expect(formatLineUser("", "U1234567890abcdef")).toContain("U1234567");
|
|
468
|
-
});
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
describe("buildLineDeepLink", () => {
|
|
472
|
-
it("builds deep link URL", () => {
|
|
473
|
-
const link = buildLineDeepLink("user", "U123");
|
|
474
|
-
expect(link).toBe("line://ti/p/U123");
|
|
475
|
-
});
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
describe("resolveLineSystemLocation", () => {
|
|
479
|
-
it("formats user chat location", () => {
|
|
480
|
-
const result = resolveLineSystemLocation({
|
|
481
|
-
chatType: "user",
|
|
482
|
-
chatId: "U12345678",
|
|
483
|
-
chatName: "Alice",
|
|
484
|
-
});
|
|
485
|
-
expect(result).toBe("LINE user:Alice");
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
it("falls back to truncated chat ID", () => {
|
|
489
|
-
const result = resolveLineSystemLocation({
|
|
490
|
-
chatType: "group",
|
|
491
|
-
chatId: "C1234567890abcdef",
|
|
492
|
-
});
|
|
493
|
-
expect(result).toContain("LINE group:");
|
|
494
|
-
});
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
describe("isGroupChat", () => {
|
|
498
|
-
it("returns true for group", () => {
|
|
499
|
-
expect(isGroupChat({ groupId: "C123" })).toBe(true);
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
it("returns true for room", () => {
|
|
503
|
-
expect(isGroupChat({ roomId: "R123" })).toBe(true);
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
it("returns false for DM", () => {
|
|
507
|
-
expect(isGroupChat({})).toBe(false);
|
|
508
|
-
});
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
describe("getChatId", () => {
|
|
512
|
-
it("prefers groupId", () => {
|
|
513
|
-
expect(
|
|
514
|
-
getChatId({ userId: "U1", groupId: "C1", roomId: "R1" }),
|
|
515
|
-
).toBe("C1");
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
it("falls back to roomId", () => {
|
|
519
|
-
expect(getChatId({ userId: "U1", roomId: "R1" })).toBe("R1");
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
it("falls back to userId", () => {
|
|
523
|
-
expect(getChatId({ userId: "U1" })).toBe("U1");
|
|
524
|
-
});
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
describe("getChatType", () => {
|
|
528
|
-
it("returns group when groupId present", () => {
|
|
529
|
-
expect(getChatType({ groupId: "C1" })).toBe("group");
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
it("returns room when roomId present", () => {
|
|
533
|
-
expect(getChatType({ roomId: "R1" })).toBe("room");
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
it("returns user when neither present", () => {
|
|
537
|
-
expect(getChatType({})).toBe("user");
|
|
538
|
-
});
|
|
539
|
-
});
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
// ===========================================================================
|
|
543
|
-
// Action validation
|
|
544
|
-
// ===========================================================================
|
|
545
|
-
|
|
546
|
-
describe("Action validation", () => {
|
|
547
|
-
it("sendMessage action has correct metadata", () => {
|
|
548
|
-
expect(sendMessage.name).toBe("LINE_SEND_MESSAGE");
|
|
549
|
-
expect(sendMessage.description).toContain("LINE");
|
|
550
|
-
expect(sendMessage.similes).toContain("SEND_LINE_MESSAGE");
|
|
551
|
-
expect(sendMessage.similes).toContain("LINE_MESSAGE");
|
|
552
|
-
expect(Array.isArray(sendMessage.examples)).toBe(true);
|
|
553
|
-
expect(sendMessage.examples.length).toBeGreaterThan(0);
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
it("sendFlexMessage action has correct metadata", () => {
|
|
557
|
-
expect(sendFlexMessage.name).toBe("LINE_SEND_FLEX_MESSAGE");
|
|
558
|
-
expect(sendFlexMessage.description).toContain("LINE");
|
|
559
|
-
expect(sendFlexMessage.similes).toContain("LINE_FLEX");
|
|
560
|
-
expect(sendFlexMessage.similes).toContain("LINE_CARD");
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
it("sendLocation action has correct metadata", () => {
|
|
564
|
-
expect(sendLocation.name).toBe("LINE_SEND_LOCATION");
|
|
565
|
-
expect(sendLocation.description).toContain("LINE");
|
|
566
|
-
expect(sendLocation.similes).toContain("LINE_LOCATION");
|
|
567
|
-
expect(sendLocation.similes).toContain("LINE_MAP");
|
|
568
|
-
});
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
// ===========================================================================
|
|
572
|
-
// Provider output
|
|
573
|
-
// ===========================================================================
|
|
574
|
-
|
|
575
|
-
describe("Provider output", () => {
|
|
576
|
-
it("userContextProvider has correct metadata", () => {
|
|
577
|
-
expect(userContextProvider.name).toBe("lineUserContext");
|
|
578
|
-
expect(userContextProvider.description).toContain("LINE user");
|
|
579
|
-
expect(typeof userContextProvider.get).toBe("function");
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
it("chatContextProvider has correct metadata", () => {
|
|
583
|
-
expect(chatContextProvider.name).toBe("lineChatContext");
|
|
584
|
-
expect(chatContextProvider.description).toContain("LINE chat");
|
|
585
|
-
expect(typeof chatContextProvider.get).toBe("function");
|
|
586
|
-
});
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
// ===========================================================================
|
|
590
|
-
// Accounts utilities
|
|
591
|
-
// ===========================================================================
|
|
592
|
-
|
|
593
|
-
describe("Accounts utilities", () => {
|
|
594
|
-
it("DEFAULT_ACCOUNT_ID is 'default'", () => {
|
|
595
|
-
expect(DEFAULT_ACCOUNT_ID).toBe("default");
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
it("normalizeAccountId returns default for null/undefined", () => {
|
|
599
|
-
expect(normalizeAccountId(null)).toBe("default");
|
|
600
|
-
expect(normalizeAccountId(undefined)).toBe("default");
|
|
601
|
-
expect(normalizeAccountId("")).toBe("default");
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
it("normalizeAccountId lowercases and trims", () => {
|
|
605
|
-
expect(normalizeAccountId(" MyAccount ")).toBe("myaccount");
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
it("normalizeAccountId returns default for 'default' input", () => {
|
|
609
|
-
expect(normalizeAccountId("default")).toBe("default");
|
|
610
|
-
expect(normalizeAccountId("DEFAULT")).toBe("default");
|
|
611
|
-
});
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
// ===========================================================================
|
|
615
|
-
// Webhook signature validation
|
|
616
|
-
// ===========================================================================
|
|
617
|
-
|
|
618
|
-
describe("Webhook signature validation", () => {
|
|
619
|
-
const channelSecret = "test_channel_secret";
|
|
620
|
-
|
|
621
|
-
function computeSignature(body: string, secret: string): string {
|
|
622
|
-
return createHmac("SHA256", secret).update(body).digest("base64");
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
it("produces consistent signatures for same input", () => {
|
|
626
|
-
const body = '{"events":[]}';
|
|
627
|
-
const sig1 = computeSignature(body, channelSecret);
|
|
628
|
-
const sig2 = computeSignature(body, channelSecret);
|
|
629
|
-
expect(sig1).toBe(sig2);
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
it("produces different signatures for different secrets", () => {
|
|
633
|
-
const body = '{"events":[]}';
|
|
634
|
-
const sig1 = computeSignature(body, channelSecret);
|
|
635
|
-
const sig2 = computeSignature(body, "other_secret");
|
|
636
|
-
expect(sig1).not.toBe(sig2);
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
it("produces different signatures for different bodies", () => {
|
|
640
|
-
const sig1 = computeSignature("body1", channelSecret);
|
|
641
|
-
const sig2 = computeSignature("body2", channelSecret);
|
|
642
|
-
expect(sig1).not.toBe(sig2);
|
|
643
|
-
});
|
|
644
|
-
|
|
645
|
-
it("signature is non-empty base64", () => {
|
|
646
|
-
const sig = computeSignature("{}", channelSecret);
|
|
647
|
-
expect(sig.length).toBeGreaterThan(0);
|
|
648
|
-
// Base64 chars: A-Z, a-z, 0-9, +, /, =
|
|
649
|
-
expect(sig).toMatch(/^[A-Za-z0-9+/=]+$/);
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
it("validates against correct recomputation", () => {
|
|
653
|
-
const body = '{"events":[{"type":"follow"}]}';
|
|
654
|
-
const sig = computeSignature(body, channelSecret);
|
|
655
|
-
const recomputed = computeSignature(body, channelSecret);
|
|
656
|
-
expect(sig).toBe(recomputed);
|
|
657
|
-
});
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
// ===========================================================================
|
|
661
|
-
// Webhook event parsing
|
|
662
|
-
// ===========================================================================
|
|
663
|
-
|
|
664
|
-
describe("Webhook event parsing", () => {
|
|
665
|
-
it("parses follow event structure", () => {
|
|
666
|
-
const event = {
|
|
667
|
-
type: "follow",
|
|
668
|
-
timestamp: 1234567890,
|
|
669
|
-
source: { type: "user", userId: "U123" },
|
|
670
|
-
replyToken: "rt1",
|
|
671
|
-
};
|
|
672
|
-
expect(event.type).toBe("follow");
|
|
673
|
-
expect(event.source.userId).toBe("U123");
|
|
674
|
-
});
|
|
675
|
-
|
|
676
|
-
it("parses unfollow event structure", () => {
|
|
677
|
-
const event = {
|
|
678
|
-
type: "unfollow",
|
|
679
|
-
timestamp: 1234567890,
|
|
680
|
-
source: { type: "user", userId: "U123" },
|
|
681
|
-
};
|
|
682
|
-
expect(event.type).toBe("unfollow");
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
it("parses join event structure", () => {
|
|
686
|
-
const event = {
|
|
687
|
-
type: "join",
|
|
688
|
-
timestamp: 1234567890,
|
|
689
|
-
source: { type: "group", groupId: "C123" },
|
|
690
|
-
replyToken: "rt2",
|
|
691
|
-
};
|
|
692
|
-
expect(event.type).toBe("join");
|
|
693
|
-
expect(event.source.groupId).toBe("C123");
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
it("parses leave event structure", () => {
|
|
697
|
-
const event = {
|
|
698
|
-
type: "leave",
|
|
699
|
-
timestamp: 1234567890,
|
|
700
|
-
source: { type: "room", roomId: "R123" },
|
|
701
|
-
};
|
|
702
|
-
expect(event.type).toBe("leave");
|
|
703
|
-
expect(event.source.roomId).toBe("R123");
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
it("parses postback event structure", () => {
|
|
707
|
-
const event = {
|
|
708
|
-
type: "postback",
|
|
709
|
-
timestamp: 1234567890,
|
|
710
|
-
source: { type: "user", userId: "U123" },
|
|
711
|
-
replyToken: "rt3",
|
|
712
|
-
postback: { data: "action=buy", params: { date: "2024-01-01" } },
|
|
713
|
-
};
|
|
714
|
-
expect(event.type).toBe("postback");
|
|
715
|
-
expect(event.postback.data).toBe("action=buy");
|
|
716
|
-
});
|
|
717
|
-
|
|
718
|
-
it("parses message event structure", () => {
|
|
719
|
-
const event = {
|
|
720
|
-
type: "message",
|
|
721
|
-
timestamp: 1234567890,
|
|
722
|
-
source: { type: "user", userId: "U123" },
|
|
723
|
-
replyToken: "rt4",
|
|
724
|
-
message: { id: "msg1", type: "text", text: "Hello!" },
|
|
725
|
-
};
|
|
726
|
-
expect(event.type).toBe("message");
|
|
727
|
-
expect(event.message.text).toBe("Hello!");
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
it("handles multiple events in body", () => {
|
|
731
|
-
const body = {
|
|
732
|
-
events: [
|
|
733
|
-
{
|
|
734
|
-
type: "follow",
|
|
735
|
-
timestamp: 1,
|
|
736
|
-
source: { type: "user", userId: "U1" },
|
|
737
|
-
},
|
|
738
|
-
{
|
|
739
|
-
type: "message",
|
|
740
|
-
timestamp: 2,
|
|
741
|
-
source: { type: "user", userId: "U2" },
|
|
742
|
-
message: { id: "m1", type: "text", text: "Hi" },
|
|
743
|
-
},
|
|
744
|
-
],
|
|
745
|
-
};
|
|
746
|
-
expect(body.events).toHaveLength(2);
|
|
747
|
-
expect(body.events[0].type).toBe("follow");
|
|
748
|
-
expect(body.events[1].type).toBe("message");
|
|
749
|
-
});
|
|
750
|
-
});
|
|
751
|
-
|
|
752
|
-
// ===========================================================================
|
|
753
|
-
// Service lifecycle
|
|
754
|
-
// ===========================================================================
|
|
755
|
-
|
|
756
|
-
describe("Service lifecycle", () => {
|
|
757
|
-
it("has correct static service type", () => {
|
|
758
|
-
expect(LineService.serviceType).toBe("line");
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
it("can be constructed without runtime", () => {
|
|
762
|
-
const service = new LineService();
|
|
763
|
-
expect(service.isConnected()).toBe(false);
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
it("returns false from isConnected before start", () => {
|
|
767
|
-
const service = new LineService();
|
|
768
|
-
expect(service.isConnected()).toBe(false);
|
|
769
|
-
});
|
|
770
|
-
|
|
771
|
-
it("returns null settings before configuration", () => {
|
|
772
|
-
const service = new LineService();
|
|
773
|
-
expect(service.getSettings()).toBeNull();
|
|
774
|
-
});
|
|
775
|
-
|
|
776
|
-
it("stop works even when not started", async () => {
|
|
777
|
-
const service = new LineService();
|
|
778
|
-
await service.stop();
|
|
779
|
-
expect(service.isConnected()).toBe(false);
|
|
780
|
-
expect(service.getSettings()).toBeNull();
|
|
781
|
-
});
|
|
782
|
-
});
|