@zhushanwen/subagent-core 0.7.1 → 0.9.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/dist/{chunk-VURUAGMM.js → chunk-LZJOEDGQ.js} +603 -154
- package/dist/engine-discovery-scan-Da4Dkzsi.d.cts +818 -0
- package/dist/engine-discovery-scan-Da4Dkzsi.d.ts +818 -0
- package/dist/execution/engine/engine-discovery-scan.cjs +325 -154
- package/dist/execution/engine/engine-discovery-scan.d.cts +1 -2
- package/dist/execution/engine/engine-discovery-scan.d.ts +1 -2
- package/dist/execution/engine/engine-discovery-scan.js +1 -1
- package/dist/execution/relay-env.js +5 -4
- package/dist/index.cjs +9799 -7954
- package/dist/index.d.cts +1896 -1360
- package/dist/index.d.ts +1896 -1360
- package/dist/index.js +8952 -7513
- package/dist.bundle/index.cjs +7897 -6054
- package/package.json +5 -5
- package/src/__tests__/manifest-store.test.ts +16 -15
- package/src/__tests__/record-store-last-line.test.ts +12 -0
- package/src/__tests__/robustness-medium-batch1.test.ts +10 -14
- package/src/__tests__/robustness-medium-batch2.test.ts +7 -2
- package/src/core/logger.ts +1 -1
- package/src/execution/__tests__/alive-store.test.ts +117 -1
- package/src/execution/__tests__/batch-finalized.test.ts +206 -0
- package/src/execution/__tests__/chat-engine-routing.test.ts +117 -1
- package/src/execution/__tests__/{cold-resurrect.test.ts → cold-lookup.test.ts} +133 -33
- package/src/execution/__tests__/collect-coordinator-service.test.ts +7 -5
- package/src/execution/__tests__/conversation-continuation.test.ts +1452 -0
- package/src/execution/__tests__/delivery-methods.test.ts +90 -176
- package/src/execution/__tests__/dialog-queue.test.ts +70 -3
- package/src/execution/__tests__/dispose-manifest-recovery.test.ts +475 -0
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +3 -3
- package/src/execution/__tests__/execute-nesting.test.ts +8 -5
- package/src/execution/__tests__/execution-record.test.ts +8 -148
- package/src/execution/__tests__/finalize-record.test.ts +295 -129
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +21 -29
- package/src/execution/__tests__/helpers/fake-engine-port.ts +13 -68
- package/src/execution/__tests__/helpers/legacy-sidecar.ts +33 -0
- package/src/execution/__tests__/helpers/subagent-service-mocks.ts +9 -5
- package/src/execution/__tests__/idle-gc.test.ts +60 -0
- package/src/execution/__tests__/inflight-production-wiring.test.ts +223 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +3 -254
- package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +43 -22
- package/src/execution/__tests__/model-config-service.test.ts +111 -0
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +10 -6
- package/src/execution/__tests__/nested-visibility.test.ts +13 -1
- package/src/execution/__tests__/notify-ledger.test.ts +42 -1
- package/src/execution/__tests__/rebuild-indexes.test.ts +329 -0
- package/src/execution/__tests__/record-binding.test.ts +613 -0
- package/src/execution/__tests__/record-origin.test.ts +318 -0
- package/src/execution/__tests__/record-store-intent-api.test.ts +600 -0
- package/src/execution/__tests__/record-store-orphan-revive.test.ts +71 -3
- package/src/execution/__tests__/record-store.test.ts +72 -54
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +8 -5
- package/src/execution/__tests__/round-supervisor-workflow-origin.test.ts +205 -0
- package/src/execution/__tests__/run-orchestration-write-lease.test.ts +293 -0
- package/src/execution/__tests__/run-orchestration.test.ts +199 -0
- package/src/execution/__tests__/session-file-gc.test.ts +35 -4
- package/src/execution/__tests__/session-reconstructor.test.ts +78 -0
- package/src/execution/__tests__/state-marker.test.ts +288 -0
- package/src/execution/__tests__/stream-sink-retirement.test.ts +8 -5
- package/src/execution/__tests__/subagent-actions-core.test.ts +67 -16
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +0 -1
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +27 -11
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +17 -11
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +130 -53
- package/src/execution/__tests__/subagent-service.test.ts +17 -215
- package/src/execution/__tests__/subprocess-agent-runner-no-progress-killall.test.ts +138 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +105 -660
- package/src/execution/__tests__/sync-collect-recovery.test.ts +23 -16
- package/src/execution/__tests__/terminal-write-latency.test.ts +79 -0
- package/src/execution/__tests__/workflow-agent-dispatch.test.ts +782 -0
- package/src/execution/alive-store.ts +37 -28
- package/src/execution/{cold-resurrect.ts → cold-lookup.ts} +48 -45
- package/src/execution/config.ts +1 -1
- package/src/execution/conversation-continuation.ts +541 -0
- package/src/execution/dialog-queue.ts +60 -12
- package/src/execution/engine/__tests__/common/capability-gate.test.ts +1 -1
- package/src/execution/engine/__tests__/common/journal-wiring.test.ts +16 -1
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +17 -51
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +0 -5
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +1 -1
- package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +5 -9
- package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +55 -53
- package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +0 -3
- package/src/execution/engine/__tests__/engine-discovery.test.ts +0 -3
- package/src/execution/engine/__tests__/inflight-snapshot.test.ts +180 -0
- package/src/execution/engine/__tests__/model-prompt.test.ts +0 -3
- package/src/execution/engine/__tests__/registry.test.ts +0 -1
- package/src/execution/engine/__tests__/routing.test.ts +0 -1
- package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +0 -3
- package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +0 -3
- package/src/execution/engine/client/__tests__/engine-client-reap.test.ts +212 -0
- package/src/execution/engine/client/__tests__/mirror.test.ts +73 -1
- package/src/execution/engine/client/__tests__/protocol-closure.test.ts +2 -9
- package/src/execution/engine/client/__tests__/remote-engine.test.ts +46 -12
- package/src/execution/engine/client/engine-client.ts +111 -26
- package/src/execution/engine/client/mirror.ts +9 -0
- package/src/execution/engine/client/remote-engine.ts +52 -34
- package/src/execution/engine/client/reverse-router.ts +6 -27
- package/src/execution/engine/common/__tests__/run-signals.test.ts +79 -0
- package/src/execution/engine/common/capability-gate.ts +27 -0
- package/src/execution/engine/common/data-dir.ts +3 -2
- package/src/execution/engine/common/errors.ts +1 -1
- package/src/execution/engine/common/journal-wiring.ts +18 -9
- package/src/execution/engine/common/run-signals.ts +83 -0
- package/src/execution/engine/d8-compat.ts +1 -1
- package/src/execution/engine/host/__tests__/host-bridge.test.ts +3 -18
- package/src/execution/engine/host/host-bridge.ts +26 -41
- package/src/execution/engine/host/pi-host-binding.ts +5 -5
- package/src/execution/engine/host/spawned-children.ts +1 -1
- package/src/execution/engine/inflight-snapshot.ts +92 -0
- package/src/execution/engine/port.ts +52 -54
- package/src/execution/engine/types.ts +11 -14
- package/src/execution/execution-record.ts +15 -47
- package/src/execution/finalize-record.ts +94 -197
- package/src/execution/idle-gc.ts +9 -3
- package/src/execution/lifecycle-manager.ts +25 -361
- package/src/execution/lifecycle-predicates.ts +22 -19
- package/src/execution/manifest-store.ts +40 -42
- package/src/execution/model-config-service.ts +1 -1
- package/src/execution/notifier.ts +39 -1
- package/src/execution/notify-host.ts +13 -4
- package/src/execution/path-encoding.ts +6 -0
- package/src/execution/record-entry.ts +20 -2
- package/src/execution/record-store.ts +925 -160
- package/src/execution/relay-env.ts +8 -3
- package/src/execution/round-supervisor/domain.ts +17 -5
- package/src/execution/round-supervisor/service-binding.test.ts +89 -53
- package/src/execution/round-supervisor/service-binding.ts +71 -21
- package/src/execution/round-supervisor/supervisor.ts +23 -9
- package/src/execution/service/record-access.ts +547 -0
- package/src/execution/service/record-lifecycle.ts +542 -0
- package/src/execution/service/run-orchestration.ts +1647 -0
- package/src/execution/service/service-bootstrap.ts +111 -0
- package/src/execution/service/service-constants.ts +27 -0
- package/src/execution/service/session-baselines.ts +401 -0
- package/src/execution/service/sync-collect-domain.ts +442 -0
- package/src/execution/service/workflow-dispatch.ts +567 -0
- package/src/execution/session-file-gc.ts +11 -8
- package/src/execution/session-reconstructor.ts +44 -1
- package/src/execution/sessions-index.ts +20 -3
- package/src/execution/settled-watchdog.ts +86 -43
- package/src/execution/state-marker.ts +516 -0
- package/src/execution/stream-sink.ts +1 -1
- package/src/execution/subagent-actions-core.ts +53 -31
- package/src/execution/subagent-service.ts +643 -2955
- package/src/execution/subprocess-agent-runner.ts +46 -323
- package/src/execution/types.ts +60 -43
- package/src/execution/ui-request-handler-factory.ts +5 -0
- package/src/execution/worktree-manager.ts +1 -1
- package/src/execution/worktree-registry.ts +12 -8
- package/src/index.ts +16 -5
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -3
- package/src/orchestration/__tests__/agent-call-stream.test.ts +24 -49
- package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +4 -4
- package/src/orchestration/__tests__/file-run-store.test.ts +5 -7
- package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +2 -3
- package/src/orchestration/__tests__/run-snapshot.test.ts +27 -43
- package/src/orchestration/__tests__/worker-message-pump-workflow-dispatch.test.ts +274 -0
- package/src/orchestration/execute-agent-call.ts +1 -1
- package/src/orchestration/file-run-store.ts +1 -1
- package/src/orchestration/models/ports.ts +20 -13
- package/src/orchestration/models/types.ts +11 -13
- package/src/orchestration/run-snapshot.ts +18 -22
- package/src/orchestration/worker-message-pump.ts +33 -57
- package/src/shared/atomic-write.ts +10 -8
- package/dist/chunk-T2SYBW3I.js +0 -24
- package/dist/engine-discovery-scan-ocpM8FmI.d.cts +0 -1608
- package/dist/engine-discovery-scan-ocpM8FmI.d.ts +0 -1608
- package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +0 -255
- package/src/execution/__tests__/finalized-marker.test.ts +0 -104
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +0 -211
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +0 -488
- package/src/execution/__tests__/subprocess-agent-runner-timeout.test.ts +0 -75
- package/src/execution/__tests__/tombstone-store.test.ts +0 -73
- package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +0 -230
- package/src/execution/finalized-marker.ts +0 -70
- package/src/execution/tombstone-store.ts +0 -72
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
//
|
|
5
5
|
// 形态对齐协议 v1(stdout 独占 NDJSON;stdin 帧驱动):
|
|
6
6
|
// - FAKE_PROTOCOL_FIXTURE:fixture JSON 路径(必填);本件 = 该 fixture 的回放引擎;
|
|
7
|
-
// - 正向方法(
|
|
8
|
-
//
|
|
7
|
+
// - 正向方法(9 个):initialize / probe / run / cancel / read / listModels /
|
|
8
|
+
// validateModel / dispose / ping——结果取 fixture 对应段([H1 U5] interact 已随
|
|
9
|
+
// chat-run 统一退役);
|
|
9
10
|
// - 反向通道(8 个):host/log / host/askUser / host/permission / host/streamDelta /
|
|
10
11
|
// host/poolResolved / host/handleReady / host/childSpawned / host/childStateChanged
|
|
11
|
-
// ——run 期间按 fixture.run.script
|
|
12
|
+
// ——run 期间按 fixture.run.script 逐动作播放([H1 U5] 曾有的轮次相位通道已随
|
|
13
|
+
// chat-run 统一退役);
|
|
12
14
|
// - 错误帧:FAKE_PROTOCOL_ERROR=run_failed(run 回错误帧)| unknown_method(对任意
|
|
13
15
|
// 未知名方法回 engine_method_unknown——不在 v1 错误码表内的码也必须原样透传,
|
|
14
16
|
// 断言的是帧形态而非码表);FAKE_PROTOCOL_VERSION 越界 → initialize 回
|
|
@@ -20,18 +22,16 @@
|
|
|
20
22
|
// - FAKE_PID_FILE:启动时写自身 pid(SIGTERM 用例的 kill 定位锚——core 侧 pidfile
|
|
21
23
|
// 归 EngineClient 所有,fake 侧不触碰,路径自建自删归测试);
|
|
22
24
|
// - FAKE_CAPABILITIES_CONVERSATION:覆写 initialize 应答的
|
|
23
|
-
// capabilities.conversation(
|
|
25
|
+
// capabilities.conversation(resume 能力位 gate 负向:旧形态引擎 manifest 无该位);
|
|
24
26
|
// - 新 script op(env 注入 script,沿 FAKE_RUN_HANG 先例):
|
|
25
27
|
// { op:"stderr", text } → 引擎 stderr 写点(engine_crashed
|
|
26
28
|
// 的 stderr 尾 400 窗口实证);
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// - FAKE_RUN_LIFECYCLE=1 / FAKE_RUN_RECORD_DELTA=1 / FAKE_RUN_STDERR_HANG=1:
|
|
34
|
-
// run script 注入上述动作段(三相位 × 双键 / recordId 键 delta / stderr+hang)。
|
|
29
|
+
// streamDelta op 支持 action.recordId → recordId 键 delta(chat 续聊轮关联键);
|
|
30
|
+
// - 数据面应答回声:recordId-delta 帧的②应答到达时回发 host/log("ack <label>")——
|
|
31
|
+
// core 侧断言「宿主确认收到」回执链闭合的观测锚;
|
|
32
|
+
// - FAKE_RUN_RECORD_DELTA=1 / FAKE_RUN_STDERR_HANG=1:run script 注入上述动作段
|
|
33
|
+
// (recordId 键 delta / stderr+hang)。[H1 U5] FAKE_RUN_LIFECYCLE(轮次相位 ×
|
|
34
|
+
// 双键注入段)已随通道退役删除。
|
|
35
35
|
|
|
36
36
|
import { spawn } from "node:child_process";
|
|
37
37
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
@@ -119,8 +119,8 @@ async function playRunScript(runId, script) {
|
|
|
119
119
|
});
|
|
120
120
|
break;
|
|
121
121
|
case "streamDelta": {
|
|
122
|
-
// [W6] action.recordId 有值 = 续聊轮 recordId 键 delta(关联键分路——
|
|
123
|
-
// D1-A
|
|
122
|
+
// [W6] action.recordId 有值 = chat 续聊轮 recordId 键 delta(关联键分路——
|
|
123
|
+
// D1-A:续聊轮经 handle.sessionRef 的 recordId 关联)。
|
|
124
124
|
const streamReqId = `rev-stream-${nextRequestId++}`;
|
|
125
125
|
if (action.recordId !== undefined) {
|
|
126
126
|
ackEchoLabels.set(streamReqId, `streamDelta recordId=${action.recordId}`);
|
|
@@ -141,24 +141,6 @@ async function playRunScript(runId, script) {
|
|
|
141
141
|
// 后续动作可用 host/log 做到达锚)。
|
|
142
142
|
process.stderr.write(action.text ?? "");
|
|
143
143
|
break;
|
|
144
|
-
case "roundLifecycle": {
|
|
145
|
-
// [W6] host/roundLifecycle 帧(三相位 × runId|recordId 双关联键)。
|
|
146
|
-
// failed 相位必含 error{code,message}(isHostRoundLifecycleParams 判据)。
|
|
147
|
-
const lcReqId = `rev-lc-${nextRequestId++}`;
|
|
148
|
-
const keyed =
|
|
149
|
-
action.keyedBy === "recordId"
|
|
150
|
-
? { recordId: action.key }
|
|
151
|
-
: { runId: action.key };
|
|
152
|
-
ackEchoLabels.set(lcReqId, `roundLifecycle ${action.key} ${action.phase}`);
|
|
153
|
-
reverseRequest(lcReqId, "host/roundLifecycle", {
|
|
154
|
-
...keyed,
|
|
155
|
-
phase: action.phase,
|
|
156
|
-
...(action.usage !== undefined ? { usage: action.usage } : {}),
|
|
157
|
-
...(action.error !== undefined ? { error: action.error } : {}),
|
|
158
|
-
...(action.anchor !== undefined ? { anchor: action.anchor } : {}),
|
|
159
|
-
});
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
162
144
|
case "permission":
|
|
163
145
|
reverseRequest(`rev-perm-${nextRequestId++}`, "host/permission", {
|
|
164
146
|
runId,
|
|
@@ -187,7 +169,7 @@ async function playRunScript(runId, script) {
|
|
|
187
169
|
}
|
|
188
170
|
|
|
189
171
|
const KNOWN_METHODS = new Set([
|
|
190
|
-
"initialize", "probe", "run", "cancel", "
|
|
172
|
+
"initialize", "probe", "run", "cancel", "read",
|
|
191
173
|
"listModels", "validateModel", "dispose", "ping",
|
|
192
174
|
]);
|
|
193
175
|
|
|
@@ -205,9 +187,8 @@ createInterface({ input: process.stdin }).on("line", (line) => {
|
|
|
205
187
|
}
|
|
206
188
|
|
|
207
189
|
// 帧②(core 对反向请求的应答):askUser 最终结果到达——回放层记录后继续。
|
|
208
|
-
// [W6] 数据面(
|
|
209
|
-
//
|
|
210
|
-
// run 应答帧到达)。
|
|
190
|
+
// [W6] 数据面(recordId-delta)应答到达 → 回发 host/log 回声(core 侧「宿主
|
|
191
|
+
// 确认收到」回执链闭合的观测锚;NDJSON 单连接有序,回声先于 run 应答帧到达)。
|
|
211
192
|
if (typeof frame.id === "string") {
|
|
212
193
|
const echoLabel = ackEchoLabels.get(frame.id);
|
|
213
194
|
if (echoLabel !== undefined) {
|
|
@@ -278,9 +259,6 @@ createInterface({ input: process.stdin }).on("line", (line) => {
|
|
|
278
259
|
case "validateModel":
|
|
279
260
|
send({ id, result: fixture.validateModel.result });
|
|
280
261
|
return;
|
|
281
|
-
case "interact":
|
|
282
|
-
send({ id, result: fixture.interact.result });
|
|
283
|
-
return;
|
|
284
262
|
case "read":
|
|
285
263
|
send({ id, result: fixture.read.result });
|
|
286
264
|
return;
|
|
@@ -327,18 +305,6 @@ createInterface({ input: process.stdin }).on("line", (line) => {
|
|
|
327
305
|
{ op: "hang" },
|
|
328
306
|
);
|
|
329
307
|
}
|
|
330
|
-
if (process.env.FAKE_RUN_LIFECYCLE === "1") {
|
|
331
|
-
injected.push(
|
|
332
|
-
// 三相位 × 双关联键(settled 带 usage;idle/failed 带 anchor;failed 带
|
|
333
|
-
// error{code,message}——isHostRoundLifecycleParams 的相位居留形状)。
|
|
334
|
-
{ op: "roundLifecycle", keyedBy: "runId", key: "run-smoke-1", phase: "settled", usage: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 } },
|
|
335
|
-
{ op: "roundLifecycle", keyedBy: "runId", key: "run-smoke-1", phase: "idle", anchor: { sessionRef: { sessionId: "sess-fixture" }, poolKey: "shared" } },
|
|
336
|
-
{ op: "roundLifecycle", keyedBy: "runId", key: "run-smoke-1", phase: "failed", error: { code: "engine_run_failed", message: "round failed (scripted)", recovery: "Inspect the task and retry." } },
|
|
337
|
-
{ op: "roundLifecycle", keyedBy: "recordId", key: "rec-chat-1", phase: "settled" },
|
|
338
|
-
{ op: "roundLifecycle", keyedBy: "recordId", key: "rec-chat-1", phase: "idle", anchor: { sessionRef: { sessionId: "sess-chat-1" }, poolKey: "shared" } },
|
|
339
|
-
{ op: "roundLifecycle", keyedBy: "recordId", key: "rec-chat-1", phase: "failed", error: { code: "engine_run_failed", message: "chat round failed (scripted)", recovery: "Inspect the task and retry." } },
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
308
|
if (process.env.FAKE_RUN_RECORD_DELTA === "1") {
|
|
343
309
|
injected.push({ op: "streamDelta", recordId: "rec-chat-1", delta: "chat-delta-not-routed-to-runId" });
|
|
344
310
|
}
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
RELAY_ENV_SOCKET,
|
|
39
39
|
RELAY_PROTOCOL_VERSION,
|
|
40
40
|
isRelayActive,
|
|
41
|
-
} from "
|
|
41
|
+
} from "@zhushanwen/subagent-engine-sdk";
|
|
42
42
|
import { assertAgentEventInvariants } from "./agent-event-invariants.ts";
|
|
43
43
|
|
|
44
44
|
const LIVE = process.env["ENGINE_CONFORMANCE_LIVE"] === "1";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// protocol-blackbox.test.ts —— W10 协议黑盒套件(基线三层①的自动层)。
|
|
2
2
|
//
|
|
3
3
|
// 覆盖(impl-plan §2.10 / 设计 §4 探针挂钩①):
|
|
4
|
-
// -
|
|
4
|
+
// - 9 正向方法(initialize/probe/run/cancel/read/listModels/
|
|
5
5
|
// validateModel/dispose/ping)× fake 引擎 CLI 往返;
|
|
6
6
|
// - 8 反向通道(host/log、host/askUser、host/permission、host/streamDelta、
|
|
7
7
|
// host/poolResolved、host/handleReady、host/childSpawned、host/childStateChanged)
|
|
@@ -137,8 +137,8 @@ function makeHarness(extraEnv: Record<string, string> = {}, routeRunId = fixture
|
|
|
137
137
|
return h;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
describe("协议黑盒:
|
|
141
|
-
it("initialize/probe/listModels/validateModel/
|
|
140
|
+
describe("协议黑盒:9 正向方法 × fake 引擎回放", () => {
|
|
141
|
+
it("initialize/probe/listModels/validateModel/read/ping 往返(fixture 白名单比对)", async () => {
|
|
142
142
|
const h = makeHarness();
|
|
143
143
|
try {
|
|
144
144
|
await h.client.ensureConnected();
|
|
@@ -162,12 +162,8 @@ describe("协议黑盒:10 正向方法 × fake 引擎回放", () => {
|
|
|
162
162
|
)) as { canonicalRef: string };
|
|
163
163
|
expect(validated.canonicalRef).toBe("fake/fake-1");
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
|
|
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);
|
|
165
|
+
// [H1 U5] interact 方法已随 chat-run 统一退役(D5)——往返断言随之删除,
|
|
166
|
+
// fake 引擎对未知方法回 engine_method_unknown(unknown_method 模式覆盖)。
|
|
171
167
|
|
|
172
168
|
const view = (await h.client.request(
|
|
173
169
|
"read",
|
|
@@ -2,23 +2,26 @@
|
|
|
2
2
|
// bash 跨 session 可见性显式断言(R4 一刀语义钉住)+ 偏差 #4 探针(goal 守卫口径)。
|
|
3
3
|
//
|
|
4
4
|
// 设计权威源:chat-domain-v1x-liveness-governance.md §3.2 D4(翻档三连带——读侧
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// 过滤三口;分档常量已随 ext-simplify-12 删除,三类型 process 档成为无条件代码
|
|
6
|
+
// 自然状态)+ 修订记录 v5-⑤(bash 跨 session 可见性一刀钉成显式选择)+ 验收 A9②
|
|
7
|
+
// + impl-plan §5 偏差 #4(W4 交接:goal 守卫消费点未传基准,W6 实测裁决是否需
|
|
8
|
+
// goal 侧两行传参)。
|
|
8
9
|
//
|
|
9
10
|
// 被测函数 = `extensions/universal/pending-notifications/src/state.ts` 的导出纯函数
|
|
10
11
|
// (零依赖、不触 Pi 运行时——文件头注自证;经相对路径源码消费,subagent-core 无该
|
|
11
12
|
// workspace 依赖声明,不加 phantom dep)。分工:
|
|
12
|
-
// - 过滤函数本体 /
|
|
13
|
+
// - 过滤函数本体 / pending_notifications 工具投影(entries 现算)的两口径行为:
|
|
14
|
+
// 本套件钉语义(conformance 视角);
|
|
13
15
|
// - 过滤①②③的**接线**(goal/subagent-workflow/pending-notifications 三消费点
|
|
14
16
|
// 透传 currentSessionId):W4 包内测试已承载(pi-pending-notifications
|
|
15
|
-
// __tests__
|
|
17
|
+
// __tests__、subagent-workflow pi-host.test);
|
|
16
18
|
// - 后代判定口(读侧过滤②)的差集口径与①共用同一函数——本套件①的断言即其
|
|
17
19
|
// 语义核心;接线透传由上述 W4 测试守护。
|
|
18
20
|
//
|
|
19
21
|
// 探针结论(偏差 #4,实测证据 = 「goal 守卫口径」describe):**有虚增**——fork 后
|
|
20
|
-
// 父 session
|
|
21
|
-
//
|
|
22
|
+
// 父 session 仍活跃的后台任务残留使无基准口径幻 defer。该结论已由 d0b9a6faf
|
|
23
|
+
// (2026-09-09,fix(goal): pass currentSessionId)关闭——goal 两处守卫现传
|
|
24
|
+
// currentSessionId 基准,下方「无基准」形态仅作历史口径对照面保留。
|
|
22
25
|
|
|
23
26
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
24
27
|
import { tmpdir } from "node:os";
|
|
@@ -27,13 +30,9 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
|
27
30
|
|
|
28
31
|
// 相对路径源码消费(7 层上溯到仓库根):state.ts 是零依赖纯函数(不触 Pi 运行时),
|
|
29
32
|
// 无需为探针/契约测试给 subagent-core 添加 workspace 依赖声明。
|
|
30
|
-
import
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
countActiveFromEntries,
|
|
34
|
-
createRegistry,
|
|
35
|
-
rebuildFromEntries,
|
|
36
|
-
} from "../../../../../../../extensions/universal/pending-notifications/src/state.ts";
|
|
33
|
+
// [ext-simplify-12] import 面收窄到 countActiveFromEntries——历史的 registry/
|
|
34
|
+
// session 档机器已整体删除,entries 是唯一状态源(跨包语义耦合面收敛到该函数)。
|
|
35
|
+
import { countActiveFromEntries } from "../../../../../../../extensions/universal/pending-notifications/src/state.ts";
|
|
37
36
|
|
|
38
37
|
import { runReconcileSweep } from "../../../round-supervisor/index.ts";
|
|
39
38
|
|
|
@@ -52,16 +51,13 @@ function registerEntry(
|
|
|
52
51
|
name: id,
|
|
53
52
|
registeredAt: 1_000,
|
|
54
53
|
sessionId,
|
|
55
|
-
//
|
|
56
|
-
//
|
|
54
|
+
// 真实落盘无 expiresAt 键(写入侧无条件省略);overrides.expiresAt 模拟历史
|
|
55
|
+
// session 文件遗留的带 TTL 键 entry——差集读取侧刻意不读该键(对照面)。
|
|
57
56
|
...(overrides.expiresAt !== undefined ? { expiresAt: overrides.expiresAt } : {}),
|
|
58
57
|
},
|
|
59
58
|
};
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
/** 语义对照面的基准时刻(远超旧 1h TTL——钉「无 TTL 清理」而非「窗口内未到期」假绿)。 */
|
|
63
|
-
const NOW = 10 * PENDING_TTL_MS + 1;
|
|
64
|
-
|
|
65
61
|
let tmpDirs: string[] = [];
|
|
66
62
|
|
|
67
63
|
afterEach(() => {
|
|
@@ -69,23 +65,23 @@ afterEach(() => {
|
|
|
69
65
|
tmpDirs = [];
|
|
70
66
|
});
|
|
71
67
|
|
|
72
|
-
describe("[W6/D4
|
|
73
|
-
it("
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
68
|
+
describe("[W6/D4 翻档余义] 三类型跨时长无 TTL 清理(entries 差集现算)", () => {
|
|
69
|
+
it("翻档后跨时长(远超旧 1h TTL)无 TTL 清理:三类型 register 均仍 active", () => {
|
|
70
|
+
// NOW = 10 * 3_600_000 + 1:语义对照面基准时刻(旧 1h TTL = 3_600_000ms 的
|
|
71
|
+
// 10 倍 + 1ms,远超旧 1h TTL)。entries 差集不接收时间参数——差集语义刻意
|
|
72
|
+
// 不校验时间,NOW 仅标注对照语义:若旧 session 档 TTL 机器仍在,NOW 时刻
|
|
73
|
+
// bg-1/wf-1 早已被过期清理。
|
|
74
|
+
const NOW = 10 * 3_600_000 + 1;
|
|
75
|
+
// 护栏:钉住对照面确实覆盖「超 TTL」域而非「窗口内未到期」假绿。
|
|
76
|
+
expect(NOW - 1_000).toBeGreaterThan(3_600_000);
|
|
79
77
|
const entries = [
|
|
80
|
-
registerEntry("bg-1", "subagent", "sess-root", { expiresAt: 1_000 + 1 }), //
|
|
78
|
+
registerEntry("bg-1", "subagent", "sess-root", { expiresAt: 1_000 + 1 }), // 历史遗留 TTL 键(读取侧不读)
|
|
81
79
|
registerEntry("wf-1", "workflow", "sess-root", { expiresAt: 1_000 + 1 }),
|
|
82
80
|
registerEntry("bt-1", "bash", "sess-root"), // 真实落盘形态:无 expiresAt 键
|
|
83
81
|
];
|
|
84
|
-
const result = rebuildFromEntries(registry, entries, "sess-root", NOW);
|
|
85
82
|
// 旧 session 档语义(1h TTL + 跨 session 补注销)曾把长任务/跨重启注册静默
|
|
86
|
-
// 清除 → 守卫失明(事故环 4
|
|
87
|
-
expect(
|
|
88
|
-
expect(result.activeIds).toEqual(["bg-1", "wf-1", "bt-1"]);
|
|
83
|
+
// 清除 → 守卫失明(事故环 4 放大器);三类型全 process 档后差集一律续存。
|
|
84
|
+
expect(countActiveFromEntries(entries).ids).toEqual(["bg-1", "wf-1", "bt-1"]);
|
|
89
85
|
});
|
|
90
86
|
});
|
|
91
87
|
|
|
@@ -121,41 +117,45 @@ describe("[W6/R4 一刀] bash 跨 session 可见性显式断言(钉成显式
|
|
|
121
117
|
expect(scoped.count).toBe(1); // 只数本 session 的任务
|
|
122
118
|
});
|
|
123
119
|
|
|
124
|
-
it("
|
|
120
|
+
it("旧行为显式登记:无基准对照面(历史形态)父 bash 残留仍入计数(= defer 驱动)", () => {
|
|
125
121
|
const childEntries = [registerEntry("parent-bt", "bash", "sess-parent")];
|
|
126
122
|
// 显式钉住「一刀的对照面」:不传基准 = 不过滤 = 子 session goal 仍为父 bash
|
|
127
123
|
// 任务 defer。这是读侧过滤一刀**改变的旧行为**——保留此断言防止有人误以为
|
|
128
|
-
// 过滤是缺省语义(偏差 #4:goal
|
|
124
|
+
// 过滤是缺省语义(偏差 #4:goal 消费点历史形态即此口径,已由 d0b9a6faf 修复
|
|
125
|
+
// 传基准,见探针结论)。
|
|
129
126
|
expect(countActiveFromEntries(childEntries).count).toBe(1);
|
|
130
127
|
});
|
|
131
128
|
});
|
|
132
129
|
|
|
133
|
-
describe("[W6/D4 读侧过滤③]
|
|
134
|
-
it("跨 session
|
|
135
|
-
const registry = createRegistry();
|
|
130
|
+
describe("[W6/D4 读侧过滤③] pending_notifications 工具投影(entries 现算)", () => {
|
|
131
|
+
it("跨 session 残留不进差集、不产生任何写回(跳过不补注销——落盘收口归 core sweep / bte 对账通道)", () => {
|
|
136
132
|
const entries = [
|
|
137
133
|
registerEntry("parent-bg", "subagent", "sess-parent"),
|
|
138
134
|
registerEntry("own-bg", "subagent", "sess-child"),
|
|
139
135
|
];
|
|
140
|
-
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
136
|
+
// 工具投影面(count/list)不虚报继承残留(A9②:count/list 不含继承残留)——
|
|
137
|
+
// 现算后与读侧过滤①共用同一 countActiveFromEntries 扫描,「落盘了什么」与
|
|
138
|
+
// 「查询到什么」构造性一致。
|
|
139
|
+
const result = countActiveFromEntries(entries, { currentSessionId: "sess-child" });
|
|
140
|
+
expect(result.ids).toEqual(["own-bg"]);
|
|
144
141
|
// 「跳过不补注销」:残留 entry 留在 session 文件(读侧①③口各自兜住差集消费
|
|
145
|
-
//
|
|
146
|
-
|
|
142
|
+
// 方),countActiveFromEntries 只读不写——跨 session 写达域缺口由 core 对账
|
|
143
|
+
// sweep 收口(bte 对账同理直接 appendEntry)。只读性实证:输入数组长度不变,
|
|
144
|
+
// 无补发的 unregister entry。
|
|
145
|
+
expect(entries).toHaveLength(2);
|
|
147
146
|
});
|
|
148
147
|
});
|
|
149
148
|
|
|
150
|
-
describe("[W6/偏差 #4 探针] goal 守卫口径(无基准)× fork
|
|
149
|
+
describe("[W6/偏差 #4 探针] goal 守卫口径(无基准)× fork 残留——实测结论(历史口径)", () => {
|
|
151
150
|
// 偏差 #4(impl-plan §5,W4 交接):W4 读侧过滤①的 currentSessionId 为可选参数,
|
|
152
151
|
// goal 守卫消费点未传基准。本探针在 fork 场景实测该口径是否被父 session 注册
|
|
153
152
|
// 残留虚增,结论二选一回写报告:
|
|
154
153
|
// (a) 无影响(其他机制覆盖)→ 偏差 #4 关闭,goal 零改动;
|
|
155
154
|
// (b) 有虚增 → 列出确需的两行传参改动点(不改 goal——红线),留主会话裁决。
|
|
156
155
|
//
|
|
157
|
-
// **实测结论:(b)
|
|
158
|
-
//
|
|
156
|
+
// **实测结论:(b) 有虚增。已由 d0b9a6faf(2026-09-09)关闭——goal 现传
|
|
157
|
+
// currentSessionId 基准,下方证据链与用例为无基准历史形态的对照面。**
|
|
158
|
+
// 证据链(历史口径,d0b9a6faf 前的 goal 守卫两处消费点均无基准调用):
|
|
159
159
|
// `extensions/universal/goal/src/adapters/event-handlers/agent-end.ts:198`
|
|
160
160
|
// `const pendingOps = countActiveFromEntries(entries);`(defer 分支判据
|
|
161
161
|
// = `pendingOps.count > 0`)
|
|
@@ -166,21 +166,23 @@ describe("[W6/偏差 #4 探针] goal 守卫口径(无基准)× fork 残留
|
|
|
166
166
|
// 用例 2 证明仅当父任务 record 已终态(core sweep 补注销落盘)才归零——
|
|
167
167
|
// 「父任务仍活跃」窗口内无任何机制覆盖(W4 翻档后 U4 不再中性化)。
|
|
168
168
|
//
|
|
169
|
-
//
|
|
169
|
+
// 已执行的修复(d0b9a6faf,即当时登记的两行传参改动点):
|
|
170
170
|
// agent-end.ts:198 → `countActiveFromEntries(entries, { currentSessionId: ctx.sessionManager.getSessionId() })`
|
|
171
171
|
// agent-end.ts:294 → `countActiveFromEntries(ctx.sessionManager.getEntries(), { currentSessionId: ctx.sessionManager.getSessionId() })`
|
|
172
172
|
// (getSessionId 是 extension 侧既有取 sessionId 形态——pending-notifications
|
|
173
|
-
// index.ts:216、subagent-workflow session-lifecycle.ts
|
|
173
|
+
// index.ts:216、subagent-workflow session-lifecycle.ts 同款先例;现行
|
|
174
|
+
// agent-end.ts 守卫注释反向引用本探针作为传基准依据。)
|
|
174
175
|
|
|
175
|
-
it("用例 1:父 session 活跃 subagent 残留 →
|
|
176
|
+
it("用例 1:父 session 活跃 subagent 残留 → 无基准对照面(历史形态)count>0 → defer 分支命中(虚增)", () => {
|
|
176
177
|
// fork 时刻快照:父 session 派了后台子代理(record 仍 running-resumable),
|
|
177
178
|
// 子 session 继承其 register 残留;goal 守卫读到的 entries = 本 session 文件
|
|
178
|
-
//
|
|
179
|
+
// 全量(含继承残留)。历史形态 = 无基准调用(d0b9a6faf 前 agent-end.ts:198
|
|
180
|
+
// 逐字形态;现行已传基准,此对照面锁定「不传则虚增」的机制事实)。
|
|
179
181
|
const childEntries = [
|
|
180
182
|
registerEntry("parent-bg", "subagent", "sess-parent"),
|
|
181
183
|
registerEntry("child-bg", "subagent", "sess-child"),
|
|
182
184
|
];
|
|
183
|
-
const pendingOps = countActiveFromEntries(childEntries); // ←
|
|
185
|
+
const pendingOps = countActiveFromEntries(childEntries); // ← 无基准对照面(历史形态)
|
|
184
186
|
// defer 分支判据(agent-end.ts:199 `if (pendingOps.count > 0)`)命中:
|
|
185
187
|
// 父 session 的任务让子 session 的 goal 停发 continuation 并 notify
|
|
186
188
|
// 「Goal waiting for 2 background task(s)」——计数含非本 session 任务 = 虚增。
|
|
@@ -209,9 +211,9 @@ describe("[W6/偏差 #4 探针] goal 守卫口径(无基准)× fork 残留
|
|
|
209
211
|
});
|
|
210
212
|
expect(result.reconciled).toEqual(["parent-bg"]);
|
|
211
213
|
|
|
212
|
-
// 落盘后 goal
|
|
213
|
-
//
|
|
214
|
-
//
|
|
214
|
+
// 落盘后 goal 守卫(无基准口径,历史形态)重读 entries:计数归零——sweep 是
|
|
215
|
+
// 无基准口径的唯一收敛通道,且只覆盖「record 可判定终态」的窗口;「父任务仍
|
|
216
|
+
// 活跃」窗口(用例 1)无机制覆盖 → 虚增结论 (b) 成立。
|
|
215
217
|
const entriesAfter = readFileSync(sessionFile, "utf8")
|
|
216
218
|
.trim()
|
|
217
219
|
.split("\n")
|
|
@@ -125,9 +125,6 @@ function stubEngine(id: string): EnginePort {
|
|
|
125
125
|
run: async () => {
|
|
126
126
|
throw new Error("unused");
|
|
127
127
|
},
|
|
128
|
-
interact: async () => {
|
|
129
|
-
throw new Error("unused");
|
|
130
|
-
},
|
|
131
128
|
read: async () => ({ engineId: id, turns: [], source: "outcome-only" }),
|
|
132
129
|
};
|
|
133
130
|
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// inflight-snapshot.test.ts —— core→壳在途事件出口 + EnginePort.inFlightSnapshot? 缺省语义
|
|
2
|
+
//(u7a,设计权威源:docs/design/crash-forensics-and-watchdog.md §3.3 D5)。
|
|
3
|
+
//
|
|
4
|
+
// 三视角:
|
|
5
|
+
// ①使用者(壳层 reporter 视角)——setInFlightListener 注册后,core 状态迁移点
|
|
6
|
+
// (子进程注册/移除、idle timer arm)推来最新绝对计数快照;getInFlightSnapshot
|
|
7
|
+
// 随时可拉(初始上报数据源)。
|
|
8
|
+
// ②构建者——计数 = 双谓词过滤(hasLiveProcessHandle && !hasIdleTimer,与
|
|
9
|
+
// notify-host hasRunningBackground 同源):活句柄且无 armed idle timer 才算在途,
|
|
10
|
+
// Path A 保活(timer armed)不算——D5 防推迟恒真的核心裁决。
|
|
11
|
+
// ③观察者——EnginePort 可选成员向后兼容:pi 引擎不实现(undefined = 无在途),
|
|
12
|
+
// zcode 引擎实现(快照面在场);出口监听者抛错不反噬 core。
|
|
13
|
+
|
|
14
|
+
import { describe, expect, it, beforeEach, afterEach } from "vitest";
|
|
15
|
+
import { EventEmitter } from "node:events";
|
|
16
|
+
import type { ChildProcess } from "node:child_process";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
armIdleTimer,
|
|
20
|
+
disarmIdleTimer,
|
|
21
|
+
_resetLifecycleState,
|
|
22
|
+
} from "../../lifecycle-manager.ts";
|
|
23
|
+
import {
|
|
24
|
+
getInFlightSnapshot,
|
|
25
|
+
notifyInFlightChanged,
|
|
26
|
+
setInFlightListener,
|
|
27
|
+
type InFlightSnapshot,
|
|
28
|
+
} from "../inflight-snapshot.ts";
|
|
29
|
+
import type { EnginePort } from "../port.ts";
|
|
30
|
+
// W3 合并改写:inproc 引擎已删——句柄记账走 host/spawned-children 镜像(register +
|
|
31
|
+
// reset 测试面);「引擎无 inFlightSnapshot 可选成员」断言改桩(EnginePort 缺席形态)。
|
|
32
|
+
import {
|
|
33
|
+
killRecordChildWithEscalation,
|
|
34
|
+
registerSpawnedChildForRecord,
|
|
35
|
+
_resetCoreSpawnedChildrenMirrorForTest,
|
|
36
|
+
} from "../host/spawned-children.ts";
|
|
37
|
+
import { createHostBridge } from "../host/host-bridge.ts";
|
|
38
|
+
|
|
39
|
+
/** 最小 fake 子进程(EventEmitter + killed/pid 字段——hasLiveProcessHandle 消费面)。 */
|
|
40
|
+
function makeFakeChild(): ChildProcess & EventEmitter {
|
|
41
|
+
const child = new EventEmitter() as ChildProcess & EventEmitter;
|
|
42
|
+
(child as { pid?: number }).pid = 4242;
|
|
43
|
+
(child as { killed: boolean }).killed = false;
|
|
44
|
+
return child;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
_resetLifecycleState();
|
|
49
|
+
setInFlightListener(null);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
// 清理本测试注册的句柄镜像(W3 后记账在 host/spawned-children,reset 即清;close 事件
|
|
54
|
+
// 摘除路径已由「close 摘除自动触发通知」用例覆盖),防跨用例泄漏。
|
|
55
|
+
_resetCoreSpawnedChildrenMirrorForTest();
|
|
56
|
+
_resetLifecycleState();
|
|
57
|
+
setInFlightListener(null);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("getInFlightSnapshot:双谓词绝对计数(D5 求值位置 = 状态所在进程)", () => {
|
|
61
|
+
it("空态 → 0(无任何 subagent 的 session 初始上报数据源)", () => {
|
|
62
|
+
expect(getInFlightSnapshot()).toEqual({ inFlight: 0 });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("注册活句柄 → 1;arm idle timer(Path A 保活)→ 0;disarm → 1", () => {
|
|
66
|
+
const id = "sa-inflight-arm";
|
|
67
|
+
registerSpawnedChildForRecord(id, makeFakeChild());
|
|
68
|
+
expect(getInFlightSnapshot().inFlight).toBe(1);
|
|
69
|
+
|
|
70
|
+
armIdleTimer(id, () => undefined, 60_000);
|
|
71
|
+
expect(getInFlightSnapshot().inFlight).toBe(0);
|
|
72
|
+
|
|
73
|
+
disarmIdleTimer(id);
|
|
74
|
+
expect(getInFlightSnapshot().inFlight).toBe(1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("killed 句柄不算活(hasLiveProcessHandle 的 !killed 子句)", () => {
|
|
78
|
+
const id = "sa-inflight-killed";
|
|
79
|
+
const child = makeFakeChild();
|
|
80
|
+
registerSpawnedChildForRecord(id, child);
|
|
81
|
+
// W3 镜像形态:killed 判据由镜像置死位承载(注册后改 child 引用字段不进镜像——
|
|
82
|
+
// 生产置死路径 = killRecordChildWithEscalation 杀链记账面 / 引擎反向通道)。
|
|
83
|
+
killRecordChildWithEscalation(id, "test");
|
|
84
|
+
expect(getInFlightSnapshot().inFlight).toBe(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("多 record 混合形态:在途/保活/垂死并存时按谓词逐一过滤", () => {
|
|
88
|
+
const running = makeFakeChild();
|
|
89
|
+
const keepAlive = makeFakeChild();
|
|
90
|
+
const dying = makeFakeChild();
|
|
91
|
+
(dying as { killed: boolean }).killed = true;
|
|
92
|
+
registerSpawnedChildForRecord("sa-inflight-run", running);
|
|
93
|
+
registerSpawnedChildForRecord("sa-inflight-keep", keepAlive);
|
|
94
|
+
registerSpawnedChildForRecord("sa-inflight-dying", dying);
|
|
95
|
+
armIdleTimer("sa-inflight-keep", () => undefined, 60_000);
|
|
96
|
+
expect(getInFlightSnapshot().inFlight).toBe(1);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("notifyInFlightChanged:迁移点 → 壳层监听者(同步 fire-and-forget)", () => {
|
|
101
|
+
it("监听者收到最新快照(迁移时刻求值,非注册时刻)", () => {
|
|
102
|
+
const seen: InFlightSnapshot[] = [];
|
|
103
|
+
setInFlightListener((s) => seen.push({ ...s }));
|
|
104
|
+
|
|
105
|
+
notifyInFlightChanged();
|
|
106
|
+
expect(seen).toEqual([{ inFlight: 0 }]);
|
|
107
|
+
|
|
108
|
+
registerSpawnedChildForRecord("sa-inflight-notify", makeFakeChild());
|
|
109
|
+
notifyInFlightChanged();
|
|
110
|
+
expect(seen[seen.length - 1]).toEqual({ inFlight: 1 });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("监听者抛错不反噬 core(壳层故障不打断生命周期主链)", () => {
|
|
114
|
+
setInFlightListener(() => {
|
|
115
|
+
throw new Error("shell reporter bug");
|
|
116
|
+
});
|
|
117
|
+
expect(() => notifyInFlightChanged()).not.toThrow();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("无监听者时 no-op;注册覆盖语义(后注册者收到,先注册者不再收)", () => {
|
|
121
|
+
expect(() => notifyInFlightChanged()).not.toThrow();
|
|
122
|
+
|
|
123
|
+
const first: InFlightSnapshot[] = [];
|
|
124
|
+
const second: InFlightSnapshot[] = [];
|
|
125
|
+
setInFlightListener((s) => first.push(s));
|
|
126
|
+
setInFlightListener((s) => second.push(s));
|
|
127
|
+
notifyInFlightChanged();
|
|
128
|
+
expect(first).toEqual([]);
|
|
129
|
+
expect(second).toEqual([{ inFlight: 0 }]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("host-bridge armIdleTimer / disarmIdleTimer 委托点自动触发通知(迁移点接线证据)", () => {
|
|
133
|
+
// W3 镜像形态:register 本身不再通知(子进程终止经引擎反向通道),core 侧唯一挂
|
|
134
|
+
// notifyInFlightChanged 的委托面 = host-bridge 的 arm/disarm(u7a 迁移点新宿主)。
|
|
135
|
+
const seen: InFlightSnapshot[] = [];
|
|
136
|
+
setInFlightListener((s) => seen.push(s));
|
|
137
|
+
registerSpawnedChildForRecord("sa-inflight-auto", makeFakeChild());
|
|
138
|
+
|
|
139
|
+
const bridge = createHostBridge({
|
|
140
|
+
service: {} as Parameters<typeof createHostBridge>[0]["service"],
|
|
141
|
+
onIdleTimeout: () => undefined,
|
|
142
|
+
});
|
|
143
|
+
bridge.armIdleTimer("sa-inflight-auto", 60_000);
|
|
144
|
+
expect(seen.at(-1)).toEqual({ inFlight: 0 });
|
|
145
|
+
|
|
146
|
+
bridge.disarmIdleTimer("sa-inflight-auto");
|
|
147
|
+
expect(seen.at(-1)).toEqual({ inFlight: 1 });
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("EnginePort.inFlightSnapshot? 可选成员缺省语义(D5:pi 不实现,zcode 实现)", () => {
|
|
152
|
+
it("未实现该成员的引擎桩 → undefined(= 无引擎侧在途面,pi 形态由 extension 聚合上报覆盖)", () => {
|
|
153
|
+
// 经 EnginePort 接口面访问(可选成员缺席的正当形态);裸桩无此声明(W3 后 inproc
|
|
154
|
+
// pi 引擎已删,缺席形态以桩承载——zcode 真实现的断言移 zcode-subagent-cli 测试),
|
|
155
|
+
// 消费方必须走 port 类型——本身就是「缺席正当」契约的一部分。
|
|
156
|
+
const pi: EnginePort = { capabilities: () => ({ id: "pi-stub" }) } as unknown as EnginePort;
|
|
157
|
+
expect(pi.inFlightSnapshot).toBeUndefined();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("实现该成员的引擎桩 → 同步快照;未初始化恒 0(空闲常驻≠在途——真实现断言在 zcode-subagent-cli 的 zcode-engine-inflight.test)", () => {
|
|
161
|
+
const engine: EnginePort = {
|
|
162
|
+
capabilities: () => ({ id: "zcode-stub" }),
|
|
163
|
+
inFlightSnapshot: () => ({ inFlight: 0 }),
|
|
164
|
+
} as unknown as EnginePort;
|
|
165
|
+
expect(typeof engine.inFlightSnapshot).toBe("function");
|
|
166
|
+
expect(engine.inFlightSnapshot?.()).toEqual({ inFlight: 0 });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("未实现该成员的引擎对象仍满足 EnginePort(向后兼容,可选成员不强制)", () => {
|
|
170
|
+
// 结构化证据:EnginePort 的既有可选成员扩展先例(listModels/validateModel/dispose)
|
|
171
|
+
// 同款——缺席成员不破坏实现关系(裸桩承载缺席形态)。W3 后 inproc pi 引擎已删,
|
|
172
|
+
// id 不再由引擎身份推导断言(桩无 id 字面量),能力面 id 经 capabilities() 取。
|
|
173
|
+
const pi: EnginePort = { capabilities: () => ({ id: "pi-stub" }) } as unknown as EnginePort;
|
|
174
|
+
// EngineCapabilities 契约面无 id 成员(contract-types.ts)——运行期桩携带的 id 经
|
|
175
|
+
// 窄化读取断言(HEAD 既有类型红此处修复:pi.capabilities().id 直取 TS2339)。
|
|
176
|
+
expect((pi.capabilities() as unknown as { id: string }).id).toBe("pi-stub");
|
|
177
|
+
expect(typeof pi.capabilities).toBe("function");
|
|
178
|
+
expect(pi.inFlightSnapshot).toBeUndefined();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -33,9 +33,6 @@ function fakeEngine(id: string, models: Array<{ id: string; name?: string }> | n
|
|
|
33
33
|
run: async () => {
|
|
34
34
|
throw new Error("not used in this test");
|
|
35
35
|
},
|
|
36
|
-
interact: async () => {
|
|
37
|
-
throw new Error("not used in this test");
|
|
38
|
-
},
|
|
39
36
|
read: async () => ({ engineId: id, turns: [], source: "outcome-only" }),
|
|
40
37
|
...(models !== null ? { listModels: () => models } : {}),
|
|
41
38
|
};
|
|
@@ -53,7 +53,6 @@ function makeFakeEngine(id: string): EnginePort {
|
|
|
53
53
|
Promise.resolve({ ok: true, engineVersion: "0.0.0-test", checks: [{ name: "stub", ok: true }] }),
|
|
54
54
|
run: (_task: AgentCallOpts, _ctx: RunContext) =>
|
|
55
55
|
Promise.reject(new Error("fake engine: run not implemented")),
|
|
56
|
-
interact: () => Promise.resolve({ ok: false, code: "engine_capability_unsupported", message: "stub" }),
|
|
57
56
|
read: (_handle: Parameters<EnginePort["read"]>[0]): Promise<SessionView> =>
|
|
58
57
|
Promise.resolve({ engineId: id, turns: [], source: "outcome-only" }),
|
|
59
58
|
};
|
|
@@ -40,7 +40,6 @@ function makeFakeEngine(id: string, probeOk: boolean): EnginePort {
|
|
|
40
40
|
} satisfies ProbeReport,
|
|
41
41
|
),
|
|
42
42
|
run: (_t: AgentCallOpts, _c: RunContext) => Promise.reject(new Error("fake: run not implemented")),
|
|
43
|
-
interact: () => Promise.resolve({ ok: false, code: "engine_capability_unsupported", message: "stub" }),
|
|
44
43
|
read: (_h: Parameters<EnginePort["read"]>[0]): Promise<SessionView> =>
|
|
45
44
|
Promise.resolve({ engineId: id, turns: [], source: "outcome-only" }),
|
|
46
45
|
};
|