@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
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// src/execution/__tests__/chat-round-first-round-watchdog.test.ts
|
|
2
|
+
//
|
|
3
|
+
// [F-2 修复] 首轮/冷续轮 settled-watchdog arm 重接 + [F-5 修复] chat 轮路由终态化注销。
|
|
4
|
+
//
|
|
5
|
+
// 背景:armMidRoundNoProgress 此前唯一调用点在 deliverChatMessage(热路径续聊轮);
|
|
6
|
+
// kickOffChatRound(spawn 首轮 + 冷续 resume 轮)内无任何 arm——被删的 inproc
|
|
7
|
+
// stdout-pump 是首轮唯一中段守护,协议化后引擎侧 spawn-runner 仅 turn 计数无墙钟,
|
|
8
|
+
// 首轮 wedged(LC-1 场景①:无事件行输出)无任何熔断。修复 = run 派发前补 arm
|
|
9
|
+
//(refresh 源 = runId 键 ctx.onEvent 事件行 + ctx.onRoundLifecycle settled 交棒——
|
|
10
|
+
// 引擎侧 spawn-runner 对 text_delta 同时走 onEvent 与 onDelta,刷新面与热路径等效)。
|
|
11
|
+
//
|
|
12
|
+
// [F-5] chat 轮路由(chatRoundRoutes)此前仅 cancelBackground 注销;finalize 链终态化
|
|
13
|
+
// (closeChatIdle / closeAfterRoundSettled / finalizeRecord)不注销 → 路由闭包持
|
|
14
|
+
// record/stream/守护引用泄漏。修复 = doFinalizeRecord 的 onFinalized 钩子单一汇聚点。
|
|
15
|
+
//
|
|
16
|
+
// 替身形态与 subagent-service-recovery-bounds.test.ts 同源(fake-engine-port 协议 seam)。
|
|
17
|
+
|
|
18
|
+
import * as fs from "node:fs";
|
|
19
|
+
import * as os from "node:os";
|
|
20
|
+
import * as path from "node:path";
|
|
21
|
+
|
|
22
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
23
|
+
|
|
24
|
+
const { loggerMock } = vi.hoisted(() => ({
|
|
25
|
+
loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
26
|
+
}));
|
|
27
|
+
vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
|
|
28
|
+
|
|
29
|
+
const { killChildSpy } = vi.hoisted(() => ({ killChildSpy: vi.fn() }));
|
|
30
|
+
vi.mock("../engine/host/spawned-children.ts", async (importOriginal) => {
|
|
31
|
+
const actual = await importOriginal<typeof import("../engine/host/spawned-children.ts")>();
|
|
32
|
+
return { ...actual, killRecordChildWithEscalation: killChildSpy };
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
import { clearEngines } from "../engine/registry.ts";
|
|
36
|
+
import { registerFakePiEngine, type FakePiEnginePort } from "./helpers/fake-engine-port.ts";
|
|
37
|
+
import { createRecord } from "../execution-record.ts";
|
|
38
|
+
import { ModelConfigService } from "../model-config-service.ts";
|
|
39
|
+
import type { ModelInfo, ModelRegistryLike } from "../model-resolver.ts";
|
|
40
|
+
import type { RecordStore } from "../record-store.ts";
|
|
41
|
+
import { SubagentService } from "../subagent-service.ts";
|
|
42
|
+
import type { PiLike } from "../subagent-service.ts";
|
|
43
|
+
import {
|
|
44
|
+
getSettledWatchdogPhase,
|
|
45
|
+
hasSettledWatchdog,
|
|
46
|
+
SETTLED_MID_ROUND_NO_PROGRESS_MS,
|
|
47
|
+
SETTLED_WATCHDOG_TIMEOUT_MS,
|
|
48
|
+
_resetSettledWatchdogsForTest,
|
|
49
|
+
} from "../settled-watchdog.ts";
|
|
50
|
+
import { _resetLifecycleState } from "../lifecycle-manager.ts";
|
|
51
|
+
import { _resetCoreSpawnedChildrenMirrorForTest } from "../engine/host/spawned-children.ts";
|
|
52
|
+
import type { ExecutionRecord } from "../types.ts";
|
|
53
|
+
|
|
54
|
+
function makePi(): PiLike {
|
|
55
|
+
return {
|
|
56
|
+
appendEntry: vi.fn(),
|
|
57
|
+
events: { emit: vi.fn() },
|
|
58
|
+
sendMessage: vi.fn(),
|
|
59
|
+
} as unknown as PiLike;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface ServiceInternals {
|
|
63
|
+
store: RecordStore;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const CTX_MODEL: ModelInfo = { id: "m", name: "M", provider: "p", reasoning: false };
|
|
67
|
+
|
|
68
|
+
function makeEmptyRegistry(): ModelRegistryLike {
|
|
69
|
+
return { getAvailable: () => [], find: () => undefined, hasConfiguredAuth: () => true };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function setup(): { agentDir: string; service: SubagentService; store: RecordStore; pi: PiLike; fake: FakePiEnginePort } {
|
|
73
|
+
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "chat-round-watchdog-"));
|
|
74
|
+
clearEngines();
|
|
75
|
+
const fake = registerFakePiEngine();
|
|
76
|
+
const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
|
|
77
|
+
modelService.initModel({ modelRegistry: makeEmptyRegistry(), sessionId: "root-session", ctxModel: CTX_MODEL });
|
|
78
|
+
const service = new SubagentService({ cwd: agentDir, modelService });
|
|
79
|
+
const pi = makePi();
|
|
80
|
+
service.initSession({ pi, sessionId: "root-session" });
|
|
81
|
+
const store = (service as unknown as ServiceInternals).store;
|
|
82
|
+
return { agentDir, service, store, pi, fake };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 模拟 kickOffChatRound 的路由注册面(Service map + fake 引擎两端同挂)。 */
|
|
86
|
+
function registerRoute(service: SubagentService, fake: FakePiEnginePort, recordId: string): void {
|
|
87
|
+
const unregister = fake.registerChatRoundRoute(recordId, {
|
|
88
|
+
onStreamDelta: () => {},
|
|
89
|
+
onRoundLifecycle: () => {},
|
|
90
|
+
});
|
|
91
|
+
(
|
|
92
|
+
service as unknown as { chatRoundRoutes: Map<string, () => void> }
|
|
93
|
+
).chatRoundRoutes.set(recordId, unregister);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function makeChatRecord(id: string): ExecutionRecord {
|
|
97
|
+
return createRecord(id, {
|
|
98
|
+
agent: "general-purpose",
|
|
99
|
+
model: "test/model",
|
|
100
|
+
mode: "background",
|
|
101
|
+
task: "test",
|
|
102
|
+
slug: "test",
|
|
103
|
+
startedAt: 1000,
|
|
104
|
+
rootSessionId: "root-session",
|
|
105
|
+
chatMode: true,
|
|
106
|
+
controller: new AbortController(),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
describe("[F-2] 首轮/冷续轮 settled-watchdog arm(kickOffChatRound run 派发点)", () => {
|
|
111
|
+
let agentDir: string;
|
|
112
|
+
let service: SubagentService;
|
|
113
|
+
let store: RecordStore;
|
|
114
|
+
let pi: PiLike;
|
|
115
|
+
let fake: FakePiEnginePort;
|
|
116
|
+
|
|
117
|
+
beforeEach(() => {
|
|
118
|
+
vi.restoreAllMocks();
|
|
119
|
+
killChildSpy.mockClear();
|
|
120
|
+
({ agentDir, service, store, pi, fake } = setup());
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
afterEach(() => {
|
|
124
|
+
service.dispose();
|
|
125
|
+
clearEngines();
|
|
126
|
+
vi.useRealTimers();
|
|
127
|
+
_resetLifecycleState();
|
|
128
|
+
_resetSettledWatchdogsForTest();
|
|
129
|
+
_resetCoreSpawnedChildrenMirrorForTest();
|
|
130
|
+
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("首轮 run 派发后中段守护 armed(修复前首轮无任何守护)", async () => {
|
|
134
|
+
const handle = await service.execute({ task: "first round", slug: "t", ctxModel: CTX_MODEL, conversation: true });
|
|
135
|
+
expect(fake.runs).toHaveLength(1);
|
|
136
|
+
expect(hasSettledWatchdog(handle.subagentId)).toBe(true);
|
|
137
|
+
expect(getSettledWatchdogPhase(handle.subagentId)).toBe("mid-round");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("冷续轮进行中无产出 → 中段守护按既有超时语义触发:kill + 引擎 cancel + 回退 idle-resumable(fake timers)", async () => {
|
|
141
|
+
// fake timers 必须先于 arm 生效(useFakeTimers 不接管已存在的真实 timer)
|
|
142
|
+
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
|
|
143
|
+
const record = makeChatRecord("sa-first");
|
|
144
|
+
record.sessionFile = path.join(agentDir, "prior-session.jsonl");
|
|
145
|
+
fs.writeFileSync(record.sessionFile, "{}\n", "utf-8");
|
|
146
|
+
store.register(record);
|
|
147
|
+
await (
|
|
148
|
+
service as unknown as { resumeColdRound: (r: ExecutionRecord, t: string) => void }
|
|
149
|
+
).resumeColdRound(record, "cold resume text");
|
|
150
|
+
expect(fake.runs).toHaveLength(1); // 冷续轮 run 已派发
|
|
151
|
+
expect(hasSettledWatchdog(record.id)).toBe(true);
|
|
152
|
+
|
|
153
|
+
// 冷续轮全程静默(无协议事件行)满中段窗长
|
|
154
|
+
await vi.advanceTimersByTimeAsync(SETTLED_MID_ROUND_NO_PROGRESS_MS + 1);
|
|
155
|
+
|
|
156
|
+
expect(killChildSpy).toHaveBeenCalledWith(record.id, "settled watchdog (hot path)");
|
|
157
|
+
expect(hasSettledWatchdog(record.id)).toBe(false); // 到期自清
|
|
158
|
+
// chatMode MF-6:回退 idle-resumable(不销毁对话)
|
|
159
|
+
expect(record.resumable).toBe(true);
|
|
160
|
+
expect(record.status).toBe("running");
|
|
161
|
+
expect(record.result).toContain("settled watchdog");
|
|
162
|
+
expect(record.result).toContain("Recovery");
|
|
163
|
+
// 引擎侧终止意图(协议 interact cancel)
|
|
164
|
+
const cancel = fake.interacts.find((c) => c.action.kind === "cancel");
|
|
165
|
+
expect(cancel).toBeDefined();
|
|
166
|
+
void pi;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("中段事件行(text_delta 经 onEvent)刷新静默计时:半窗 + 刷新 + 半窗不触发,再满窗才触发", async () => {
|
|
170
|
+
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
|
|
171
|
+
const handle = await service.execute({ task: "streaming round", slug: "t", ctxModel: CTX_MODEL, conversation: true });
|
|
172
|
+
const run = fake.runs[0]!;
|
|
173
|
+
|
|
174
|
+
await vi.advanceTimersByTimeAsync(SETTLED_MID_ROUND_NO_PROGRESS_MS / 2);
|
|
175
|
+
// 首轮 runId 键协议事件行到达(text_delta——引擎侧同时走 onEvent 通道)
|
|
176
|
+
run.emitEvent({ type: "text_delta", delta: "chunk" });
|
|
177
|
+
await vi.advanceTimersByTimeAsync(SETTLED_MID_ROUND_NO_PROGRESS_MS / 2 + 1);
|
|
178
|
+
expect(hasSettledWatchdog(handle.subagentId)).toBe(true); // 刷新后未到期
|
|
179
|
+
expect(killChildSpy).not.toHaveBeenCalled();
|
|
180
|
+
|
|
181
|
+
// 刷新后再满整个静默窗才触发
|
|
182
|
+
await vi.advanceTimersByTimeAsync(SETTLED_MID_ROUND_NO_PROGRESS_MS + 1);
|
|
183
|
+
expect(killChildSpy).toHaveBeenCalledWith(handle.subagentId, "settled watchdog (hot path)");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("首轮 settled 相位交棒:中段窗内相位到达 → 切收尾段(中段窗不再触发,收尾段硬顶触发)", async () => {
|
|
187
|
+
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
|
|
188
|
+
const handle = await service.execute({ task: "stuck after settle", slug: "t", ctxModel: CTX_MODEL, conversation: true });
|
|
189
|
+
const run = fake.runs[0]!;
|
|
190
|
+
|
|
191
|
+
await vi.advanceTimersByTimeAsync(SETTLED_MID_ROUND_NO_PROGRESS_MS / 2);
|
|
192
|
+
// 首轮 runId 键 roundLifecycle settled 相位(W3 报告称已接——本用例核实链路生效)
|
|
193
|
+
run.emitLifecycle({ phase: "settled", usage: { input: 1, output: 1, cacheRead: 0, cacheWrite: 0 } });
|
|
194
|
+
expect(getSettledWatchdogPhase(handle.subagentId)).toBe("settled"); // 已交棒收尾段
|
|
195
|
+
|
|
196
|
+
// 中段不再计时(交棒清掉,不继承——30min 静默判据对收尾段失效);收尾段未满(<600s)不触发
|
|
197
|
+
await vi.advanceTimersByTimeAsync(SETTLED_WATCHDOG_TIMEOUT_MS - 1);
|
|
198
|
+
expect(killChildSpy).not.toHaveBeenCalled();
|
|
199
|
+
expect(hasSettledWatchdog(handle.subagentId)).toBe(true); // 收尾段仍在计时
|
|
200
|
+
|
|
201
|
+
// 收尾段硬顶(600s 默认)到期触发
|
|
202
|
+
await vi.advanceTimersByTimeAsync(2);
|
|
203
|
+
expect(killChildSpy).toHaveBeenCalledWith(handle.subagentId, "settled watchdog (hot path)");
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("[F-5] chat 轮路由终态化注销(doFinalizeRecord onFinalized 汇聚点)", () => {
|
|
208
|
+
let agentDir: string;
|
|
209
|
+
let service: SubagentService;
|
|
210
|
+
let store: RecordStore;
|
|
211
|
+
let fake: FakePiEnginePort;
|
|
212
|
+
|
|
213
|
+
beforeEach(() => {
|
|
214
|
+
vi.restoreAllMocks();
|
|
215
|
+
killChildSpy.mockClear();
|
|
216
|
+
({ agentDir, service, store, fake } = setup());
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
afterEach(() => {
|
|
220
|
+
service.dispose();
|
|
221
|
+
clearEngines();
|
|
222
|
+
_resetLifecycleState();
|
|
223
|
+
_resetSettledWatchdogsForTest();
|
|
224
|
+
_resetCoreSpawnedChildrenMirrorForTest();
|
|
225
|
+
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("closeChatIdle 终态化路径注销 chat 轮路由(修复前仅 cancelBackground 注销 → 泄漏)", async () => {
|
|
229
|
+
const record = makeChatRecord("sa-route-leak");
|
|
230
|
+
store.register(record);
|
|
231
|
+
// 模拟首轮已注册的 recordId 键路由(kickOffChatRound 注册面:Service map + fake 引擎两端)
|
|
232
|
+
registerRoute(service, fake, record.id);
|
|
233
|
+
// Path A:idle timer armed(进程保活等待续聊)→ closeSubagent(false) 走 closeChatIdle
|
|
234
|
+
const { armIdleTimer } = (await import("../lifecycle-manager.ts")) as typeof import("../lifecycle-manager.ts");
|
|
235
|
+
armIdleTimer(record.id, () => {}, 60_000);
|
|
236
|
+
|
|
237
|
+
await service.chatActions.closeSubagent(record, false);
|
|
238
|
+
|
|
239
|
+
expect(fake.chatRouteUnregisters).toContain(record.id);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("closeAfterRoundSettled(优雅关闭消费)路径注销 chat 轮路由", async () => {
|
|
243
|
+
const record = makeChatRecord("sa-route-close-round");
|
|
244
|
+
store.register(record);
|
|
245
|
+
registerRoute(service, fake, record.id);
|
|
246
|
+
record.closeAfterRound = true;
|
|
247
|
+
|
|
248
|
+
await (
|
|
249
|
+
service as unknown as { closeAfterRoundSettled: (r: ExecutionRecord) => Promise<void> }
|
|
250
|
+
).closeAfterRoundSettled(record);
|
|
251
|
+
|
|
252
|
+
expect(fake.chatRouteUnregisters).toContain(record.id);
|
|
253
|
+
expect(record.status).toBe("closed");
|
|
254
|
+
});
|
|
255
|
+
});
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
// 5. flush 屏障时序:manifest 写完成先于 notifyBatch 写账(通知可达 ⇒ 索引就位
|
|
13
13
|
// 的构造性保证,时序竞态修复)。
|
|
14
14
|
//
|
|
15
|
-
// mock 手法对齐 ended-message-and-fork-from.test.ts:
|
|
16
|
-
//
|
|
15
|
+
// mock 手法对齐 ended-message-and-fork-from.test.ts:[W3 改写] 替身从
|
|
16
|
+
// vi.mock session-runner(不 spawn 真子进程)改为协议 seam(registerFakePiEngine 替身,
|
|
17
|
+
// 测试显式 settle 应答)+ logger;record-store / config 走真实实现(tmpdir 自建自删,红线)。
|
|
17
18
|
|
|
18
19
|
import * as fs from "node:fs";
|
|
19
20
|
import * as os from "node:os";
|
|
@@ -21,35 +22,16 @@ import * as path from "node:path";
|
|
|
21
22
|
|
|
22
23
|
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vitest";
|
|
23
24
|
|
|
24
|
-
const { loggerMock
|
|
25
|
+
const { loggerMock } = vi.hoisted(() => ({
|
|
25
26
|
loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn(), info: vi.fn() },
|
|
26
|
-
/** runSpawn mock 实例提升:测试内直接引用控制 resolve 时序(不依赖 import 路径解析——
|
|
27
|
-
* vi.mock 字符串与本文件既有写法保持一致,mock 实例经 hoisted 闭包共享)。 */
|
|
28
|
-
runSpawnMock: vi.fn(async () => ({
|
|
29
|
-
text: "ok",
|
|
30
|
-
turns: 1,
|
|
31
|
-
durationMs: 10,
|
|
32
|
-
success: true,
|
|
33
|
-
sessionId: "spawned",
|
|
34
|
-
toolCalls: [],
|
|
35
|
-
})),
|
|
36
27
|
}));
|
|
37
28
|
vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
|
|
38
29
|
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// runSpawnMock.calls 恒 0 → 受控 promise 用例 until 超时)。
|
|
45
|
-
vi.mock("../engine/engines/pi/session-runner.ts", () => ({
|
|
46
|
-
runSpawn: runSpawnMock,
|
|
47
|
-
killAllSpawnedChildren: vi.fn(),
|
|
48
|
-
killRecordChildWithEscalation: vi.fn(),
|
|
49
|
-
getChildByRecord: vi.fn(() => undefined),
|
|
50
|
-
registerSpawnedChildForRecord: vi.fn(),
|
|
51
|
-
spawnedChildren: new Map(),
|
|
52
|
-
}));
|
|
30
|
+
// [W3] execute 链经 executeViaEngine → kickOffChatRound → engine.run(协议替身)。
|
|
31
|
+
// 替身 run 挂起不自动 settle:测试取 fake.runs.at(-1) 显式 settle(受控时序——
|
|
32
|
+
// 原 runSpawnMock 的 resolve 时序控制等价面)。
|
|
33
|
+
import { clearEngines } from "../engine/registry.ts";
|
|
34
|
+
import { registerFakePiEngine, type FakePiEnginePort } from "./helpers/fake-engine-port.ts";
|
|
53
35
|
|
|
54
36
|
import { getSubagentSessionDir, getSubagentRecordsDir } from "../path-encoding.ts";
|
|
55
37
|
import { ModelConfigService } from "../model-config-service.ts";
|
|
@@ -116,9 +98,12 @@ describe("collectCoordinator service integration (U2)", () => {
|
|
|
116
98
|
let agentDir: string;
|
|
117
99
|
let service: SubagentService;
|
|
118
100
|
let pi: ReturnType<typeof makePi>;
|
|
101
|
+
let fake: FakePiEnginePort;
|
|
119
102
|
|
|
120
103
|
beforeEach(() => {
|
|
121
104
|
for (const k of IDENTITY_ENV_KEYS) delete process.env[k];
|
|
105
|
+
clearEngines();
|
|
106
|
+
fake = registerFakePiEngine();
|
|
122
107
|
agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "core-collect-coord-"));
|
|
123
108
|
fs.mkdirSync(getSubagentSessionDir(agentDir, agentDir), { recursive: true });
|
|
124
109
|
pi = makePi();
|
|
@@ -127,6 +112,7 @@ describe("collectCoordinator service integration (U2)", () => {
|
|
|
127
112
|
|
|
128
113
|
afterEach(() => {
|
|
129
114
|
service.dispose();
|
|
115
|
+
clearEngines();
|
|
130
116
|
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
131
117
|
});
|
|
132
118
|
|
|
@@ -154,6 +140,8 @@ describe("collectCoordinator service integration (U2)", () => {
|
|
|
154
140
|
// 批投递(下方用例),async 成员直通。本条记录 execute 链零异常完成。
|
|
155
141
|
const spy = spyNotifier(service);
|
|
156
142
|
const handle = await service.execute({ task: "collect me", slug: "collect-me", collect: "sync" });
|
|
143
|
+
await until(() => fake.runs.length >= 1);
|
|
144
|
+
await settleLast();
|
|
157
145
|
await until(() => spy.notifyBatch.mock.calls.length > 0);
|
|
158
146
|
expect(handle.subagentId).toMatch(/^sa-/);
|
|
159
147
|
});
|
|
@@ -161,6 +149,8 @@ describe("collectCoordinator service integration (U2)", () => {
|
|
|
161
149
|
it("routes a finished sync member into a single-member batch (U3 接线:单成员闭合 → notifyBatch)", async () => {
|
|
162
150
|
const spy = spyNotifier(service);
|
|
163
151
|
const handle = await service.execute({ task: "collect me", slug: "collect-me", collect: "sync" });
|
|
152
|
+
await until(() => fake.runs.length >= 1);
|
|
153
|
+
await settleLast();
|
|
164
154
|
await until(() => spy.notifyBatch.mock.calls.length > 0);
|
|
165
155
|
// U3 接线后:sync 成员走单条批投递(不再逐条降级 notify)
|
|
166
156
|
expect(spy.notify).not.toHaveBeenCalled();
|
|
@@ -173,6 +163,8 @@ describe("collectCoordinator service integration (U2)", () => {
|
|
|
173
163
|
const spy = spyNotifier(service);
|
|
174
164
|
spy.notifyBatch.mockReturnValue(false); // 模拟同成员集批已在账(E1 重建重发形态)
|
|
175
165
|
const handle = await service.execute({ task: "collect me", slug: "collect-me", collect: "sync" });
|
|
166
|
+
await until(() => fake.runs.length >= 1);
|
|
167
|
+
await settleLast();
|
|
176
168
|
await until(() => spy.notifyBatch.mock.calls.length > 0);
|
|
177
169
|
expect(handle.subagentId).toMatch(/^sa-/);
|
|
178
170
|
// 落标出口①绑「写账成功」:accepted=false → 无带 batchFinalized 标记的 subagent-record
|
|
@@ -188,6 +180,8 @@ describe("collectCoordinator service integration (U2)", () => {
|
|
|
188
180
|
it("routes an async member through the same notifyAsync path (现状直通)", async () => {
|
|
189
181
|
const spy = spyNotifier(service);
|
|
190
182
|
const handle = await service.execute({ task: "plain", slug: "plain" });
|
|
183
|
+
await until(() => fake.runs.length >= 1);
|
|
184
|
+
await settleLast();
|
|
191
185
|
await until(() => spy.notify.mock.calls.length > 0);
|
|
192
186
|
const notified = spy.notify.mock.calls.map((c) => c[0] as { id: string });
|
|
193
187
|
expect(notified.some((n) => n.id === handle.subagentId)).toBe(true);
|
|
@@ -247,28 +241,27 @@ describe("collectCoordinator service integration (U2)", () => {
|
|
|
247
241
|
fs.writeFileSync(path.join(sessionsDir, `${recordId}.jsonl`), `${header}\n${identity}\n${recordEntry}\n`, "utf-8");
|
|
248
242
|
}
|
|
249
243
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
244
|
+
/** 受控替身 settle:等替身 run 到位(execute 返回后的异步链内发起),按需 settle。
|
|
245
|
+
* 原 runSpawnMock 受控 promise 的等价面(content "ok" 对齐原 AgentResult.text)。 */
|
|
246
|
+
const settleLast = async (): Promise<void> => {
|
|
247
|
+
await until(() => fake.runs.length >= 1);
|
|
248
|
+
fake.runs[fake.runs.length - 1]!.settle({ content: "ok" });
|
|
249
|
+
};
|
|
253
250
|
|
|
254
251
|
it("跨轮续累(D2 service 层):两轮派 2 sync → 闭合时单批 2 成员", async () => {
|
|
255
|
-
let resolve1!: (v: ReturnType<typeof spawnOk>) => void;
|
|
256
|
-
let resolve2!: (v: ReturnType<typeof spawnOk>) => void;
|
|
257
|
-
runSpawnMock.mockImplementationOnce(() => new Promise((res) => { resolve1 = res; }));
|
|
258
|
-
runSpawnMock.mockImplementationOnce(() => new Promise((res) => { resolve2 = res; }));
|
|
259
252
|
const spy = spyNotifier(service);
|
|
260
253
|
const h1 = await service.execute({ task: "t1", slug: "one", collect: "sync" });
|
|
261
254
|
const h2 = await service.execute({ task: "t2", slug: "two", collect: "sync" });
|
|
262
|
-
//
|
|
263
|
-
await until(() =>
|
|
255
|
+
// engine.run 在 execute 返回后的异步链内才被发起——等两个替身 run 都到位
|
|
256
|
+
await until(() => fake.runs.length >= 2);
|
|
264
257
|
|
|
265
258
|
// 第一台终态:第二台仍 running → 缓冲不闭合(零投递)
|
|
266
|
-
|
|
259
|
+
fake.runs[0]!.settle({ content: "ok" });
|
|
267
260
|
await new Promise((resolve) => setTimeout(resolve, 50)); // microtask 链排空
|
|
268
261
|
expect(spy.notifyBatch.mock.calls).toHaveLength(0);
|
|
269
262
|
|
|
270
263
|
// 第二台终态:闭合 → 单批 2 成员(含第一台的快照,跨轮续累不丢)
|
|
271
|
-
|
|
264
|
+
fake.runs[1]!.settle({ content: "ok" });
|
|
272
265
|
await until(() => spy.notifyBatch.mock.calls.length > 0);
|
|
273
266
|
expect(spy.notifyBatch.mock.calls).toHaveLength(1);
|
|
274
267
|
const batch = spy.notifyBatch.mock.calls[0]![0] as { id: string }[];
|
|
@@ -277,14 +270,12 @@ describe("collectCoordinator service integration (U2)", () => {
|
|
|
277
270
|
});
|
|
278
271
|
|
|
279
272
|
it("batchFinalized 落标(真文件通路):闭合 flush 写账成功后末条 entry 带标记(E1 排除判据)", async () => {
|
|
280
|
-
let resolveSpawn!: (v: ReturnType<typeof spawnOk>) => void;
|
|
281
|
-
runSpawnMock.mockImplementationOnce(() => new Promise((res) => { resolveSpawn = res; }));
|
|
282
273
|
const spy = spyNotifier(service);
|
|
283
274
|
const handle = await service.execute({ task: "collect me", slug: "collect-me", collect: "sync" });
|
|
284
|
-
await until(() =>
|
|
285
|
-
// 子 session 文件在闭合判定/flush
|
|
275
|
+
await until(() => fake.runs.length >= 1); // 替身 run 已到位
|
|
276
|
+
// 子 session 文件在闭合判定/flush 前就位(真实生产链中引擎轮次已写好 identity)
|
|
286
277
|
writeChildSessionFile(handle.subagentId, "/agents/worker.md", "ok");
|
|
287
|
-
|
|
278
|
+
await settleLast();
|
|
288
279
|
await until(() => spy.notifyBatch.mock.calls.length > 0);
|
|
289
280
|
|
|
290
281
|
// 观察者形态:主 session 落盘的末条 subagent-record entry 带 batchFinalized=true +
|
|
@@ -307,11 +298,9 @@ describe("collectCoordinator service integration (U2)", () => {
|
|
|
307
298
|
// 构造性保证(by construction)。断言打在 notifyBatch(写账入口)被调用的时刻:
|
|
308
299
|
// 磁盘上 records/<sa-id>.json 必已存在(修复前该时刻大概率 false,探针实测 mtime
|
|
309
300
|
// 相对 notify entry ±2/3ms 方向不定)。
|
|
310
|
-
let resolveSpawn!: (v: ReturnType<typeof spawnOk>) => void;
|
|
311
|
-
runSpawnMock.mockImplementationOnce(() => new Promise((res) => { resolveSpawn = res; }));
|
|
312
301
|
const spy = spyNotifier(service);
|
|
313
302
|
const handle = await service.execute({ task: "collect me", slug: "collect-me", collect: "sync" });
|
|
314
|
-
await until(() =>
|
|
303
|
+
await until(() => fake.runs.length >= 1); // 替身 run 已到位
|
|
315
304
|
writeChildSessionFile(handle.subagentId, "/agents/worker.md", "ok");
|
|
316
305
|
const manifestPath = path.join(getSubagentRecordsDir(agentDir, agentDir), `${handle.subagentId}.json`);
|
|
317
306
|
// 写账时刻钩子:mock 内联捕获 manifest 存在性(此刻即指针行消费可用性)
|
|
@@ -320,7 +309,7 @@ describe("collectCoordinator service integration (U2)", () => {
|
|
|
320
309
|
manifestExistsAtLedgerWrite = fs.existsSync(manifestPath);
|
|
321
310
|
return true;
|
|
322
311
|
});
|
|
323
|
-
|
|
312
|
+
await settleLast();
|
|
324
313
|
await until(() => spy.notifyBatch.mock.calls.length > 0);
|
|
325
314
|
expect(manifestExistsAtLedgerWrite).toBe(true);
|
|
326
315
|
});
|
|
@@ -27,32 +27,15 @@ import * as path from "node:path";
|
|
|
27
27
|
|
|
28
28
|
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vitest";
|
|
29
29
|
|
|
30
|
-
const { loggerMock
|
|
30
|
+
const { loggerMock } = vi.hoisted(() => ({
|
|
31
31
|
loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn(), info: vi.fn() },
|
|
32
|
-
runSpawnMock: vi.fn(async () => ({
|
|
33
|
-
text: "ok",
|
|
34
|
-
turns: 1,
|
|
35
|
-
durationMs: 10,
|
|
36
|
-
success: true,
|
|
37
|
-
sessionId: "spawned",
|
|
38
|
-
toolCalls: [],
|
|
39
|
-
})),
|
|
40
32
|
}));
|
|
41
33
|
vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
|
|
42
34
|
|
|
43
|
-
// mock
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
vi.mock("../engine/engines/pi/session-runner.ts", () => ({
|
|
48
|
-
runSpawn: runSpawnMock,
|
|
49
|
-
killAllSpawnedChildren: vi.fn(),
|
|
50
|
-
killRecordChildWithEscalation: vi.fn(),
|
|
51
|
-
getChildByRecord: vi.fn(() => undefined),
|
|
52
|
-
registerSpawnedChildForRecord: vi.fn(),
|
|
53
|
-
spawnedChildren: new Map(),
|
|
54
|
-
}));
|
|
55
|
-
|
|
35
|
+
// [W3 改写] 替身从 vi.mock session-runner 的受控 runSpawn 改为协议 seam 的受控
|
|
36
|
+
// engine.run(registerFakePiEngine 替身 + 显式 settle)。
|
|
37
|
+
import { registerFakePiEngine, type FakePiEnginePort } from "./helpers/fake-engine-port.ts";
|
|
38
|
+
import { clearEngines } from "../engine/registry.ts";
|
|
56
39
|
import { getSubagentSessionDir } from "../path-encoding.ts";
|
|
57
40
|
import { ModelConfigService } from "../model-config-service.ts";
|
|
58
41
|
import type { ModelRegistryLike } from "../model-resolver.ts";
|
|
@@ -106,31 +89,10 @@ async function until(cond: () => boolean, timeoutMs = 3000): Promise<void> {
|
|
|
106
89
|
}
|
|
107
90
|
}
|
|
108
91
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
success: true,
|
|
114
|
-
sessionId: "spawned",
|
|
115
|
-
toolCalls: [],
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
/** 受控 runSpawn:每次调用挂起返回 resolver,测试按成员序号放行终态。
|
|
119
|
-
* resolver 在 runSpawn 实际调用时(execute 异步链内)才构造——release 函数按
|
|
120
|
-
* 索引延迟取用,不能在挂起注册时立刻闭包捕获(彼时尚为 undefined)。 */
|
|
121
|
-
function controlledSpawns(n: number): Array<(v?: ReturnType<typeof spawnOk>) => void> {
|
|
122
|
-
const resolvers: Array<(v: ReturnType<typeof spawnOk>) => void> = [];
|
|
123
|
-
for (let i = 0; i < n; i += 1) {
|
|
124
|
-
runSpawnMock.mockImplementationOnce(
|
|
125
|
-
() =>
|
|
126
|
-
new Promise((res) => {
|
|
127
|
-
resolvers[i] = res;
|
|
128
|
-
}),
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
return Array.from({ length: n }, (_, i) => (v?: ReturnType<typeof spawnOk>) =>
|
|
132
|
-
resolvers[i]!(v ?? spawnOk()),
|
|
133
|
-
);
|
|
92
|
+
/** 受控替身 run:等 n 个替身 run 到位(execute 异步链内发起),按索引放行 settle。
|
|
93
|
+
* 与原受控 runSpawn 的 resolver 形态同构(索引 = execute 派发序)。 */
|
|
94
|
+
function makeRelease(fk: FakePiEnginePort, n: number): Array<() => void> {
|
|
95
|
+
return Array.from({ length: n }, (_, i) => () => fk.runs[i]!.settle({ content: "ok" }));
|
|
134
96
|
}
|
|
135
97
|
|
|
136
98
|
describe("A8 混派正交 service e2e(同轮 2 sync + 1 async,U8)", () => {
|
|
@@ -138,8 +100,12 @@ describe("A8 混派正交 service e2e(同轮 2 sync + 1 async,U8)", () =>
|
|
|
138
100
|
let service: SubagentService;
|
|
139
101
|
let pi: ReturnType<typeof makePi>;
|
|
140
102
|
|
|
103
|
+
let fake: FakePiEnginePort;
|
|
104
|
+
|
|
141
105
|
beforeEach(() => {
|
|
142
106
|
for (const k of IDENTITY_ENV_KEYS) delete process.env[k];
|
|
107
|
+
clearEngines();
|
|
108
|
+
fake = registerFakePiEngine();
|
|
143
109
|
agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "core-mixed-dispatch-"));
|
|
144
110
|
fs.mkdirSync(getSubagentSessionDir(agentDir, agentDir), { recursive: true });
|
|
145
111
|
pi = makePi();
|
|
@@ -160,6 +126,7 @@ describe("A8 混派正交 service e2e(同轮 2 sync + 1 async,U8)", () =>
|
|
|
160
126
|
|
|
161
127
|
afterEach(() => {
|
|
162
128
|
service.dispose();
|
|
129
|
+
clearEngines();
|
|
163
130
|
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
164
131
|
});
|
|
165
132
|
|
|
@@ -167,11 +134,11 @@ describe("A8 混派正交 service e2e(同轮 2 sync + 1 async,U8)", () =>
|
|
|
167
134
|
const spy = spyNotifier(service);
|
|
168
135
|
// 同轮三连派发:2 sync + 1 async(派发顺序 sync, sync, async——同一条消息内的
|
|
169
136
|
// 混派形态;runSpawn 调用序与 execute 序一致)
|
|
170
|
-
const [doneSync1, doneSync2, doneAsync] =
|
|
137
|
+
const [doneSync1, doneSync2, doneAsync] = makeRelease(fake, 3);
|
|
171
138
|
const hSync1 = await service.execute({ task: "s1", slug: "sync-one", collect: "sync" });
|
|
172
139
|
const hSync2 = await service.execute({ task: "s2", slug: "sync-two", collect: "sync" });
|
|
173
140
|
const hAsync = await service.execute({ task: "a1", slug: "async-one" });
|
|
174
|
-
await until(() =>
|
|
141
|
+
await until(() => fake.runs.length >= 3); // 三个替身 run 均已到位
|
|
175
142
|
|
|
176
143
|
// async 成员先终态:立即单条 notify(现状路径),批零投递、零扣留
|
|
177
144
|
doneAsync();
|
|
@@ -198,11 +165,11 @@ describe("A8 混派正交 service e2e(同轮 2 sync + 1 async,U8)", () =>
|
|
|
198
165
|
|
|
199
166
|
it("sync 批先闭合(async 仍在跑):背靠背紧窗口合批单批;async 随后独立单条 notify", async () => {
|
|
200
167
|
const spy = spyNotifier(service);
|
|
201
|
-
const [doneSync1, doneSync2, doneAsync] =
|
|
168
|
+
const [doneSync1, doneSync2, doneAsync] = makeRelease(fake, 3);
|
|
202
169
|
const hSync1 = await service.execute({ task: "s1", slug: "sync-one", collect: "sync" });
|
|
203
170
|
const hSync2 = await service.execute({ task: "s2", slug: "sync-two", collect: "sync" });
|
|
204
171
|
const hAsync = await service.execute({ task: "a1", slug: "async-one" });
|
|
205
|
-
await until(() =>
|
|
172
|
+
await until(() => fake.runs.length >= 3);
|
|
206
173
|
|
|
207
174
|
// 两个 sync 同步段背靠背终态(窄于 finalize 链宽度——U8 拆批盲窗的精确复现形态:
|
|
208
175
|
// 先 archive 的成员 route 时,后者的 notifyComplete 尚在 finalize 链间隙,闭合
|
|
@@ -228,11 +195,11 @@ describe("A8 混派正交 service e2e(同轮 2 sync + 1 async,U8)", () =>
|
|
|
228
195
|
|
|
229
196
|
it("观察者形态:同轮混派三 record 的落盘 entry 各自携带正确 collectMode", async () => {
|
|
230
197
|
const spy = spyNotifier(service);
|
|
231
|
-
const [, , doneAsync] =
|
|
198
|
+
const [, , doneAsync] = makeRelease(fake, 3);
|
|
232
199
|
const hSync1 = await service.execute({ task: "s1", slug: "sync-one", collect: "sync" });
|
|
233
200
|
const hSync2 = await service.execute({ task: "s2", slug: "sync-two", collect: "sync" });
|
|
234
201
|
const hAsync = await service.execute({ task: "a1", slug: "async-one" });
|
|
235
|
-
await until(() =>
|
|
202
|
+
await until(() => fake.runs.length >= 3);
|
|
236
203
|
doneAsync();
|
|
237
204
|
await until(() => spy.notify.mock.calls.length > 0);
|
|
238
205
|
// 批不必闭合即可断言落盘(register 期 entry 即带 collectMode):
|