@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 +13 -0
- package/README.md +187 -0
- package/dist/index.cjs +335 -0
- package/dist/index.d.cts +338 -0
- package/dist/index.d.ts +338 -0
- package/dist/index.js +314 -0
- package/package.json +47 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// src/context.ts
|
|
2
|
+
var silentLogger = {
|
|
3
|
+
log: () => {
|
|
4
|
+
},
|
|
5
|
+
error: () => {
|
|
6
|
+
},
|
|
7
|
+
warn: () => {
|
|
8
|
+
},
|
|
9
|
+
debug: () => {
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var consoleLogger = {
|
|
13
|
+
log: (...args) => console.log("[TEST]", ...args),
|
|
14
|
+
error: (...args) => console.error("[TEST]", ...args),
|
|
15
|
+
warn: (...args) => console.warn("[TEST]", ...args),
|
|
16
|
+
debug: (...args) => console.debug("[TEST]", ...args)
|
|
17
|
+
};
|
|
18
|
+
function createMockResponse(data = {}) {
|
|
19
|
+
return new Response(JSON.stringify(data), {
|
|
20
|
+
status: 200,
|
|
21
|
+
headers: { "Content-Type": "application/json" }
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
var mockHttpClient = {
|
|
25
|
+
get: async () => createMockResponse(),
|
|
26
|
+
post: async () => createMockResponse(),
|
|
27
|
+
put: async () => createMockResponse(),
|
|
28
|
+
delete: async () => createMockResponse(),
|
|
29
|
+
patch: async () => createMockResponse()
|
|
30
|
+
};
|
|
31
|
+
function resolveLogger(logger) {
|
|
32
|
+
if (!logger || logger === "silent") {
|
|
33
|
+
return silentLogger;
|
|
34
|
+
}
|
|
35
|
+
if (logger === "console") {
|
|
36
|
+
return consoleLogger;
|
|
37
|
+
}
|
|
38
|
+
return logger;
|
|
39
|
+
}
|
|
40
|
+
function createTestContext(options) {
|
|
41
|
+
const {
|
|
42
|
+
userId = "test-user",
|
|
43
|
+
tenantId = "test-tenant",
|
|
44
|
+
appId = "test-app",
|
|
45
|
+
logger,
|
|
46
|
+
platformHttpClient = mockHttpClient,
|
|
47
|
+
isDebug = false
|
|
48
|
+
} = options ?? {};
|
|
49
|
+
return {
|
|
50
|
+
userContext: {
|
|
51
|
+
userId,
|
|
52
|
+
tenantId,
|
|
53
|
+
appId
|
|
54
|
+
},
|
|
55
|
+
logger: resolveLogger(logger),
|
|
56
|
+
platformHttpClient,
|
|
57
|
+
isDebug
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/runner.ts
|
|
62
|
+
function createTestRunner(plugin, config, options) {
|
|
63
|
+
const instance = plugin.create(config);
|
|
64
|
+
const defaultContextOptions = options?.defaultContext ?? {};
|
|
65
|
+
return {
|
|
66
|
+
async run(actionName, input, contextOverrides) {
|
|
67
|
+
const mergedContextOptions = {
|
|
68
|
+
...defaultContextOptions,
|
|
69
|
+
...contextOverrides
|
|
70
|
+
};
|
|
71
|
+
const context = createTestContext(mergedContextOptions);
|
|
72
|
+
return instance.run(actionName, context, input);
|
|
73
|
+
},
|
|
74
|
+
getInstance() {
|
|
75
|
+
return instance;
|
|
76
|
+
},
|
|
77
|
+
getInputSchema(actionName) {
|
|
78
|
+
return instance.getInputSchema(actionName);
|
|
79
|
+
},
|
|
80
|
+
getOutputSchema(actionName, input) {
|
|
81
|
+
return instance.getOutputSchema(actionName, input);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/mocks/logger.ts
|
|
87
|
+
function createMockLogger() {
|
|
88
|
+
const logs = [];
|
|
89
|
+
const createLogMethod = (level) => {
|
|
90
|
+
return (...args) => {
|
|
91
|
+
logs.push({
|
|
92
|
+
level,
|
|
93
|
+
args,
|
|
94
|
+
timestamp: Date.now()
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
log: createLogMethod("log"),
|
|
100
|
+
error: createLogMethod("error"),
|
|
101
|
+
warn: createLogMethod("warn"),
|
|
102
|
+
debug: createLogMethod("debug"),
|
|
103
|
+
getLogs() {
|
|
104
|
+
return [...logs];
|
|
105
|
+
},
|
|
106
|
+
getLogsByLevel(level) {
|
|
107
|
+
return logs.filter((log) => log.level === level);
|
|
108
|
+
},
|
|
109
|
+
clear() {
|
|
110
|
+
logs.length = 0;
|
|
111
|
+
},
|
|
112
|
+
hasLog(level, messagePattern) {
|
|
113
|
+
return logs.some((log) => {
|
|
114
|
+
if (log.level !== level) return false;
|
|
115
|
+
return log.args.some((arg) => {
|
|
116
|
+
if (typeof arg !== "string") {
|
|
117
|
+
try {
|
|
118
|
+
arg = JSON.stringify(arg);
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (typeof messagePattern === "string") {
|
|
124
|
+
return arg.includes(messagePattern);
|
|
125
|
+
}
|
|
126
|
+
return messagePattern.test(arg);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/stream.ts
|
|
134
|
+
async function collectStream(stream) {
|
|
135
|
+
const chunks = [];
|
|
136
|
+
for await (const chunk of stream) {
|
|
137
|
+
chunks.push(chunk);
|
|
138
|
+
}
|
|
139
|
+
return chunks;
|
|
140
|
+
}
|
|
141
|
+
async function collectStreamData(stream) {
|
|
142
|
+
const chunks = [];
|
|
143
|
+
for await (const chunk of stream) {
|
|
144
|
+
chunks.push(chunk.data);
|
|
145
|
+
}
|
|
146
|
+
return chunks;
|
|
147
|
+
}
|
|
148
|
+
async function collectStreamText(stream) {
|
|
149
|
+
let result = "";
|
|
150
|
+
for await (const chunk of stream) {
|
|
151
|
+
result += chunk.data.content;
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
async function firstChunk(stream) {
|
|
156
|
+
for await (const chunk of stream) {
|
|
157
|
+
return chunk;
|
|
158
|
+
}
|
|
159
|
+
return void 0;
|
|
160
|
+
}
|
|
161
|
+
async function lastChunk(stream) {
|
|
162
|
+
let last;
|
|
163
|
+
for await (const chunk of stream) {
|
|
164
|
+
last = chunk;
|
|
165
|
+
}
|
|
166
|
+
return last;
|
|
167
|
+
}
|
|
168
|
+
async function countChunks(stream) {
|
|
169
|
+
let count = 0;
|
|
170
|
+
for await (const _item of stream) {
|
|
171
|
+
count++;
|
|
172
|
+
}
|
|
173
|
+
return count;
|
|
174
|
+
}
|
|
175
|
+
async function assertStreamChunks(stream, expected) {
|
|
176
|
+
const actual = await collectStream(stream);
|
|
177
|
+
if (actual.length !== expected.length) {
|
|
178
|
+
throw new Error(`Expected ${expected.length} chunks, but got ${actual.length}`);
|
|
179
|
+
}
|
|
180
|
+
for (let i = 0; i < expected.length; i++) {
|
|
181
|
+
const actualStr = JSON.stringify(actual[i]);
|
|
182
|
+
const expectedStr = JSON.stringify(expected[i]);
|
|
183
|
+
if (actualStr !== expectedStr) {
|
|
184
|
+
throw new Error(`Chunk ${i} mismatch:
|
|
185
|
+
Expected: ${expectedStr}
|
|
186
|
+
Actual: ${actualStr}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function assertEveryChunk(stream, predicate) {
|
|
191
|
+
let index = 0;
|
|
192
|
+
for await (const chunk of stream) {
|
|
193
|
+
if (!predicate(chunk, index)) {
|
|
194
|
+
throw new Error(`Chunk ${index} failed assertion: ${JSON.stringify(chunk)}`);
|
|
195
|
+
}
|
|
196
|
+
index++;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/stream-events.ts
|
|
201
|
+
async function collectStreamEvents(stream) {
|
|
202
|
+
const events = [];
|
|
203
|
+
const data = [];
|
|
204
|
+
let done;
|
|
205
|
+
let error;
|
|
206
|
+
for await (const event of stream) {
|
|
207
|
+
events.push(event);
|
|
208
|
+
switch (event.type) {
|
|
209
|
+
case "data":
|
|
210
|
+
data.push(event.data);
|
|
211
|
+
break;
|
|
212
|
+
case "done":
|
|
213
|
+
done = event;
|
|
214
|
+
break;
|
|
215
|
+
case "error":
|
|
216
|
+
error = event;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
events,
|
|
222
|
+
data,
|
|
223
|
+
done,
|
|
224
|
+
error,
|
|
225
|
+
completed: done !== void 0 && error === void 0
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
async function collectEventData(stream) {
|
|
229
|
+
const data = [];
|
|
230
|
+
for await (const event of stream) {
|
|
231
|
+
if (event.type === "data") {
|
|
232
|
+
data.push(event.data);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return data;
|
|
236
|
+
}
|
|
237
|
+
async function waitForDone(stream) {
|
|
238
|
+
for await (const event of stream) {
|
|
239
|
+
if (event.type === "done") {
|
|
240
|
+
return event.metadata;
|
|
241
|
+
}
|
|
242
|
+
if (event.type === "error") {
|
|
243
|
+
throw new Error(`Stream ended with error: [${event.error.code}] ${event.error.message}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
throw new Error("Stream ended without done or error event");
|
|
247
|
+
}
|
|
248
|
+
async function waitForError(stream) {
|
|
249
|
+
for await (const event of stream) {
|
|
250
|
+
if (event.type === "error") {
|
|
251
|
+
return event.error;
|
|
252
|
+
}
|
|
253
|
+
if (event.type === "done") {
|
|
254
|
+
throw new Error("Expected stream to error, but it completed successfully");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
throw new Error("Stream ended without done or error event");
|
|
258
|
+
}
|
|
259
|
+
async function expectStreamCompletes(stream) {
|
|
260
|
+
const result = await collectStreamEvents(stream);
|
|
261
|
+
if (!result.completed) {
|
|
262
|
+
if (result.error) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
`Expected stream to complete, but got error: [${result.error.error.code}] ${result.error.error.message}`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
throw new Error("Expected stream to complete, but no done event received");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async function expectStreamErrors(stream, expectedCode) {
|
|
271
|
+
const result = await collectStreamEvents(stream);
|
|
272
|
+
if (!result.error) {
|
|
273
|
+
throw new Error("Expected stream to error, but it completed successfully");
|
|
274
|
+
}
|
|
275
|
+
if (expectedCode && result.error.error.code !== expectedCode) {
|
|
276
|
+
throw new Error(`Expected error code "${expectedCode}", but got "${result.error.error.code}"`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function expectAggregatedResult(stream, expected) {
|
|
280
|
+
const metadata = await waitForDone(stream);
|
|
281
|
+
const actual = JSON.stringify(metadata.aggregated);
|
|
282
|
+
const expectedStr = JSON.stringify(expected);
|
|
283
|
+
if (actual !== expectedStr) {
|
|
284
|
+
throw new Error(`Aggregated result mismatch:
|
|
285
|
+
Expected: ${expectedStr}
|
|
286
|
+
Actual: ${actual}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/schema.ts
|
|
291
|
+
function parseInput(instance, actionName, input) {
|
|
292
|
+
const schema = instance.getInputSchema(actionName);
|
|
293
|
+
if (!schema) {
|
|
294
|
+
throw new Error(`Action "${actionName}" not found`);
|
|
295
|
+
}
|
|
296
|
+
return schema.parse(input);
|
|
297
|
+
}
|
|
298
|
+
function expectInputRejected(instance, actionName, input) {
|
|
299
|
+
const schema = instance.getInputSchema(actionName);
|
|
300
|
+
if (!schema) {
|
|
301
|
+
throw new Error(`Action "${actionName}" not found`);
|
|
302
|
+
}
|
|
303
|
+
let threw = false;
|
|
304
|
+
try {
|
|
305
|
+
schema.parse(input);
|
|
306
|
+
} catch {
|
|
307
|
+
threw = true;
|
|
308
|
+
}
|
|
309
|
+
if (!threw) {
|
|
310
|
+
throw new Error(`Expected input to be rejected: ${JSON.stringify(input)}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export { assertEveryChunk, assertStreamChunks, collectEventData, collectStream, collectStreamData, collectStreamEvents, collectStreamText, countChunks, createMockLogger, createTestContext, createTestRunner, expectAggregatedResult, expectInputRejected, expectStreamCompletes, expectStreamErrors, firstChunk, lastChunk, parseInput, waitForDone, waitForError };
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lark-apaas/action-plugin-testing",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Testing utilities for action plugins",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"require": "./dist/index.cjs"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"action",
|
|
28
|
+
"plugin",
|
|
29
|
+
"testing",
|
|
30
|
+
"vitest"
|
|
31
|
+
],
|
|
32
|
+
"author": "",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@lark-apaas/action-plugin-core": "workspace:*"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^20.0.0",
|
|
39
|
+
"tsup": "^8.5.1",
|
|
40
|
+
"typescript": "^5.3.0",
|
|
41
|
+
"vitest": "^1.0.0",
|
|
42
|
+
"zod": "^3.25.76"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"zod": "^3.25.76"
|
|
46
|
+
}
|
|
47
|
+
}
|