@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.
@@ -0,0 +1,117 @@
1
+ import type { ProcessExecutor } from "@actiondock/core";
2
+ import type { 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 MockProcessExecutorOptions {
55
+ /** 未命中任何模拟规则时是否回退到真实子进程执行(默认 false,未命中即抛错) */
56
+ fallbackToReal?: boolean;
57
+ }
58
+ /**
59
+ * 模拟进程执行器实现。
60
+ * 遵循 ProcessExecutor 接口契约,支持预设命令响应、跟踪调用历史并模拟超时与取消场景。
61
+ *
62
+ * 默认不回退真实子进程执行:未命中任何模拟规则时抛出明确错误,避免测试中的拼写失误穿透到真实系统命令。
63
+ * 如确需真实回退(例如集成本地 CLI),可显式传入 fallbackToReal: true。
64
+ */
65
+ export declare class MockProcessExecutor implements ProcessExecutor {
66
+ private mocks;
67
+ calls: ProcessCall[];
68
+ defaultPid: number;
69
+ private readonly fallbackToReal;
70
+ constructor(options?: MockProcessExecutorOptions);
71
+ /**
72
+ * 注册模拟命令匹配与返回结果。
73
+ *
74
+ * @param matcher 匹配器(命令字符串、正则表达式或判断函数)
75
+ * @param handlerOrResult 预设执行结果或动态处理函数
76
+ */
77
+ register(matcher: CommandMatcher, handlerOrResult: MockProcessHandler | MockProcessResultOptions): this;
78
+ /**
79
+ * 执行外部命令并返回模拟结果。
80
+ *
81
+ * @param command 执行命令
82
+ * @param args 参数列表
83
+ * @param options 执行选项
84
+ */
85
+ exec(command: string, args?: string[], options?: ProcessExecOptions): Promise<ProcessResult>;
86
+ spawn(command: string, args?: string[], options?: ProcessExecOptions): Promise<ProcessResult>;
87
+ /**
88
+ * 获取指定命令的历史调用记录。
89
+ *
90
+ * @param command 可选命令筛选
91
+ */
92
+ getCalls(command?: string): ProcessCall[];
93
+ /**
94
+ * 获取最近一次命令调用记录。
95
+ */
96
+ getLastCall(): ProcessCall | undefined;
97
+ /**
98
+ * 检查指定命令是否被调用过。
99
+ *
100
+ * @param command 目标命令
101
+ */
102
+ hasCalled(command: string): boolean;
103
+ /**
104
+ * 清空历史调用记录。
105
+ */
106
+ clearHistory(): void;
107
+ /**
108
+ * 重置所有注册规则与历史记录。
109
+ */
110
+ reset(): void;
111
+ /**
112
+ * 渲染已注册匹配器列表,辅助定位拼写失误。
113
+ */
114
+ private describeMatchers;
115
+ private findMock;
116
+ private waitDelay;
117
+ }
@@ -0,0 +1,280 @@
1
+ import { execCli } from "./cli.js";
2
+ /**
3
+ * 模拟进程执行器实现。
4
+ * 遵循 ProcessExecutor 接口契约,支持预设命令响应、跟踪调用历史并模拟超时与取消场景。
5
+ *
6
+ * 默认不回退真实子进程执行:未命中任何模拟规则时抛出明确错误,避免测试中的拼写失误穿透到真实系统命令。
7
+ * 如确需真实回退(例如集成本地 CLI),可显式传入 fallbackToReal: true。
8
+ */
9
+ export class MockProcessExecutor {
10
+ mocks = [];
11
+ calls = [];
12
+ defaultPid = 10001;
13
+ fallbackToReal;
14
+ constructor(options = {}) {
15
+ this.fallbackToReal = options.fallbackToReal ?? false;
16
+ }
17
+ /**
18
+ * 注册模拟命令匹配与返回结果。
19
+ *
20
+ * @param matcher 匹配器(命令字符串、正则表达式或判断函数)
21
+ * @param handlerOrResult 预设执行结果或动态处理函数
22
+ */
23
+ register(matcher, handlerOrResult) {
24
+ this.mocks.push({ matcher, handler: handlerOrResult });
25
+ return this;
26
+ }
27
+ /**
28
+ * 执行外部命令并返回模拟结果。
29
+ *
30
+ * @param command 执行命令
31
+ * @param args 参数列表
32
+ * @param options 执行选项
33
+ */
34
+ async exec(command, args = [], options = {}) {
35
+ const startTime = Date.now();
36
+ this.calls.push({
37
+ command,
38
+ args: [...args],
39
+ options: { ...options },
40
+ timestamp: startTime,
41
+ });
42
+ // 检查调用前是否已中断
43
+ if (options.signal?.aborted) {
44
+ const res = {
45
+ ok: false,
46
+ exitCode: -1,
47
+ signal: "SIGTERM",
48
+ stdout: "",
49
+ stderr: "Command aborted before execution by signal",
50
+ raw: new Uint8Array(),
51
+ timedOut: false,
52
+ cancelled: true,
53
+ durationMs: 0,
54
+ error: {
55
+ code: "PROCESS_CANCELLED",
56
+ message: "Process was cancelled by AbortSignal",
57
+ },
58
+ };
59
+ if (options.throwOnError) {
60
+ throw new Error(res.stderr);
61
+ }
62
+ return res;
63
+ }
64
+ const matchedMock = this.findMock(command, args, options);
65
+ const fullCommandLine = [command, ...args].join(" ").trim();
66
+ let resolved;
67
+ if (!matchedMock) {
68
+ if (!this.fallbackToReal) {
69
+ throw new Error(`MockProcessExecutor: 未命中任何模拟规则,且未开启 fallbackToReal,拒绝执行真实命令: ${fullCommandLine}\n已注册匹配器列表:\n${this.describeMatchers()}`);
70
+ }
71
+ try {
72
+ const cliRes = await execCli(command, args, {
73
+ cwd: options.cwd,
74
+ env: options.env,
75
+ signal: options.signal,
76
+ timeout: options.timeoutMs,
77
+ input: options.input,
78
+ encoding: options.encoding,
79
+ });
80
+ resolved = {
81
+ ok: cliRes.ok,
82
+ exitCode: cliRes.exitCode,
83
+ stdout: cliRes.stdout,
84
+ stderr: cliRes.stderr,
85
+ raw: cliRes.raw,
86
+ timedOut: cliRes.timedOut,
87
+ durationMs: cliRes.durationMs,
88
+ };
89
+ }
90
+ catch (err) {
91
+ resolved = {
92
+ ok: false,
93
+ exitCode: -1,
94
+ stdout: "",
95
+ stderr: err?.message || String(err),
96
+ raw: new Uint8Array(),
97
+ durationMs: Date.now() - startTime,
98
+ };
99
+ }
100
+ }
101
+ else if (typeof matchedMock.handler === "function") {
102
+ resolved = await matchedMock.handler(command, args, options);
103
+ }
104
+ else {
105
+ resolved = matchedMock.handler;
106
+ }
107
+ // 模拟延时控制
108
+ const maybeMock = resolved;
109
+ if (typeof maybeMock.delayMs === "number" && maybeMock.delayMs > 0) {
110
+ await this.waitDelay(maybeMock.delayMs, options);
111
+ }
112
+ // 组装标准化结果
113
+ const timedOut = Boolean(resolved.timedOut);
114
+ const cancelled = Boolean(resolved.cancelled || options.signal?.aborted);
115
+ const stdout = resolved.stdout ?? "";
116
+ const stderr = resolved.stderr ?? (timedOut ? "Process timed out" : cancelled ? "Process cancelled" : "");
117
+ const raw = resolved.raw ?? new TextEncoder().encode(stdout);
118
+ const exitCode = resolved.exitCode !== undefined
119
+ ? resolved.exitCode
120
+ : timedOut || cancelled
121
+ ? null
122
+ : resolved.ok === false
123
+ ? 1
124
+ : 0;
125
+ const ok = resolved.ok !== undefined
126
+ ? resolved.ok
127
+ : exitCode === 0 && !timedOut && !cancelled && !resolved.error;
128
+ const durationMs = resolved.durationMs ?? Date.now() - startTime;
129
+ let error = resolved.error;
130
+ if (!error) {
131
+ if (timedOut) {
132
+ error = {
133
+ code: "PROCESS_TIMEOUT",
134
+ message: `Process exceeded timeout of ${options.timeoutMs ?? durationMs}ms`,
135
+ };
136
+ }
137
+ else if (cancelled) {
138
+ error = {
139
+ code: "PROCESS_CANCELLED",
140
+ message: "Process was cancelled by AbortSignal",
141
+ };
142
+ }
143
+ else if (!ok) {
144
+ error = {
145
+ code: "PROCESS_FAILED",
146
+ message: stderr || `Process exited with code ${exitCode}`,
147
+ };
148
+ }
149
+ }
150
+ const finalResult = {
151
+ ok,
152
+ exitCode,
153
+ signal: resolved.signal,
154
+ stdout,
155
+ stderr,
156
+ raw,
157
+ timedOut,
158
+ cancelled,
159
+ durationMs,
160
+ error,
161
+ };
162
+ if (!ok && options.throwOnError) {
163
+ throw new Error(stderr || `Process exited with code ${exitCode}`);
164
+ }
165
+ return finalResult;
166
+ }
167
+ async spawn(command, args = [], options = {}) {
168
+ return this.exec(command, args, options);
169
+ }
170
+ /**
171
+ * 获取指定命令的历史调用记录。
172
+ *
173
+ * @param command 可选命令筛选
174
+ */
175
+ getCalls(command) {
176
+ if (!command) {
177
+ return [...this.calls];
178
+ }
179
+ return this.calls.filter((c) => c.command === command);
180
+ }
181
+ /**
182
+ * 获取最近一次命令调用记录。
183
+ */
184
+ getLastCall() {
185
+ return this.calls[this.calls.length - 1];
186
+ }
187
+ /**
188
+ * 检查指定命令是否被调用过。
189
+ *
190
+ * @param command 目标命令
191
+ */
192
+ hasCalled(command) {
193
+ return this.calls.some((c) => c.command === command);
194
+ }
195
+ /**
196
+ * 清空历史调用记录。
197
+ */
198
+ clearHistory() {
199
+ this.calls = [];
200
+ }
201
+ /**
202
+ * 重置所有注册规则与历史记录。
203
+ */
204
+ reset() {
205
+ this.mocks = [];
206
+ this.calls = [];
207
+ }
208
+ /**
209
+ * 渲染已注册匹配器列表,辅助定位拼写失误。
210
+ */
211
+ describeMatchers() {
212
+ if (this.mocks.length === 0) {
213
+ return "(无任何已注册匹配器)";
214
+ }
215
+ return this.mocks
216
+ .map((m) => {
217
+ const desc = typeof m.matcher === "string"
218
+ ? `"${m.matcher}"`
219
+ : m.matcher instanceof RegExp
220
+ ? `/${m.matcher.source}/${m.matcher.flags}`
221
+ : "[Function]";
222
+ return `- ${desc}`;
223
+ })
224
+ .join("\n");
225
+ }
226
+ findMock(command, args, options) {
227
+ const fullCommandLine = [command, ...args].join(" ").trim();
228
+ // 逆序查找,优先匹配最新注册的规则;字符串匹配器仅支持命令名精确匹配与全命令行精确匹配
229
+ for (let i = this.mocks.length - 1; i >= 0; i--) {
230
+ const mock = this.mocks[i];
231
+ if (typeof mock.matcher === "string") {
232
+ if (mock.matcher === command || mock.matcher === fullCommandLine) {
233
+ return mock;
234
+ }
235
+ }
236
+ else if (mock.matcher instanceof RegExp) {
237
+ if (mock.matcher.test(fullCommandLine) || mock.matcher.test(command)) {
238
+ return mock;
239
+ }
240
+ }
241
+ else if (typeof mock.matcher === "function") {
242
+ if (mock.matcher(command, args, options)) {
243
+ return mock;
244
+ }
245
+ }
246
+ }
247
+ return undefined;
248
+ }
249
+ async waitDelay(delayMs, options) {
250
+ return new Promise((resolve) => {
251
+ let timer;
252
+ let onAbort;
253
+ const cleanup = () => {
254
+ if (timer) {
255
+ clearTimeout(timer);
256
+ timer = undefined;
257
+ }
258
+ if (options.signal && onAbort) {
259
+ options.signal.removeEventListener("abort", onAbort);
260
+ onAbort = undefined;
261
+ }
262
+ };
263
+ if (options.signal) {
264
+ if (options.signal.aborted) {
265
+ resolve();
266
+ return;
267
+ }
268
+ onAbort = () => {
269
+ cleanup();
270
+ resolve();
271
+ };
272
+ options.signal.addEventListener("abort", onAbort, { once: true });
273
+ }
274
+ timer = setTimeout(() => {
275
+ cleanup();
276
+ resolve();
277
+ }, delayMs);
278
+ });
279
+ }
280
+ }
@@ -0,0 +1,208 @@
1
+ import { ActionRunner, type ExecutionService, type ExecutionStartOptions, InMemoryEventSink, type ProjectConfig, type RuntimePlatform } from "@actiondock/core";
2
+ import type { Clock } from "@actiondock/core";
3
+ import type { ActionDefinition, Config, ExecutionEvent, ExecutionResult, Logger, StateStore } from "@actiondock/sdk";
4
+ import { FakeClock } from "./clock.js";
5
+ import { MockProcessExecutor } from "./process.js";
6
+ import { MemoryStorage } from "./storage.js";
7
+ /**
8
+ * 基于内存 Map 的只读/可写配置实现,专供单元测试使用。
9
+ */
10
+ export declare class MemoryConfig implements Config {
11
+ private store;
12
+ constructor(initial?: Record<string, unknown>);
13
+ get<T = unknown>(key: string): T | undefined;
14
+ get<T = unknown>(key: string, defaultValue: T): T;
15
+ has(key: string): boolean;
16
+ /**
17
+ * 在测试期间动态更新或插入配置值。
18
+ * @param key 配置键名
19
+ * @param value 配置值
20
+ */
21
+ set(key: string, value: unknown): void;
22
+ /**
23
+ * 删除指定配置项。
24
+ * @param key 配置键名
25
+ */
26
+ delete(key: string): boolean;
27
+ /**
28
+ * 列出所有已存储配置项。
29
+ */
30
+ list(): Record<string, unknown>;
31
+ }
32
+ /**
33
+ * 内存状态条目结构体,包含数据值与可选的过期时间戳。
34
+ */
35
+ export interface MemoryStateEntry {
36
+ value: unknown;
37
+ expiresAt?: number;
38
+ }
39
+ import { decodeStateKey, encodeStateKey, escapeStateSegment, unescapeStateSegment } from "@actiondock/sdk";
40
+ export { decodeStateKey, encodeStateKey, escapeStateSegment, unescapeStateSegment };
41
+ /**
42
+ * 基于内存 Map 的状态存储实现,支持命名空间隔离与 TTL 自动失效,专供单元测试使用。
43
+ */
44
+ export declare class MemoryStateStore implements StateStore {
45
+ private store;
46
+ private namespace;
47
+ private clock;
48
+ constructor(store?: Map<string, any>, namespace?: string, clock?: Clock);
49
+ /** 获取注入时钟的当前时间戳(毫秒),TTL 过期判定单一事实入口 */
50
+ private nowMs;
51
+ private qualify;
52
+ private extractEntry;
53
+ get<T = unknown>(key: string): Promise<T | undefined>;
54
+ set<T = unknown>(key: string, value: T, ttl?: number): Promise<void>;
55
+ delete(key: string): Promise<boolean>;
56
+ clear(prefix?: string): Promise<number>;
57
+ keys(prefix?: string): Promise<string[]>;
58
+ scope(namespace: string): StateStore;
59
+ }
60
+ /**
61
+ * 内存日志记录器实现,将所有日志记录在数组中以便在测试断言中检索。
62
+ */
63
+ export declare class MemoryLogger implements Logger {
64
+ logs: Array<{
65
+ level: string;
66
+ message: string;
67
+ data?: unknown;
68
+ }>;
69
+ debug(message: string, data?: unknown): void;
70
+ info(message: string, data?: unknown): void;
71
+ warn(message: string, data?: unknown): void;
72
+ error(message: string, data?: unknown): void;
73
+ }
74
+ import { ActionRuntimeError } from "@actiondock/sdk";
75
+ export { ActionRuntimeError };
76
+ /**
77
+ * 带有写入和调试能力的配置接口。
78
+ */
79
+ export interface TestConfig extends Config {
80
+ /** 写入配置键值 */
81
+ set(key: string, value: unknown): void;
82
+ /** 删除指定配置键 */
83
+ delete(key: string): boolean;
84
+ /** 列出所有已存储配置项 */
85
+ list(): Record<string, unknown>;
86
+ }
87
+ /**
88
+ * 测试配置管理器实现。
89
+ */
90
+ export declare class TestConfigStore implements TestConfig {
91
+ private runtimeConfig;
92
+ private storage;
93
+ constructor(storage: MemoryStorage, projectConfig?: ProjectConfig, overrides?: Record<string, unknown>);
94
+ get<T = unknown>(key: string): T | undefined;
95
+ get<T = unknown>(key: string, defaultValue: T): T;
96
+ has(key: string): boolean;
97
+ set(key: string, value: unknown): void;
98
+ delete(key: string): boolean;
99
+ list(): Record<string, unknown>;
100
+ }
101
+ /**
102
+ * 测试事件接收器实现。
103
+ * 记录执行期间产生的所有事件并支持历史检索。
104
+ */
105
+ export declare class TestEventSink extends InMemoryEventSink {
106
+ private allEvents;
107
+ private sequenceCounter;
108
+ /** 获取下一个单调自增序号 */
109
+ nextSequence(): number;
110
+ emit(event: ExecutionEvent): void;
111
+ /**
112
+ * 检索历史事件列表。
113
+ *
114
+ * @param runId 可选运行标识筛选
115
+ */
116
+ getEvents(runId?: string): ExecutionEvent[];
117
+ /**
118
+ * 清理所有捕获的事件记录。
119
+ */
120
+ clearAll(): void;
121
+ }
122
+ /**
123
+ * 测试运行时初始化选项。
124
+ */
125
+ export interface TestRuntimeOptions {
126
+ /** 绑定的 Package 标识 */
127
+ packageId?: string;
128
+ /** 初始注入的配置键值映射 */
129
+ config?: Record<string, unknown>;
130
+ /** 运行级别临时配置覆写字典 */
131
+ configOverrides?: Record<string, unknown>;
132
+ /** 初始注入的状态键值映射 */
133
+ state?: Record<string, unknown>;
134
+ /** 可选注入的模拟时钟实例 */
135
+ clock?: FakeClock;
136
+ /** 可选注入的模拟进程执行器 */
137
+ process?: MockProcessExecutor;
138
+ /** 可选注入的日志记录器(默认使用 MemoryLogger) */
139
+ logger?: Logger;
140
+ /** 可选注入的底层存储实例 */
141
+ storage?: MemoryStorage;
142
+ /** 项目静态配置元数据 */
143
+ projectConfig?: ProjectConfig;
144
+ /** 预注册的 Action 动作列表 */
145
+ actions?: Array<{
146
+ id: string;
147
+ action: ActionDefinition;
148
+ } | (ActionDefinition & {
149
+ id: string;
150
+ })> | Record<string, ActionDefinition> | Map<string, ActionDefinition>;
151
+ /** 可选注入的标准运行时平台实例 */
152
+ platform?: RuntimePlatform;
153
+ }
154
+ /**
155
+ * 测试运行时接口。
156
+ */
157
+ export interface TestRuntime {
158
+ /** 调试配置接口 */
159
+ config: TestConfig;
160
+ /** 调试状态持久化接口 */
161
+ state: StateStore;
162
+ /** 调试模拟时钟接口 */
163
+ clock: FakeClock;
164
+ /** 调试模拟进程执行接口 */
165
+ process: MockProcessExecutor;
166
+ /** 调试执行事件捕获接口 */
167
+ events: TestEventSink;
168
+ /** 调试日志记录捕获接口 */
169
+ logger: MemoryLogger;
170
+ /** 底层存储引擎 */
171
+ storage: MemoryStorage;
172
+ /** 统一执行服务 */
173
+ executionService: ExecutionService;
174
+ /** 核心执行器引擎(向后兼容保留) */
175
+ runner: ActionRunner;
176
+ /** 注册 Action 动作定义 */
177
+ registerAction(id: string, action: ActionDefinition): void;
178
+ registerAction(action: ({
179
+ id: string;
180
+ action?: ActionDefinition;
181
+ } & Partial<ActionDefinition>) | ActionDefinition): void;
182
+ /** 获取已注册的 Action 动作定义 */
183
+ getAction(id: string): ActionDefinition | undefined;
184
+ /** 列出已注册的所有 Action 动作定义 */
185
+ listActions(): ActionDefinition[];
186
+ /**
187
+ * 执行 Action 并直接返回业务结果数据,失败时抛出 ActionRuntimeError 规范化异常。
188
+ *
189
+ * @param action Action 动作定义或已注册标识
190
+ * @param input 输入参数数据
191
+ */
192
+ run<I = unknown, O = unknown>(action: ActionDefinition<I, O> | string, input?: I): Promise<O>;
193
+ /**
194
+ * 执行 Action 并返回完整的 ExecutionResult 信封结构。
195
+ *
196
+ * @param action Action 动作定义或已注册标识
197
+ * @param input 输入参数数据
198
+ * @param options 可选执行控制参数
199
+ */
200
+ execute<I = unknown, O = unknown>(action: ActionDefinition<I, O> | string, input?: I, options?: ExecutionStartOptions): Promise<ExecutionResult<O>>;
201
+ }
202
+ /**
203
+ * 创建全功能测试运行时实例。
204
+ * 基于统一 ExecutionService 协调执行全生命周期,并暴露配置、状态、时钟、进程与事件等调试接口。
205
+ *
206
+ * @param options 测试运行时选项
207
+ */
208
+ export declare function createTestRuntime(options?: TestRuntimeOptions): TestRuntime;