@zhushanwen/subagent-core 0.6.0 → 0.7.1
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 +1 -1
- package/dist/chunk-KXVBIVOU.js +16 -0
- package/dist/chunk-T2SYBW3I.js +24 -0
- package/dist/chunk-VURUAGMM.js +2305 -0
- package/dist/{types-C3fE3R9x.d.cts → engine-discovery-scan-ocpM8FmI.d.cts} +770 -668
- package/dist/{types-C3fE3R9x.d.ts → engine-discovery-scan-ocpM8FmI.d.ts} +770 -668
- package/dist/execution/engine/engine-discovery-scan.cjs +2210 -0
- package/dist/execution/engine/engine-discovery-scan.d.cts +4 -0
- package/dist/execution/engine/engine-discovery-scan.d.ts +4 -0
- package/dist/execution/engine/engine-discovery-scan.js +20 -0
- package/dist/execution/engine/paths.cjs +6 -23
- package/dist/execution/engine/paths.d.cts +1 -8
- package/dist/execution/engine/paths.d.ts +1 -8
- package/dist/execution/engine/paths.js +1 -1
- package/dist/execution/relay-env.cjs +12 -28
- package/dist/execution/relay-env.d.cts +1 -33
- package/dist/execution/relay-env.d.ts +1 -33
- package/dist/execution/relay-env.js +5 -3
- package/dist/index.cjs +14045 -16059
- package/dist/index.d.cts +1035 -1579
- package/dist/index.d.ts +1035 -1579
- package/dist/index.js +9838 -13800
- package/dist.bundle/index.cjs +28875 -30323
- package/package.json +20 -26
- package/src/__tests__/record-store-last-line.test.ts +16 -7
- package/src/core/error-message.ts +7 -6
- package/src/core/host-services.ts +8 -2
- package/src/core/notify-ports.ts +17 -2
- package/src/execution/__tests__/chat-engine-routing.test.ts +62 -28
- package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +255 -0
- package/src/execution/__tests__/collect-coordinator-service.test.ts +36 -47
- package/src/execution/__tests__/collect-mixed-dispatch.test.ts +20 -53
- package/src/execution/__tests__/delivery-methods.test.ts +179 -237
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -2
- package/src/execution/__tests__/execute-nesting.test.ts +20 -13
- package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +20 -33
- package/src/execution/__tests__/gc-timer.test.ts +7 -22
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +20 -10
- package/src/execution/__tests__/helpers/fake-engine-port.ts +187 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +36 -9
- package/src/execution/__tests__/helpers/subagent-service-mocks.ts +1 -1
- package/src/execution/__tests__/idle-gc.test.ts +200 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +26 -21
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +5 -2
- package/src/execution/__tests__/reconcile-sweep.test.ts +200 -0
- package/src/execution/__tests__/record-store.test.ts +60 -6
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +20 -13
- package/src/execution/__tests__/round-supervisor.test.ts +293 -0
- package/src/execution/__tests__/session-pending.test.ts +119 -17
- package/src/execution/__tests__/stream-sink-retirement.test.ts +10 -10
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +1 -11
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +33 -30
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +2 -9
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +59 -129
- package/src/execution/__tests__/subagent-service.test.ts +2 -2
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +109 -7
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +108 -36
- package/src/execution/__tests__/sync-collect-recovery.test.ts +21 -36
- package/src/execution/__tests__/workflow-state-root.test.ts +232 -0
- package/src/execution/agent-registry.ts +6 -2
- package/src/execution/dialog-queue.ts +6 -73
- package/src/execution/engine/__tests__/common/capability-gate.test.ts +115 -15
- package/src/execution/engine/__tests__/common/errors.test.ts +11 -18
- package/src/execution/engine/__tests__/common/kill-chain.test.ts +7 -0
- package/src/execution/engine/__tests__/common/nesting-guard.test.ts +9 -3
- package/src/execution/engine/__tests__/common/session-view-service.test.ts +90 -3
- package/src/execution/engine/__tests__/conformance/H9-test-disposition.md +116 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +377 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/recorded.fixture.json +331 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +213 -0
- package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +230 -0
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +69 -42
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +4 -81
- package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +67 -44
- package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +81 -54
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +133 -70
- package/src/execution/engine/__tests__/conformance/engine-crash.test.ts +143 -0
- package/src/execution/engine/__tests__/conformance/fake-engine-capabilities.ts +19 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +7 -2
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +6 -5
- package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +398 -0
- package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +221 -0
- package/src/execution/engine/__tests__/conformance/round-liveness-supervisor.test.ts +302 -0
- package/src/execution/engine/__tests__/conformance/static-spawn-guard.test.ts +58 -0
- package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +558 -0
- package/src/execution/engine/__tests__/host-task-spec.test.ts +45 -0
- package/src/execution/engine/__tests__/model-ref-split.test.ts +74 -0
- package/src/execution/engine/__tests__/registry.test.ts +206 -1
- package/src/execution/engine/__tests__/routing.test.ts +97 -0
- package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +97 -0
- package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +294 -0
- package/src/execution/engine/client/__tests__/engine-client.test.ts +516 -0
- package/src/execution/engine/client/__tests__/mirror.test.ts +67 -0
- package/src/execution/engine/client/__tests__/pid-file.test.ts +257 -0
- package/src/execution/engine/client/__tests__/protocol-closure.test.ts +95 -0
- package/src/execution/engine/client/__tests__/remote-engine.test.ts +457 -0
- package/src/execution/engine/client/client-options.ts +103 -0
- package/src/execution/engine/client/engine-client.ts +687 -0
- package/src/execution/engine/client/mirror.ts +167 -0
- package/src/execution/engine/client/pid-file.ts +392 -0
- package/src/execution/engine/client/reaper.ts +95 -0
- package/src/execution/engine/client/remote-engine.ts +441 -0
- package/src/execution/engine/client/reverse-router.ts +255 -0
- package/src/execution/engine/common/capability-gate.ts +109 -22
- package/src/execution/engine/common/errors.ts +9 -25
- package/src/execution/engine/common/event-journal.ts +4 -50
- package/src/execution/engine/common/journal-replay.ts +3 -1
- package/src/execution/engine/common/kill-chain.ts +18 -251
- package/src/execution/engine/common/nesting-guard.ts +18 -114
- package/src/execution/engine/common/pool-manager.ts +4 -0
- package/src/execution/engine/common/session-view-service.ts +56 -84
- package/src/execution/engine/config.ts +151 -0
- package/src/execution/engine/d8-compat.ts +346 -0
- package/src/execution/engine/engine-discovery-roots.ts +107 -0
- package/src/execution/engine/engine-discovery-scan.ts +269 -0
- package/src/execution/engine/engine-discovery.ts +30 -7
- package/src/execution/engine/engine-inspect-package.ts +282 -0
- package/src/execution/engine/engine-manifest.ts +244 -0
- package/src/execution/engine/host/__tests__/host-askuser-endpoint.test.ts +123 -0
- package/src/execution/engine/host/__tests__/host-bridge.test.ts +135 -0
- package/src/execution/engine/host/__tests__/spawned-children.test.ts +73 -0
- package/src/execution/engine/host/host-bridge.ts +150 -0
- package/src/execution/engine/host/host-ui-endpoint.ts +47 -0
- package/src/execution/engine/host/pi-host-binding.ts +91 -0
- package/src/execution/engine/host/spawned-children.ts +139 -0
- package/src/execution/engine/host-task-spec.ts +29 -2
- package/src/execution/engine/model-validation.ts +55 -2
- package/src/execution/engine/paths.ts +16 -41
- package/src/execution/engine/port.ts +48 -2
- package/src/execution/engine/registry.ts +153 -34
- package/src/execution/engine/routing.ts +225 -52
- package/src/execution/engine/types.ts +25 -200
- package/src/execution/finalize-record.ts +25 -0
- package/src/execution/idle-gc.ts +89 -14
- package/src/execution/lifecycle-manager.ts +10 -7
- package/src/execution/lifecycle-predicates.ts +10 -8
- package/src/execution/record-store.ts +29 -2
- package/src/execution/relay-env.ts +19 -36
- package/src/execution/round-supervisor/domain.ts +89 -0
- package/src/execution/round-supervisor/index.ts +33 -0
- package/src/execution/round-supervisor/notify-accounting.ts +87 -0
- package/src/execution/round-supervisor/reconcile-sweep.ts +235 -0
- package/src/execution/round-supervisor/service-binding.test.ts +661 -0
- package/src/execution/round-supervisor/service-binding.ts +273 -0
- package/src/execution/round-supervisor/supervisor.ts +426 -0
- package/src/execution/session-pending.ts +68 -74
- package/src/execution/settled-watchdog.ts +39 -0
- package/src/execution/subagent-service.ts +746 -400
- package/src/execution/subprocess-agent-runner.ts +16 -8
- package/src/execution/types.ts +41 -117
- package/src/execution/ui-channels.ts +13 -214
- package/src/execution/ui-request-handler-factory.ts +7 -4
- package/src/execution/ui-request-observability.ts +2 -2
- package/src/execution/workflow-state-root.ts +67 -0
- package/src/execution/worktree-git-ops.ts +5 -0
- package/src/execution/worktree-manager.ts +11 -1
- package/src/index.ts +55 -26
- package/src/orchestration/__tests__/execute-agent-call.test.ts +1 -1
- package/src/orchestration/execute-agent-call.ts +1 -1
- package/src/orchestration/file-run-store.ts +144 -59
- package/src/orchestration/models/types.ts +43 -42
- package/src/shared/zcode-model-ref.ts +31 -0
- package/dist/chunk-43ONBFZX.js +0 -240
- package/dist/chunk-A4IVZWVX.js +0 -31
- package/dist/chunk-APZY4IME.js +0 -27
- package/dist/chunk-V3VHZ2VX.js +0 -59
- package/dist/execution/engine/engines/zcode/constants.cjs +0 -107
- package/dist/execution/engine/engines/zcode/constants.d.cts +0 -135
- package/dist/execution/engine/engines/zcode/constants.d.ts +0 -135
- package/dist/execution/engine/engines/zcode/constants.js +0 -54
- package/dist/execution/engine/engines/zcode/reader.cjs +0 -276
- package/dist/execution/engine/engines/zcode/reader.d.cts +0 -21
- package/dist/execution/engine/engines/zcode/reader.d.ts +0 -21
- package/dist/execution/engine/engines/zcode/reader.js +0 -9
- package/src/__tests__/append-system-prompt-assembly.test.ts +0 -243
- package/src/__tests__/fr4-get-state-handshake.test.ts +0 -125
- package/src/__tests__/session-runner.test.ts +0 -69
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -191
- package/src/execution/__tests__/descendant-sweep-guards.test.ts +0 -220
- package/src/execution/__tests__/descendant-sweep.test.ts +0 -269
- package/src/execution/__tests__/engine-model-validation.test.ts +0 -741
- package/src/execution/__tests__/epipe-fallback.test.ts +0 -248
- package/src/execution/__tests__/execution-runtime-face.test.ts +0 -272
- package/src/execution/__tests__/get-state-handshake.test.ts +0 -127
- package/src/execution/__tests__/helpers/session-runner-mocks.ts +0 -113
- package/src/execution/__tests__/keep-alive-no-progress.test.ts +0 -280
- package/src/execution/__tests__/kill-all-escalation.test.ts +0 -196
- package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +0 -113
- package/src/execution/__tests__/output-collector.test.ts +0 -469
- package/src/execution/__tests__/pi-invocation.test.ts +0 -166
- package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -149
- package/src/execution/__tests__/rpc-mode.test.ts +0 -89
- package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +0 -387
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -277
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -254
- package/src/execution/__tests__/run-spawn-edges.test.ts +0 -1079
- package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
- package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +0 -207
- package/src/execution/__tests__/service-kill-escalation.test.ts +0 -91
- package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +0 -110
- package/src/execution/__tests__/session-runner-close-prune.test.ts +0 -181
- package/src/execution/__tests__/session-runner-dispatch.test.ts +0 -489
- package/src/execution/__tests__/session-runner-epipe.test.ts +0 -179
- package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +0 -227
- package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +0 -290
- package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -352
- package/src/execution/__tests__/settled-watchdog.test.ts +0 -524
- package/src/execution/__tests__/spawn-args.test.ts +0 -446
- package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -192
- package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -208
- package/src/execution/__tests__/spawned-children.test.ts +0 -236
- package/src/execution/__tests__/start-sync-model-guard.test.ts +0 -150
- package/src/execution/__tests__/stdin-writer.test.ts +0 -464
- package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -693
- package/src/execution/__tests__/temp-prompt.test.ts +0 -53
- package/src/execution/__tests__/timeout-integration.test.ts +0 -616
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
- package/src/execution/__tests__/turn-limiter.test.ts +0 -65
- package/src/execution/__tests__/ui-request-handler.test.ts +0 -205
- package/src/execution/__tests__/ui-request-queue.test.ts +0 -298
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -230
- package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
- package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +0 -149
- package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
- package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +0 -128
- package/src/execution/engine/__tests__/paths.test.ts +0 -39
- package/src/execution/engine/common/schema-emulation.ts +0 -189
- package/src/execution/engine/common/session-view-projection.ts +0 -51
- package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -615
- package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
- package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +0 -257
- package/src/execution/engine/engines/pi/argv-mirror.ts +0 -135
- package/src/execution/engine/engines/pi/get-state-handshake.ts +0 -179
- package/src/execution/engine/engines/pi/output-collector.ts +0 -317
- package/src/execution/engine/engines/pi/pi-engine.ts +0 -811
- package/src/execution/engine/engines/pi/pi-invocation.ts +0 -120
- package/src/execution/engine/engines/pi/reader.ts +0 -59
- package/src/execution/engine/engines/pi/registration.ts +0 -37
- package/src/execution/engine/engines/pi/session-runner.ts +0 -2840
- package/src/execution/engine/engines/pi/spawn-event-adapter.ts +0 -404
- package/src/execution/engine/engines/pi/stdin-writer.ts +0 -199
- package/src/execution/engine/engines/pi/temp-prompt.ts +0 -62
- package/src/execution/engine/engines/pi/turn-limiter.ts +0 -102
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +0 -301
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +0 -36
- package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +0 -321
- package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +0 -473
- package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -43
- package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -124
- package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
- package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -71
- package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +0 -247
- package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +0 -451
- package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +0 -792
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +0 -782
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +0 -168
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +0 -210
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +0 -353
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +0 -282
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +0 -372
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -145
- package/src/execution/engine/engines/zcode/appserver-launcher.ts +0 -167
- package/src/execution/engine/engines/zcode/connection.ts +0 -598
- package/src/execution/engine/engines/zcode/constants.ts +0 -176
- package/src/execution/engine/engines/zcode/golden-sample.ts +0 -47
- package/src/execution/engine/engines/zcode/parser.ts +0 -102
- package/src/execution/engine/engines/zcode/preparer.ts +0 -235
- package/src/execution/engine/engines/zcode/reader.ts +0 -381
- package/src/execution/engine/engines/zcode/registration.ts +0 -37
- package/src/execution/engine/engines/zcode/session-channel.ts +0 -968
- package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -1431
- package/src/execution/round-settlement.ts +0 -93
- package/src/execution/ui-request-queue.ts +0 -215
- package/src/shared/schema-env.ts +0 -44
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
// src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts
|
|
2
|
-
//
|
|
3
|
-
// [F-R2] stdout data 同步回调链内 fail-fast throw 不逃逸(不升级为 uncaughtException 崩宿主)。
|
|
4
|
-
//
|
|
5
|
-
// 背景:session-runner 的两处 timer 安全校验调用位于 child.stdout.on("data") 同步回调链内:
|
|
6
|
-
// ① agent_end keep-alive 分支的 resolveSpawnWatchdogMs(→ assertSafeTimerDelay fail-fast)
|
|
7
|
-
// ② chatMode agent_settled 分支的 armIdleTimer(→ assertSafeTimerDelay fail-fast)
|
|
8
|
-
// 旧实现任一处 throw 都会逃出事件回调 = Node uncaughtException 崩宿主进程。
|
|
9
|
-
// 修复:调用点包 try/catch 降级(不挂 timer),错误经 bestEffort("error") 可见。
|
|
10
|
-
//
|
|
11
|
-
// mock 结构与 run-spawn-chatmode-settled.test.ts 一致(FakeChild + lifecycle-manager 真实模块)。
|
|
12
|
-
// logger 整体 mock(loggerMock spy)断言错误可见——「fail-fast 语义保留(错误可见、行为明确)
|
|
13
|
-
// 但不升级为进程崩溃」。
|
|
14
|
-
import { spawn } from "node:child_process";
|
|
15
|
-
import * as fs from "node:fs";
|
|
16
|
-
|
|
17
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
18
|
-
|
|
19
|
-
// logger mock:bestEffort / session-runner 共享同一 loggerMock(断言错误可见性)。
|
|
20
|
-
// vi.hoisted:vi.mock 工厂被提升到顶层 const 之前执行,loggerMock 必须经 vi.hoisted 创建。
|
|
21
|
-
const { loggerMock } = vi.hoisted(() => ({
|
|
22
|
-
loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
23
|
-
}));
|
|
24
|
-
vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
|
|
25
|
-
|
|
26
|
-
vi.mock("node:child_process", async () => {
|
|
27
|
-
const { FakeChild } = await import("./helpers/spawn-mock.ts");
|
|
28
|
-
return {
|
|
29
|
-
spawn: vi.fn(() => new FakeChild()),
|
|
30
|
-
// buildEnvBlock 的 git branch 调用(execFile 异步):默认 err-first 兜底 → catch → branch=""
|
|
31
|
-
execFile: vi.fn(
|
|
32
|
-
(
|
|
33
|
-
_cmd: string,
|
|
34
|
-
_args: readonly string[],
|
|
35
|
-
_opts: unknown,
|
|
36
|
-
cb: (err: Error | null, stdout?: string, stderr?: string) => void,
|
|
37
|
-
) => cb(new Error("execFile not configured in this test")),
|
|
38
|
-
),
|
|
39
|
-
};
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
vi.mock("node:fs", async () => {
|
|
43
|
-
const actual = await import("node:fs");
|
|
44
|
-
return {
|
|
45
|
-
default: {
|
|
46
|
-
...actual,
|
|
47
|
-
mkdirSync: vi.fn(),
|
|
48
|
-
existsSync: vi.fn(() => false),
|
|
49
|
-
appendFileSync: vi.fn(),
|
|
50
|
-
writeFileSync: vi.fn(),
|
|
51
|
-
readdirSync: vi.fn(() => []),
|
|
52
|
-
},
|
|
53
|
-
mkdirSync: vi.fn(),
|
|
54
|
-
existsSync: vi.fn(() => false),
|
|
55
|
-
appendFileSync: vi.fn(),
|
|
56
|
-
writeFileSync: vi.fn(),
|
|
57
|
-
readdirSync: vi.fn(() => []),
|
|
58
|
-
promises: actual.promises,
|
|
59
|
-
};
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
vi.mock("../alive-store.ts", () => ({
|
|
63
|
-
writeAliveMarker: vi.fn(),
|
|
64
|
-
// [T5②] keep-alive 心跳刷新读取现有 marker id(缺失兜底 record.id)
|
|
65
|
-
readAliveMarker: vi.fn(() => undefined),
|
|
66
|
-
isProcessAlive: vi.fn(() => false),
|
|
67
|
-
}));
|
|
68
|
-
|
|
69
|
-
// 场景 ① 依赖 keep-alive 分支命中:count=1(有活跃后代 → 不 kill → 重挂 watchdog)
|
|
70
|
-
vi.mock("../session-pending.ts", () => ({
|
|
71
|
-
readActivePendingFromSessionFile: vi.fn(() => ({ count: 1 })),
|
|
72
|
-
prunePendingCursor: vi.fn(),
|
|
73
|
-
}));
|
|
74
|
-
|
|
75
|
-
vi.mock("../engine/engines/pi/temp-prompt.ts", () => ({
|
|
76
|
-
writePromptToTempFile: vi.fn(async (agent: string) => {
|
|
77
|
-
const safeName = agent.replace(/[^\w.-]+/g, "_");
|
|
78
|
-
return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` };
|
|
79
|
-
}),
|
|
80
|
-
cleanupTempPrompt: vi.fn(async () => {}),
|
|
81
|
-
}));
|
|
82
|
-
|
|
83
|
-
import { runSpawn, SPAWN_WATCHDOG_ENV } from "../engine/engines/pi/session-runner.ts";
|
|
84
|
-
import type { SessionRunnerContext } from "../engine/engines/pi/session-runner.ts";
|
|
85
|
-
import { hasIdleTimer, _resetLifecycleState } from "../lifecycle-manager.ts";
|
|
86
|
-
import { createRecord } from "../execution-record.ts";
|
|
87
|
-
import type { ExecutionRecord } from "../types.ts";
|
|
88
|
-
import {
|
|
89
|
-
emitStdoutLine,
|
|
90
|
-
type FakeChild,
|
|
91
|
-
lastSpawnedChild as lastSpawnedChildOf,
|
|
92
|
-
makeCtx,
|
|
93
|
-
makeOpts,
|
|
94
|
-
makeRecord,
|
|
95
|
-
sessionHeader,
|
|
96
|
-
waitForSpawn as waitForSpawnOf,
|
|
97
|
-
} from "./helpers/spawn-mock.ts";
|
|
98
|
-
|
|
99
|
-
const mockSpawn = vi.mocked(spawn);
|
|
100
|
-
|
|
101
|
-
const lastSpawnedChild = (): FakeChild => lastSpawnedChildOf(mockSpawn);
|
|
102
|
-
const waitForSpawn = (timeoutMs = 1000): Promise<void> => waitForSpawnOf(mockSpawn, timeoutMs);
|
|
103
|
-
|
|
104
|
-
/** 构造 chatMode record(idleTimeoutMs 设为超出 setTimeout 安全域的值 → assertSafeTimerDelay throw)。 */
|
|
105
|
-
function makeChatModeRecord(id = "sa-f2-idle"): ExecutionRecord {
|
|
106
|
-
return createRecord(id, {
|
|
107
|
-
agent: "general-purpose",
|
|
108
|
-
model: "test-model",
|
|
109
|
-
mode: "background",
|
|
110
|
-
task: "chat task",
|
|
111
|
-
slug: "chat",
|
|
112
|
-
startedAt: 1_000_000,
|
|
113
|
-
rootSessionId: "root-session",
|
|
114
|
-
parentRecordId: undefined,
|
|
115
|
-
depth: 0,
|
|
116
|
-
chatMode: true,
|
|
117
|
-
// > 2^31-1:Node setTimeout 溢出域,assertSafeTimerDelay fail-fast
|
|
118
|
-
idleTimeoutMs: Number.MAX_SAFE_INTEGER,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
describe("[F-R2] stdout 回调链内 fail-fast throw 不逃逸", () => {
|
|
123
|
-
beforeEach(() => {
|
|
124
|
-
vi.clearAllMocks();
|
|
125
|
-
_resetLifecycleState();
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
afterEach(() => {
|
|
129
|
-
vi.restoreAllMocks();
|
|
130
|
-
_resetLifecycleState();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("① agent_end keep-alive 分支 resolveSpawnWatchdogMs throw → 降级不 re-arm,run 正常收尾,error 可见", async () => {
|
|
134
|
-
const record = makeRecord();
|
|
135
|
-
const promise = runSpawn(record, "Task: keepalive", makeOpts(), makeCtx());
|
|
136
|
-
|
|
137
|
-
await waitForSpawn();
|
|
138
|
-
const child = lastSpawnedChild();
|
|
139
|
-
|
|
140
|
-
emitStdoutLine(child, sessionHeader("sess-fr2a"));
|
|
141
|
-
// 初始 watchdog 解析在 spawn 后同步块内已完成(env 未设 → 不挂)。
|
|
142
|
-
// 此刻设非法 env:agent_end keep-alive 分支再读 env → assertSafeTimerDelay throw。
|
|
143
|
-
process.env[SPAWN_WATCHDOG_ENV] = String(Number.MAX_SAFE_INTEGER);
|
|
144
|
-
try {
|
|
145
|
-
emitStdoutLine(child, { type: "agent_end", willRetry: false });
|
|
146
|
-
// 给 stream flush 留一个 tick(对齐 chatmode 测试模式):throw 若逃逸回调,
|
|
147
|
-
// vitest 会以 unhandled error 判本测试失败——能走到断言即「未崩宿主」。
|
|
148
|
-
await new Promise((r) => setTimeout(r, 20));
|
|
149
|
-
|
|
150
|
-
// 降级语义:不 re-arm(无 timer 可直接观察的是——子进程未被 kill、run 未被中断)
|
|
151
|
-
expect(child.killed).toBe(false);
|
|
152
|
-
|
|
153
|
-
// 错误可见(fail-fast 语义保留):bestEffort("error") 经 logger.error 落日志
|
|
154
|
-
expect(loggerMock.error).toHaveBeenCalledWith(
|
|
155
|
-
expect.stringContaining("resolveSpawnWatchdogMs"),
|
|
156
|
-
expect.objectContaining({ detail: expect.stringContaining("exceeds the Node setTimeout limit") }),
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
// 收尾:run 正常完成(未被回调异常打断)
|
|
160
|
-
child.stdout.end();
|
|
161
|
-
child.stderr.end();
|
|
162
|
-
child.emit("close", 0);
|
|
163
|
-
const result = await promise;
|
|
164
|
-
expect(result.success).toBe(true);
|
|
165
|
-
} finally {
|
|
166
|
-
delete process.env[SPAWN_WATCHDOG_ENV];
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("② chatMode agent_settled armIdleTimer throw → 降级挂 DEFAULT idle timer + warn,onRoundSettled 照常、提前 resolve", async () => {
|
|
171
|
-
const record = makeChatModeRecord("sa-f2-idle");
|
|
172
|
-
const onRoundSettled = vi.fn();
|
|
173
|
-
const ctx: Partial<SessionRunnerContext> = { onRoundSettled };
|
|
174
|
-
const promise = runSpawn(record, "Task: idle", makeOpts(), makeCtx(ctx as SessionRunnerContext));
|
|
175
|
-
|
|
176
|
-
await waitForSpawn();
|
|
177
|
-
const child = lastSpawnedChild();
|
|
178
|
-
|
|
179
|
-
emitStdoutLine(child, sessionHeader("sess-fr2b"));
|
|
180
|
-
emitStdoutLine(child, { type: "agent_settled" });
|
|
181
|
-
await new Promise((r) => setTimeout(r, 20));
|
|
182
|
-
|
|
183
|
-
// [T4② / PS-4] 降级语义修正:旧「不挂」(丢通知放行门 + 进程回收)改为
|
|
184
|
-
// 「挂 DEFAULT_IDLE_TIMEOUT_MS + warn」——防御性兜底保住两个不变量且可见
|
|
185
|
-
expect(hasIdleTimer(record.id)).toBe(true);
|
|
186
|
-
expect(child.killed).toBe(false);
|
|
187
|
-
|
|
188
|
-
// catch 后续语句照常执行:本轮完成通知不因 GC timer 故障丢失
|
|
189
|
-
expect(onRoundSettled).toHaveBeenCalledTimes(1);
|
|
190
|
-
|
|
191
|
-
// 错误可见(原失败留痕)+ 降级动作可见(T4②)
|
|
192
|
-
expect(loggerMock.error).toHaveBeenCalledWith(
|
|
193
|
-
expect.stringContaining("armIdleTimer"),
|
|
194
|
-
expect.objectContaining({ detail: expect.stringContaining("exceeds the Node setTimeout limit") }),
|
|
195
|
-
);
|
|
196
|
-
expect(loggerMock.warn).toHaveBeenCalledWith(
|
|
197
|
-
expect.stringContaining("fell back to DEFAULT_IDLE_TIMEOUT_MS"),
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
// 收尾:chatMode 首轮 agent_settled 已 resolveRun(0),close 后 runSpawn 正常返回
|
|
201
|
-
child.stdout.end();
|
|
202
|
-
child.stderr.end();
|
|
203
|
-
child.emit("close", 0);
|
|
204
|
-
const result = await promise;
|
|
205
|
-
expect(result.success).toBe(true);
|
|
206
|
-
});
|
|
207
|
-
});
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
// src/execution/__tests__/service-kill-escalation.test.ts
|
|
2
|
-
//
|
|
3
|
-
// [u-svc / T2④ / LC-2] killRecordChildWithEscalation 真实现行为(服务侧 kill 收敛入口,
|
|
4
|
-
// 无 SpawnRunState 调用方):
|
|
5
|
-
// - child 不在 spawnedChildren / 已 killed → no-op(与旧 `child && !child.killed` 守卫
|
|
6
|
-
// 语义逐字对齐);
|
|
7
|
-
// - SIGTERM 后 30s 未见 exit → SIGKILL 升级(SIGTERM 被无视的幽灵进程防线);
|
|
8
|
-
// - exit 到达 → 升级 timer 自动清除;
|
|
9
|
-
// - 同 record 重复调用先清旧升级 timer(防叠加)。
|
|
10
|
-
|
|
11
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
12
|
-
|
|
13
|
-
import type { ChildProcess } from "node:child_process";
|
|
14
|
-
|
|
15
|
-
import { FakeChild } from "./helpers/spawn-mock.ts";
|
|
16
|
-
import {
|
|
17
|
-
_resetServiceKillStateForTest,
|
|
18
|
-
killRecordChildWithEscalation,
|
|
19
|
-
spawnedChildren,
|
|
20
|
-
} from "../engine/engines/pi/session-runner.ts";
|
|
21
|
-
|
|
22
|
-
const SIGKILL_ESCALATION_MS = 30_000;
|
|
23
|
-
|
|
24
|
-
describe("T2④ killRecordChildWithEscalation", () => {
|
|
25
|
-
beforeEach(() => {
|
|
26
|
-
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
|
|
27
|
-
_resetServiceKillStateForTest();
|
|
28
|
-
spawnedChildren.clear();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
afterEach(() => {
|
|
32
|
-
vi.useRealTimers();
|
|
33
|
-
vi.restoreAllMocks();
|
|
34
|
-
_resetServiceKillStateForTest();
|
|
35
|
-
spawnedChildren.clear();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("is a no-op when the record has no registered child", () => {
|
|
39
|
-
expect(() => killRecordChildWithEscalation("sa-missing", "test")).not.toThrow();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("is a no-op when the child already received a kill request (escalation owned by prior path)", () => {
|
|
43
|
-
const child = new FakeChild();
|
|
44
|
-
child.killed = true;
|
|
45
|
-
const killSpy = vi.spyOn(child, "kill");
|
|
46
|
-
spawnedChildren.set("sa-killed", child as unknown as ChildProcess);
|
|
47
|
-
killRecordChildWithEscalation("sa-killed", "test");
|
|
48
|
-
expect(killSpy).not.toHaveBeenCalled();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("sends SIGTERM and escalates to SIGKILL after 30s without exit", async () => {
|
|
52
|
-
const child = new FakeChild();
|
|
53
|
-
const killSpy = vi.spyOn(child, "kill");
|
|
54
|
-
spawnedChildren.set("sa-escalate", child as unknown as ChildProcess);
|
|
55
|
-
|
|
56
|
-
killRecordChildWithEscalation("sa-escalate", "closeChatIdle");
|
|
57
|
-
expect(killSpy).toHaveBeenNthCalledWith(1, "SIGTERM");
|
|
58
|
-
|
|
59
|
-
await vi.advanceTimersByTimeAsync(SIGKILL_ESCALATION_MS - 1);
|
|
60
|
-
expect(killSpy).toHaveBeenCalledTimes(1); // 未到期不升级
|
|
61
|
-
|
|
62
|
-
await vi.advanceTimersByTimeAsync(1);
|
|
63
|
-
expect(killSpy).toHaveBeenNthCalledWith(2, "SIGKILL");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("clears the escalation timer when the child exits in time", async () => {
|
|
67
|
-
const child = new FakeChild();
|
|
68
|
-
const killSpy = vi.spyOn(child, "kill");
|
|
69
|
-
spawnedChildren.set("sa-exit", child as unknown as ChildProcess);
|
|
70
|
-
|
|
71
|
-
killRecordChildWithEscalation("sa-exit", "cancelBackground");
|
|
72
|
-
child.emit("exit", 0, null);
|
|
73
|
-
await vi.advanceTimersByTimeAsync(SIGKILL_ESCALATION_MS + 1_000);
|
|
74
|
-
expect(killSpy).toHaveBeenCalledTimes(1); // 只有 SIGTERM,无 SIGKILL
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("repeated calls clear the previous escalation timer (no stacking)", async () => {
|
|
78
|
-
const child = new FakeChild();
|
|
79
|
-
const killSpy = vi.spyOn(child, "kill");
|
|
80
|
-
spawnedChildren.set("sa-repeat", child as unknown as ChildProcess);
|
|
81
|
-
|
|
82
|
-
killRecordChildWithEscalation("sa-repeat", "first");
|
|
83
|
-
await vi.advanceTimersByTimeAsync(10_000);
|
|
84
|
-
// 第二次调用前 child.killed 已为 true(首次 SIGTERM 置位)→ no-op 路径
|
|
85
|
-
killRecordChildWithEscalation("sa-repeat", "second");
|
|
86
|
-
expect(killSpy).toHaveBeenCalledTimes(1);
|
|
87
|
-
await vi.advanceTimersByTimeAsync(SIGKILL_ESCALATION_MS);
|
|
88
|
-
expect(killSpy).toHaveBeenCalledTimes(2); // 首次 timer 正常触发(未被重复调用清掉)
|
|
89
|
-
expect(killSpy).toHaveBeenNthCalledWith(2, "SIGKILL");
|
|
90
|
-
});
|
|
91
|
-
});
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
// src/execution/__tests__/session-runner-branch-cache-lru.test.ts
|
|
2
|
-
//
|
|
3
|
-
// [LC-8/T6③] branchCache LRU 上界。
|
|
4
|
-
//
|
|
5
|
-
// 背景(设计 §4.3 LC-8):缓存 key=cwd,worktree 路径每次唯一 → 模块级 Map 永不淘汰,
|
|
6
|
-
// 条目按 path 永久累积(长寿命 orchestrator 内存无界,实锤·轻微)。T6③ 加 LRU 上限
|
|
7
|
-
// BRANCH_CACHE_MAX_ENTRIES=64(常量导出)。
|
|
8
|
-
//
|
|
9
|
-
// 覆盖(验收:65 个路径插入后规模 64,最旧被淘汰):
|
|
10
|
-
// - 65 次 miss 插入 → size 封顶 64,execFile 恰 65 次;
|
|
11
|
-
// - 最旧条目已被淘汰(再查重新 spawn git),次旧条目仍在(LRU 只淘汰最旧);
|
|
12
|
-
// - get 命中刷新 LRU 序(touch 后不再是淘汰候选);
|
|
13
|
-
// - 常量导出值 = 64。
|
|
14
|
-
|
|
15
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
16
|
-
|
|
17
|
-
// buildEnvBlock 用 execFile 异步取 git branch:按 opts.cwd 回填可区分的 branch 名。
|
|
18
|
-
const { execFileMock } = vi.hoisted(() => ({ execFileMock: vi.fn() }));
|
|
19
|
-
vi.mock("node:child_process", async () => {
|
|
20
|
-
const actual = await import("node:child_process");
|
|
21
|
-
return { ...actual, execFile: execFileMock };
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
// Mock 共享 logger(buildEnvBlock 的 git 失败 debug 留痕不落真实日志盘)。
|
|
25
|
-
const { loggerMock } = vi.hoisted(() => ({
|
|
26
|
-
loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
27
|
-
}));
|
|
28
|
-
vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
|
|
29
|
-
|
|
30
|
-
import {
|
|
31
|
-
BRANCH_CACHE_MAX_ENTRIES,
|
|
32
|
-
_getBranchCacheSizeForTest,
|
|
33
|
-
_resetBranchCacheForTest,
|
|
34
|
-
buildEnvBlock,
|
|
35
|
-
} from "../engine/engines/pi/session-runner.ts";
|
|
36
|
-
|
|
37
|
-
/** execFile mock 回填 br-<cwd>(branch 与 cwd 一一对应,淘汰行为可断言)。 */
|
|
38
|
-
function stubGitBranchSuccess(): void {
|
|
39
|
-
execFileMock.mockImplementation(
|
|
40
|
-
(
|
|
41
|
-
_cmd: string,
|
|
42
|
-
_args: readonly string[],
|
|
43
|
-
opts: { cwd?: string },
|
|
44
|
-
cb: (err: Error | null, stdout?: string, stderr?: string) => void,
|
|
45
|
-
) => cb(null, `br-${opts?.cwd ?? "unknown"}`, ""),
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
beforeEach(() => {
|
|
50
|
-
_resetBranchCacheForTest();
|
|
51
|
-
stubGitBranchSuccess();
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
afterEach(() => {
|
|
55
|
-
_resetBranchCacheForTest();
|
|
56
|
-
execFileMock.mockReset();
|
|
57
|
-
loggerMock.debug.mockClear();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
describe("[LC-8] branchCache LRU 上界", () => {
|
|
61
|
-
it("常量导出 BRANCH_CACHE_MAX_ENTRIES = 64", () => {
|
|
62
|
-
expect(BRANCH_CACHE_MAX_ENTRIES).toBe(64);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("65 个路径插入后规模封顶 64,最旧被淘汰(重新 spawn git)", async () => {
|
|
66
|
-
const cwds = Array.from({ length: BRANCH_CACHE_MAX_ENTRIES + 1 }, (_, i) => `/tmp/lru-cwd-${i}`);
|
|
67
|
-
for (const cwd of cwds) {
|
|
68
|
-
const block = await buildEnvBlock(cwd);
|
|
69
|
-
expect(block).toContain(`Git branch: br-${cwd}`);
|
|
70
|
-
}
|
|
71
|
-
// 65 次 miss 插入,第 65 次插入时淘汰最旧(cwd-0)→ execFile 恰 65 次、规模 64
|
|
72
|
-
expect(execFileMock).toHaveBeenCalledTimes(BRANCH_CACHE_MAX_ENTRIES + 1);
|
|
73
|
-
expect(_getBranchCacheSizeForTest()).toBe(BRANCH_CACHE_MAX_ENTRIES);
|
|
74
|
-
|
|
75
|
-
// 最旧(cwd-0)已被 LRU 淘汰 → 再次查询重新 spawn git(66 次);重插后淘汰次旧
|
|
76
|
-
//(cwd-1),更近插入的 cwd-2 不受影响
|
|
77
|
-
const block0 = await buildEnvBlock("/tmp/lru-cwd-0");
|
|
78
|
-
expect(block0).toContain("Git branch: br-/tmp/lru-cwd-0");
|
|
79
|
-
expect(execFileMock).toHaveBeenCalledTimes(BRANCH_CACHE_MAX_ENTRIES + 2);
|
|
80
|
-
|
|
81
|
-
// cwd-2(仍近插入序)仍在缓存 → 查询不再 spawn(仍 66 次)
|
|
82
|
-
await buildEnvBlock("/tmp/lru-cwd-2");
|
|
83
|
-
expect(execFileMock).toHaveBeenCalledTimes(BRANCH_CACHE_MAX_ENTRIES + 2);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("get 命中刷新 LRU 序:被 touch 的条目不再是最旧淘汰候选", async () => {
|
|
87
|
-
const cwdA = "/tmp/lru-touch-a";
|
|
88
|
-
const cwdB = "/tmp/lru-touch-b";
|
|
89
|
-
await buildEnvBlock(cwdA); // A 先插
|
|
90
|
-
await buildEnvBlock(cwdB); // B 后插
|
|
91
|
-
expect(execFileMock).toHaveBeenCalledTimes(2);
|
|
92
|
-
|
|
93
|
-
await buildEnvBlock(cwdA); // 命中并 touch → A 变最新,B 变最旧;零 spawn
|
|
94
|
-
expect(execFileMock).toHaveBeenCalledTimes(2);
|
|
95
|
-
|
|
96
|
-
// 再插 63 个新条目挤爆缓存(2 + 63 = 65 → 淘汰恰 1 个最旧):B(最旧)被淘汰、
|
|
97
|
-
// A(被 touch 过,次新)存活
|
|
98
|
-
for (let i = 0; i < BRANCH_CACHE_MAX_ENTRIES - 1; i++) {
|
|
99
|
-
await buildEnvBlock(`/tmp/lru-flood-${i}`);
|
|
100
|
-
}
|
|
101
|
-
expect(_getBranchCacheSizeForTest()).toBe(BRANCH_CACHE_MAX_ENTRIES);
|
|
102
|
-
const spawnsAfterFlood = execFileMock.mock.calls.length;
|
|
103
|
-
|
|
104
|
-
await buildEnvBlock(cwdA); // A 被 touch 过 → 仍在缓存
|
|
105
|
-
expect(execFileMock).toHaveBeenCalledTimes(spawnsAfterFlood);
|
|
106
|
-
|
|
107
|
-
await buildEnvBlock(cwdB); // B 最早未 touch → 已被淘汰,重新 spawn
|
|
108
|
-
expect(execFileMock).toHaveBeenCalledTimes(spawnsAfterFlood + 1);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
// src/execution/__tests__/session-runner-close-prune.test.ts
|
|
2
|
-
//
|
|
3
|
-
// [LC-6/T6②] 进程 close 接线:waitForChildExit close handler 调 prunePendingCursor
|
|
4
|
-
// 剪枝该进程 sessionFile 的 pending 增量游标(session-runner 领地内的单点接线,主
|
|
5
|
-
// agent 裁决授权)。
|
|
6
|
-
//
|
|
7
|
-
// 覆盖:
|
|
8
|
-
// 1. close(resume 锁定 sessionFile)后 cursor 被剪枝——行为观察面:覆写同名文件
|
|
9
|
-
// (新 register 行落在旧 offset 之内、size ≥ 旧 offset),判定应从头全量读出
|
|
10
|
-
// 新条目;cursor 未剪时会从旧 offset 续读丢掉它。
|
|
11
|
-
// 2. sessionFile 未回填(无 resume、无 header 回填)时 close 跳过剪枝不抛错
|
|
12
|
-
// (此时也无 cursor:判定从未发生)。
|
|
13
|
-
//
|
|
14
|
-
// mock 拓扑对齐 run-spawn-resume.test.ts(FakeChild / fs 部分 mock / temp-prompt /
|
|
15
|
-
// alive-store);session-pending 刻意**不 mock**——接线行为的观察面就是真实游标
|
|
16
|
-
// 剪枝语义(statSync/readFileSync 不在 fs mock 名单,走真实 fs)。
|
|
17
|
-
|
|
18
|
-
import { spawn } from "node:child_process";
|
|
19
|
-
import * as fs from "node:fs";
|
|
20
|
-
import { writeFile } from "node:fs/promises";
|
|
21
|
-
import * as os from "node:os";
|
|
22
|
-
import * as path from "node:path";
|
|
23
|
-
|
|
24
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
25
|
-
|
|
26
|
-
const { loggerMock } = vi.hoisted(() => ({
|
|
27
|
-
loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
28
|
-
}));
|
|
29
|
-
vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
|
|
30
|
-
|
|
31
|
-
vi.mock("node:child_process", async () => {
|
|
32
|
-
const { FakeChild } = await import("./helpers/spawn-mock.ts");
|
|
33
|
-
return {
|
|
34
|
-
spawn: vi.fn(() => new FakeChild()),
|
|
35
|
-
execFile: vi.fn(
|
|
36
|
-
(
|
|
37
|
-
_cmd: string,
|
|
38
|
-
_args: readonly string[],
|
|
39
|
-
_opts: unknown,
|
|
40
|
-
cb: (err: Error | null, stdout?: string, stderr?: string) => void,
|
|
41
|
-
) => cb(new Error("execFile not configured in this test")),
|
|
42
|
-
),
|
|
43
|
-
};
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// [注意] 工厂内必须 importOriginal 拿真 actual——不带它 `await import("node:fs")` 自引用
|
|
47
|
-
// 返回 mock 面。命名面先 ...actual 展开(session-pending 的 statSync/readFileSync、
|
|
48
|
-
// 测试 fixture 的 mkdtempSync/rmSync 走真实 fs;fixture 写盘走真实透传的
|
|
49
|
-
// node:fs/promises——writeFileSync 本身被 mock 隔离 runSpawn 的生产写入),再覆盖
|
|
50
|
-
// runSpawn 生产写入面的 5 个方法(隔离 ~/.xyz-agent 生产路径写)。
|
|
51
|
-
vi.mock("node:fs", async (importOriginal) => {
|
|
52
|
-
const actual = await importOriginal<typeof import("node:fs")>();
|
|
53
|
-
return {
|
|
54
|
-
default: {
|
|
55
|
-
...actual,
|
|
56
|
-
mkdirSync: vi.fn(),
|
|
57
|
-
existsSync: vi.fn(() => false),
|
|
58
|
-
appendFileSync: vi.fn(),
|
|
59
|
-
writeFileSync: vi.fn(),
|
|
60
|
-
readdirSync: vi.fn(() => []),
|
|
61
|
-
},
|
|
62
|
-
...actual,
|
|
63
|
-
mkdirSync: vi.fn(),
|
|
64
|
-
existsSync: vi.fn(() => false),
|
|
65
|
-
appendFileSync: vi.fn(),
|
|
66
|
-
writeFileSync: vi.fn(),
|
|
67
|
-
readdirSync: vi.fn(() => []),
|
|
68
|
-
promises: actual.promises,
|
|
69
|
-
};
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
vi.mock("../alive-store.ts", () => ({
|
|
73
|
-
writeAliveMarker: vi.fn(),
|
|
74
|
-
}));
|
|
75
|
-
|
|
76
|
-
vi.mock("../engine/engines/pi/temp-prompt.ts", () => ({
|
|
77
|
-
writePromptToTempFile: vi.fn(async (agent: string) => {
|
|
78
|
-
const safeName = agent.replace(/[^\w.-]+/g, "_");
|
|
79
|
-
return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` };
|
|
80
|
-
}),
|
|
81
|
-
cleanupTempPrompt: vi.fn(async () => {}),
|
|
82
|
-
}));
|
|
83
|
-
|
|
84
|
-
import { runSpawn } from "../engine/engines/pi/session-runner.ts";
|
|
85
|
-
import { clearPendingCursors, listActivePendingFromSessionFile } from "../session-pending.ts";
|
|
86
|
-
import {
|
|
87
|
-
type FakeChild,
|
|
88
|
-
lastSpawnedChild as lastSpawnedChildOf,
|
|
89
|
-
makeCtx,
|
|
90
|
-
makeOpts,
|
|
91
|
-
makeRecord,
|
|
92
|
-
waitForSpawn as waitForSpawnOf,
|
|
93
|
-
} from "./helpers/spawn-mock.ts";
|
|
94
|
-
|
|
95
|
-
const mockSpawn = vi.mocked(spawn);
|
|
96
|
-
const mockExistsSync = vi.mocked(fs.existsSync);
|
|
97
|
-
|
|
98
|
-
const lastSpawnedChild = (): FakeChild => lastSpawnedChildOf(mockSpawn);
|
|
99
|
-
const waitForSpawn = (timeoutMs = 1000): Promise<void> => waitForSpawnOf(mockSpawn, timeoutMs);
|
|
100
|
-
|
|
101
|
-
let sessionDir = "";
|
|
102
|
-
|
|
103
|
-
function entryLine(customType: string, data: Record<string, unknown>): string {
|
|
104
|
-
return `${JSON.stringify({
|
|
105
|
-
type: "custom",
|
|
106
|
-
customType,
|
|
107
|
-
data,
|
|
108
|
-
timestamp: "2026-09-01T00:00:00.000Z",
|
|
109
|
-
id: `e-${Math.random().toString(36).slice(2)}`,
|
|
110
|
-
})}\n`;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function registerData(id: string, sessionId: string): Record<string, unknown> {
|
|
114
|
-
return { id, type: "session", name: `desc-${id}`, status: "active", registeredAt: 1, expiresAt: undefined, sessionId };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** resume + close 的最短驱动:spawn 后直接 end stdout + close(handshake 经 abandon 放弃)。 */
|
|
118
|
-
async function runToClose(sessionFile: string): Promise<void> {
|
|
119
|
-
const record = makeRecord();
|
|
120
|
-
const promise = runSpawn(record, "Task: close prune", makeOpts(), makeCtx(), {
|
|
121
|
-
sessionFile,
|
|
122
|
-
});
|
|
123
|
-
await waitForSpawn();
|
|
124
|
-
const child = lastSpawnedChild();
|
|
125
|
-
// resume.sessionFile 真实存在 → existsSync 对它 true(identity 补写路径走通)
|
|
126
|
-
mockExistsSync.mockImplementation((p: unknown) => String(p) === sessionFile);
|
|
127
|
-
child.stdin.on("data", () => {}); // 消费 stdin 防背压
|
|
128
|
-
child.stdout.end();
|
|
129
|
-
child.emit("close", 0);
|
|
130
|
-
await promise;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
beforeEach(() => {
|
|
134
|
-
vi.clearAllMocks();
|
|
135
|
-
clearPendingCursors();
|
|
136
|
-
mockExistsSync.mockReturnValue(false);
|
|
137
|
-
sessionDir = fs.mkdtempSync(path.join(os.tmpdir(), "close-prune-"));
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
afterEach(() => {
|
|
141
|
-
clearPendingCursors();
|
|
142
|
-
vi.restoreAllMocks();
|
|
143
|
-
fs.rmSync(sessionDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
describe("[LC-6] 进程 close 剪枝 pending 游标", () => {
|
|
147
|
-
it("close 后 cursor 被剪枝:覆写同名文件后判定从头全量读(旧 offset 不残留)", async () => {
|
|
148
|
-
const f = path.join(sessionDir, "closed-proc.jsonl");
|
|
149
|
-
// gen1:15 字节/行 × 10 行 filler → 预建 cursor offset = 150
|
|
150
|
-
const gen1 = Array.from({ length: 10 }, (_, i) => `filler-line-${String(i).padStart(2, "0")}\n`).join("");
|
|
151
|
-
await writeFile(f, gen1); // 真实写盘(同步 writeFileSync 被 mock 隔离生产写入)
|
|
152
|
-
expect(listActivePendingFromSessionFile(f).items).toHaveLength(0); // cursor 建立
|
|
153
|
-
|
|
154
|
-
// 覆写(模拟磁盘状态变化):register bg-y 落在旧 offset(150)之内、size > 150——
|
|
155
|
-
// cursor 未剪时会从 byte 150 续读丢掉它。
|
|
156
|
-
const gen2 =
|
|
157
|
-
entryLine("pending:register", registerData("bg-y", "sess-y")) +
|
|
158
|
-
"filler".padEnd(150, ".") +
|
|
159
|
-
"\n";
|
|
160
|
-
expect(Buffer.byteLength(gen2, "utf-8")).toBeGreaterThan(150);
|
|
161
|
-
await writeFile(f, gen2);
|
|
162
|
-
|
|
163
|
-
await runToClose(f); // resume 锁定 sessionFile → close handler 剪枝
|
|
164
|
-
|
|
165
|
-
const items = listActivePendingFromSessionFile(f).items;
|
|
166
|
-
expect(items.map((i) => i.id)).toEqual(["bg-y"]); // 剪枝生效:从头全量读
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it("sessionFile 未回填(无 resume/无 header)时 close 跳过剪枝,流程正常完成", async () => {
|
|
170
|
-
const record = makeRecord();
|
|
171
|
-
const promise = runSpawn(record, "Task: no sessionFile", makeOpts(), makeCtx());
|
|
172
|
-
await waitForSpawn();
|
|
173
|
-
const child = lastSpawnedChild();
|
|
174
|
-
child.stdin.on("data", () => {});
|
|
175
|
-
child.stdout.end();
|
|
176
|
-
child.emit("close", 0);
|
|
177
|
-
// sessionFile undefined → guard 跳过(不抛错);record.sessionFile 保持未回填
|
|
178
|
-
await expect(promise).resolves.toBeDefined();
|
|
179
|
-
expect(record.sessionFile).toBeUndefined();
|
|
180
|
-
});
|
|
181
|
-
});
|