@actiondock/testing 2.0.11 → 2.0.12
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/clock.d.ts +51 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +486 -0
- package/dist/process.d.ts +116 -0
- package/dist/runtime.d.ts +137 -0
- package/dist/storage.d.ts +20 -0
- package/package.json +12 -11
- package/src/clock.ts +0 -136
- package/src/index.ts +0 -4
- package/src/process.ts +0 -394
- package/src/runtime.ts +0 -380
- package/src/storage.ts +0 -37
package/dist/clock.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Clock } from "@actiondock/core";
|
|
2
|
+
/**
|
|
3
|
+
* 模拟时钟初始化选项。
|
|
4
|
+
*/
|
|
5
|
+
export interface FakeClockOptions {
|
|
6
|
+
/** 初始时间戳或日期对象 */
|
|
7
|
+
now?: Date | number | string;
|
|
8
|
+
/** 初始单调时间戳毫秒数 */
|
|
9
|
+
startMonotonic?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 确定性测试模拟时钟实现。
|
|
13
|
+
* 遵循 Clock 接口契约,支持手动单调推进时间并调度计时器。
|
|
14
|
+
*/
|
|
15
|
+
export declare class FakeClock implements Clock {
|
|
16
|
+
private currentNow;
|
|
17
|
+
private currentMonotonic;
|
|
18
|
+
private nextTimerId;
|
|
19
|
+
private pendingSleeps;
|
|
20
|
+
constructor(options?: FakeClockOptions);
|
|
21
|
+
/**
|
|
22
|
+
* 获取当前模拟墙上时间。
|
|
23
|
+
*/
|
|
24
|
+
now(): Date;
|
|
25
|
+
/**
|
|
26
|
+
* 获取当前模拟单调时间戳(毫秒)。
|
|
27
|
+
*/
|
|
28
|
+
monotonic(): number;
|
|
29
|
+
/**
|
|
30
|
+
* 异步休眠指定毫秒。
|
|
31
|
+
* 等待通过 advance 方法推进时间至目标时刻后完成。
|
|
32
|
+
*
|
|
33
|
+
* @param ms 休眠毫秒数
|
|
34
|
+
*/
|
|
35
|
+
sleep(ms: number): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* 手动向前推进指定毫秒时间。
|
|
38
|
+
* 严格按时间戳递增顺序触发并完成所有到期的休眠计时器。
|
|
39
|
+
*
|
|
40
|
+
* @param ms 推进的毫秒数
|
|
41
|
+
*/
|
|
42
|
+
advance(ms: number): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* 获取当前等待中的计时器数量。
|
|
45
|
+
*/
|
|
46
|
+
get pendingCount(): number;
|
|
47
|
+
/**
|
|
48
|
+
* 清除并取消所有等待中的计时器。
|
|
49
|
+
*/
|
|
50
|
+
clear(): void;
|
|
51
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
// packages/testing/src/index.ts
|
|
2
|
+
import { registerTestRuntimeProvider } from "@actiondock/sdk";
|
|
3
|
+
|
|
4
|
+
// packages/testing/src/runtime.ts
|
|
5
|
+
import {
|
|
6
|
+
DefaultExecutionService,
|
|
7
|
+
InMemoryEventSink,
|
|
8
|
+
RuntimeConfig,
|
|
9
|
+
RuntimeStateStore
|
|
10
|
+
} from "@actiondock/core";
|
|
11
|
+
import {
|
|
12
|
+
MemoryLogger
|
|
13
|
+
} from "@actiondock/sdk";
|
|
14
|
+
|
|
15
|
+
// packages/testing/src/clock.ts
|
|
16
|
+
class FakeClock {
|
|
17
|
+
currentNow;
|
|
18
|
+
currentMonotonic;
|
|
19
|
+
nextTimerId = 1;
|
|
20
|
+
pendingSleeps = [];
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
if (options.now !== undefined) {
|
|
23
|
+
this.currentNow = new Date(options.now).getTime();
|
|
24
|
+
} else {
|
|
25
|
+
this.currentNow = Date.now();
|
|
26
|
+
}
|
|
27
|
+
this.currentMonotonic = options.startMonotonic ?? 0;
|
|
28
|
+
}
|
|
29
|
+
now() {
|
|
30
|
+
return new Date(this.currentNow);
|
|
31
|
+
}
|
|
32
|
+
monotonic() {
|
|
33
|
+
return this.currentMonotonic;
|
|
34
|
+
}
|
|
35
|
+
sleep(ms) {
|
|
36
|
+
if (ms <= 0) {
|
|
37
|
+
return Promise.resolve();
|
|
38
|
+
}
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const targetMonotonic = this.currentMonotonic + ms;
|
|
41
|
+
const targetNow = this.currentNow + ms;
|
|
42
|
+
this.pendingSleeps.push({
|
|
43
|
+
id: this.nextTimerId++,
|
|
44
|
+
targetMonotonic,
|
|
45
|
+
targetNow,
|
|
46
|
+
resolve,
|
|
47
|
+
reject
|
|
48
|
+
});
|
|
49
|
+
this.pendingSleeps.sort((a, b) => a.targetMonotonic - b.targetMonotonic);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async advance(ms) {
|
|
53
|
+
if (ms < 0) {
|
|
54
|
+
throw new Error("Cannot advance clock by negative time");
|
|
55
|
+
}
|
|
56
|
+
if (ms === 0) {
|
|
57
|
+
await Promise.resolve();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const destinationMonotonic = this.currentMonotonic + ms;
|
|
61
|
+
const destinationNow = this.currentNow + ms;
|
|
62
|
+
while (this.pendingSleeps.length > 0) {
|
|
63
|
+
const nextSleep = this.pendingSleeps[0];
|
|
64
|
+
if (nextSleep.targetMonotonic > destinationMonotonic) {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
this.pendingSleeps.shift();
|
|
68
|
+
this.currentMonotonic = nextSleep.targetMonotonic;
|
|
69
|
+
this.currentNow = nextSleep.targetNow;
|
|
70
|
+
nextSleep.resolve();
|
|
71
|
+
await Promise.resolve();
|
|
72
|
+
}
|
|
73
|
+
this.currentMonotonic = destinationMonotonic;
|
|
74
|
+
this.currentNow = destinationNow;
|
|
75
|
+
await Promise.resolve();
|
|
76
|
+
}
|
|
77
|
+
get pendingCount() {
|
|
78
|
+
return this.pendingSleeps.length;
|
|
79
|
+
}
|
|
80
|
+
clear() {
|
|
81
|
+
const sleeps = this.pendingSleeps;
|
|
82
|
+
this.pendingSleeps = [];
|
|
83
|
+
for (const item of sleeps) {
|
|
84
|
+
item.reject(new Error("FakeClock timer cancelled"));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// packages/testing/src/process.ts
|
|
90
|
+
class MockProcessExecutor {
|
|
91
|
+
mocks = [];
|
|
92
|
+
calls = [];
|
|
93
|
+
detachedCalls = [];
|
|
94
|
+
defaultPid = 10001;
|
|
95
|
+
register(matcher, handlerOrResult) {
|
|
96
|
+
this.mocks.push({ matcher, handler: handlerOrResult });
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
async exec(command, args = [], options = {}) {
|
|
100
|
+
const startTime = Date.now();
|
|
101
|
+
this.calls.push({
|
|
102
|
+
command,
|
|
103
|
+
args: [...args],
|
|
104
|
+
options: { ...options },
|
|
105
|
+
timestamp: startTime
|
|
106
|
+
});
|
|
107
|
+
if (options.signal?.aborted) {
|
|
108
|
+
const res = {
|
|
109
|
+
ok: false,
|
|
110
|
+
exitCode: null,
|
|
111
|
+
signal: "SIGTERM",
|
|
112
|
+
stdout: "",
|
|
113
|
+
stderr: "Process was cancelled by AbortSignal",
|
|
114
|
+
raw: new Uint8Array,
|
|
115
|
+
timedOut: false,
|
|
116
|
+
cancelled: true,
|
|
117
|
+
durationMs: 0,
|
|
118
|
+
error: {
|
|
119
|
+
code: "PROCESS_CANCELLED",
|
|
120
|
+
message: "Process was cancelled by AbortSignal"
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
if (options.throwOnError) {
|
|
124
|
+
throw new Error(res.stderr);
|
|
125
|
+
}
|
|
126
|
+
return res;
|
|
127
|
+
}
|
|
128
|
+
const matchedMock = this.findMock(command, args, options);
|
|
129
|
+
let resolved;
|
|
130
|
+
if (!matchedMock) {
|
|
131
|
+
resolved = {
|
|
132
|
+
ok: true,
|
|
133
|
+
exitCode: 0,
|
|
134
|
+
stdout: "",
|
|
135
|
+
stderr: ""
|
|
136
|
+
};
|
|
137
|
+
} else if (typeof matchedMock.handler === "function") {
|
|
138
|
+
resolved = await matchedMock.handler(command, args, options);
|
|
139
|
+
} else {
|
|
140
|
+
resolved = matchedMock.handler;
|
|
141
|
+
}
|
|
142
|
+
const maybeMock = resolved;
|
|
143
|
+
if (typeof maybeMock.delayMs === "number" && maybeMock.delayMs > 0) {
|
|
144
|
+
await this.waitDelay(maybeMock.delayMs, options);
|
|
145
|
+
}
|
|
146
|
+
const timedOut = Boolean(resolved.timedOut);
|
|
147
|
+
const cancelled = Boolean(resolved.cancelled || options.signal?.aborted);
|
|
148
|
+
const stdout = resolved.stdout ?? "";
|
|
149
|
+
const stderr = resolved.stderr ?? (timedOut ? "Process timed out" : cancelled ? "Process cancelled" : "");
|
|
150
|
+
const raw = resolved.raw ?? new TextEncoder().encode(stdout);
|
|
151
|
+
const exitCode = resolved.exitCode !== undefined ? resolved.exitCode : timedOut || cancelled ? null : resolved.ok === false ? 1 : 0;
|
|
152
|
+
const ok = resolved.ok !== undefined ? resolved.ok : exitCode === 0 && !timedOut && !cancelled && !resolved.error;
|
|
153
|
+
const durationMs = resolved.durationMs ?? Date.now() - startTime;
|
|
154
|
+
let error = resolved.error;
|
|
155
|
+
if (!error) {
|
|
156
|
+
if (timedOut) {
|
|
157
|
+
error = {
|
|
158
|
+
code: "PROCESS_TIMEOUT",
|
|
159
|
+
message: `Process exceeded timeout of ${options.timeoutMs ?? durationMs}ms`
|
|
160
|
+
};
|
|
161
|
+
} else if (cancelled) {
|
|
162
|
+
error = {
|
|
163
|
+
code: "PROCESS_CANCELLED",
|
|
164
|
+
message: "Process was cancelled by AbortSignal"
|
|
165
|
+
};
|
|
166
|
+
} else if (!ok) {
|
|
167
|
+
error = {
|
|
168
|
+
code: "PROCESS_FAILED",
|
|
169
|
+
message: stderr || `Process exited with code ${exitCode}`
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const finalResult = {
|
|
174
|
+
ok,
|
|
175
|
+
exitCode,
|
|
176
|
+
signal: resolved.signal,
|
|
177
|
+
stdout,
|
|
178
|
+
stderr,
|
|
179
|
+
raw,
|
|
180
|
+
timedOut,
|
|
181
|
+
cancelled,
|
|
182
|
+
durationMs,
|
|
183
|
+
error
|
|
184
|
+
};
|
|
185
|
+
if (!ok && options.throwOnError) {
|
|
186
|
+
throw new Error(stderr || `Process exited with code ${exitCode}`);
|
|
187
|
+
}
|
|
188
|
+
return finalResult;
|
|
189
|
+
}
|
|
190
|
+
async spawnDetached(options) {
|
|
191
|
+
const startTime = Date.now();
|
|
192
|
+
this.detachedCalls.push({
|
|
193
|
+
options: { ...options },
|
|
194
|
+
timestamp: startTime
|
|
195
|
+
});
|
|
196
|
+
if (options.signal?.aborted) {
|
|
197
|
+
return {
|
|
198
|
+
ok: false,
|
|
199
|
+
ready: false,
|
|
200
|
+
durationMs: 0,
|
|
201
|
+
error: {
|
|
202
|
+
code: "PROCESS_CANCELLED",
|
|
203
|
+
message: "Process was cancelled by AbortSignal"
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (options.probe) {
|
|
208
|
+
const fakeResult = {
|
|
209
|
+
ok: true,
|
|
210
|
+
exitCode: 0,
|
|
211
|
+
stdout: "ready",
|
|
212
|
+
stderr: "",
|
|
213
|
+
raw: new TextEncoder().encode("ready"),
|
|
214
|
+
timedOut: false,
|
|
215
|
+
cancelled: false,
|
|
216
|
+
durationMs: 0
|
|
217
|
+
};
|
|
218
|
+
const isReady = await options.probe(fakeResult);
|
|
219
|
+
return {
|
|
220
|
+
ok: isReady,
|
|
221
|
+
pid: this.defaultPid++,
|
|
222
|
+
ready: isReady,
|
|
223
|
+
durationMs: Date.now() - startTime
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
ok: true,
|
|
228
|
+
pid: this.defaultPid++,
|
|
229
|
+
ready: true,
|
|
230
|
+
durationMs: Date.now() - startTime
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
getCalls(command) {
|
|
234
|
+
if (!command) {
|
|
235
|
+
return [...this.calls];
|
|
236
|
+
}
|
|
237
|
+
return this.calls.filter((c) => c.command === command);
|
|
238
|
+
}
|
|
239
|
+
getLastCall() {
|
|
240
|
+
return this.calls[this.calls.length - 1];
|
|
241
|
+
}
|
|
242
|
+
hasCalled(command) {
|
|
243
|
+
return this.calls.some((c) => c.command === command);
|
|
244
|
+
}
|
|
245
|
+
clearHistory() {
|
|
246
|
+
this.calls = [];
|
|
247
|
+
this.detachedCalls = [];
|
|
248
|
+
}
|
|
249
|
+
reset() {
|
|
250
|
+
this.mocks = [];
|
|
251
|
+
this.calls = [];
|
|
252
|
+
this.detachedCalls = [];
|
|
253
|
+
}
|
|
254
|
+
findMock(command, args, options) {
|
|
255
|
+
const fullCommandLine = [command, ...args].join(" ").trim();
|
|
256
|
+
for (let i = this.mocks.length - 1;i >= 0; i--) {
|
|
257
|
+
const mock = this.mocks[i];
|
|
258
|
+
if (typeof mock.matcher === "string") {
|
|
259
|
+
if (mock.matcher === command || mock.matcher === fullCommandLine || fullCommandLine.startsWith(mock.matcher)) {
|
|
260
|
+
return mock;
|
|
261
|
+
}
|
|
262
|
+
} else if (mock.matcher instanceof RegExp) {
|
|
263
|
+
if (mock.matcher.test(fullCommandLine) || mock.matcher.test(command)) {
|
|
264
|
+
return mock;
|
|
265
|
+
}
|
|
266
|
+
} else if (typeof mock.matcher === "function") {
|
|
267
|
+
if (mock.matcher(command, args, options)) {
|
|
268
|
+
return mock;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
async waitDelay(delayMs, options) {
|
|
275
|
+
return new Promise((resolve) => {
|
|
276
|
+
let timer;
|
|
277
|
+
const cleanup = () => {
|
|
278
|
+
if (timer)
|
|
279
|
+
clearTimeout(timer);
|
|
280
|
+
};
|
|
281
|
+
if (options.signal) {
|
|
282
|
+
options.signal.addEventListener("abort", () => {
|
|
283
|
+
cleanup();
|
|
284
|
+
resolve();
|
|
285
|
+
}, { once: true });
|
|
286
|
+
}
|
|
287
|
+
timer = setTimeout(() => {
|
|
288
|
+
cleanup();
|
|
289
|
+
resolve();
|
|
290
|
+
}, delayMs);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// packages/testing/src/storage.ts
|
|
296
|
+
import {
|
|
297
|
+
SqliteRuntimeStorage
|
|
298
|
+
} from "@actiondock/core";
|
|
299
|
+
|
|
300
|
+
class MemoryStorage extends SqliteRuntimeStorage {
|
|
301
|
+
constructor(options = {}) {
|
|
302
|
+
super({
|
|
303
|
+
packageId: options.packageId || "test-pkg",
|
|
304
|
+
dbPath: ":memory:",
|
|
305
|
+
driver: options.driver,
|
|
306
|
+
clock: options.clock
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// packages/testing/src/runtime.ts
|
|
312
|
+
class ActionRuntimeError extends Error {
|
|
313
|
+
code;
|
|
314
|
+
details;
|
|
315
|
+
cause;
|
|
316
|
+
constructor(error) {
|
|
317
|
+
super(error.message);
|
|
318
|
+
this.name = "ActionRuntimeError";
|
|
319
|
+
this.code = error.code;
|
|
320
|
+
this.details = error.details;
|
|
321
|
+
this.cause = error.cause;
|
|
322
|
+
Object.setPrototypeOf(this, ActionRuntimeError.prototype);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
class TestConfigStore {
|
|
327
|
+
runtimeConfig;
|
|
328
|
+
storage;
|
|
329
|
+
constructor(storage, projectConfig, overrides) {
|
|
330
|
+
this.storage = storage;
|
|
331
|
+
this.runtimeConfig = new RuntimeConfig(storage, overrides, projectConfig, undefined);
|
|
332
|
+
}
|
|
333
|
+
get(key, defaultValue) {
|
|
334
|
+
return this.runtimeConfig.get(key, defaultValue);
|
|
335
|
+
}
|
|
336
|
+
has(key) {
|
|
337
|
+
return this.runtimeConfig.has(key);
|
|
338
|
+
}
|
|
339
|
+
set(key, value) {
|
|
340
|
+
this.storage.setConfig(key, value);
|
|
341
|
+
}
|
|
342
|
+
delete(key) {
|
|
343
|
+
return this.storage.deleteConfig(key);
|
|
344
|
+
}
|
|
345
|
+
list() {
|
|
346
|
+
return this.storage.listConfig();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
class TestEventSink extends InMemoryEventSink {
|
|
351
|
+
allEvents = [];
|
|
352
|
+
sequenceCounter = 0;
|
|
353
|
+
nextSequence() {
|
|
354
|
+
return this.sequenceCounter++;
|
|
355
|
+
}
|
|
356
|
+
emit(event) {
|
|
357
|
+
this.allEvents.push(event);
|
|
358
|
+
super.emit(event);
|
|
359
|
+
}
|
|
360
|
+
getEvents(runId) {
|
|
361
|
+
if (runId) {
|
|
362
|
+
return this.allEvents.filter((e) => e.runId === runId);
|
|
363
|
+
}
|
|
364
|
+
return [...this.allEvents];
|
|
365
|
+
}
|
|
366
|
+
clearAll() {
|
|
367
|
+
this.allEvents = [];
|
|
368
|
+
this.sequenceCounter = 0;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function createTestRuntime(options = {}) {
|
|
372
|
+
const packageId = options.packageId || "test-pkg";
|
|
373
|
+
const clock = options.clock ?? new FakeClock;
|
|
374
|
+
const process = options.process ?? new MockProcessExecutor;
|
|
375
|
+
const storage = options.storage ?? new MemoryStorage({
|
|
376
|
+
packageId,
|
|
377
|
+
clock
|
|
378
|
+
});
|
|
379
|
+
if (options.config) {
|
|
380
|
+
for (const [key, val] of Object.entries(options.config)) {
|
|
381
|
+
storage.setConfig(key, val);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (options.state) {
|
|
385
|
+
for (const [key, val] of Object.entries(options.state)) {
|
|
386
|
+
storage.setState("", key, val);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const events = new TestEventSink;
|
|
390
|
+
const memoryLogger = options.logger instanceof MemoryLogger ? options.logger : new MemoryLogger;
|
|
391
|
+
const actionsMap = new Map;
|
|
392
|
+
if (options.actions) {
|
|
393
|
+
if (Array.isArray(options.actions)) {
|
|
394
|
+
for (const act of options.actions) {
|
|
395
|
+
actionsMap.set(act.id, act);
|
|
396
|
+
}
|
|
397
|
+
} else if (options.actions instanceof Map) {
|
|
398
|
+
for (const [k, v] of options.actions) {
|
|
399
|
+
actionsMap.set(k, v);
|
|
400
|
+
}
|
|
401
|
+
} else if (typeof options.actions === "object") {
|
|
402
|
+
for (const [k, v] of Object.entries(options.actions)) {
|
|
403
|
+
actionsMap.set(k, v);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const executionService = new DefaultExecutionService({
|
|
408
|
+
packageId,
|
|
409
|
+
storage,
|
|
410
|
+
projectConfig: options.projectConfig,
|
|
411
|
+
configOverrides: options.configOverrides,
|
|
412
|
+
actions: actionsMap,
|
|
413
|
+
process,
|
|
414
|
+
clock,
|
|
415
|
+
logger: memoryLogger,
|
|
416
|
+
eventSink: events
|
|
417
|
+
});
|
|
418
|
+
const testConfig = new TestConfigStore(storage, options.projectConfig, options.configOverrides);
|
|
419
|
+
const testState = new RuntimeStateStore(storage);
|
|
420
|
+
const registerAction = (action) => {
|
|
421
|
+
executionService.registerAction(action);
|
|
422
|
+
};
|
|
423
|
+
const getAction = (id) => {
|
|
424
|
+
return executionService.getAction(id);
|
|
425
|
+
};
|
|
426
|
+
const listActions = () => {
|
|
427
|
+
return executionService.listActions();
|
|
428
|
+
};
|
|
429
|
+
const execute = async (action, input = {}, execOptions = {}) => {
|
|
430
|
+
if (typeof action !== "string") {
|
|
431
|
+
executionService.registerAction(action);
|
|
432
|
+
}
|
|
433
|
+
const actionRef = typeof action === "string" ? action : action.id;
|
|
434
|
+
const ticket = await executionService.start(actionRef, input, {
|
|
435
|
+
signal: execOptions.signal,
|
|
436
|
+
timeoutMs: execOptions.timeoutMs,
|
|
437
|
+
config: execOptions.configOverrides,
|
|
438
|
+
parentRunId: execOptions.parentRunId,
|
|
439
|
+
rootRunId: execOptions.rootRunId,
|
|
440
|
+
maxCallDepth: execOptions.maxCallDepth,
|
|
441
|
+
logger: execOptions.logger,
|
|
442
|
+
progress: execOptions.progress,
|
|
443
|
+
process: execOptions.process || process
|
|
444
|
+
});
|
|
445
|
+
const result = await ticket.result;
|
|
446
|
+
return result;
|
|
447
|
+
};
|
|
448
|
+
const run = async (action, input = {}) => {
|
|
449
|
+
const result = await execute(action, input);
|
|
450
|
+
if (!result.ok) {
|
|
451
|
+
throw new ActionRuntimeError(result.error);
|
|
452
|
+
}
|
|
453
|
+
return result.data;
|
|
454
|
+
};
|
|
455
|
+
return {
|
|
456
|
+
config: testConfig,
|
|
457
|
+
state: testState,
|
|
458
|
+
clock,
|
|
459
|
+
process,
|
|
460
|
+
events,
|
|
461
|
+
logger: memoryLogger,
|
|
462
|
+
storage,
|
|
463
|
+
executionService,
|
|
464
|
+
runner: executionService.runner,
|
|
465
|
+
registerAction,
|
|
466
|
+
getAction,
|
|
467
|
+
listActions,
|
|
468
|
+
run,
|
|
469
|
+
execute
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// packages/testing/src/index.ts
|
|
474
|
+
function registerTestingAsSdkProvider() {
|
|
475
|
+
registerTestRuntimeProvider(createTestRuntime);
|
|
476
|
+
}
|
|
477
|
+
export {
|
|
478
|
+
ActionRuntimeError,
|
|
479
|
+
FakeClock,
|
|
480
|
+
MemoryStorage,
|
|
481
|
+
MockProcessExecutor,
|
|
482
|
+
TestConfigStore,
|
|
483
|
+
TestEventSink,
|
|
484
|
+
createTestRuntime,
|
|
485
|
+
registerTestingAsSdkProvider
|
|
486
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { ProcessExecutor } from "@actiondock/core";
|
|
2
|
+
import type { DetachedProcessOptions, DetachedProcessResult, ProcessExecOptions, ProcessResult, RuntimeError } from "@actiondock/sdk";
|
|
3
|
+
/**
|
|
4
|
+
* 模拟命令匹配器。
|
|
5
|
+
*/
|
|
6
|
+
export type CommandMatcher = string | RegExp | ((command: string, args: string[], options: ProcessExecOptions) => boolean);
|
|
7
|
+
/**
|
|
8
|
+
* 模拟进程执行结果选项。
|
|
9
|
+
*/
|
|
10
|
+
export interface MockProcessResultOptions {
|
|
11
|
+
/** 命令是否执行成功 */
|
|
12
|
+
ok?: boolean;
|
|
13
|
+
/** 退出状态码 */
|
|
14
|
+
exitCode?: number | null;
|
|
15
|
+
/** 终止信号名称 */
|
|
16
|
+
signal?: string;
|
|
17
|
+
/** 标准输出内容 */
|
|
18
|
+
stdout?: string;
|
|
19
|
+
/** 标准错误内容 */
|
|
20
|
+
stderr?: string;
|
|
21
|
+
/** 原始字节数组输出 */
|
|
22
|
+
raw?: Uint8Array;
|
|
23
|
+
/** 是否标记为超时 */
|
|
24
|
+
timedOut?: boolean;
|
|
25
|
+
/** 是否标记为已取消 */
|
|
26
|
+
cancelled?: boolean;
|
|
27
|
+
/** 执行耗时毫秒数 */
|
|
28
|
+
durationMs?: number;
|
|
29
|
+
/** 运行时结构化错误 */
|
|
30
|
+
error?: RuntimeError;
|
|
31
|
+
/** 模拟执行延迟毫秒数 */
|
|
32
|
+
delayMs?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 模拟进程处理器函数。
|
|
36
|
+
*/
|
|
37
|
+
export type MockProcessHandler = (command: string, args: string[], options: ProcessExecOptions) => MockProcessResultOptions | ProcessResult | Promise<MockProcessResultOptions | ProcessResult>;
|
|
38
|
+
/**
|
|
39
|
+
* 已记录的命令调用历史条目。
|
|
40
|
+
*/
|
|
41
|
+
export interface ProcessCall {
|
|
42
|
+
/** 执行命令名称 */
|
|
43
|
+
command: string;
|
|
44
|
+
/** 执行参数列表 */
|
|
45
|
+
args: string[];
|
|
46
|
+
/** 执行选项配置 */
|
|
47
|
+
options: ProcessExecOptions;
|
|
48
|
+
/** 调用发生时的时间戳 */
|
|
49
|
+
timestamp: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 已记录的后台守护进程调用历史条目。
|
|
53
|
+
*/
|
|
54
|
+
export interface DetachedProcessCall {
|
|
55
|
+
/** 启动参数选项 */
|
|
56
|
+
options: DetachedProcessOptions;
|
|
57
|
+
/** 调用发生时的时间戳 */
|
|
58
|
+
timestamp: number;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 模拟进程执行器实现。
|
|
62
|
+
* 遵循 ProcessExecutor 接口契约,支持预设命令响应、跟踪调用历史并模拟超时与取消场景。
|
|
63
|
+
*/
|
|
64
|
+
export declare class MockProcessExecutor implements ProcessExecutor {
|
|
65
|
+
private mocks;
|
|
66
|
+
calls: ProcessCall[];
|
|
67
|
+
detachedCalls: DetachedProcessCall[];
|
|
68
|
+
defaultPid: number;
|
|
69
|
+
/**
|
|
70
|
+
* 注册模拟命令匹配与返回结果。
|
|
71
|
+
*
|
|
72
|
+
* @param matcher 匹配器(命令字符串、正则表达式或判断函数)
|
|
73
|
+
* @param handlerOrResult 预设执行结果或动态处理函数
|
|
74
|
+
*/
|
|
75
|
+
register(matcher: CommandMatcher, handlerOrResult: MockProcessHandler | MockProcessResultOptions): this;
|
|
76
|
+
/**
|
|
77
|
+
* 执行外部命令并返回模拟结果。
|
|
78
|
+
*
|
|
79
|
+
* @param command 执行命令
|
|
80
|
+
* @param args 参数列表
|
|
81
|
+
* @param options 执行选项
|
|
82
|
+
*/
|
|
83
|
+
exec(command: string, args?: string[], options?: ProcessExecOptions): Promise<ProcessResult>;
|
|
84
|
+
/**
|
|
85
|
+
* 启动模拟脱离父进程的后台进程。
|
|
86
|
+
*
|
|
87
|
+
* @param options 守护进程启动选项
|
|
88
|
+
*/
|
|
89
|
+
spawnDetached(options: DetachedProcessOptions): Promise<DetachedProcessResult>;
|
|
90
|
+
/**
|
|
91
|
+
* 获取指定命令的历史调用记录。
|
|
92
|
+
*
|
|
93
|
+
* @param command 可选命令筛选
|
|
94
|
+
*/
|
|
95
|
+
getCalls(command?: string): ProcessCall[];
|
|
96
|
+
/**
|
|
97
|
+
* 获取最近一次命令调用记录。
|
|
98
|
+
*/
|
|
99
|
+
getLastCall(): ProcessCall | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* 检查指定命令是否被调用过。
|
|
102
|
+
*
|
|
103
|
+
* @param command 目标命令
|
|
104
|
+
*/
|
|
105
|
+
hasCalled(command: string): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* 清空历史调用记录。
|
|
108
|
+
*/
|
|
109
|
+
clearHistory(): void;
|
|
110
|
+
/**
|
|
111
|
+
* 重置所有注册规则与历史记录。
|
|
112
|
+
*/
|
|
113
|
+
reset(): void;
|
|
114
|
+
private findMock;
|
|
115
|
+
private waitDelay;
|
|
116
|
+
}
|