@actiondock/testing 2.0.12 → 2.2.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/README.md +45 -47
- package/dist/cli.d.ts +35 -0
- package/dist/cli.js +151 -0
- package/dist/clock.d.ts +3 -0
- package/dist/clock.js +108 -0
- package/dist/index.d.ts +6 -8
- package/dist/index.js +6 -486
- package/dist/platform.d.ts +48 -0
- package/dist/platform.js +61 -0
- package/dist/process.d.ts +15 -14
- package/dist/process.js +280 -0
- package/dist/runtime.d.ts +85 -14
- package/dist/runtime.js +426 -0
- package/dist/storage.js +16 -0
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,486 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
};
|
|
1
|
+
export * from "./clock.js";
|
|
2
|
+
export * from "./process.js";
|
|
3
|
+
export * from "./storage.js";
|
|
4
|
+
export * from "./runtime.js";
|
|
5
|
+
export * from "./platform.js";
|
|
6
|
+
export * from "./cli.js";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type EventSink, type FileSystem, type ModuleLoader, type RuntimePlatform, type RuntimeStorage, type StorageFactory } from "@actiondock/core";
|
|
2
|
+
import { FakeClock } from "./clock.js";
|
|
3
|
+
import { MockProcessExecutor } from "./process.js";
|
|
4
|
+
/**
|
|
5
|
+
* 测试平台构建配置选项。
|
|
6
|
+
*/
|
|
7
|
+
export interface TestPlatformOptions {
|
|
8
|
+
/** 可选注入的确定性虚拟时钟 */
|
|
9
|
+
clock?: FakeClock;
|
|
10
|
+
/** 可选注入的统一存储实例(若指定则所有 Package 存储均回退至该实例) */
|
|
11
|
+
storage?: RuntimeStorage;
|
|
12
|
+
/** 可选注入的跨 Package 全局存储实例 */
|
|
13
|
+
globalStorage?: RuntimeStorage;
|
|
14
|
+
/** 可选注入的模拟进程执行器 */
|
|
15
|
+
process?: MockProcessExecutor;
|
|
16
|
+
/** 可选注入的执行事件接收器 */
|
|
17
|
+
eventSink?: EventSink;
|
|
18
|
+
/** 可选注入的文件系统抽象驱动 */
|
|
19
|
+
files?: FileSystem;
|
|
20
|
+
/** 可选注入的源码模块加载器 */
|
|
21
|
+
modules?: ModuleLoader;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 测试运行时平台接口契约。
|
|
25
|
+
* 完整实现 RuntimePlatform,并显式暴露测试组件类型。
|
|
26
|
+
*/
|
|
27
|
+
export interface TestPlatform extends RuntimePlatform {
|
|
28
|
+
readonly name: "test";
|
|
29
|
+
readonly clock: FakeClock;
|
|
30
|
+
readonly files: FileSystem;
|
|
31
|
+
readonly modules: ModuleLoader;
|
|
32
|
+
readonly process: MockProcessExecutor;
|
|
33
|
+
readonly storage: StorageFactory;
|
|
34
|
+
readonly eventSink: EventSink;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 创建纯内存确定性测试平台实例。
|
|
38
|
+
* 组装测试核心组件:
|
|
39
|
+
* - FakeClock 确定性虚拟时钟
|
|
40
|
+
* - MockProcessExecutor 模拟进程执行器
|
|
41
|
+
* - MemoryStorage 纯内存数据库存储
|
|
42
|
+
* - TestEventSink 确定性事件接收器
|
|
43
|
+
* - DefaultModuleLoader 动态模块加载器
|
|
44
|
+
* - NodeFileSystem 文件系统
|
|
45
|
+
*
|
|
46
|
+
* @param options 测试平台配置选项
|
|
47
|
+
*/
|
|
48
|
+
export declare function createTestPlatform(options?: TestPlatformOptions): TestPlatform;
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { DefaultModuleLoader, NodeFileSystem, } from "@actiondock/core";
|
|
2
|
+
import { FakeClock } from "./clock.js";
|
|
3
|
+
import { MockProcessExecutor } from "./process.js";
|
|
4
|
+
import { TestEventSink } from "./runtime.js";
|
|
5
|
+
import { MemoryStorage } from "./storage.js";
|
|
6
|
+
/**
|
|
7
|
+
* 创建纯内存确定性测试平台实例。
|
|
8
|
+
* 组装测试核心组件:
|
|
9
|
+
* - FakeClock 确定性虚拟时钟
|
|
10
|
+
* - MockProcessExecutor 模拟进程执行器
|
|
11
|
+
* - MemoryStorage 纯内存数据库存储
|
|
12
|
+
* - TestEventSink 确定性事件接收器
|
|
13
|
+
* - DefaultModuleLoader 动态模块加载器
|
|
14
|
+
* - NodeFileSystem 文件系统
|
|
15
|
+
*
|
|
16
|
+
* @param options 测试平台配置选项
|
|
17
|
+
*/
|
|
18
|
+
export function createTestPlatform(options = {}) {
|
|
19
|
+
const clock = options.clock ?? new FakeClock();
|
|
20
|
+
const process = options.process ?? new MockProcessExecutor();
|
|
21
|
+
const eventSink = options.eventSink ?? new TestEventSink();
|
|
22
|
+
const files = options.files ?? new NodeFileSystem();
|
|
23
|
+
const modules = options.modules ?? new DefaultModuleLoader();
|
|
24
|
+
const packageStorages = new Map();
|
|
25
|
+
let globalStorageInstance = options.globalStorage;
|
|
26
|
+
const storageFactory = {
|
|
27
|
+
createStorage(packageId, _opts) {
|
|
28
|
+
if (options.storage) {
|
|
29
|
+
return options.storage;
|
|
30
|
+
}
|
|
31
|
+
let existing = packageStorages.get(packageId);
|
|
32
|
+
if (!existing) {
|
|
33
|
+
existing = new MemoryStorage({
|
|
34
|
+
packageId,
|
|
35
|
+
clock,
|
|
36
|
+
});
|
|
37
|
+
packageStorages.set(packageId, existing);
|
|
38
|
+
}
|
|
39
|
+
return existing;
|
|
40
|
+
},
|
|
41
|
+
createGlobalStorage(_opts) {
|
|
42
|
+
if (globalStorageInstance) {
|
|
43
|
+
return globalStorageInstance;
|
|
44
|
+
}
|
|
45
|
+
globalStorageInstance = new MemoryStorage({
|
|
46
|
+
packageId: "__global__",
|
|
47
|
+
clock,
|
|
48
|
+
});
|
|
49
|
+
return globalStorageInstance;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
name: "test",
|
|
54
|
+
clock,
|
|
55
|
+
files,
|
|
56
|
+
modules,
|
|
57
|
+
process,
|
|
58
|
+
storage: storageFactory,
|
|
59
|
+
eventSink,
|
|
60
|
+
};
|
|
61
|
+
}
|
package/dist/process.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ProcessExecutor } from "@actiondock/core";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ProcessExecOptions, ProcessResult, RuntimeError } from "@actiondock/sdk";
|
|
3
3
|
/**
|
|
4
4
|
* 模拟命令匹配器。
|
|
5
5
|
*/
|
|
@@ -49,23 +49,25 @@ export interface ProcessCall {
|
|
|
49
49
|
timestamp: number;
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
|
-
*
|
|
52
|
+
* 模拟进程执行器构造选项。
|
|
53
53
|
*/
|
|
54
|
-
export interface
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
/** 调用发生时的时间戳 */
|
|
58
|
-
timestamp: number;
|
|
54
|
+
export interface MockProcessExecutorOptions {
|
|
55
|
+
/** 未命中任何模拟规则时是否回退到真实子进程执行(默认 false,未命中即抛错) */
|
|
56
|
+
fallbackToReal?: boolean;
|
|
59
57
|
}
|
|
60
58
|
/**
|
|
61
59
|
* 模拟进程执行器实现。
|
|
62
60
|
* 遵循 ProcessExecutor 接口契约,支持预设命令响应、跟踪调用历史并模拟超时与取消场景。
|
|
61
|
+
*
|
|
62
|
+
* 默认不回退真实子进程执行:未命中任何模拟规则时抛出明确错误,避免测试中的拼写失误穿透到真实系统命令。
|
|
63
|
+
* 如确需真实回退(例如集成本地 CLI),可显式传入 fallbackToReal: true。
|
|
63
64
|
*/
|
|
64
65
|
export declare class MockProcessExecutor implements ProcessExecutor {
|
|
65
66
|
private mocks;
|
|
66
67
|
calls: ProcessCall[];
|
|
67
|
-
detachedCalls: DetachedProcessCall[];
|
|
68
68
|
defaultPid: number;
|
|
69
|
+
private readonly fallbackToReal;
|
|
70
|
+
constructor(options?: MockProcessExecutorOptions);
|
|
69
71
|
/**
|
|
70
72
|
* 注册模拟命令匹配与返回结果。
|
|
71
73
|
*
|
|
@@ -81,12 +83,7 @@ export declare class MockProcessExecutor implements ProcessExecutor {
|
|
|
81
83
|
* @param options 执行选项
|
|
82
84
|
*/
|
|
83
85
|
exec(command: string, args?: string[], options?: ProcessExecOptions): Promise<ProcessResult>;
|
|
84
|
-
|
|
85
|
-
* 启动模拟脱离父进程的后台进程。
|
|
86
|
-
*
|
|
87
|
-
* @param options 守护进程启动选项
|
|
88
|
-
*/
|
|
89
|
-
spawnDetached(options: DetachedProcessOptions): Promise<DetachedProcessResult>;
|
|
86
|
+
spawn(command: string, args?: string[], options?: ProcessExecOptions): Promise<ProcessResult>;
|
|
90
87
|
/**
|
|
91
88
|
* 获取指定命令的历史调用记录。
|
|
92
89
|
*
|
|
@@ -111,6 +108,10 @@ export declare class MockProcessExecutor implements ProcessExecutor {
|
|
|
111
108
|
* 重置所有注册规则与历史记录。
|
|
112
109
|
*/
|
|
113
110
|
reset(): void;
|
|
111
|
+
/**
|
|
112
|
+
* 渲染已注册匹配器列表,辅助定位拼写失误。
|
|
113
|
+
*/
|
|
114
|
+
private describeMatchers;
|
|
114
115
|
private findMock;
|
|
115
116
|
private waitDelay;
|
|
116
117
|
}
|