@zhushanwen/subagent-core 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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,6 +1,6 @@
|
|
|
1
1
|
// golden-replay.zcode.test.ts —— zcode 引擎 golden 回放层(conformance 免 LLM 默认
|
|
2
2
|
// CI 层)。语料 = app-server NDJSON 帧序列(附录 A.2 任务生命周期帧序;采集 SSOT
|
|
3
|
-
// =
|
|
3
|
+
// = @zhushanwen/zcode-subagent-cli src/__golden__/zcode-golden-appserver.json)——双副本 diff
|
|
4
4
|
// 校验(fixture 与 golden-sample.ts 内嵌副本一致,防漂移:更新样本必须同步两处,
|
|
5
5
|
// 探针的干跑校验消费内嵌副本,两处不一致 = 探针在测旧契约)+ 帧形态不变量断言
|
|
6
6
|
// (A8「golden 帧序列语料 diff 通过」的本仓段)。
|
|
@@ -15,13 +15,14 @@ import { fileURLToPath } from "node:url";
|
|
|
15
15
|
|
|
16
16
|
import { describe, expect, it } from "vitest";
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
// 基线三层②(W10):引擎层 golden 随引擎包——内嵌副本经 zcode 包公共导出面
|
|
19
|
+
// 消费,fixture SSOT = zcode 包 src/__golden__/(core 不再深路径 import 内建副本)。
|
|
20
|
+
import { ZCODE_APPSERVER_GOLDEN } from "@zhushanwen/zcode-subagent-cli";
|
|
19
21
|
|
|
20
|
-
// conformance 目录(engine/__tests__/conformance)→
|
|
22
|
+
// conformance 目录(engine/__tests__/conformance)→ 引擎包 __golden__ 的相对定位
|
|
21
23
|
const appserverFixturePath = join(
|
|
22
24
|
dirname(fileURLToPath(import.meta.url)),
|
|
23
|
-
"
|
|
24
|
-
"engines", "zcode", "__tests__", "__fixtures__", "zcode-golden-appserver.json",
|
|
25
|
+
"../../../../../../zcode-subagent-cli/src/__golden__", "zcode-golden-appserver.json",
|
|
25
26
|
);
|
|
26
27
|
|
|
27
28
|
interface AppServerGoldenFile {
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// protocol-blackbox.test.ts —— W10 协议黑盒套件(基线三层①的自动层)。
|
|
2
|
+
//
|
|
3
|
+
// 覆盖(impl-plan §2.10 / 设计 §4 探针挂钩①):
|
|
4
|
+
// - 10 正向方法(initialize/probe/run/cancel/interact/read/listModels/
|
|
5
|
+
// validateModel/dispose/ping)× fake 引擎 CLI 往返;
|
|
6
|
+
// - 8 反向通道(host/log、host/askUser、host/permission、host/streamDelta、
|
|
7
|
+
// host/poolResolved、host/handleReady、host/childSpawned、host/childStateChanged)
|
|
8
|
+
// 的 core 侧到达断言;
|
|
9
|
+
// - 错误帧:run 失败帧 / 未知方法帧(码原样透传)/ initialize 版本越界
|
|
10
|
+
// (engine_protocol_mismatch → 客户端直接不可用,不重建);
|
|
11
|
+
// - fixture 回放结构等价:事件类型序列 / 顺序 / seq 单调 / 字段白名单逐字段比对
|
|
12
|
+
// (排除 text_delta 文本内容等不可复现字段——设计 §4 基线三层①)+ journal 往返
|
|
13
|
+
// 保真 + assertAgentEventInvariants;
|
|
14
|
+
// - POSIX 组探测(R9-4/A3 前提):childSpawned 上报的 pid 做 kill(-pid, 0),
|
|
15
|
+
// 不在同组即告警——本套件验证探测通路;Windows 无外部判据,仅 SDK 层保证
|
|
16
|
+
// (显式登记,见文件末 describe 与 deviations)。
|
|
17
|
+
//
|
|
18
|
+
// 措辞统一(R9-1):「子进程零残留」一律指一代子进程 + 组内后代;引擎自身
|
|
19
|
+
// detached 后代不判 fail(设计 §3.9 已接受代价)。
|
|
20
|
+
|
|
21
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
23
|
+
import { dirname, join } from "node:path";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
26
|
+
|
|
27
|
+
import { EngineClient } from "../../client/engine-client.ts";
|
|
28
|
+
import type { MirrorChangeEvent } from "../../client/mirror.ts";
|
|
29
|
+
import { JournalWriter, replayJournal } from "../../common/event-journal.ts";
|
|
30
|
+
import type { AgentEvent } from "../../../types.ts";
|
|
31
|
+
import { assertAgentEventInvariants } from "./agent-event-invariants.ts";
|
|
32
|
+
|
|
33
|
+
const FIXTURE_DIR = join(dirname(fileURLToPath(import.meta.url)), "__fixtures__", "engine-protocol");
|
|
34
|
+
const FAKE_ENGINE = join(FIXTURE_DIR, "fake-engine-protocol.mjs");
|
|
35
|
+
const FIXTURE = join(FIXTURE_DIR, "smoke-run.fixture.json");
|
|
36
|
+
|
|
37
|
+
/** fixture 的白名单可比形态(record:engine-fixtures 产物 schema v1)。 */
|
|
38
|
+
interface EngineProtocolFixture {
|
|
39
|
+
schemaVersion: number;
|
|
40
|
+
engineId: string;
|
|
41
|
+
protocolVersion: number;
|
|
42
|
+
run: {
|
|
43
|
+
params: { runId: string; ctx: Record<string, unknown> };
|
|
44
|
+
result: { handle: Record<string, unknown>; outcome: Record<string, unknown> };
|
|
45
|
+
};
|
|
46
|
+
events: Array<{ type: string }>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const fixture: EngineProtocolFixture = JSON.parse(readFileSync(FIXTURE, "utf8"));
|
|
50
|
+
if (fixture.schemaVersion !== 1) {
|
|
51
|
+
throw new Error(`unsupported fixture schemaVersion ${String(fixture.schemaVersion)}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** AgentEvent 字段白名单(结构等价比对的逐字段面;text_delta.delta 内容显式排除)。 */
|
|
55
|
+
const FIELD_WHITELIST: Record<string, readonly string[]> = {
|
|
56
|
+
text_delta: ["type", "delta"],
|
|
57
|
+
message_end: ["type", "usage"],
|
|
58
|
+
turn_end: ["type", "usage"],
|
|
59
|
+
error: ["type", "message"],
|
|
60
|
+
tool_start: ["type", "toolName", "input"],
|
|
61
|
+
tool_end: ["type", "toolName", "output", "isError"],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
let dataDir: string;
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
dataDir = mkdtempSync(join(tmpdir(), "w10-engine-protocol-"));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
rmSync(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
interface Harness {
|
|
75
|
+
client: EngineClient;
|
|
76
|
+
events: AgentEvent[];
|
|
77
|
+
mirrorEvents: MirrorChangeEvent[];
|
|
78
|
+
logs: Array<{ level: string; message: string }>;
|
|
79
|
+
askUserRequests: unknown[];
|
|
80
|
+
permissionRequests: unknown[];
|
|
81
|
+
streamDeltas: string[];
|
|
82
|
+
poolResolved: string[];
|
|
83
|
+
handleReady: Array<{ sessionRef: Record<string, string>; poolKey: string }>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function makeHarness(extraEnv: Record<string, string> = {}, routeRunId = fixture.run.params.runId): Harness {
|
|
87
|
+
const h: Harness = {
|
|
88
|
+
client: undefined as unknown as EngineClient,
|
|
89
|
+
events: [],
|
|
90
|
+
mirrorEvents: [],
|
|
91
|
+
logs: [],
|
|
92
|
+
askUserRequests: [],
|
|
93
|
+
permissionRequests: [],
|
|
94
|
+
streamDeltas: [],
|
|
95
|
+
poolResolved: [],
|
|
96
|
+
handleReady: [],
|
|
97
|
+
};
|
|
98
|
+
h.client = new EngineClient({
|
|
99
|
+
engineId: fixture.engineId,
|
|
100
|
+
command: process.execPath,
|
|
101
|
+
args: [FAKE_ENGINE],
|
|
102
|
+
hostKind: "test",
|
|
103
|
+
hostVersion: "w10-protocol-blackbox",
|
|
104
|
+
dataDir,
|
|
105
|
+
envPrefixes: [],
|
|
106
|
+
log: (p) => h.logs.push({ level: p.level, message: p.message }),
|
|
107
|
+
uiRequestHandler: async (req) => {
|
|
108
|
+
h.askUserRequests.push(req);
|
|
109
|
+
return { value: "option-a" };
|
|
110
|
+
},
|
|
111
|
+
permissionHandler: async (params) => {
|
|
112
|
+
h.permissionRequests.push(params);
|
|
113
|
+
return { approved: true };
|
|
114
|
+
},
|
|
115
|
+
onMirrorChanged: (event) => h.mirrorEvents.push(event),
|
|
116
|
+
baseEnv: {
|
|
117
|
+
...process.env,
|
|
118
|
+
FAKE_PROTOCOL_FIXTURE: FIXTURE,
|
|
119
|
+
...extraEnv,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
const unregister = h.client.registerRunRoute(routeRunId, {
|
|
123
|
+
onEvent: (event) => {
|
|
124
|
+
h.events.push(event as AgentEvent);
|
|
125
|
+
},
|
|
126
|
+
onStreamDelta: (delta) => {
|
|
127
|
+
h.streamDeltas.push(delta);
|
|
128
|
+
},
|
|
129
|
+
onPoolResolved: (poolKey) => {
|
|
130
|
+
h.poolResolved.push(poolKey);
|
|
131
|
+
},
|
|
132
|
+
onHandleReady: (partial) => {
|
|
133
|
+
h.handleReady.push(partial);
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
void unregister;
|
|
137
|
+
return h;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
describe("协议黑盒:10 正向方法 × fake 引擎回放", () => {
|
|
141
|
+
it("initialize/probe/listModels/validateModel/interact/read/ping 往返(fixture 白名单比对)", async () => {
|
|
142
|
+
const h = makeHarness();
|
|
143
|
+
try {
|
|
144
|
+
await h.client.ensureConnected();
|
|
145
|
+
expect(h.client.currentState).toBe("ready");
|
|
146
|
+
|
|
147
|
+
const probe = (await h.client.request("probe", {}, { timeoutMs: 5_000 })) as {
|
|
148
|
+
ok: boolean; checks: Array<{ name: string; ok: boolean }>;
|
|
149
|
+
};
|
|
150
|
+
expect(probe.ok).toBe(true);
|
|
151
|
+
expect(probe.checks.every((c) => typeof c.name === "string" && typeof c.ok === "boolean")).toBe(true);
|
|
152
|
+
|
|
153
|
+
const models = (await h.client.request("listModels", {}, { timeoutMs: 5_000 })) as {
|
|
154
|
+
models: Array<{ id: string }>;
|
|
155
|
+
};
|
|
156
|
+
expect(models.models.map((m) => m.id)).toEqual(["fake-1"]);
|
|
157
|
+
|
|
158
|
+
const validated = (await h.client.request(
|
|
159
|
+
"validateModel",
|
|
160
|
+
{ modelRef: "fake/fake-1" },
|
|
161
|
+
{ timeoutMs: 5_000 },
|
|
162
|
+
)) as { canonicalRef: string };
|
|
163
|
+
expect(validated.canonicalRef).toBe("fake/fake-1");
|
|
164
|
+
|
|
165
|
+
const interact = (await h.client.request(
|
|
166
|
+
"interact",
|
|
167
|
+
{ handle: fixture.run.result.handle, action: { kind: "message", text: "hi" } },
|
|
168
|
+
{ timeoutMs: 5_000 },
|
|
169
|
+
)) as { accepted: boolean };
|
|
170
|
+
expect(interact.accepted).toBe(true);
|
|
171
|
+
|
|
172
|
+
const view = (await h.client.request(
|
|
173
|
+
"read",
|
|
174
|
+
{ handle: fixture.run.result.handle, dataDir },
|
|
175
|
+
{ timeoutMs: 5_000 },
|
|
176
|
+
)) as { entries: unknown[] };
|
|
177
|
+
expect(Array.isArray(view.entries)).toBe(true);
|
|
178
|
+
|
|
179
|
+
await h.client.ping(); // 诊断面:不 throw 即通
|
|
180
|
+
} finally {
|
|
181
|
+
await h.client.dispose();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("run:终态应答 handle+outcome 与 fixture 白名单逐字段一致", async () => {
|
|
186
|
+
const h = makeHarness();
|
|
187
|
+
try {
|
|
188
|
+
await h.client.ensureConnected();
|
|
189
|
+
const result = (await h.client.request("run", fixture.run.params)) as {
|
|
190
|
+
handle: Record<string, unknown>;
|
|
191
|
+
outcome: Record<string, unknown>;
|
|
192
|
+
};
|
|
193
|
+
expect(result.handle).toEqual(fixture.run.result.handle);
|
|
194
|
+
expect(result.outcome).toEqual(fixture.run.result.outcome);
|
|
195
|
+
} finally {
|
|
196
|
+
await h.client.dispose();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("cancel:受理应答(收敛语义由 W2 client 套件的 3s 窗口用例覆盖,此处断言帧往返)", async () => {
|
|
201
|
+
const h = makeHarness();
|
|
202
|
+
try {
|
|
203
|
+
await h.client.ensureConnected();
|
|
204
|
+
const ack = (await h.client.request(
|
|
205
|
+
"cancel",
|
|
206
|
+
{ runId: "run-smoke-1", reason: "test" },
|
|
207
|
+
{ timeoutMs: 5_000 },
|
|
208
|
+
)) as { ok: boolean };
|
|
209
|
+
expect(ack.ok).toBe(true);
|
|
210
|
+
} finally {
|
|
211
|
+
await h.client.dispose();
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("dispose:幂等收口(应答 ok + 进程退出)", async () => {
|
|
216
|
+
const h = makeHarness();
|
|
217
|
+
await h.client.ensureConnected();
|
|
218
|
+
const r = (await h.client.request("dispose", {}, { timeoutMs: 5_000 })) as { ok: boolean };
|
|
219
|
+
expect(r.ok).toBe(true);
|
|
220
|
+
await h.client.dispose();
|
|
221
|
+
await expect(h.client.dispose()).resolves.toBeUndefined();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("协议黑盒:8 反向通道 core 侧到达", () => {
|
|
226
|
+
it("run 期间全部 8 通道到达并被正确路由", async () => {
|
|
227
|
+
const h = makeHarness();
|
|
228
|
+
try {
|
|
229
|
+
await h.client.ensureConnected();
|
|
230
|
+
await h.client.request("run", fixture.run.params);
|
|
231
|
+
|
|
232
|
+
// host/log
|
|
233
|
+
expect(h.logs.some((l) => l.message.includes("run started"))).toBe(true);
|
|
234
|
+
// host/streamDelta
|
|
235
|
+
expect(h.streamDeltas).toEqual(["Hello", " world"]);
|
|
236
|
+
// host/poolResolved
|
|
237
|
+
expect(h.poolResolved).toEqual(["shared"]);
|
|
238
|
+
// host/handleReady
|
|
239
|
+
expect(h.handleReady).toEqual([{ sessionRef: { sessionId: "sess-fixture" }, poolKey: "shared" }]);
|
|
240
|
+
// host/childSpawned + host/childStateChanged(镜像落项 + 状态更新)
|
|
241
|
+
const spawned = h.mirrorEvents.filter((e) => e.reason === "childSpawned");
|
|
242
|
+
expect(spawned).toHaveLength(1);
|
|
243
|
+
expect(typeof spawned[0]?.pid).toBe("number");
|
|
244
|
+
const stateChanged = h.mirrorEvents.filter((e) => e.reason === "childStateChanged");
|
|
245
|
+
expect(stateChanged).toHaveLength(1);
|
|
246
|
+
expect(stateChanged[0]?.recordId).toBe(fixture.run.params.runId);
|
|
247
|
+
// host/askUser + host/permission(交互面 ack 两阶段)
|
|
248
|
+
expect(h.askUserRequests).toHaveLength(1);
|
|
249
|
+
expect(h.permissionRequests).toHaveLength(1);
|
|
250
|
+
} finally {
|
|
251
|
+
await h.client.dispose();
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe("协议黑盒:错误帧", () => {
|
|
257
|
+
it("run 失败帧 → EngineSdkError(code/message/recovery 透传)", async () => {
|
|
258
|
+
const h = makeHarness({ FAKE_PROTOCOL_ERROR: "run_failed" });
|
|
259
|
+
try {
|
|
260
|
+
await h.client.ensureConnected();
|
|
261
|
+
await expect(h.client.request("run", fixture.run.params)).rejects.toMatchObject({
|
|
262
|
+
code: "engine_run_failed",
|
|
263
|
+
message: expect.stringContaining("scripted run failure"),
|
|
264
|
+
});
|
|
265
|
+
} finally {
|
|
266
|
+
await h.client.dispose();
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("未知方法帧 → 错误码原样透传(帧形态权威,非码表枚举)", async () => {
|
|
271
|
+
const h = makeHarness({ FAKE_PROTOCOL_ERROR: "unknown_method" });
|
|
272
|
+
try {
|
|
273
|
+
await h.client.ensureConnected();
|
|
274
|
+
await expect(h.client.request("bogus/method", {}, { timeoutMs: 5_000 })).rejects.toMatchObject({
|
|
275
|
+
code: "engine_method_unknown",
|
|
276
|
+
});
|
|
277
|
+
} finally {
|
|
278
|
+
await h.client.dispose();
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("initialize 版本越界 → engine_protocol_mismatch + 客户端直接不可用(不重建)", async () => {
|
|
283
|
+
const h = makeHarness({ FAKE_PROTOCOL_VERSION: "99" });
|
|
284
|
+
try {
|
|
285
|
+
await expect(h.client.ensureConnected()).rejects.toMatchObject({ code: "engine_protocol_mismatch" });
|
|
286
|
+
expect(h.client.currentState).toBe("unavailable");
|
|
287
|
+
} finally {
|
|
288
|
+
await h.client.dispose();
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe("协议黑盒:fixture 回放结构等价(基线三层①)", () => {
|
|
294
|
+
it("事件类型序列/顺序/seq 单调/字段白名单(排除 text_delta 文本内容)", async () => {
|
|
295
|
+
const h = makeHarness();
|
|
296
|
+
try {
|
|
297
|
+
await h.client.ensureConnected();
|
|
298
|
+
const result = (await h.client.request("run", fixture.run.params)) as {
|
|
299
|
+
outcome: { content: string };
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// ① 类型序列 + 顺序(与 fixture.events 白名单形态逐项一致)
|
|
303
|
+
expect(h.events.map((e) => e.type)).toEqual(fixture.events.map((e) => e.type));
|
|
304
|
+
|
|
305
|
+
// ② seq 单调(协议通知帧的 seq 字段经 fake 引擎按 fixture 播放)
|
|
306
|
+
// ——事件流本身不透传 seq,单调性由通知帧序承担;此处断言到达顺序稳定。
|
|
307
|
+
|
|
308
|
+
// ③ 字段白名单:每条事件的实际键 ⊆ 白名单
|
|
309
|
+
for (const event of h.events) {
|
|
310
|
+
const whitelist = FIELD_WHITELIST[event.type];
|
|
311
|
+
expect(whitelist, `missing whitelist for event type ${event.type}`).toBeDefined();
|
|
312
|
+
for (const key of Object.keys(event)) {
|
|
313
|
+
expect(whitelist).toContain(key);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ④ 排除项自证:text_delta 文本内容参与流式拼接不变量(3a),但内容本身
|
|
318
|
+
// 不做逐字段 diff(不可复现字段——设计 §4)
|
|
319
|
+
assertAgentEventInvariants(h.events, { granularity: "stream", content: result.outcome.content });
|
|
320
|
+
|
|
321
|
+
// ⑤ journal 往返保真(回放层事件落 journal 后重读等价)
|
|
322
|
+
const journalPath = join(dataDir, "blackbox-journal.jsonl");
|
|
323
|
+
const writer = new JournalWriter({
|
|
324
|
+
path: journalPath,
|
|
325
|
+
taskId: "w10-blackbox",
|
|
326
|
+
engineId: fixture.engineId,
|
|
327
|
+
});
|
|
328
|
+
for (const event of h.events) writer.append(event);
|
|
329
|
+
await writer.close();
|
|
330
|
+
const replayed = replayJournal(journalPath) as AgentEvent[];
|
|
331
|
+
expect(replayed.map((e) => e.type)).toEqual(h.events.map((e) => e.type));
|
|
332
|
+
expect(JSON.stringify(replayed)).toBe(JSON.stringify(h.events));
|
|
333
|
+
} finally {
|
|
334
|
+
await h.client.dispose();
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("录制产物回放校验:recorded.fixture.json(record:engine-fixtures 产物)结构不变量", async () => {
|
|
339
|
+
// 录制/复跑闭环(设计 §4):录制脚本产物 schemaVersion 1 + 脱敏不变量 +
|
|
340
|
+
// 事件通知 seq 单调 + 反向通道覆盖。真机引擎录制时产物替换本文件即可把同一
|
|
341
|
+
// 套结构断言升格为该引擎的协议层基线(不逐字段 diff 正文)。
|
|
342
|
+
const recorded = JSON.parse(
|
|
343
|
+
readFileSync(join(FIXTURE_DIR, "recorded.fixture.json"), "utf8"),
|
|
344
|
+
) as {
|
|
345
|
+
schemaVersion: number;
|
|
346
|
+
wire: {
|
|
347
|
+
engineToHost: Array<{ method?: string; params?: { seq?: number; runId?: string } }>;
|
|
348
|
+
hostToEngine: Array<{ id: number | string; method: string }>;
|
|
349
|
+
};
|
|
350
|
+
events: Array<{ type: string }>;
|
|
351
|
+
};
|
|
352
|
+
expect(recorded.schemaVersion).toBe(1);
|
|
353
|
+
|
|
354
|
+
// 事件通知 seq 单调(同 runId)
|
|
355
|
+
const seqs = recorded.wire.engineToHost
|
|
356
|
+
.filter((f) => f.method === "event" && typeof f.params?.seq === "number")
|
|
357
|
+
.map((f) => f.params!.seq!);
|
|
358
|
+
expect(seqs).toEqual([...seqs].sort((a, b) => a - b));
|
|
359
|
+
|
|
360
|
+
// 反向通道覆盖(录制期间 8 通道全部上线)
|
|
361
|
+
const reverse = new Set(
|
|
362
|
+
recorded.wire.engineToHost
|
|
363
|
+
.filter((f) => typeof (f as { id?: string }).id === "string")
|
|
364
|
+
.map((f) => (f as { method: string }).method),
|
|
365
|
+
);
|
|
366
|
+
for (const ch of [
|
|
367
|
+
"host/log", "host/askUser", "host/permission", "host/streamDelta",
|
|
368
|
+
"host/poolResolved", "host/handleReady", "host/childSpawned", "host/childStateChanged",
|
|
369
|
+
]) {
|
|
370
|
+
expect(reverse.has(ch)).toBe(true);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// 结构白名单形态与 curated fixture 同源
|
|
374
|
+
expect(recorded.events).toEqual(fixture.events);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("POSIX 组探测:childSpawned 上报 pid 非组长(在引擎收割组内;A3 前提;Windows 显式跳过)", async () => {
|
|
378
|
+
if (process.platform === "win32") {
|
|
379
|
+
// Windows 无外部组判据(无 kill(-pid,0) 等价物)——仅 SDK 层 spawnEngineChild
|
|
380
|
+
// 的 detached/stdio 规约保证,真机面挂 A3 手动门(taskkill /T /F 形态)。
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const h = makeHarness();
|
|
384
|
+
try {
|
|
385
|
+
await h.client.ensureConnected();
|
|
386
|
+
await h.client.request("run", fixture.run.params);
|
|
387
|
+
const spawned = h.mirrorEvents.find((e) => e.reason === "childSpawned");
|
|
388
|
+
expect(spawned?.pid).toBeDefined();
|
|
389
|
+
// 预期形态(reverse-router 组探测同款语义):任务子进程与引擎同组(收割 =
|
|
390
|
+
// 引擎组级 kill(-enginePid))→ 该 pid 非组长 → kill(-pid,0) ESRCH。
|
|
391
|
+
// kill(-pid,0) 成功 = 自成进程组 = 不在收割组内 = 告警面(R9-1 限定:一代
|
|
392
|
+
// 子进程 + 组内后代;引擎自身 detached 后代不判 fail,设计 §3.9 已接受代价)。
|
|
393
|
+
expect(() => process.kill(-spawned!.pid!, 0)).toThrow();
|
|
394
|
+
} finally {
|
|
395
|
+
await h.client.dispose();
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// registry-fork-filter.test.ts —— [W6] 翻档 × fork 继承残留的读侧过滤契约 +
|
|
2
|
+
// bash 跨 session 可见性显式断言(R4 一刀语义钉住)+ 偏差 #4 探针(goal 守卫口径)。
|
|
3
|
+
//
|
|
4
|
+
// 设计权威源:chat-domain-v1x-liveness-governance.md §3.2 D4(翻档三连带——读侧
|
|
5
|
+
// 过滤三口 + PENDING_LIFECYCLE 翻 process 档)+ 修订记录 v5-⑤(bash 跨 session
|
|
6
|
+
// 可见性一刀钉成显式选择)+ 验收 A9② + impl-plan §5 偏差 #4(W4 交接:goal 守卫
|
|
7
|
+
// 消费点未传基准,W6 实测裁决是否需 goal 侧两行传参)。
|
|
8
|
+
//
|
|
9
|
+
// 被测函数 = `extensions/universal/pending-notifications/src/state.ts` 的导出纯函数
|
|
10
|
+
// (零依赖、不触 Pi 运行时——文件头注自证;经相对路径源码消费,subagent-core 无该
|
|
11
|
+
// workspace 依赖声明,不加 phantom dep)。分工:
|
|
12
|
+
// - 过滤函数本体 / rebuild 投影的两口径行为:本套件钉语义(conformance 视角);
|
|
13
|
+
// - 过滤①②③的**接线**(goal/subagent-workflow/pending-notifications 三消费点
|
|
14
|
+
// 透传 currentSessionId):W4 包内测试已承载(pi-pending-notifications
|
|
15
|
+
// __tests__ :523/:532、subagent-workflow pi-host.test :199-223);
|
|
16
|
+
// - 后代判定口(读侧过滤②)的差集口径与①共用同一函数——本套件①的断言即其
|
|
17
|
+
// 语义核心;接线透传由上述 W4 测试守护。
|
|
18
|
+
//
|
|
19
|
+
// 探针结论(偏差 #4,实测证据 = 「goal 守卫口径」describe):**有虚增**——fork 后
|
|
20
|
+
// 父 session 仍活跃的后台任务残留使 goal 守卫现状口径(无基准)幻 defer;确需的
|
|
21
|
+
// 两行传参改动点列于该 describe 注释,留给主会话裁决(本单元不改 goal——红线)。
|
|
22
|
+
|
|
23
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { tmpdir } from "node:os";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
27
|
+
|
|
28
|
+
// 相对路径源码消费(7 层上溯到仓库根):state.ts 是零依赖纯函数(不触 Pi 运行时),
|
|
29
|
+
// 无需为探针/契约测试给 subagent-core 添加 workspace 依赖声明。
|
|
30
|
+
import {
|
|
31
|
+
PENDING_LIFECYCLE,
|
|
32
|
+
PENDING_TTL_MS,
|
|
33
|
+
countActiveFromEntries,
|
|
34
|
+
createRegistry,
|
|
35
|
+
rebuildFromEntries,
|
|
36
|
+
} from "../../../../../../../extensions/universal/pending-notifications/src/state.ts";
|
|
37
|
+
|
|
38
|
+
import { runReconcileSweep } from "../../../round-supervisor/index.ts";
|
|
39
|
+
|
|
40
|
+
/** pending:register entry 的落盘形态(对齐 pending-notifications 写入侧契约)。 */
|
|
41
|
+
function registerEntry(
|
|
42
|
+
id: string,
|
|
43
|
+
type: "subagent" | "workflow" | "bash",
|
|
44
|
+
sessionId: string,
|
|
45
|
+
overrides: { expiresAt?: number } = {},
|
|
46
|
+
): { customType: string; data: Record<string, unknown> } {
|
|
47
|
+
return {
|
|
48
|
+
customType: "pending:register",
|
|
49
|
+
data: {
|
|
50
|
+
id,
|
|
51
|
+
type,
|
|
52
|
+
name: id,
|
|
53
|
+
registeredAt: 1_000,
|
|
54
|
+
sessionId,
|
|
55
|
+
// bash 真实落盘无 expiresAt 键(写入侧省略,D16);subagent/workflow 翻
|
|
56
|
+
// process 档后同理——overrides 仅用于 TTL 对照面。
|
|
57
|
+
...(overrides.expiresAt !== undefined ? { expiresAt: overrides.expiresAt } : {}),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 语义对照面的基准时刻(远超旧 1h TTL——钉「无 TTL 清理」而非「窗口内未到期」假绿)。 */
|
|
63
|
+
const NOW = 10 * PENDING_TTL_MS + 1;
|
|
64
|
+
|
|
65
|
+
let tmpDirs: string[] = [];
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
for (const dir of tmpDirs) rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
69
|
+
tmpDirs = [];
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("[W6/D4 翻档] PENDING_LIFECYCLE 三类型全 process 档(跨时长无 TTL 清理的判据锚)", () => {
|
|
73
|
+
it("subagent/workflow 翻 process 档(bash 原生 process 档)——翻档事实钉住", () => {
|
|
74
|
+
expect(PENDING_LIFECYCLE).toEqual({ subagent: "process", workflow: "process", bash: "process" });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("翻档后跨时长(远超旧 1h TTL)无 TTL 清理:三类型 register 均不进 expiredToFlush", () => {
|
|
78
|
+
const registry = createRegistry();
|
|
79
|
+
const entries = [
|
|
80
|
+
registerEntry("bg-1", "subagent", "sess-root", { expiresAt: 1_000 + 1 }), // 远过去
|
|
81
|
+
registerEntry("wf-1", "workflow", "sess-root", { expiresAt: 1_000 + 1 }),
|
|
82
|
+
registerEntry("bt-1", "bash", "sess-root"), // 真实落盘形态:无 expiresAt 键
|
|
83
|
+
];
|
|
84
|
+
const result = rebuildFromEntries(registry, entries, "sess-root", NOW);
|
|
85
|
+
// 旧 session 档语义(1h TTL + 跨 session 补注销)曾把长任务/跨重启注册静默
|
|
86
|
+
// 清除 → 守卫失明(事故环 4 放大器);翻档后三类型全部续存。
|
|
87
|
+
expect(result.expiredToFlush).toEqual([]);
|
|
88
|
+
expect(result.activeIds).toEqual(["bg-1", "wf-1", "bt-1"]);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("[W6/D4 读侧过滤①] fork 继承残留 × countActiveFromEntries 基准", () => {
|
|
93
|
+
it("带基准(currentSessionId)→ 父 session 残留不入差集、本 session 注册计入", () => {
|
|
94
|
+
const entries = [
|
|
95
|
+
registerEntry("parent-bg", "subagent", "sess-parent"),
|
|
96
|
+
registerEntry("own-bg", "subagent", "sess-child"),
|
|
97
|
+
registerEntry("own-bt", "bash", "sess-child"),
|
|
98
|
+
];
|
|
99
|
+
const result = countActiveFromEntries(entries, { currentSessionId: "sess-child" });
|
|
100
|
+
expect(result.ids).toEqual(["own-bg", "own-bt"]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("缺 sessionId 的旧形态条目视为本 session(宁放行不误杀——防误逐活跃计数)", () => {
|
|
104
|
+
const entries = [{ customType: "pending:register", data: { id: "legacy-bg", type: "subagent" } }];
|
|
105
|
+
expect(countActiveFromEntries(entries, { currentSessionId: "sess-child" }).ids).toEqual(["legacy-bg"]);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("[W6/R4 一刀] bash 跨 session 可见性显式断言(钉成显式选择的语义变更)", () => {
|
|
110
|
+
it("读侧过滤一刀后:子 session 的 goal 不再为父 session 的 bash 任务 defer(带基准口径)", () => {
|
|
111
|
+
// fork 继承:子 session entries 复制父 session 的 bash 后台任务注册
|
|
112
|
+
// (process 档跨 session 存活——U4 补注销对翻档类型不再中性化,残留永久留存)。
|
|
113
|
+
const childEntries = [
|
|
114
|
+
registerEntry("parent-bt", "bash", "sess-parent"),
|
|
115
|
+
registerEntry("child-bt", "bash", "sess-child"),
|
|
116
|
+
];
|
|
117
|
+
// 设计选择(R4):按当前 session 过滤——父 session 的 bash 任务不再让子
|
|
118
|
+
// session 的 goal 守卫 defer(子 session 有自己的等待域)。
|
|
119
|
+
const scoped = countActiveFromEntries(childEntries, { currentSessionId: "sess-child" });
|
|
120
|
+
expect(scoped.ids).toEqual(["child-bt"]);
|
|
121
|
+
expect(scoped.count).toBe(1); // 只数本 session 的任务
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("旧行为显式登记:无基准口径(goal 守卫现状)父 bash 残留仍入计数(= defer 驱动)", () => {
|
|
125
|
+
const childEntries = [registerEntry("parent-bt", "bash", "sess-parent")];
|
|
126
|
+
// 显式钉住「一刀的对照面」:不传基准 = 不过滤 = 子 session goal 仍为父 bash
|
|
127
|
+
// 任务 defer。这是读侧过滤一刀**改变的旧行为**——保留此断言防止有人误以为
|
|
128
|
+
// 过滤是缺省语义(偏差 #4:goal 消费点现状即此口径,见探针结论)。
|
|
129
|
+
expect(countActiveFromEntries(childEntries).count).toBe(1);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("[W6/D4 读侧过滤③] registry rebuild 投影(pending_notifications 工具读侧)", () => {
|
|
134
|
+
it("跨 session 残留不入 registry、不补 unregister entry(跳过不补注销——落盘收口归 core sweep)", () => {
|
|
135
|
+
const registry = createRegistry();
|
|
136
|
+
const entries = [
|
|
137
|
+
registerEntry("parent-bg", "subagent", "sess-parent"),
|
|
138
|
+
registerEntry("own-bg", "subagent", "sess-child"),
|
|
139
|
+
];
|
|
140
|
+
const result = rebuildFromEntries(registry, entries, "sess-child", NOW);
|
|
141
|
+
// 工具投影面(count/list)不虚报继承残留(A9②:count/list 不含继承残留)。
|
|
142
|
+
expect(result.activeIds).toEqual(["own-bg"]);
|
|
143
|
+
expect(registry.operations.has("parent-bg")).toBe(false);
|
|
144
|
+
// 「跳过不补注销」:残留 entry 留在 session 文件(读侧①③口各自兜住差集消费
|
|
145
|
+
// 方),禁止 rebuild 写侧补发——跨 session 写达域缺口由 core 对账 sweep 收口。
|
|
146
|
+
expect(result.expiredToFlush).toEqual([]);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("[W6/偏差 #4 探针] goal 守卫口径(无基准)× fork 残留——实测结论", () => {
|
|
151
|
+
// 偏差 #4(impl-plan §5,W4 交接):W4 读侧过滤①的 currentSessionId 为可选参数,
|
|
152
|
+
// goal 守卫消费点未传基准。本探针在 fork 场景实测该口径是否被父 session 注册
|
|
153
|
+
// 残留虚增,结论二选一回写报告:
|
|
154
|
+
// (a) 无影响(其他机制覆盖)→ 偏差 #4 关闭,goal 零改动;
|
|
155
|
+
// (b) 有虚增 → 列出确需的两行传参改动点(不改 goal——红线),留主会话裁决。
|
|
156
|
+
//
|
|
157
|
+
// **实测结论:(b) 有虚增。**
|
|
158
|
+
// 证据链:goal 守卫两处消费点现状均无基准调用——
|
|
159
|
+
// `extensions/universal/goal/src/adapters/event-handlers/agent-end.ts:198`
|
|
160
|
+
// `const pendingOps = countActiveFromEntries(entries);`(defer 分支判据
|
|
161
|
+
// = `pendingOps.count > 0`)
|
|
162
|
+
// `extensions/universal/goal/src/adapters/event-handlers/agent-end.ts:294`
|
|
163
|
+
// `if (countActiveFromEntries(ctx.sessionManager.getEntries()).count > 0) return;`
|
|
164
|
+
// (backoff 延迟发射前守卫)
|
|
165
|
+
// 下方用例 1 证明该口径在 fork 残留下 count>0 → defer 命中(幻 defer);
|
|
166
|
+
// 用例 2 证明仅当父任务 record 已终态(core sweep 补注销落盘)才归零——
|
|
167
|
+
// 「父任务仍活跃」窗口内无任何机制覆盖(W4 翻档后 U4 不再中性化)。
|
|
168
|
+
//
|
|
169
|
+
// 确需的两行改动点(主会话裁决后由 goal 侧单元执行):
|
|
170
|
+
// agent-end.ts:198 → `countActiveFromEntries(entries, { currentSessionId: ctx.sessionManager.getSessionId() })`
|
|
171
|
+
// agent-end.ts:294 → `countActiveFromEntries(ctx.sessionManager.getEntries(), { currentSessionId: ctx.sessionManager.getSessionId() })`
|
|
172
|
+
// (getSessionId 是 extension 侧既有取 sessionId 形态——pending-notifications
|
|
173
|
+
// index.ts:216、subagent-workflow session-lifecycle.ts 同款先例。)
|
|
174
|
+
|
|
175
|
+
it("用例 1:父 session 活跃 subagent 残留 → goal 现状口径(无基准)count>0 → defer 分支命中(虚增)", () => {
|
|
176
|
+
// fork 时刻快照:父 session 派了后台子代理(record 仍 running-resumable),
|
|
177
|
+
// 子 session 继承其 register 残留;goal 守卫读到的 entries = 本 session 文件
|
|
178
|
+
// 全量(含继承残留)。现状口径 = 无基准调用(agent-end.ts:198 逐字形态)。
|
|
179
|
+
const childEntries = [
|
|
180
|
+
registerEntry("parent-bg", "subagent", "sess-parent"),
|
|
181
|
+
registerEntry("child-bg", "subagent", "sess-child"),
|
|
182
|
+
];
|
|
183
|
+
const pendingOps = countActiveFromEntries(childEntries); // ← goal 现状口径
|
|
184
|
+
// defer 分支判据(agent-end.ts:199 `if (pendingOps.count > 0)`)命中:
|
|
185
|
+
// 父 session 的任务让子 session 的 goal 停发 continuation 并 notify
|
|
186
|
+
// 「Goal waiting for 2 background task(s)」——计数含非本 session 任务 = 虚增。
|
|
187
|
+
expect(pendingOps.count).toBe(2);
|
|
188
|
+
expect(pendingOps.ids).toContain("parent-bg");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("用例 2:父任务 record 终态后,core sweep 补注销落盘使现状口径归零(虚增仅限活跃窗口)", () => {
|
|
192
|
+
const dir = mkdtempSync(join(tmpdir(), "w6-goal-probe-sweep-"));
|
|
193
|
+
tmpDirs.push(dir);
|
|
194
|
+
const sessionFile = join(dir, "session-child.jsonl");
|
|
195
|
+
writeFileSync(
|
|
196
|
+
sessionFile,
|
|
197
|
+
`${JSON.stringify(registerEntry("parent-bg", "subagent", "sess-parent"))}\n`,
|
|
198
|
+
"utf8",
|
|
199
|
+
);
|
|
200
|
+
// 父任务 record 已终态(closed):core 对账 sweep 差集补发——appendEntry 权威
|
|
201
|
+
// 落盘到子 session 文件(sweep 的注销对「无基准口径」也生效:差集读侧不过滤,
|
|
202
|
+
// 补发的 unregister entry 抵消 register)。
|
|
203
|
+
const result = runReconcileSweep({
|
|
204
|
+
sessionFile,
|
|
205
|
+
lookupRecordState: () => ({ terminal: true, closedReason: "completed" }),
|
|
206
|
+
appendEntry: (customType, data) => {
|
|
207
|
+
writeFileSync(sessionFile, `${JSON.stringify({ customType, data })}\n`, { flag: "a" });
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
expect(result.reconciled).toEqual(["parent-bg"]);
|
|
211
|
+
|
|
212
|
+
// 落盘后 goal 守卫(无基准口径)重读 entries:计数归零——sweep 是现状口径的
|
|
213
|
+
// 唯一收敛通道,且只覆盖「record 可判定终态」的窗口;「父任务仍活跃」窗口
|
|
214
|
+
// (用例 1)无机制覆盖 → 虚增结论 (b) 成立。
|
|
215
|
+
const entriesAfter = readFileSync(sessionFile, "utf8")
|
|
216
|
+
.trim()
|
|
217
|
+
.split("\n")
|
|
218
|
+
.map((l) => JSON.parse(l));
|
|
219
|
+
expect(countActiveFromEntries(entriesAfter).count).toBe(0);
|
|
220
|
+
});
|
|
221
|
+
});
|