@lark-apaas/action-plugin-testing 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Lark Technologies Pte. Ltd. and/or its affiliates
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted,provided that the above copyright notice and this permission notice appear in all copies.
6
+
7
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
8
+ IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE
9
+ INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO
10
+ EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
11
+ CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
12
+ DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
13
+ ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # @lark-apaas/action-plugin-testing
2
+
3
+ Testing utilities for `@lark-apaas/action-plugin-core` — provides mock context, test runner, stream helpers, and schema validation tools.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -D @lark-apaas/action-plugin-testing
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { createTestContext, createTestRunner } from '@lark-apaas/action-plugin-testing';
15
+ import plugin from './my-plugin';
16
+
17
+ const runner = createTestRunner(plugin);
18
+ const context = createTestContext();
19
+
20
+ // Run an action
21
+ const result = await runner.run('sayHello', context, { name: 'World' });
22
+ console.log(result); // { greeting: 'Hello, World!' }
23
+ ```
24
+
25
+ ## API
26
+
27
+ ### Test Context
28
+
29
+ #### `createTestContext(options?)`
30
+
31
+ Creates a mock `ActionContext` for testing.
32
+
33
+ ```typescript
34
+ import { createTestContext } from '@lark-apaas/action-plugin-testing';
35
+
36
+ // Default context
37
+ const ctx = createTestContext();
38
+
39
+ // Custom context
40
+ const ctx = createTestContext({
41
+ userId: 'user-123',
42
+ tenantId: 'tenant-456',
43
+ appId: 'app-789',
44
+ });
45
+ ```
46
+
47
+ ### Test Runner
48
+
49
+ #### `createTestRunner(plugin, options?)`
50
+
51
+ Wraps a plugin instance with convenience methods for testing.
52
+
53
+ ```typescript
54
+ import { createTestRunner } from '@lark-apaas/action-plugin-testing';
55
+
56
+ const runner = createTestRunner(plugin);
57
+
58
+ // Unary action
59
+ const result = await runner.run('actionName', context, input);
60
+
61
+ // Stream action
62
+ const stream = runner.runStream('actionName', context, input);
63
+
64
+ // Schema access
65
+ const inputSchema = runner.getInputSchema('actionName');
66
+ const outputSchema = runner.getOutputSchema('actionName');
67
+ ```
68
+
69
+ ### Mock Logger
70
+
71
+ #### `createMockLogger()`
72
+
73
+ Creates a logger that records all log entries for assertions.
74
+
75
+ ```typescript
76
+ import { createMockLogger } from '@lark-apaas/action-plugin-testing';
77
+
78
+ const logger = createMockLogger();
79
+ const ctx = createTestContext({ logger });
80
+
81
+ await runner.run('action', ctx, input);
82
+
83
+ // Query logs
84
+ logger.getLogs(); // all entries
85
+ logger.getLogs('error'); // filter by level
86
+ logger.hasLog('error'); // check existence
87
+ logger.getLastLog('info'); // last entry of level
88
+ ```
89
+
90
+ ### Stream Helpers
91
+
92
+ Utilities for testing async iterable streams.
93
+
94
+ ```typescript
95
+ import {
96
+ collectStream,
97
+ collectStreamData,
98
+ collectStreamText,
99
+ firstChunk,
100
+ lastChunk,
101
+ countChunks,
102
+ assertStreamChunks,
103
+ assertEveryChunk,
104
+ } from '@lark-apaas/action-plugin-testing';
105
+
106
+ const stream = runner.runStream('chat', context, input);
107
+
108
+ // Collect all chunks
109
+ const chunks = await collectStream(stream);
110
+
111
+ // Extract data fields from StreamChunk items
112
+ const data = await collectStreamData(stream);
113
+
114
+ // Concatenate text content
115
+ const text = await collectStreamText(stream);
116
+
117
+ // Access specific chunks
118
+ const first = await firstChunk(stream);
119
+ const last = await lastChunk(stream);
120
+
121
+ // Count without storing
122
+ const count = await countChunks(stream);
123
+
124
+ // Assertions
125
+ await assertStreamChunks(stream, [expected1, expected2]);
126
+ await assertEveryChunk(stream, (chunk) => chunk.data.content.length > 0);
127
+ ```
128
+
129
+ ### StreamEvent Helpers
130
+
131
+ Utilities for testing `StreamEvent` streams.
132
+
133
+ ```typescript
134
+ import {
135
+ collectStreamEvents,
136
+ collectEventData,
137
+ waitForDone,
138
+ waitForError,
139
+ expectStreamCompletes,
140
+ expectStreamErrors,
141
+ expectAggregatedResult,
142
+ } from '@lark-apaas/action-plugin-testing';
143
+
144
+ // Collect and categorize events
145
+ const { dataEvents, doneEvent, errorEvent } = await collectStreamEvents(stream);
146
+
147
+ // Collect only data payloads
148
+ const data = await collectEventData(stream);
149
+
150
+ // Wait for completion / error
151
+ const meta = await waitForDone(stream);
152
+ const error = await waitForError(stream);
153
+
154
+ // Assertions
155
+ await expectStreamCompletes(stream);
156
+ await expectStreamErrors(stream, 'ERROR_CODE');
157
+ await expectAggregatedResult(stream, expectedResult);
158
+ ```
159
+
160
+ ### Schema Validation
161
+
162
+ ```typescript
163
+ import { parseInput, expectInputRejected } from '@lark-apaas/action-plugin-testing';
164
+
165
+ // Validate input against action schema
166
+ const parsed = parseInput(runner, 'actionName', { name: 'test' });
167
+
168
+ // Assert invalid input is rejected
169
+ expectInputRejected(runner, 'actionName', { name: 123 });
170
+ ```
171
+
172
+ ## Types
173
+
174
+ ```typescript
175
+ import type {
176
+ TestContextOptions,
177
+ TestRunner,
178
+ TestRunnerOptions,
179
+ MockLogger,
180
+ LogEntry,
181
+ StreamEventResult,
182
+ } from '@lark-apaas/action-plugin-testing';
183
+ ```
184
+
185
+ ## License
186
+
187
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,335 @@
1
+ 'use strict';
2
+
3
+ // src/context.ts
4
+ var silentLogger = {
5
+ log: () => {
6
+ },
7
+ error: () => {
8
+ },
9
+ warn: () => {
10
+ },
11
+ debug: () => {
12
+ }
13
+ };
14
+ var consoleLogger = {
15
+ log: (...args) => console.log("[TEST]", ...args),
16
+ error: (...args) => console.error("[TEST]", ...args),
17
+ warn: (...args) => console.warn("[TEST]", ...args),
18
+ debug: (...args) => console.debug("[TEST]", ...args)
19
+ };
20
+ function createMockResponse(data = {}) {
21
+ return new Response(JSON.stringify(data), {
22
+ status: 200,
23
+ headers: { "Content-Type": "application/json" }
24
+ });
25
+ }
26
+ var mockHttpClient = {
27
+ get: async () => createMockResponse(),
28
+ post: async () => createMockResponse(),
29
+ put: async () => createMockResponse(),
30
+ delete: async () => createMockResponse(),
31
+ patch: async () => createMockResponse()
32
+ };
33
+ function resolveLogger(logger) {
34
+ if (!logger || logger === "silent") {
35
+ return silentLogger;
36
+ }
37
+ if (logger === "console") {
38
+ return consoleLogger;
39
+ }
40
+ return logger;
41
+ }
42
+ function createTestContext(options) {
43
+ const {
44
+ userId = "test-user",
45
+ tenantId = "test-tenant",
46
+ appId = "test-app",
47
+ logger,
48
+ platformHttpClient = mockHttpClient,
49
+ isDebug = false
50
+ } = options ?? {};
51
+ return {
52
+ userContext: {
53
+ userId,
54
+ tenantId,
55
+ appId
56
+ },
57
+ logger: resolveLogger(logger),
58
+ platformHttpClient,
59
+ isDebug
60
+ };
61
+ }
62
+
63
+ // src/runner.ts
64
+ function createTestRunner(plugin, config, options) {
65
+ const instance = plugin.create(config);
66
+ const defaultContextOptions = options?.defaultContext ?? {};
67
+ return {
68
+ async run(actionName, input, contextOverrides) {
69
+ const mergedContextOptions = {
70
+ ...defaultContextOptions,
71
+ ...contextOverrides
72
+ };
73
+ const context = createTestContext(mergedContextOptions);
74
+ return instance.run(actionName, context, input);
75
+ },
76
+ getInstance() {
77
+ return instance;
78
+ },
79
+ getInputSchema(actionName) {
80
+ return instance.getInputSchema(actionName);
81
+ },
82
+ getOutputSchema(actionName, input) {
83
+ return instance.getOutputSchema(actionName, input);
84
+ }
85
+ };
86
+ }
87
+
88
+ // src/mocks/logger.ts
89
+ function createMockLogger() {
90
+ const logs = [];
91
+ const createLogMethod = (level) => {
92
+ return (...args) => {
93
+ logs.push({
94
+ level,
95
+ args,
96
+ timestamp: Date.now()
97
+ });
98
+ };
99
+ };
100
+ return {
101
+ log: createLogMethod("log"),
102
+ error: createLogMethod("error"),
103
+ warn: createLogMethod("warn"),
104
+ debug: createLogMethod("debug"),
105
+ getLogs() {
106
+ return [...logs];
107
+ },
108
+ getLogsByLevel(level) {
109
+ return logs.filter((log) => log.level === level);
110
+ },
111
+ clear() {
112
+ logs.length = 0;
113
+ },
114
+ hasLog(level, messagePattern) {
115
+ return logs.some((log) => {
116
+ if (log.level !== level) return false;
117
+ return log.args.some((arg) => {
118
+ if (typeof arg !== "string") {
119
+ try {
120
+ arg = JSON.stringify(arg);
121
+ } catch {
122
+ return false;
123
+ }
124
+ }
125
+ if (typeof messagePattern === "string") {
126
+ return arg.includes(messagePattern);
127
+ }
128
+ return messagePattern.test(arg);
129
+ });
130
+ });
131
+ }
132
+ };
133
+ }
134
+
135
+ // src/stream.ts
136
+ async function collectStream(stream) {
137
+ const chunks = [];
138
+ for await (const chunk of stream) {
139
+ chunks.push(chunk);
140
+ }
141
+ return chunks;
142
+ }
143
+ async function collectStreamData(stream) {
144
+ const chunks = [];
145
+ for await (const chunk of stream) {
146
+ chunks.push(chunk.data);
147
+ }
148
+ return chunks;
149
+ }
150
+ async function collectStreamText(stream) {
151
+ let result = "";
152
+ for await (const chunk of stream) {
153
+ result += chunk.data.content;
154
+ }
155
+ return result;
156
+ }
157
+ async function firstChunk(stream) {
158
+ for await (const chunk of stream) {
159
+ return chunk;
160
+ }
161
+ return void 0;
162
+ }
163
+ async function lastChunk(stream) {
164
+ let last;
165
+ for await (const chunk of stream) {
166
+ last = chunk;
167
+ }
168
+ return last;
169
+ }
170
+ async function countChunks(stream) {
171
+ let count = 0;
172
+ for await (const _item of stream) {
173
+ count++;
174
+ }
175
+ return count;
176
+ }
177
+ async function assertStreamChunks(stream, expected) {
178
+ const actual = await collectStream(stream);
179
+ if (actual.length !== expected.length) {
180
+ throw new Error(`Expected ${expected.length} chunks, but got ${actual.length}`);
181
+ }
182
+ for (let i = 0; i < expected.length; i++) {
183
+ const actualStr = JSON.stringify(actual[i]);
184
+ const expectedStr = JSON.stringify(expected[i]);
185
+ if (actualStr !== expectedStr) {
186
+ throw new Error(`Chunk ${i} mismatch:
187
+ Expected: ${expectedStr}
188
+ Actual: ${actualStr}`);
189
+ }
190
+ }
191
+ }
192
+ async function assertEveryChunk(stream, predicate) {
193
+ let index = 0;
194
+ for await (const chunk of stream) {
195
+ if (!predicate(chunk, index)) {
196
+ throw new Error(`Chunk ${index} failed assertion: ${JSON.stringify(chunk)}`);
197
+ }
198
+ index++;
199
+ }
200
+ }
201
+
202
+ // src/stream-events.ts
203
+ async function collectStreamEvents(stream) {
204
+ const events = [];
205
+ const data = [];
206
+ let done;
207
+ let error;
208
+ for await (const event of stream) {
209
+ events.push(event);
210
+ switch (event.type) {
211
+ case "data":
212
+ data.push(event.data);
213
+ break;
214
+ case "done":
215
+ done = event;
216
+ break;
217
+ case "error":
218
+ error = event;
219
+ break;
220
+ }
221
+ }
222
+ return {
223
+ events,
224
+ data,
225
+ done,
226
+ error,
227
+ completed: done !== void 0 && error === void 0
228
+ };
229
+ }
230
+ async function collectEventData(stream) {
231
+ const data = [];
232
+ for await (const event of stream) {
233
+ if (event.type === "data") {
234
+ data.push(event.data);
235
+ }
236
+ }
237
+ return data;
238
+ }
239
+ async function waitForDone(stream) {
240
+ for await (const event of stream) {
241
+ if (event.type === "done") {
242
+ return event.metadata;
243
+ }
244
+ if (event.type === "error") {
245
+ throw new Error(`Stream ended with error: [${event.error.code}] ${event.error.message}`);
246
+ }
247
+ }
248
+ throw new Error("Stream ended without done or error event");
249
+ }
250
+ async function waitForError(stream) {
251
+ for await (const event of stream) {
252
+ if (event.type === "error") {
253
+ return event.error;
254
+ }
255
+ if (event.type === "done") {
256
+ throw new Error("Expected stream to error, but it completed successfully");
257
+ }
258
+ }
259
+ throw new Error("Stream ended without done or error event");
260
+ }
261
+ async function expectStreamCompletes(stream) {
262
+ const result = await collectStreamEvents(stream);
263
+ if (!result.completed) {
264
+ if (result.error) {
265
+ throw new Error(
266
+ `Expected stream to complete, but got error: [${result.error.error.code}] ${result.error.error.message}`
267
+ );
268
+ }
269
+ throw new Error("Expected stream to complete, but no done event received");
270
+ }
271
+ }
272
+ async function expectStreamErrors(stream, expectedCode) {
273
+ const result = await collectStreamEvents(stream);
274
+ if (!result.error) {
275
+ throw new Error("Expected stream to error, but it completed successfully");
276
+ }
277
+ if (expectedCode && result.error.error.code !== expectedCode) {
278
+ throw new Error(`Expected error code "${expectedCode}", but got "${result.error.error.code}"`);
279
+ }
280
+ }
281
+ async function expectAggregatedResult(stream, expected) {
282
+ const metadata = await waitForDone(stream);
283
+ const actual = JSON.stringify(metadata.aggregated);
284
+ const expectedStr = JSON.stringify(expected);
285
+ if (actual !== expectedStr) {
286
+ throw new Error(`Aggregated result mismatch:
287
+ Expected: ${expectedStr}
288
+ Actual: ${actual}`);
289
+ }
290
+ }
291
+
292
+ // src/schema.ts
293
+ function parseInput(instance, actionName, input) {
294
+ const schema = instance.getInputSchema(actionName);
295
+ if (!schema) {
296
+ throw new Error(`Action "${actionName}" not found`);
297
+ }
298
+ return schema.parse(input);
299
+ }
300
+ function expectInputRejected(instance, actionName, input) {
301
+ const schema = instance.getInputSchema(actionName);
302
+ if (!schema) {
303
+ throw new Error(`Action "${actionName}" not found`);
304
+ }
305
+ let threw = false;
306
+ try {
307
+ schema.parse(input);
308
+ } catch {
309
+ threw = true;
310
+ }
311
+ if (!threw) {
312
+ throw new Error(`Expected input to be rejected: ${JSON.stringify(input)}`);
313
+ }
314
+ }
315
+
316
+ exports.assertEveryChunk = assertEveryChunk;
317
+ exports.assertStreamChunks = assertStreamChunks;
318
+ exports.collectEventData = collectEventData;
319
+ exports.collectStream = collectStream;
320
+ exports.collectStreamData = collectStreamData;
321
+ exports.collectStreamEvents = collectStreamEvents;
322
+ exports.collectStreamText = collectStreamText;
323
+ exports.countChunks = countChunks;
324
+ exports.createMockLogger = createMockLogger;
325
+ exports.createTestContext = createTestContext;
326
+ exports.createTestRunner = createTestRunner;
327
+ exports.expectAggregatedResult = expectAggregatedResult;
328
+ exports.expectInputRejected = expectInputRejected;
329
+ exports.expectStreamCompletes = expectStreamCompletes;
330
+ exports.expectStreamErrors = expectStreamErrors;
331
+ exports.firstChunk = firstChunk;
332
+ exports.lastChunk = lastChunk;
333
+ exports.parseInput = parseInput;
334
+ exports.waitForDone = waitForDone;
335
+ exports.waitForError = waitForError;