@actiondock/testing 2.2.2 → 2.4.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,376 @@
1
+ import { randomUUID } from "node:crypto";
2
+ /**
3
+ * 确定性测试专用进程驱动桩。
4
+ * 遵循《ActionDock Managed Process 设计 v2》第 11、12 节契约:
5
+ * - 支持确定性模拟输出(emitOutput)
6
+ * - 支持确定性模拟退出(emitExit)
7
+ * - 支持确定性模拟输出关闭(emitOutputClosed)
8
+ * - 支持确定性模拟故障注入(emitFault)
9
+ * - 记录完整写入(writes)、EOF 调用、resize 与 terminate 操作历史
10
+ * - 支持模拟各阶段操作失败注入用于健壮性测试
11
+ */
12
+ export class FakeProcessDriver {
13
+ capabilities;
14
+ handles = new Map();
15
+ /** 记录所有派生调用 */
16
+ spawnCalls = [];
17
+ /** 记录所有写入调用 */
18
+ writes = [];
19
+ /** 记录所有 EOF 调用 */
20
+ eofCalls = [];
21
+ /** 记录所有前台中断调用 */
22
+ interruptCalls = [];
23
+ /** 记录所有调整尺寸调用 */
24
+ resizeCalls = [];
25
+ /** 记录所有终止调用 */
26
+ terminateCalls = [];
27
+ /** 记录所有销毁调用 */
28
+ disposeCalls = [];
29
+ /** 故障注入:下一次派生将抛出的异常 */
30
+ nextSpawnError;
31
+ /** 故障注入:写入时将抛出的异常 */
32
+ nextWriteError;
33
+ /** 故障注入:调整尺寸时将抛出的异常 */
34
+ nextResizeError;
35
+ /** 故障注入:终止时将抛出的异常 */
36
+ nextTerminateError;
37
+ /** 自动响应回调:在进程派生后触发 */
38
+ onSpawn;
39
+ constructor(capabilities) {
40
+ this.capabilities = {
41
+ pty: true,
42
+ resize: true,
43
+ inputEOF: true,
44
+ interruptForeground: true,
45
+ terminationScope: "process-tree",
46
+ ...capabilities,
47
+ };
48
+ }
49
+ /**
50
+ * 获取驱动能力集合。
51
+ */
52
+ getCapabilities() {
53
+ return { ...this.capabilities };
54
+ }
55
+ /**
56
+ * 覆盖驱动能力集合。
57
+ */
58
+ setCapabilities(caps) {
59
+ this.capabilities = { ...this.capabilities, ...caps };
60
+ }
61
+ async spawn(specOrProcessId, observerOrSpec, maybeCallbacks) {
62
+ if (typeof specOrProcessId === "string") {
63
+ const processId = specOrProcessId;
64
+ const spec = observerOrSpec;
65
+ const callbacks = maybeCallbacks;
66
+ return this.spawnLegacy(processId, spec, callbacks);
67
+ }
68
+ const spec = specOrProcessId;
69
+ const observer = observerOrSpec;
70
+ return this.spawnStandard(spec, observer);
71
+ }
72
+ /**
73
+ * 标准接口实现派生新进程。
74
+ */
75
+ async spawnStandard(spec, observer, customId) {
76
+ if (this.nextSpawnError) {
77
+ const err = this.nextSpawnError;
78
+ this.nextSpawnError = undefined;
79
+ // 与 NodeProcessDriver 的 spawn 失败契约对齐:fault + exited(spawn 失败语义)+ outputClosed 三件套
80
+ observer.fault?.(err);
81
+ observer.exited({ code: null, signal: null });
82
+ observer.outputClosed("natural");
83
+ throw err;
84
+ }
85
+ const id = customId ?? randomUUID();
86
+ const pid = Math.floor(10000 + Math.random() * 90000);
87
+ const internalHandle = {
88
+ id,
89
+ pid,
90
+ spec,
91
+ observer,
92
+ disposed: false,
93
+ write: (data) => this.write(internalHandle, data),
94
+ sendInputEOF: () => this.inputEOF(internalHandle),
95
+ interruptForeground: () => this.interruptForeground(internalHandle),
96
+ resize: (cols, rows) => this.resize(internalHandle, cols, rows),
97
+ terminate: (graceMs) => this.terminate(internalHandle, graceMs),
98
+ emitOutput: (stream, data) => this.emitOutput(id, stream, data),
99
+ emitExit: (result) => this.emitExit(id, result),
100
+ emitOutputClosed: (reason) => this.emitOutputClosed(id, reason),
101
+ emitFault: (err) => this.emitFault(id, err),
102
+ };
103
+ this.handles.set(id, internalHandle);
104
+ const recorded = {
105
+ handle: internalHandle,
106
+ spec,
107
+ observer,
108
+ timestamp: Date.now(),
109
+ };
110
+ this.spawnCalls.push(recorded);
111
+ if (this.onSpawn) {
112
+ this.onSpawn(internalHandle, spec, observer);
113
+ }
114
+ return internalHandle;
115
+ }
116
+ /**
117
+ * 模拟写入数据并记录历史。
118
+ */
119
+ async write(handle, data) {
120
+ if (this.nextWriteError) {
121
+ const err = this.nextWriteError;
122
+ this.nextWriteError = undefined;
123
+ throw err;
124
+ }
125
+ const copy = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
126
+ this.writes.push({
127
+ handle,
128
+ data: copy,
129
+ timestamp: Date.now(),
130
+ });
131
+ }
132
+ /**
133
+ * 模拟输入流关闭并记录历史。
134
+ */
135
+ async inputEOF(handle) {
136
+ this.eofCalls.push({
137
+ handle,
138
+ timestamp: Date.now(),
139
+ });
140
+ }
141
+ /**
142
+ * 模拟中断前台作业并记录历史。
143
+ */
144
+ async interruptForeground(handle) {
145
+ this.interruptCalls.push({
146
+ handle,
147
+ timestamp: Date.now(),
148
+ });
149
+ }
150
+ /**
151
+ * 模拟调整终端尺寸并记录历史。
152
+ */
153
+ async resize(handle, cols, rows) {
154
+ if (this.nextResizeError) {
155
+ const err = this.nextResizeError;
156
+ this.nextResizeError = undefined;
157
+ throw err;
158
+ }
159
+ this.resizeCalls.push({
160
+ handle,
161
+ cols,
162
+ rows,
163
+ timestamp: Date.now(),
164
+ });
165
+ }
166
+ async terminate(handleOrId, graceMs) {
167
+ if (this.nextTerminateError) {
168
+ const err = this.nextTerminateError;
169
+ this.nextTerminateError = undefined;
170
+ throw err;
171
+ }
172
+ const targetHandle = typeof handleOrId === "string" ? this.handles.get(handleOrId) : handleOrId;
173
+ if (targetHandle) {
174
+ this.terminateCalls.push({
175
+ handle: targetHandle,
176
+ graceMs,
177
+ timestamp: Date.now(),
178
+ });
179
+ // 终止即退出:以 SIGTERM 语义通知观察者,保证上层等待链路收敛
180
+ this.emitExit(targetHandle, { code: null, signal: "SIGTERM" });
181
+ }
182
+ }
183
+ /**
184
+ * 模拟销毁进程并记录历史。
185
+ */
186
+ async dispose(handle) {
187
+ const internal = this.handles.get(handle.id);
188
+ if (internal) {
189
+ internal.disposed = true;
190
+ }
191
+ this.disposeCalls.push({
192
+ handle,
193
+ timestamp: Date.now(),
194
+ });
195
+ }
196
+ /**
197
+ * 确定性模拟向观察者发送输出。
198
+ */
199
+ emitOutput(handleOrId, stream, data) {
200
+ const internal = this.resolveInternal(handleOrId);
201
+ const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
202
+ internal.observer.output(stream, bytes);
203
+ }
204
+ /**
205
+ * 确定性模拟进程退出事件。
206
+ */
207
+ emitExit(handleOrId, result) {
208
+ const internal = this.resolveInternal(handleOrId);
209
+ let code = 0;
210
+ let signal = null;
211
+ if (typeof result === "number") {
212
+ code = result;
213
+ }
214
+ else if (result) {
215
+ code = result.code !== undefined ? result.code : 0;
216
+ signal = result.signal ?? null;
217
+ }
218
+ internal.observer.exited({ code, signal });
219
+ }
220
+ /**
221
+ * 确定性模拟输出流彻底关闭事件。
222
+ */
223
+ emitOutputClosed(handleOrId, reason = "natural") {
224
+ const internal = this.resolveInternal(handleOrId);
225
+ internal.observer.outputClosed(reason);
226
+ }
227
+ /**
228
+ * 确定性模拟驱动故障通知。
229
+ */
230
+ emitFault(handleOrId, error) {
231
+ const internal = this.resolveInternal(handleOrId);
232
+ internal.observer.fault?.(error);
233
+ }
234
+ /**
235
+ * 注入下一次 spawn 故障异常。
236
+ */
237
+ simulateSpawnFailure(error) {
238
+ this.nextSpawnError = error;
239
+ }
240
+ /**
241
+ * 注入下一次 write 故障异常。
242
+ */
243
+ simulateWriteFailure(error) {
244
+ this.nextWriteError = error;
245
+ }
246
+ /**
247
+ * 注入下一次 resize 故障异常。
248
+ */
249
+ simulateResizeFailure(error) {
250
+ this.nextResizeError = error;
251
+ }
252
+ /**
253
+ * 注入下一次 terminate 故障异常。
254
+ */
255
+ simulateTerminateFailure(error) {
256
+ this.nextTerminateError = error;
257
+ }
258
+ /**
259
+ * 获取最近创建的进程句柄。
260
+ */
261
+ getLastHandle() {
262
+ const last = this.spawnCalls[this.spawnCalls.length - 1];
263
+ return last?.handle;
264
+ }
265
+ /**
266
+ * 查询指定句柄标识对应的句柄实例。
267
+ */
268
+ getHandle(handleOrId) {
269
+ const id = typeof handleOrId === "string" ? handleOrId : handleOrId.id;
270
+ return this.handles.get(id);
271
+ }
272
+ /**
273
+ * 查询指定句柄对应的观察者。
274
+ */
275
+ getObserver(handleOrId) {
276
+ const id = typeof handleOrId === "string" ? handleOrId : handleOrId.id;
277
+ return this.handles.get(id)?.observer;
278
+ }
279
+ /**
280
+ * 获取针对指定句柄(或全部句柄)的写入字节切片列表。
281
+ */
282
+ getWrites(handleOrId) {
283
+ if (!handleOrId) {
284
+ return this.writes.map((w) => w.data);
285
+ }
286
+ const id = typeof handleOrId === "string" ? handleOrId : handleOrId.id;
287
+ return this.writes.filter((w) => w.handle.id === id).map((w) => w.data);
288
+ }
289
+ /**
290
+ * 获取针对指定句柄(或全部句柄)写入的 UTF-8 解码文本列表。
291
+ */
292
+ getWrittenStrings(handleOrId) {
293
+ const decoder = new TextDecoder();
294
+ return this.getWrites(handleOrId).map((chunk) => decoder.decode(chunk));
295
+ }
296
+ /**
297
+ * 判断指定句柄是否接收到了 EOF 输入结束通知。
298
+ */
299
+ hasInputEOF(handleOrId) {
300
+ if (!handleOrId) {
301
+ return this.eofCalls.length > 0;
302
+ }
303
+ const id = typeof handleOrId === "string" ? handleOrId : handleOrId.id;
304
+ return this.eofCalls.some((c) => c.handle.id === id);
305
+ }
306
+ /**
307
+ * 判断指定句柄是否接收到了前台作业中断通知。
308
+ */
309
+ hasInterrupted(handleOrId) {
310
+ if (!handleOrId) {
311
+ return this.interruptCalls.length > 0;
312
+ }
313
+ const id = typeof handleOrId === "string" ? handleOrId : handleOrId.id;
314
+ return this.interruptCalls.some((c) => c.handle.id === id);
315
+ }
316
+ /**
317
+ * 清除历史调用记录。
318
+ */
319
+ clearHistory() {
320
+ this.spawnCalls.length = 0;
321
+ this.writes.length = 0;
322
+ this.eofCalls.length = 0;
323
+ this.interruptCalls.length = 0;
324
+ this.resizeCalls.length = 0;
325
+ this.terminateCalls.length = 0;
326
+ this.disposeCalls.length = 0;
327
+ }
328
+ /**
329
+ * 重置全部句柄、模拟规则与调用历史。
330
+ */
331
+ reset() {
332
+ this.clearHistory();
333
+ this.handles.clear();
334
+ this.nextSpawnError = undefined;
335
+ this.nextWriteError = undefined;
336
+ this.nextResizeError = undefined;
337
+ this.nextTerminateError = undefined;
338
+ this.onSpawn = undefined;
339
+ }
340
+ /**
341
+ * 兼容旧版基于 processId 派生新进程。
342
+ */
343
+ async spawnLegacy(processId, spec, callbacks) {
344
+ const observer = {
345
+ output(stream, data) {
346
+ callbacks.onOutput(stream, data);
347
+ },
348
+ exited(result) {
349
+ callbacks.onExit(result);
350
+ },
351
+ outputClosed(reason) {
352
+ callbacks.onOutputClosed?.(reason);
353
+ },
354
+ fault(err) {
355
+ callbacks.onError(err);
356
+ },
357
+ };
358
+ const handle = await this.spawnStandard(spec, observer, processId);
359
+ return handle;
360
+ }
361
+ /**
362
+ * 内部解析指定句柄或标识。
363
+ */
364
+ resolveInternal(handleOrId) {
365
+ const id = typeof handleOrId === "string" ? handleOrId : handleOrId.id;
366
+ const handle = this.handles.get(id);
367
+ if (!handle) {
368
+ throw new Error(`FakeProcessDriver: No handle found with id '${id}'`);
369
+ }
370
+ return handle;
371
+ }
372
+ }
373
+ /**
374
+ * 兼容原有命名导出。
375
+ */
376
+ export { FakeProcessDriver as MockProcessDriver };
package/dist/process.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import type { ProcessExecutor } from "@actiondock/core";
2
- import type { ProcessExecOptions, ProcessResult, RuntimeError } from "@actiondock/sdk";
1
+ import { ProcessManager, type ProcessDriver, type ProcessExecutor, type ProcessOwner } from "@actiondock/core";
2
+ import { type CallOptions, type ControlGrant, type OperationReceipt, type ProcessAcquireInput, type ProcessControlInput, type ProcessExecOptions, type ProcessInfo, type ProcessListInput, type ProcessListResult, type ProcessReadInput, type ProcessResult, type ProcessRunInput, type ProcessRunResult, type ProcessStartInput, type ProcessStartResult, type ProcessStopInput, type ProcessWriteInput, type ReadResult, type RuntimeError } from "@actiondock/sdk";
3
+ import type { Clock } from "@actiondock/core";
3
4
  /**
4
5
  * 模拟命令匹配器。
5
6
  */
@@ -54,10 +55,19 @@ export interface ProcessCall {
54
55
  export interface MockProcessExecutorOptions {
55
56
  /** 未命中任何模拟规则时是否回退到真实子进程执行(默认 false,未命中即抛错) */
56
57
  fallbackToReal?: boolean;
58
+ /** 可选注入的底层进程驱动(默认使用 FakeProcessDriver) */
59
+ driver?: ProcessDriver;
60
+ /** 可选注入的受管进程管理器 */
61
+ processManager?: ProcessManager;
62
+ /** 默认受管进程归属所有者 */
63
+ owner?: ProcessOwner;
64
+ /** 可选注入的时钟:提供时模拟延时 delayMs 由时钟驱动(FakeClock 可确定性推进),未提供时回退真实 setTimeout */
65
+ clock?: Clock;
57
66
  }
58
67
  /**
59
68
  * 模拟进程执行器实现。
60
- * 遵循 ProcessExecutor 接口契约,支持预设命令响应、跟踪调用历史并模拟超时与取消场景。
69
+ * 遵循 ProcessExecutor / ProcessAPI 接口契约,支持预设命令响应、跟踪调用历史、
70
+ * 并无缝接入 ProcessManager 与 FakeProcessDriver 支撑受管进程全生命周期。
61
71
  *
62
72
  * 默认不回退真实子进程执行:未命中任何模拟规则时抛出明确错误,避免测试中的拼写失误穿透到真实系统命令。
63
73
  * 如确需真实回退(例如集成本地 CLI),可显式传入 fallbackToReal: true。
@@ -67,6 +77,11 @@ export declare class MockProcessExecutor implements ProcessExecutor {
67
77
  calls: ProcessCall[];
68
78
  defaultPid: number;
69
79
  private readonly fallbackToReal;
80
+ /** 可选时钟:提供时 waitDelay 以 clock.sleep 驱动,保证确定性测试 */
81
+ private readonly clock?;
82
+ readonly driver: ProcessDriver;
83
+ readonly processManager: ProcessManager;
84
+ readonly owner: ProcessOwner;
70
85
  constructor(options?: MockProcessExecutorOptions);
71
86
  /**
72
87
  * 注册模拟命令匹配与返回结果。
@@ -84,6 +99,63 @@ export declare class MockProcessExecutor implements ProcessExecutor {
84
99
  */
85
100
  exec(command: string, args?: string[], options?: ProcessExecOptions): Promise<ProcessResult>;
86
101
  spawn(command: string, args?: string[], options?: ProcessExecOptions): Promise<ProcessResult>;
102
+ /**
103
+ /**
104
+ * 优先匹配 mock 规则运行命令,未命中时委托至 ProcessManager。
105
+ */
106
+ private runWithMock;
107
+ /**
108
+ * 一次性运行外部命令并收集输出。
109
+ */
110
+ run(input: ProcessRunInput, call?: CallOptions): Promise<ProcessRunResult>;
111
+ /**
112
+ * 启动新的受管进程资源。
113
+ */
114
+ start(input: ProcessStartInput, call?: CallOptions): Promise<ProcessStartResult>;
115
+ /**
116
+ * 查看指定受管进程资源的状态快照。
117
+ */
118
+ inspect(id: string, call?: CallOptions): Promise<ProcessInfo>;
119
+ /**
120
+ * 列出当前作用域内可见的受管进程资源。
121
+ */
122
+ list(input: ProcessListInput, call?: CallOptions): Promise<ProcessListResult>;
123
+ /**
124
+ * 申请指定受管进程的独占控制令牌。
125
+ */
126
+ acquire(id: string, input: ProcessAcquireInput, call?: CallOptions): Promise<ControlGrant>;
127
+ /**
128
+ * 延长当前有效控制令牌的存活时间。
129
+ */
130
+ renew(id: string, token: string, ttlMs: number, call?: CallOptions): Promise<ControlGrant>;
131
+ /**
132
+ * 显式释放控制令牌。
133
+ */
134
+ release(id: string, token: string, call?: CallOptions): Promise<void>;
135
+ /**
136
+ * 向受管进程输入流写入原始字节数据。
137
+ */
138
+ write(id: string, input: ProcessWriteInput, call?: CallOptions): Promise<OperationReceipt>;
139
+ /**
140
+ * 查询指定请求标识的操作执行收据。
141
+ */
142
+ operation(id: string, requestId: string, call?: CallOptions): Promise<OperationReceipt>;
143
+ /**
144
+ * 按游标读取受管进程输出流。
145
+ */
146
+ read(id: string, input: ProcessReadInput, call?: CallOptions): Promise<ReadResult>;
147
+ /**
148
+ * 向受管进程发送结构化控制指令。
149
+ */
150
+ control(id: string, input: ProcessControlInput, call?: CallOptions): Promise<OperationReceipt>;
151
+ /**
152
+ * 终止指定的受管进程资源。
153
+ */
154
+ stop(id: string, input: ProcessStopInput, call?: CallOptions): Promise<ProcessInfo>;
155
+ /**
156
+ * 绑定指定所有者身份创建上下文进程接口,保持 mock 拦截与生命周期追踪。
157
+ */
158
+ forOwner(owner: ProcessOwner, runId?: string, signal?: AbortSignal): import("@actiondock/core").ContextProcessAPI;
87
159
  /**
88
160
  * 获取指定命令的历史调用记录。
89
161
  *
package/dist/process.js CHANGED
@@ -1,7 +1,11 @@
1
+ import { PROCESS_OUTPUT_LIMIT, ProcessManager, } from "@actiondock/core";
1
2
  import { execCli } from "./cli.js";
3
+ import { encodeBytes, } from "@actiondock/sdk";
4
+ import { FakeProcessDriver } from "./process-driver.js";
2
5
  /**
3
6
  * 模拟进程执行器实现。
4
- * 遵循 ProcessExecutor 接口契约,支持预设命令响应、跟踪调用历史并模拟超时与取消场景。
7
+ * 遵循 ProcessExecutor / ProcessAPI 接口契约,支持预设命令响应、跟踪调用历史、
8
+ * 并无缝接入 ProcessManager 与 FakeProcessDriver 支撑受管进程全生命周期。
5
9
  *
6
10
  * 默认不回退真实子进程执行:未命中任何模拟规则时抛出明确错误,避免测试中的拼写失误穿透到真实系统命令。
7
11
  * 如确需真实回退(例如集成本地 CLI),可显式传入 fallbackToReal: true。
@@ -11,8 +15,23 @@ export class MockProcessExecutor {
11
15
  calls = [];
12
16
  defaultPid = 10001;
13
17
  fallbackToReal;
18
+ /** 可选时钟:提供时 waitDelay 以 clock.sleep 驱动,保证确定性测试 */
19
+ clock;
20
+ driver;
21
+ processManager;
22
+ owner;
14
23
  constructor(options = {}) {
15
24
  this.fallbackToReal = options.fallbackToReal ?? false;
25
+ this.clock = options.clock;
26
+ this.driver = options.driver ?? new FakeProcessDriver();
27
+ this.processManager =
28
+ options.processManager ?? new ProcessManager({ driver: this.driver });
29
+ this.owner = options.owner ?? {
30
+ tenantId: "test-tenant",
31
+ principalId: "test-principal",
32
+ packageInstanceId: "test-package",
33
+ generationId: "test-generation",
34
+ };
16
35
  }
17
36
  /**
18
37
  * 注册模拟命令匹配与返回结果。
@@ -76,6 +95,7 @@ export class MockProcessExecutor {
76
95
  timeout: options.timeoutMs,
77
96
  input: options.input,
78
97
  encoding: options.encoding,
98
+ maxOutputBytes: options.maxOutputBytes,
79
99
  });
80
100
  resolved = {
81
101
  ok: cliRes.ok,
@@ -85,6 +105,12 @@ export class MockProcessExecutor {
85
105
  raw: cliRes.raw,
86
106
  timedOut: cliRes.timedOut,
87
107
  durationMs: cliRes.durationMs,
108
+ error: cliRes.truncated && !cliRes.ok
109
+ ? {
110
+ code: PROCESS_OUTPUT_LIMIT,
111
+ message: `Process output exceeded limit of ${options.maxOutputBytes} bytes`,
112
+ }
113
+ : undefined,
88
114
  };
89
115
  }
90
116
  catch (err) {
@@ -167,6 +193,141 @@ export class MockProcessExecutor {
167
193
  async spawn(command, args = [], options = {}) {
168
194
  return this.exec(command, args, options);
169
195
  }
196
+ /**
197
+ /**
198
+ * 优先匹配 mock 规则运行命令,未命中时委托至 ProcessManager。
199
+ */
200
+ async runWithMock(input, owner, call) {
201
+ const args = input.spec.args ?? [];
202
+ const matchedMock = this.findMock(input.spec.executable, args, {
203
+ cwd: input.spec.cwd,
204
+ env: input.spec.env?.set,
205
+ timeoutMs: input.timeoutMs,
206
+ maxOutputBytes: input.maxOutputBytes,
207
+ signal: call?.signal,
208
+ });
209
+ // 仅在确实命中 mock 或显式开启真实回退时走 exec 路径;
210
+ // 其余情况(含已注册其他 mock 但本命令未命中)一律落入受管进程路径,
211
+ // 避免任意 mock 注册后未命中命令被错误拦截并抛「未命中」
212
+ if (matchedMock || this.fallbackToReal) {
213
+ const res = await this.exec(input.spec.executable, args, {
214
+ cwd: input.spec.cwd,
215
+ env: input.spec.env?.set,
216
+ timeoutMs: input.timeoutMs,
217
+ maxOutputBytes: input.maxOutputBytes,
218
+ signal: call?.signal,
219
+ });
220
+ const chunks = [];
221
+ if (res.stdout) {
222
+ chunks.push({
223
+ stream: "stdout",
224
+ data: encodeBytes(res.stdout),
225
+ });
226
+ }
227
+ if (res.stderr) {
228
+ chunks.push({
229
+ stream: "stderr",
230
+ data: encodeBytes(res.stderr),
231
+ });
232
+ }
233
+ return {
234
+ exit: { code: res.exitCode, signal: res.signal ?? null },
235
+ chunks,
236
+ truncated: Boolean(res.error?.code === "PROCESS_OUTPUT_LIMIT"),
237
+ };
238
+ }
239
+ return this.processManager.run(owner, input, call);
240
+ }
241
+ /**
242
+ * 一次性运行外部命令并收集输出。
243
+ */
244
+ async run(input, call) {
245
+ return this.runWithMock(input, this.owner, call);
246
+ }
247
+ /**
248
+ * 启动新的受管进程资源。
249
+ */
250
+ async start(input, call) {
251
+ return this.processManager.start(this.owner, input, call);
252
+ }
253
+ /**
254
+ * 查看指定受管进程资源的状态快照。
255
+ */
256
+ async inspect(id, call) {
257
+ return this.processManager.inspect(this.owner, id, call);
258
+ }
259
+ /**
260
+ * 列出当前作用域内可见的受管进程资源。
261
+ */
262
+ async list(input, call) {
263
+ return this.processManager.list(this.owner, input, call);
264
+ }
265
+ /**
266
+ * 申请指定受管进程的独占控制令牌。
267
+ */
268
+ async acquire(id, input, call) {
269
+ return this.processManager.acquire(this.owner, id, input, call);
270
+ }
271
+ /**
272
+ * 延长当前有效控制令牌的存活时间。
273
+ */
274
+ async renew(id, token, ttlMs, call) {
275
+ return this.processManager.renew(this.owner, id, token, ttlMs, call);
276
+ }
277
+ /**
278
+ * 显式释放控制令牌。
279
+ */
280
+ async release(id, token, call) {
281
+ return this.processManager.release(this.owner, id, token, call);
282
+ }
283
+ /**
284
+ * 向受管进程输入流写入原始字节数据。
285
+ */
286
+ async write(id, input, call) {
287
+ return this.processManager.write(this.owner, id, input, call);
288
+ }
289
+ /**
290
+ * 查询指定请求标识的操作执行收据。
291
+ */
292
+ async operation(id, requestId, call) {
293
+ return this.processManager.operation(this.owner, id, requestId, call);
294
+ }
295
+ /**
296
+ * 按游标读取受管进程输出流。
297
+ */
298
+ async read(id, input, call) {
299
+ return this.processManager.read(this.owner, id, input, call);
300
+ }
301
+ /**
302
+ * 向受管进程发送结构化控制指令。
303
+ */
304
+ async control(id, input, call) {
305
+ return this.processManager.control(this.owner, id, input, call);
306
+ }
307
+ /**
308
+ * 终止指定的受管进程资源。
309
+ */
310
+ async stop(id, input, call) {
311
+ return this.processManager.stop(this.owner, id, input, call);
312
+ }
313
+ /**
314
+ * 绑定指定所有者身份创建上下文进程接口,保持 mock 拦截与生命周期追踪。
315
+ */
316
+ forOwner(owner, runId, signal) {
317
+ const bound = this.processManager.forOwner(owner, runId, signal);
318
+ return new Proxy(bound, {
319
+ get: (target, prop, receiver) => {
320
+ if (prop === "run") {
321
+ return (input, call) => {
322
+ const mergedSignal = call?.signal ?? signal;
323
+ const effectiveCall = mergedSignal ? { ...call, signal: mergedSignal } : call;
324
+ return this.runWithMock(input, owner, effectiveCall);
325
+ };
326
+ }
327
+ return Reflect.get(target, prop, receiver);
328
+ },
329
+ });
330
+ }
170
331
  /**
171
332
  * 获取指定命令的历史调用记录。
172
333
  *
@@ -204,6 +365,9 @@ export class MockProcessExecutor {
204
365
  reset() {
205
366
  this.mocks = [];
206
367
  this.calls = [];
368
+ if (this.driver instanceof FakeProcessDriver) {
369
+ this.driver.reset();
370
+ }
207
371
  }
208
372
  /**
209
373
  * 渲染已注册匹配器列表,辅助定位拼写失误。
@@ -247,6 +411,11 @@ export class MockProcessExecutor {
247
411
  return undefined;
248
412
  }
249
413
  async waitDelay(delayMs, options) {
414
+ // 注入时钟时优先 clock.sleep:由 FakeClock.advance 确定性驱动,不占用真实时间
415
+ if (this.clock) {
416
+ await this.clock.sleep(delayMs);
417
+ return;
418
+ }
250
419
  return new Promise((resolve) => {
251
420
  let timer;
252
421
  let onAbort;