@actiondock/testing 2.0.11 → 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 +54 -0
- package/dist/clock.js +108 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/platform.d.ts +48 -0
- package/dist/platform.js +61 -0
- package/dist/process.d.ts +117 -0
- package/dist/process.js +280 -0
- package/dist/runtime.d.ts +208 -0
- package/dist/runtime.js +426 -0
- package/dist/storage.d.ts +20 -0
- package/dist/storage.js +16 -0
- package/package.json +13 -12
- 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/README.md
CHANGED
|
@@ -2,87 +2,85 @@
|
|
|
2
2
|
|
|
3
3
|
ActionDock 2.0 确定性测试框架与测试运行时。
|
|
4
4
|
|
|
5
|
-
[](https://nodejs.org/)
|
|
6
6
|
[](https://www.typescriptlang.org/)
|
|
7
7
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
8
8
|
|
|
9
|
-
`@actiondock/testing` 为 ActionDock
|
|
9
|
+
`@actiondock/testing` 为 ActionDock 工具与技能开发者提供确定性、无物理外设依赖且具备完整 Core 执行语义的单元测试框架。测试工具已全面收敛至本包。
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
## 核心组件与测试能力
|
|
14
14
|
|
|
15
|
+
### createTestRuntime 测试运行时工厂
|
|
16
|
+
|
|
17
|
+
[createTestRuntime](./src/runtime.ts) 是深度复用核心执行引擎的测试脚手架:
|
|
18
|
+
|
|
19
|
+
- 真实生命周期校验:在纯内存测试中同步执行输入输出模式校验、调用环路死锁检测与超时控制。
|
|
20
|
+
- 双模态执行接口:支持通过 `run` 直接获取业务结果(失败时抛出 ActionRuntimeError 规范异常),或通过 `execute` 获取包含运行标识与元数据的完整信封。
|
|
21
|
+
- 全要素调试暴露:测试运行时直接暴露 `config`、`state`、`clock`、`process`、`events`、`logger` 与 `storage` 实例,便于注入先验数据并断言副作用。
|
|
22
|
+
|
|
15
23
|
### FakeClock 确定性时钟
|
|
16
24
|
|
|
17
|
-
|
|
25
|
+
[FakeClock](./src/clock.ts) 解耦物理系统时钟,消除异步定时器测试中的偶发性等待与竞态:
|
|
18
26
|
|
|
19
|
-
-
|
|
20
|
-
- 通过 `advance(ms)`
|
|
27
|
+
- 模拟墙上时间与单调递增时间戳。
|
|
28
|
+
- 通过 `advance(ms)` 瞬间推进模拟时间,并以确定性顺序依次唤醒挂起的计时器与延迟任务。
|
|
21
29
|
- 支持时间倒流检测与高精度时间戳快照。
|
|
22
30
|
|
|
23
31
|
### MockProcessExecutor 模拟进程执行器
|
|
24
32
|
|
|
25
|
-
在沙箱中拦截并伪造所有外部系统命令与子进程调用:
|
|
33
|
+
[MockProcessExecutor](./src/process.ts) 在沙箱中拦截并伪造所有外部系统命令与子进程调用:
|
|
26
34
|
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
35
|
+
- 灵活规则匹配:通过 `onCommand` 注册匹配器,支持字符串完全匹配、正则表达式匹配或自定义断言谓词函数。
|
|
36
|
+
- 丰富的响应定义:支持模拟标准输出、标准错误流、非零退出码、二进制字节流以及执行耗时。
|
|
37
|
+
- 异常场景复现:可直接模拟命令执行超时(`timedOut`)或取消信号阻断(`cancelled`)。
|
|
38
|
+
- 调用历史追踪:精确记录每次调用的完整入参、工作目录与环境变量,提供断言追踪支持。
|
|
31
39
|
|
|
32
40
|
### MemoryStorage 纯内存存储
|
|
33
41
|
|
|
34
|
-
|
|
42
|
+
[MemoryStorage](./src/storage.ts) 基于纯内存构建的无磁盘运行时存储实现:
|
|
35
43
|
|
|
36
44
|
- 具备与生产环境持久化存储完全相同的配置优先级解析规则与事务边界。
|
|
37
45
|
- 完整支持状态数据的命名空间隔离、前缀检索与基于存活时间的自动过期判定。
|
|
38
46
|
- 完整持久化运行历史记录与结构化输入输出快照。
|
|
39
47
|
|
|
40
|
-
### createTestRuntime 测试运行时工厂
|
|
41
|
-
|
|
42
|
-
深度复用核心引擎 `ActionRunner` 的测试脚手架:
|
|
43
|
-
|
|
44
|
-
- **真实生命周期校验**:在内存测试中同步执行输入输出 JSON Schema 校验、调用环路死锁检测与超时控制。
|
|
45
|
-
- **双模态执行接口**:支持通过 `run` 直接获取业务数据(失败时抛出带有错误码的异常),或通过 `execute` 获取包含运行标识与元数据的完整信封。
|
|
46
|
-
- **全要素调试访问**:测试运行时直接暴露 `config`、`state`、`clock`、`process`、`events` 与 `storage` 实例,便于在测试用例中注入先验数据并断言副作用。
|
|
47
|
-
|
|
48
48
|
---
|
|
49
49
|
|
|
50
50
|
## 快速使用示例
|
|
51
51
|
|
|
52
52
|
```ts
|
|
53
|
+
import { describe, it } from "node:test";
|
|
54
|
+
import assert from "node:assert/strict";
|
|
53
55
|
import { defineAction } from "@actiondock/sdk";
|
|
54
|
-
import { createTestRuntime,
|
|
55
|
-
|
|
56
|
-
const gitBranchAction = defineAction({
|
|
57
|
-
id: "git.branch",
|
|
58
|
-
inputSchema: {
|
|
59
|
-
type: "object",
|
|
60
|
-
properties: {
|
|
61
|
-
remote: { type: "boolean" },
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
async run(input, ctx) {
|
|
65
|
-
const res = await ctx.process.exec("git", ["branch"]);
|
|
66
|
-
return { output: res.stdout.trim() };
|
|
67
|
-
},
|
|
68
|
-
});
|
|
56
|
+
import { createTestRuntime, MockProcessExecutor } from "@actiondock/testing";
|
|
69
57
|
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
stdout: "* main\n feature/agent\n",
|
|
58
|
+
const gitBranchAction = defineAction(async (input: { remote?: boolean }, ctx) => {
|
|
59
|
+
const res = await ctx.process.exec("git", ["branch"]);
|
|
60
|
+
return { output: res.stdout.trim() };
|
|
74
61
|
});
|
|
75
62
|
|
|
76
|
-
|
|
77
|
-
process
|
|
63
|
+
describe("git action test", () => {
|
|
64
|
+
it("mocks process and asserts output", async () => {
|
|
65
|
+
// 初始化模拟执行器并配置预设响应
|
|
66
|
+
const processExecutor = new MockProcessExecutor();
|
|
67
|
+
processExecutor.onCommand("git", {
|
|
68
|
+
stdout: "* main\n feature/agent\n",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// 创建测试运行时并注入执行器
|
|
72
|
+
const runtime = createTestRuntime({
|
|
73
|
+
process: processExecutor,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// 执行 Action 并断言业务数据
|
|
77
|
+
const result = await runtime.run(gitBranchAction, { remote: false });
|
|
78
|
+
assert.equal(result.output, "* main\n feature/agent");
|
|
79
|
+
|
|
80
|
+
// 断言底层命令调用历史
|
|
81
|
+
assert.equal(processExecutor.getHistory().length, 1);
|
|
82
|
+
});
|
|
78
83
|
});
|
|
79
|
-
|
|
80
|
-
// 执行 Action 并断言业务数据
|
|
81
|
-
const result = await runtime.run(gitBranchAction, { remote: false });
|
|
82
|
-
console.log(result.output);
|
|
83
|
-
|
|
84
|
-
// 断言底层命令调用记录
|
|
85
|
-
console.log(processExecutor.getHistory().length === 1);
|
|
86
84
|
```
|
|
87
85
|
|
|
88
86
|
---
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { findExecutable } from "@actiondock/core";
|
|
2
|
+
export { findExecutable };
|
|
3
|
+
/**
|
|
4
|
+
* CLI 执行选项配置。
|
|
5
|
+
*/
|
|
6
|
+
export interface ExecCliOptions {
|
|
7
|
+
cwd?: string;
|
|
8
|
+
env?: Record<string, string>;
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
timeout?: number;
|
|
11
|
+
input?: string | Uint8Array;
|
|
12
|
+
encoding?: string;
|
|
13
|
+
throwOnError?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* CLI 执行结果结构体。
|
|
17
|
+
*/
|
|
18
|
+
export interface ExecCliResult {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
exitCode: number;
|
|
21
|
+
stdout: string;
|
|
22
|
+
stderr: string;
|
|
23
|
+
raw: Uint8Array;
|
|
24
|
+
timedOut?: boolean;
|
|
25
|
+
durationMs: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 执行外部 CLI 命令并收集输出。
|
|
29
|
+
* 基于异步 spawn 与 Promise 化封装(与 runtime-node 执行器风格对齐),保留既有的输出收集与退出码语义。
|
|
30
|
+
*
|
|
31
|
+
* @param command 执行命令
|
|
32
|
+
* @param args 参数列表
|
|
33
|
+
* @param options 执行选项
|
|
34
|
+
*/
|
|
35
|
+
export declare function execCli(command: string, args?: string[], options?: ExecCliOptions): Promise<ExecCliResult>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { findExecutable } from "@actiondock/core";
|
|
4
|
+
export { findExecutable };
|
|
5
|
+
/**
|
|
6
|
+
* 执行外部 CLI 命令并收集输出。
|
|
7
|
+
* 基于异步 spawn 与 Promise 化封装(与 runtime-node 执行器风格对齐),保留既有的输出收集与退出码语义。
|
|
8
|
+
*
|
|
9
|
+
* @param command 执行命令
|
|
10
|
+
* @param args 参数列表
|
|
11
|
+
* @param options 执行选项
|
|
12
|
+
*/
|
|
13
|
+
export async function execCli(command, args = [], options = {}) {
|
|
14
|
+
const startTime = performance.now();
|
|
15
|
+
if (options.signal?.aborted) {
|
|
16
|
+
const errRes = {
|
|
17
|
+
ok: false,
|
|
18
|
+
exitCode: -1,
|
|
19
|
+
stdout: "",
|
|
20
|
+
stderr: "Command aborted before execution by signal",
|
|
21
|
+
raw: new Uint8Array(0),
|
|
22
|
+
durationMs: 0,
|
|
23
|
+
};
|
|
24
|
+
if (options.throwOnError) {
|
|
25
|
+
throw new Error(errRes.stderr);
|
|
26
|
+
}
|
|
27
|
+
return errRes;
|
|
28
|
+
}
|
|
29
|
+
const hasPathSep = command.includes("/") || command.includes("\\");
|
|
30
|
+
const binPath = hasPathSep ? (existsSync(command) ? command : null) : findExecutable(command);
|
|
31
|
+
if (!binPath) {
|
|
32
|
+
const errRes = {
|
|
33
|
+
ok: false,
|
|
34
|
+
exitCode: -1,
|
|
35
|
+
stdout: "",
|
|
36
|
+
stderr: `Command '${command}' not found in PATH.`,
|
|
37
|
+
raw: new Uint8Array(0),
|
|
38
|
+
durationMs: Math.round(performance.now() - startTime),
|
|
39
|
+
};
|
|
40
|
+
if (options.throwOnError) {
|
|
41
|
+
throw new Error(errRes.stderr);
|
|
42
|
+
}
|
|
43
|
+
return errRes;
|
|
44
|
+
}
|
|
45
|
+
let stdinInput;
|
|
46
|
+
if (options.input !== undefined) {
|
|
47
|
+
if (typeof options.input === "string") {
|
|
48
|
+
stdinInput = Buffer.from(options.input);
|
|
49
|
+
}
|
|
50
|
+
else if (options.input instanceof Uint8Array) {
|
|
51
|
+
stdinInput = Buffer.from(options.input);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
let child;
|
|
56
|
+
try {
|
|
57
|
+
child = spawn(binPath, args, {
|
|
58
|
+
cwd: options.cwd || process.cwd(),
|
|
59
|
+
env: options.env ? { ...process.env, ...options.env } : process.env,
|
|
60
|
+
stdio: [stdinInput ? "pipe" : "ignore", "pipe", "pipe"],
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
const errRes = {
|
|
65
|
+
ok: false,
|
|
66
|
+
exitCode: -1,
|
|
67
|
+
stdout: "",
|
|
68
|
+
stderr: err?.message || String(err),
|
|
69
|
+
raw: new Uint8Array(0),
|
|
70
|
+
durationMs: Math.round(performance.now() - startTime),
|
|
71
|
+
};
|
|
72
|
+
if (options.throwOnError) {
|
|
73
|
+
reject(err);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
resolve(errRes);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
let settled = false;
|
|
80
|
+
let timedOut = false;
|
|
81
|
+
let spawnError;
|
|
82
|
+
const stdoutChunks = [];
|
|
83
|
+
const stderrChunks = [];
|
|
84
|
+
let timeoutTimer;
|
|
85
|
+
if (options.timeout && options.timeout > 0) {
|
|
86
|
+
timeoutTimer = setTimeout(() => {
|
|
87
|
+
timedOut = true;
|
|
88
|
+
try {
|
|
89
|
+
child.kill("SIGTERM");
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// 忽略已退出状态
|
|
93
|
+
}
|
|
94
|
+
}, options.timeout);
|
|
95
|
+
}
|
|
96
|
+
const cleanup = () => {
|
|
97
|
+
if (timeoutTimer)
|
|
98
|
+
clearTimeout(timeoutTimer);
|
|
99
|
+
};
|
|
100
|
+
const finalize = (exitCodeFromClose) => {
|
|
101
|
+
if (settled)
|
|
102
|
+
return;
|
|
103
|
+
settled = true;
|
|
104
|
+
cleanup();
|
|
105
|
+
const durationMs = Math.round(performance.now() - startTime);
|
|
106
|
+
const decoder = new TextDecoder(options.encoding || "utf-8");
|
|
107
|
+
const rawStdout = stdoutChunks.length > 0 ? new Uint8Array(Buffer.concat(stdoutChunks)) : new Uint8Array(0);
|
|
108
|
+
const rawStderr = stderrChunks.length > 0 ? new Uint8Array(Buffer.concat(stderrChunks)) : new Uint8Array(0);
|
|
109
|
+
const stdout = rawStdout.length > 0 ? decoder.decode(rawStdout).trim() : "";
|
|
110
|
+
let stderr = rawStderr.length > 0 ? decoder.decode(rawStderr).trim() : "";
|
|
111
|
+
if (timedOut && !stderr) {
|
|
112
|
+
stderr = `Command '${command}' timed out after ${options.timeout}ms`;
|
|
113
|
+
}
|
|
114
|
+
const exitCode = timedOut ? -1 : (exitCodeFromClose ?? (spawnError ? -1 : 0));
|
|
115
|
+
const ok = !timedOut && exitCode === 0;
|
|
116
|
+
const result = {
|
|
117
|
+
ok,
|
|
118
|
+
exitCode,
|
|
119
|
+
stdout,
|
|
120
|
+
stderr,
|
|
121
|
+
raw: rawStdout,
|
|
122
|
+
timedOut: timedOut || undefined,
|
|
123
|
+
durationMs,
|
|
124
|
+
};
|
|
125
|
+
if (options.throwOnError && !ok) {
|
|
126
|
+
reject(new Error(stderr || `Command '${command}' failed with exit code ${exitCode}`));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
resolve(result);
|
|
130
|
+
};
|
|
131
|
+
child.stdout?.on("data", (chunk) => {
|
|
132
|
+
stdoutChunks.push(chunk);
|
|
133
|
+
});
|
|
134
|
+
child.stderr?.on("data", (chunk) => {
|
|
135
|
+
stderrChunks.push(chunk);
|
|
136
|
+
});
|
|
137
|
+
child.on("error", (err) => {
|
|
138
|
+
spawnError = err;
|
|
139
|
+
finalize(child.exitCode);
|
|
140
|
+
});
|
|
141
|
+
child.on("close", (code) => {
|
|
142
|
+
finalize(code);
|
|
143
|
+
});
|
|
144
|
+
if (stdinInput && child.stdin) {
|
|
145
|
+
child.stdin.on("error", () => {
|
|
146
|
+
// 目标进程提前退出时忽略管道写入错误
|
|
147
|
+
});
|
|
148
|
+
child.stdin.end(stdinInput);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
package/dist/clock.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
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 initialRealNow;
|
|
19
|
+
private advancedMs;
|
|
20
|
+
private isFixed;
|
|
21
|
+
private nextTimerId;
|
|
22
|
+
private pendingSleeps;
|
|
23
|
+
constructor(options?: FakeClockOptions);
|
|
24
|
+
/**
|
|
25
|
+
* 获取当前模拟墙上时间。
|
|
26
|
+
*/
|
|
27
|
+
now(): Date;
|
|
28
|
+
/**
|
|
29
|
+
* 获取当前模拟单调时间戳(毫秒)。
|
|
30
|
+
*/
|
|
31
|
+
monotonic(): number;
|
|
32
|
+
/**
|
|
33
|
+
* 异步休眠指定毫秒。
|
|
34
|
+
* 等待通过 advance 方法推进时间至目标时刻后完成。
|
|
35
|
+
*
|
|
36
|
+
* @param ms 休眠毫秒数
|
|
37
|
+
*/
|
|
38
|
+
sleep(ms: number): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* 手动向前推进指定毫秒时间。
|
|
41
|
+
* 严格按时间戳递增顺序触发并完成所有到期的休眠计时器。
|
|
42
|
+
*
|
|
43
|
+
* @param ms 推进的毫秒数
|
|
44
|
+
*/
|
|
45
|
+
advance(ms: number): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* 获取当前等待中的计时器数量。
|
|
48
|
+
*/
|
|
49
|
+
get pendingCount(): number;
|
|
50
|
+
/**
|
|
51
|
+
* 清除并取消所有等待中的计时器。
|
|
52
|
+
*/
|
|
53
|
+
clear(): void;
|
|
54
|
+
}
|
package/dist/clock.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 确定性测试模拟时钟实现。
|
|
3
|
+
* 遵循 Clock 接口契约,支持手动单调推进时间并调度计时器。
|
|
4
|
+
*/
|
|
5
|
+
export class FakeClock {
|
|
6
|
+
currentNow;
|
|
7
|
+
currentMonotonic;
|
|
8
|
+
initialRealNow;
|
|
9
|
+
advancedMs = 0;
|
|
10
|
+
isFixed = false;
|
|
11
|
+
nextTimerId = 1;
|
|
12
|
+
pendingSleeps = [];
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
if (options.now !== undefined) {
|
|
15
|
+
this.currentNow = new Date(options.now).getTime();
|
|
16
|
+
this.isFixed = true;
|
|
17
|
+
this.initialRealNow = this.currentNow;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
this.currentNow = Date.now();
|
|
21
|
+
this.initialRealNow = this.currentNow;
|
|
22
|
+
}
|
|
23
|
+
this.currentMonotonic = options.startMonotonic ?? 0;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 获取当前模拟墙上时间。
|
|
27
|
+
*/
|
|
28
|
+
now() {
|
|
29
|
+
return new Date(this.currentNow);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 获取当前模拟单调时间戳(毫秒)。
|
|
33
|
+
*/
|
|
34
|
+
monotonic() {
|
|
35
|
+
return this.currentMonotonic;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 异步休眠指定毫秒。
|
|
39
|
+
* 等待通过 advance 方法推进时间至目标时刻后完成。
|
|
40
|
+
*
|
|
41
|
+
* @param ms 休眠毫秒数
|
|
42
|
+
*/
|
|
43
|
+
sleep(ms) {
|
|
44
|
+
if (ms <= 0) {
|
|
45
|
+
return Promise.resolve();
|
|
46
|
+
}
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const targetMonotonic = this.currentMonotonic + ms;
|
|
49
|
+
const targetNow = this.currentNow + ms;
|
|
50
|
+
this.pendingSleeps.push({
|
|
51
|
+
id: this.nextTimerId++,
|
|
52
|
+
targetMonotonic,
|
|
53
|
+
targetNow,
|
|
54
|
+
resolve,
|
|
55
|
+
reject,
|
|
56
|
+
});
|
|
57
|
+
this.pendingSleeps.sort((a, b) => a.targetMonotonic - b.targetMonotonic);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 手动向前推进指定毫秒时间。
|
|
62
|
+
* 严格按时间戳递增顺序触发并完成所有到期的休眠计时器。
|
|
63
|
+
*
|
|
64
|
+
* @param ms 推进的毫秒数
|
|
65
|
+
*/
|
|
66
|
+
async advance(ms) {
|
|
67
|
+
if (ms < 0) {
|
|
68
|
+
throw new Error("Cannot advance clock by negative time");
|
|
69
|
+
}
|
|
70
|
+
if (ms === 0) {
|
|
71
|
+
await Promise.resolve();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const destinationMonotonic = this.currentMonotonic + ms;
|
|
75
|
+
const destinationNow = this.currentNow + ms;
|
|
76
|
+
this.advancedMs += ms;
|
|
77
|
+
while (this.pendingSleeps.length > 0) {
|
|
78
|
+
const nextSleep = this.pendingSleeps[0];
|
|
79
|
+
if (nextSleep.targetMonotonic > destinationMonotonic) {
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
this.pendingSleeps.shift();
|
|
83
|
+
this.currentMonotonic = nextSleep.targetMonotonic;
|
|
84
|
+
this.currentNow = nextSleep.targetNow;
|
|
85
|
+
nextSleep.resolve();
|
|
86
|
+
await Promise.resolve();
|
|
87
|
+
}
|
|
88
|
+
this.currentMonotonic = destinationMonotonic;
|
|
89
|
+
this.currentNow = destinationNow;
|
|
90
|
+
await Promise.resolve();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 获取当前等待中的计时器数量。
|
|
94
|
+
*/
|
|
95
|
+
get pendingCount() {
|
|
96
|
+
return this.pendingSleeps.length;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* 清除并取消所有等待中的计时器。
|
|
100
|
+
*/
|
|
101
|
+
clear() {
|
|
102
|
+
const sleeps = this.pendingSleeps;
|
|
103
|
+
this.pendingSleeps = [];
|
|
104
|
+
for (const item of sleeps) {
|
|
105
|
+
item.reject(new Error("FakeClock timer cancelled"));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -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
|
+
}
|