@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
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// src/execution/__tests__/get-record-for-action-restart.test.ts
|
|
2
2
|
//
|
|
3
|
-
// [M10] getRecordForAction 跨重启磁盘重建分支测试(
|
|
3
|
+
// [M10] getRecordForAction 跨重启磁盘重建分支测试(cold-lookup.coldLookupForAction)。
|
|
4
4
|
//
|
|
5
5
|
// 背景:内存 miss → collectRecords(1000,"all",undefined).find(status==="running") →
|
|
6
|
-
// createRecord({chatMode:
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
//
|
|
6
|
+
// createRecord({chatMode: 持久化值}) → register → 回填 sessionFile/round。
|
|
7
|
+
// [H1 U6] 原「无条件 chatMode: true」(v4 A-3)已改水合保留持久化 chatMode
|
|
8
|
+
//(D4/D5:升级置位迁 Continuation revive 格 + messageHandler gate 双写点)。
|
|
9
|
+
// 该分支含多个仅此处独有的决策:
|
|
10
|
+
// - chatMode 水合保留(磁盘无持久化 → false;升级在 message 链 gate 化)
|
|
10
11
|
// - rootSessionFilter 传 undefined 后置校验(异树仍 throw not owned)
|
|
11
12
|
// - sessionFile/round 从磁盘重建结果回填(round 无磁盘持久化 → undefined)
|
|
12
13
|
//
|
|
@@ -25,11 +26,10 @@ const { loggerMock } = vi.hoisted(() => ({
|
|
|
25
26
|
}));
|
|
26
27
|
vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
|
|
27
28
|
|
|
28
|
-
// [W3 改写
|
|
29
|
-
//
|
|
30
|
-
// resumeColdRound 守卫链)。
|
|
29
|
+
// [W3 改写 → H1 U6] deliverChatMessage 走协议 seam(registerFakePiEngine 替身)——
|
|
30
|
+
// 每轮 = 新 run + resume 锚点,续聊守卫链归 Continuation(dispatchRoundGuarded)。
|
|
31
31
|
|
|
32
|
-
import {
|
|
32
|
+
import { writeFinalizedState } from "../state-marker.ts";
|
|
33
33
|
import { registerFakePiEngine } from "./helpers/fake-engine-port.ts";
|
|
34
34
|
import { clearEngines } from "../engine/registry.ts";
|
|
35
35
|
import { ModelConfigService } from "../model-config-service.ts";
|
|
@@ -158,16 +158,17 @@ describe("[M10] getRecordForAction 跨重启磁盘重建(S3 回归场景)",
|
|
|
158
158
|
for (const k of IDENTITY_ENV_KEYS) delete process.env[k];
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
-
it("内存 miss + 磁盘 running(无 .alive)→ 重建 record:chatMode
|
|
162
|
-
// identity entry 故意不带 chatMode
|
|
163
|
-
|
|
161
|
+
it("内存 miss + 磁盘 running(无 .alive)→ 重建 record:chatMode 水合保留(无持久化为 false,[H1 U6])、sessionFile 回填、register 进内存", () => {
|
|
162
|
+
// identity entry 故意不带 chatMode 字段——证明重建分支只水合持久化 chatMode
|
|
163
|
+
//([H1 U6] 无条件置位已迁 Continuation revive 格 + gate;one-shot 跨重启重建
|
|
164
|
+
// false 后经 message 链 gate 升级置位)
|
|
164
165
|
const file = writeSessionJsonl(sessionsDir, { id: "sa-restart-1", rootSessionId: "root-session" });
|
|
165
166
|
expect(store.getMutable("sa-restart-1")).toBeUndefined(); // 前置:内存确无
|
|
166
167
|
|
|
167
168
|
const record = service.chatActions.getRecordForAction("sa-restart-1");
|
|
168
169
|
|
|
169
|
-
// [v4 A-3]
|
|
170
|
-
expect(record.chatMode).toBe(
|
|
170
|
+
// [v4 A-3 → H1 U6] 水合保留:磁盘无 chatMode → false(升级在 message 链 gate 化)
|
|
171
|
+
expect(record.chatMode).toBe(false);
|
|
171
172
|
// sessionFile 从磁盘重建结果回填
|
|
172
173
|
expect(record.sessionFile).toBe(file);
|
|
173
174
|
// round 从 found 回填:light 磁盘重建无 round(内存态字段,跨重启不恢复)→ undefined
|
|
@@ -204,9 +205,9 @@ describe("[M10] getRecordForAction 跨重启磁盘重建(S3 回归场景)",
|
|
|
204
205
|
expect(() => service.chatActions.getRecordForAction("sa-grand")).toThrow(/direct parent/);
|
|
205
206
|
});
|
|
206
207
|
|
|
207
|
-
it(".
|
|
208
|
+
it(".state sidecar(closed 终态)不重建 → throw not found or not owned", () => {
|
|
208
209
|
const file = writeSessionJsonl(sessionsDir, { id: "sa-fin", rootSessionId: "root-session" });
|
|
209
|
-
|
|
210
|
+
writeFinalizedState(file); // sidecar 矩阵分支 2 → status=closed → find(status==="running") miss
|
|
210
211
|
|
|
211
212
|
expect(() => service.chatActions.getRecordForAction("sa-fin")).toThrow(/not found or not owned/);
|
|
212
213
|
expect(store.getMutable("sa-fin")).toBeUndefined(); // 未重建注册
|
|
@@ -221,12 +222,8 @@ describe("[M10] getRecordForAction 跨重启磁盘重建(S3 回归场景)",
|
|
|
221
222
|
it("[review round2] worktree record 跨重启重建 → hadWorktree 标记 + 续聊被拒(行动语言)", async () => {
|
|
222
223
|
clearEngines();
|
|
223
224
|
const fake = registerFakePiEngine();
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
ok: false,
|
|
227
|
-
code: "engine_session_not_resumable",
|
|
228
|
-
message: "no live process (cold path)",
|
|
229
|
-
};
|
|
225
|
+
// [H1 U6] 每轮 = 新 run + resume 锚点(旧 interact 冷路径替身注入随 interact 面退役),
|
|
226
|
+
// 续聊守卫链归 Continuation(dispatchRoundGuarded)。
|
|
230
227
|
writeSessionJsonl(sessionsDir, {
|
|
231
228
|
id: "sa-wt",
|
|
232
229
|
rootSessionId: "root-session",
|
|
@@ -240,7 +237,7 @@ describe("[M10] getRecordForAction 跨重启磁盘重建(S3 回归场景)",
|
|
|
240
237
|
expect(record.worktreeHandle).toBeUndefined();
|
|
241
238
|
|
|
242
239
|
// 冷路径续聊(无活进程)→ 续轮守卫拒绝,不 spawn 回落主 repo
|
|
243
|
-
await expect(service.chatActions.deliverChatMessage(record, "resume after restart"
|
|
240
|
+
await expect(service.chatActions.deliverChatMessage(record, "resume after restart")).rejects.toThrow(
|
|
244
241
|
/worktree isolation.*lost when the parent process restarted/,
|
|
245
242
|
);
|
|
246
243
|
});
|
|
@@ -248,11 +245,6 @@ describe("[M10] getRecordForAction 跨重启磁盘重建(S3 回归场景)",
|
|
|
248
245
|
it("[review round2] 非 worktree record 跨重启重建 → 续聊不受 worktree 守卫拦截(向后兼容)", async () => {
|
|
249
246
|
clearEngines();
|
|
250
247
|
const fake = registerFakePiEngine();
|
|
251
|
-
fake.interactMessageResult = {
|
|
252
|
-
ok: false,
|
|
253
|
-
code: "engine_session_not_resumable",
|
|
254
|
-
message: "no live process (cold path)",
|
|
255
|
-
};
|
|
256
248
|
// identity entry 无 worktree 字段(旧文件)→ found.worktree undefined → hadWorktree false
|
|
257
249
|
writeSessionJsonl(sessionsDir, { id: "sa-nowt", rootSessionId: "root-session" });
|
|
258
250
|
|
|
@@ -261,6 +253,6 @@ describe("[M10] getRecordForAction 跨重启磁盘重建(S3 回归场景)",
|
|
|
261
253
|
|
|
262
254
|
// 冷路径续聊不被 worktree 守卫拦截(resume 正常发起;后续 spawn 编排
|
|
263
255
|
// 超出本用例关注点——runSpawn 在本文件是 no-op mock)
|
|
264
|
-
await expect(service.chatActions.deliverChatMessage(record, "resume normal"
|
|
256
|
+
await expect(service.chatActions.deliverChatMessage(record, "resume normal")).resolves.toBeUndefined();
|
|
265
257
|
});
|
|
266
258
|
});
|
|
@@ -1,33 +1,27 @@
|
|
|
1
1
|
// src/execution/__tests__/helpers/fake-engine-port.ts
|
|
2
2
|
//
|
|
3
|
-
// [W3 chat 域收口] SubagentService 编排层测试共享的 pi EnginePort 替身。
|
|
3
|
+
// [W3 chat 域收口 → H1 U6 定形] SubagentService 编排层测试共享的 pi EnginePort 替身。
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
// 拿到 fake 实例后驱动 run/interact/roundLifecycle,断言协议交互。
|
|
5
|
+
// 背景:会话形态轮经协议客户端(registry 'pi' 的 cli 形态 port——RemoteEngine)发往
|
|
6
|
+
// pi-subagent-cli 引擎进程;原 inproc 链路的测试替身随删件消亡。本 helper 是协议 seam
|
|
7
|
+
// 的替身单源:测试经 registerEngine("pi", fake) 注入,拿到 fake 实例后驱动 run,
|
|
8
|
+
// 断言协议交互。
|
|
10
9
|
//
|
|
11
10
|
// 消费方式(与 spawn-mock.ts 同款先例):
|
|
12
11
|
// const fake = registerFakePiEngine();
|
|
13
12
|
// await service.execute({ task: "...", conversation: true });
|
|
14
|
-
// fake.runs[0].settle({ content: "round text" }); //
|
|
15
|
-
// fake.interacts[0] // interact 受理断言
|
|
13
|
+
// fake.runs[0].settle({ content: "round text" }); // 模拟 agent_settled 应答
|
|
16
14
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// - interact cancel = { ok: true, delivered: true }(引擎侧 SIGTERM → settle → 杀链);
|
|
21
|
-
// - roundLifecycle 相位由测试显式驱动(idle 帧先于 run 应答帧)。
|
|
15
|
+
// 形态对照([H1] chat-run 统一终态):每轮 = 新 run + resume 锚点(ctx.resume),
|
|
16
|
+
// run 应答时点 = agent_settled(outcome = 本轮内容非会话终态);[H1 U6] interact 面
|
|
17
|
+
// 与 recordId 键路由模拟面已随 chat 域退役删除。
|
|
22
18
|
|
|
23
|
-
import type { EnginePort, EngineRunResult, RunContext
|
|
19
|
+
import type { EnginePort, EngineRunResult, RunContext } from "../../../execution/engine/port.ts";
|
|
24
20
|
import type { AgentCallOpts } from "../../../orchestration/models/types.ts";
|
|
25
21
|
import type {
|
|
26
22
|
AgentOutcome,
|
|
27
23
|
EngineCapabilities,
|
|
28
24
|
EngineHandle,
|
|
29
|
-
InteractAction,
|
|
30
|
-
InteractResult,
|
|
31
25
|
ProbeReport,
|
|
32
26
|
SessionView,
|
|
33
27
|
} from "../../../execution/engine/types.ts";
|
|
@@ -50,7 +44,7 @@ export class FakeRun {
|
|
|
50
44
|
});
|
|
51
45
|
}
|
|
52
46
|
|
|
53
|
-
/** 模拟 run
|
|
47
|
+
/** 模拟 run 正常应答(agent_settled 口径;outcome 缺省 = 成功空轮)。 */
|
|
54
48
|
settle(outcome: Partial<AgentOutcome> = {}): void {
|
|
55
49
|
this.resolve({
|
|
56
50
|
handle: {
|
|
@@ -80,44 +74,22 @@ export class FakeRun {
|
|
|
80
74
|
this.ctx.onEvent?.(event as never);
|
|
81
75
|
}
|
|
82
76
|
|
|
83
|
-
/** 模拟 host/streamDelta(runId
|
|
77
|
+
/** 模拟 host/streamDelta(runId 键——run 作用域路由)。 */
|
|
84
78
|
emitDelta(delta: string): void {
|
|
85
79
|
this.ctx.stream?.onDelta(delta);
|
|
86
80
|
}
|
|
87
81
|
|
|
88
|
-
/** 模拟 host/roundLifecycle(runId 键——首轮)。 */
|
|
89
|
-
emitLifecycle(phase: Record<string, unknown>): void {
|
|
90
|
-
this.ctx.onRoundLifecycle?.({ runId: this.ctx.taskId, ...phase } as never);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
82
|
/** 模拟 host/handleReady(运行中句柄回填)。 */
|
|
94
83
|
emitHandleReady(sessionRef: Record<string, string>, poolKey = "shared"): void {
|
|
95
84
|
this.ctx.onHandleReady?.({ sessionRef, poolKey });
|
|
96
85
|
}
|
|
97
86
|
}
|
|
98
87
|
|
|
99
|
-
/** 一次 interact 调用的捕获(action 原样记录,供断言)。 */
|
|
100
|
-
export interface FakeInteractCall {
|
|
101
|
-
handle: EngineHandle;
|
|
102
|
-
action: InteractAction;
|
|
103
|
-
result: InteractResult | Promise<InteractResult>;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
88
|
export class FakePiEnginePort implements EnginePort {
|
|
107
89
|
readonly id = "pi";
|
|
108
90
|
|
|
109
91
|
/** run 调用捕获(按序)。 */
|
|
110
92
|
readonly runs: FakeRun[] = [];
|
|
111
|
-
/** interact 调用捕获(按序;result = 受理时返回值)。 */
|
|
112
|
-
readonly interacts: FakeInteractCall[] = [];
|
|
113
|
-
/** recordId 键路由注册捕获(recordId → 注销函数)。 */
|
|
114
|
-
readonly chatRoutes = new Map<string, ChatRoundRoute>();
|
|
115
|
-
readonly chatRouteUnregisters: string[] = [];
|
|
116
|
-
|
|
117
|
-
/** interact message 的应答形态(默认受理;测试改写为冷路径拒绝等)。 */
|
|
118
|
-
interactMessageResult: InteractResult = { ok: true, delivered: true };
|
|
119
|
-
/** interact cancel / close 的应答形态。 */
|
|
120
|
-
interactControlResult: InteractResult = { ok: true, delivered: true };
|
|
121
93
|
|
|
122
94
|
capabilities(): EngineCapabilities {
|
|
123
95
|
// 与 pi-subagent-cli manifest 逐位一致(gate 同步面放行 pi 全参数——V4⑤ 反向守护)。
|
|
@@ -146,37 +118,10 @@ export class FakePiEnginePort implements EnginePort {
|
|
|
146
118
|
return captured.promise;
|
|
147
119
|
}
|
|
148
120
|
|
|
149
|
-
async interact(handle: EngineHandle, action: InteractAction): Promise<InteractResult> {
|
|
150
|
-
const result = action.kind === "message" ? this.interactMessageResult : this.interactControlResult;
|
|
151
|
-
this.interacts.push({ handle, action, result });
|
|
152
|
-
return result;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
121
|
async read(handle: EngineHandle): Promise<SessionView> {
|
|
122
|
+
void handle;
|
|
156
123
|
return { engineId: "pi", turns: [], source: "outcome-only" };
|
|
157
124
|
}
|
|
158
|
-
|
|
159
|
-
registerChatRoundRoute(recordId: string, route: ChatRoundRoute): () => void {
|
|
160
|
-
this.chatRoutes.set(recordId, route);
|
|
161
|
-
return () => {
|
|
162
|
-
if (this.chatRoutes.get(recordId) === route) {
|
|
163
|
-
this.chatRoutes.delete(recordId);
|
|
164
|
-
this.chatRouteUnregisters.push(recordId);
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// ── 测试驱动面(recordId 键路由——interact 续聊轮的协议回流)──
|
|
170
|
-
|
|
171
|
-
/** 向指定 record 的 chat 路由发 recordId 键 delta。 */
|
|
172
|
-
emitRecordDelta(recordId: string, delta: string): void {
|
|
173
|
-
void this.chatRoutes.get(recordId)?.onStreamDelta?.(delta);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/** 向指定 record 的 chat 路由发 recordId 键 roundLifecycle 相位。 */
|
|
177
|
-
emitRecordLifecycle(recordId: string, phase: Record<string, unknown>): void {
|
|
178
|
-
void this.chatRoutes.get(recordId)?.onRoundLifecycle?.({ recordId, ...phase } as never);
|
|
179
|
-
}
|
|
180
125
|
}
|
|
181
126
|
|
|
182
127
|
/** 注册替身 pi 引擎进 registry(覆盖同 id 既有注册——测试隔离自负责清理)。 */
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// src/execution/__tests__/helpers/legacy-sidecar.ts
|
|
2
|
+
//
|
|
3
|
+
// 兼容期旧格式终态 sidecar fixture(`.finalized` / `.cancelled`)——L4 合并后生产
|
|
4
|
+
// 只写 `.state`,但读侧仍兼容旧名(存量文件不迁移)。测试需要造旧格式文件验证兼容
|
|
5
|
+
// 读路径,此处是唯一 fixture 入口(格式细节单源,避免各测试散落 fs.writeFileSync)。
|
|
6
|
+
//
|
|
7
|
+
// 形态 = 合并前两模块的写侧逐字等价:
|
|
8
|
+
// .finalized 内容 = reason 字符串(undefined → 空文件,v8.5 前格式)
|
|
9
|
+
// .cancelled 内容 = 单行 JSON tombstone {id, status, agent, startedAt, endedAt}
|
|
10
|
+
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
|
|
13
|
+
/** 旧 .cancelled tombstone 形态(合并前 CancelledTombstone 逐字等价)。 */
|
|
14
|
+
export interface LegacyCancelledTombstone {
|
|
15
|
+
id: string;
|
|
16
|
+
status: "cancelled";
|
|
17
|
+
agent: string;
|
|
18
|
+
startedAt: number;
|
|
19
|
+
endedAt: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 写旧格式 `.finalized`(测试 fixture;reason 缺省 = 空文件旧格式)。 */
|
|
23
|
+
export function writeLegacyFinalizedSidecar(sessionFile: string, reason?: string): void {
|
|
24
|
+
fs.writeFileSync(`${sessionFile}.finalized`, reason ?? "", "utf-8");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 写旧格式 `.cancelled`(测试 fixture;完整 tombstone 形态)。 */
|
|
28
|
+
export function writeLegacyCancelledSidecar(
|
|
29
|
+
sessionFile: string,
|
|
30
|
+
meta: LegacyCancelledTombstone,
|
|
31
|
+
): void {
|
|
32
|
+
fs.writeFileSync(`${sessionFile}.cancelled`, `${JSON.stringify(meta)}\n`, "utf-8");
|
|
33
|
+
}
|
|
@@ -88,11 +88,15 @@ export function aliveStoreModule(actual: typeof import("../../alive-store.ts"))
|
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
/** ../
|
|
92
|
-
|
|
91
|
+
/** ../state-marker.ts mock 模块(避免真实 fs 写 sidecar;读侧恒「无终态标记」,
|
|
92
|
+
* 对齐原 finalized-marker mock 的 no-finalized 语义)。 */
|
|
93
|
+
export function stateMarkerModule() {
|
|
93
94
|
return {
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
writeFinalizedState: vi.fn(),
|
|
96
|
+
writeCancelledState: vi.fn(),
|
|
97
|
+
readStateMarker: vi.fn(() => undefined),
|
|
98
|
+
statStateStamp: vi.fn(() => null),
|
|
99
|
+
STATE_SIDECAR_EXT: ".state",
|
|
96
100
|
};
|
|
97
101
|
}
|
|
98
102
|
|
|
@@ -107,7 +111,7 @@ export function manifestStoreModule() {
|
|
|
107
111
|
writeManifest = vi.fn(async () => {});
|
|
108
112
|
readManifest = vi.fn(async () => null);
|
|
109
113
|
listAllSync = vi.fn(() => []);
|
|
110
|
-
|
|
114
|
+
sweepTmpFiles = vi.fn(async () => 0);
|
|
111
115
|
}
|
|
112
116
|
return { ManifestStore: vi.fn(function (_recordsDir: string) { return new FakeManifestStore(); }) };
|
|
113
117
|
}
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
// 只归档不补注销(archive 不发 pending:unregister——注销统一交对账 sweep)、
|
|
5
5
|
// WorkflowRun store 纳入(startedAt 锚终态化 + save)。fake timers 推进 GC
|
|
6
6
|
// interval;RecordStore 用 mkdtemp 自建目录 + pi=null(archive 纯内存,零磁盘写)。
|
|
7
|
+
//
|
|
8
|
+
// [U2b / C1] markIdleArchived 归口:归档时 `.alive` 写权声明同步 release(D3a
|
|
9
|
+
// release 出口②)——S6 验收锚点链的单元级断言(fake clock 推进 30 天 → 归档 →
|
|
10
|
+
// marker 已删 → fork-from 探针放行 → 接管 acquire 重声明)。
|
|
7
11
|
|
|
8
12
|
import * as fs from "node:fs";
|
|
9
13
|
import * as os from "node:os";
|
|
@@ -11,6 +15,7 @@ import * as path from "node:path";
|
|
|
11
15
|
|
|
12
16
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
13
17
|
|
|
18
|
+
import { findForeignLiveInstance } from "../alive-store.ts";
|
|
14
19
|
import { createRecord } from "../execution-record.ts";
|
|
15
20
|
import { startIdleGc } from "../idle-gc.ts";
|
|
16
21
|
import { RecordStore } from "../record-store.ts";
|
|
@@ -53,6 +58,61 @@ function makeRecord(id: string, overrides: Partial<ExecutionRecord> = {}): Execu
|
|
|
53
58
|
return record;
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
describe("idle-GC 归口 markIdleArchived(U2b/C1——D3a release 出口②闭环链,S6 锚点)", () => {
|
|
62
|
+
it("fake clock 推进 30 天 → 归档 + `.alive` 已删 + fork-from 探针放行 + 接管 acquire 重声明(闭环链)", async () => {
|
|
63
|
+
const store = makeStore();
|
|
64
|
+
const sessionFile = path.join(tmpDir, "lease-session.jsonl");
|
|
65
|
+
fs.writeFileSync(sessionFile, "{}\n", "utf-8");
|
|
66
|
+
const rec = makeRecord("bg-lease", {
|
|
67
|
+
startedAt: Date.now() - 31 * DAY_MS,
|
|
68
|
+
idleSince: Date.now() - 31 * DAY_MS,
|
|
69
|
+
sessionFile,
|
|
70
|
+
});
|
|
71
|
+
store.register(rec);
|
|
72
|
+
// 持有期声明在位(模拟 spawn 侧 acquireWriteLease 已声明写权的 resumable record)。
|
|
73
|
+
store.acquireWriteLease(sessionFile, rec.id);
|
|
74
|
+
expect(fs.existsSync(`${sessionFile}.alive`)).toBe(true);
|
|
75
|
+
|
|
76
|
+
stop = startIdleGc(store);
|
|
77
|
+
await vi.advanceTimersByTimeAsync(GC_INTERVAL_MS + 1);
|
|
78
|
+
|
|
79
|
+
// ① 归档生效(内存移除——record 磁盘仍 running 可接管,非终态化)。
|
|
80
|
+
expect(store.getMutable("bg-lease")).toBeUndefined();
|
|
81
|
+
// ② marker 已删(release 生效——归档 = 放弃持有 = 放弃写权声明)。
|
|
82
|
+
expect(fs.existsSync(`${sessionFile}.alive`)).toBe(false);
|
|
83
|
+
// ③ fork-from 放行:探针无 marker 即无声明,不再被残留声明拦。
|
|
84
|
+
expect(findForeignLiveInstance(sessionFile)).toBeUndefined();
|
|
85
|
+
// ④ message 接管 acquire 重声明:接管时统一 acquireWriteLease(接管链经
|
|
86
|
+
// markResurrected 的 acquire-first,本处以 store 内部 acquire 动作直驱同语义)。
|
|
87
|
+
store.acquireWriteLease(sessionFile, "bg-lease");
|
|
88
|
+
const marker = JSON.parse(fs.readFileSync(`${sessionFile}.alive`, "utf-8")) as {
|
|
89
|
+
pid: number;
|
|
90
|
+
id: string;
|
|
91
|
+
};
|
|
92
|
+
expect(marker).toMatchObject({ pid: process.pid, id: "bg-lease" });
|
|
93
|
+
// 重声明后探针对本进程仍放行(self-pid 排除——自有声明不构成 foreign)。
|
|
94
|
+
expect(findForeignLiveInstance(sessionFile)).toBeUndefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("锚窗内的 record 不归档且 `.alive` 不 release(无早释)", async () => {
|
|
98
|
+
const store = makeStore();
|
|
99
|
+
const sessionFile = path.join(tmpDir, "lease-fresh.jsonl");
|
|
100
|
+
fs.writeFileSync(sessionFile, "{}\n", "utf-8");
|
|
101
|
+
const rec = makeRecord("bg-hold", {
|
|
102
|
+
startedAt: Date.now() - 1 * DAY_MS,
|
|
103
|
+
sessionFile,
|
|
104
|
+
});
|
|
105
|
+
store.register(rec);
|
|
106
|
+
store.acquireWriteLease(sessionFile, rec.id);
|
|
107
|
+
|
|
108
|
+
stop = startIdleGc(store);
|
|
109
|
+
await vi.advanceTimersByTimeAsync(GC_INTERVAL_MS + 1);
|
|
110
|
+
|
|
111
|
+
expect(store.getMutable("bg-hold")).toBeDefined();
|
|
112
|
+
expect(fs.existsSync(`${sessionFile}.alive`)).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
56
116
|
describe("idle-gc record 锚扩展(W4)", () => {
|
|
57
117
|
it("无 idleSince 的 resumable record 以 startedAt 为锚:超 30 天归档", async () => {
|
|
58
118
|
const store = makeStore();
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// src/execution/__tests__/inflight-production-wiring.test.ts
|
|
2
|
+
//
|
|
3
|
+
// [u7a 生产补挂] D5 在途推送的生产链路 wiring 测试(crash-forensics impl-plan §7 v6)。
|
|
4
|
+
//
|
|
5
|
+
// 背景:notifyInFlightChanged 此前唯一挂点 = host-bridge arm/disarm 委托,而
|
|
6
|
+
// createHostBridge 全仓无生产调用点——生产迁移点(subagent-service 裸 arm/disarm/
|
|
7
|
+
// 镜像置死、EngineClient 反向通道镜像)全部不推送,extension 监听者恒收不到帧,
|
|
8
|
+
// runtime 侧 inflight 镜像恒 0(Gate B A4 BLOCKED 根因 a)。
|
|
9
|
+
//
|
|
10
|
+
// 三视角:
|
|
11
|
+
// ①使用者(extension reporter 视角)——注册 setInFlightListener 后,生产迁移点
|
|
12
|
+
// (idle 相位 arm / 续聊 disarm / 守护击杀 / dispose 批量回收 / 引擎反向通道
|
|
13
|
+
// childSpawned/childStateChanged)每一处都推来含该 record 贡献的绝对计数快照。
|
|
14
|
+
// ②构建者——推送携带的是双谓词过滤后的真实计数:活句柄 + 无 armed idle timer
|
|
15
|
+
// 才计在途;镜像置死后计数即刻回落(EngineClient 镜像桥接投影)。
|
|
16
|
+
// ③观察者——EngineClient 镜像 → core 镜像的单向投影可从镜像面观察(core 镜像
|
|
17
|
+
// 条目随反向通道事件出现/置死),壳层监听者异常不反噬主链(既有出口测试覆盖,
|
|
18
|
+
// 此处不重复)。
|
|
19
|
+
|
|
20
|
+
import * as fs from "node:fs";
|
|
21
|
+
import * as os from "node:os";
|
|
22
|
+
import * as path from "node:path";
|
|
23
|
+
import { EventEmitter } from "node:events";
|
|
24
|
+
import type { ChildProcess } from "node:child_process";
|
|
25
|
+
|
|
26
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
27
|
+
|
|
28
|
+
const { loggerMock } = vi.hoisted(() => ({
|
|
29
|
+
loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
30
|
+
}));
|
|
31
|
+
vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
|
|
32
|
+
|
|
33
|
+
import { clearEngines } from "../engine/registry.ts";
|
|
34
|
+
import { registerFakePiEngine, type FakePiEnginePort } from "./helpers/fake-engine-port.ts";
|
|
35
|
+
import { createRecord } from "../execution-record.ts";
|
|
36
|
+
import { ModelConfigService } from "../model-config-service.ts";
|
|
37
|
+
import type { ModelInfo, ModelRegistryLike } from "../model-resolver.ts";
|
|
38
|
+
import type { RecordStore } from "../record-store.ts";
|
|
39
|
+
import { SubagentService, type PiLike } from "../subagent-service.ts";
|
|
40
|
+
import { _resetLifecycleState, hasIdleTimer } from "../lifecycle-manager.ts";
|
|
41
|
+
import {
|
|
42
|
+
registerSpawnedChildForRecord,
|
|
43
|
+
_resetCoreSpawnedChildrenMirrorForTest,
|
|
44
|
+
} from "../engine/host/spawned-children.ts";
|
|
45
|
+
import {
|
|
46
|
+
getInFlightSnapshot,
|
|
47
|
+
setInFlightListener,
|
|
48
|
+
type InFlightSnapshot,
|
|
49
|
+
} from "../engine/inflight-snapshot.ts";
|
|
50
|
+
import { EngineClient } from "../engine/client/engine-client.ts";
|
|
51
|
+
|
|
52
|
+
const CTX_MODEL: ModelInfo = { id: "m", name: "M", provider: "p", reasoning: false };
|
|
53
|
+
|
|
54
|
+
function makeEmptyRegistry(): ModelRegistryLike {
|
|
55
|
+
return { getAvailable: () => [], find: () => undefined, hasConfiguredAuth: () => true };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function makePi(): PiLike {
|
|
59
|
+
return {
|
|
60
|
+
appendEntry: vi.fn(),
|
|
61
|
+
events: { emit: vi.fn() },
|
|
62
|
+
sendMessage: vi.fn(),
|
|
63
|
+
} as unknown as PiLike;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 最小 fake 子进程(EventEmitter + killed/pid——hasLiveProcessHandle 消费面)。 */
|
|
67
|
+
function makeFakeChild(pid = 4242): ChildProcess & EventEmitter {
|
|
68
|
+
const child = new EventEmitter() as ChildProcess & EventEmitter;
|
|
69
|
+
(child as { pid?: number }).pid = pid;
|
|
70
|
+
(child as { killed: boolean }).killed = false;
|
|
71
|
+
return child;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface ServiceInternals {
|
|
75
|
+
store: RecordStore;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function setup(): {
|
|
79
|
+
agentDir: string;
|
|
80
|
+
service: SubagentService;
|
|
81
|
+
store: RecordStore;
|
|
82
|
+
fake: FakePiEnginePort;
|
|
83
|
+
} {
|
|
84
|
+
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "inflight-wiring-"));
|
|
85
|
+
clearEngines();
|
|
86
|
+
const fake = registerFakePiEngine();
|
|
87
|
+
const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
|
|
88
|
+
modelService.initModel({ modelRegistry: makeEmptyRegistry(), sessionId: "root-session", ctxModel: CTX_MODEL });
|
|
89
|
+
const service = new SubagentService({ cwd: agentDir, modelService });
|
|
90
|
+
service.initSession({ pi: makePi(), sessionId: "root-session" });
|
|
91
|
+
const store = (service as unknown as ServiceInternals).store;
|
|
92
|
+
return { agentDir, service, store, fake };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** chatMode 续聊态 record(首轮已完成等待续聊——v4 B-1 折入 running)。 */
|
|
96
|
+
function makeResumableRecord(id: string): ReturnType<typeof createRecord> {
|
|
97
|
+
const record = createRecord(id, {
|
|
98
|
+
agent: "general-purpose",
|
|
99
|
+
model: "test/model",
|
|
100
|
+
mode: "background",
|
|
101
|
+
task: "initial task",
|
|
102
|
+
slug: "chat",
|
|
103
|
+
startedAt: 1000,
|
|
104
|
+
rootSessionId: "root-session",
|
|
105
|
+
chatMode: true,
|
|
106
|
+
});
|
|
107
|
+
record.status = "running";
|
|
108
|
+
record.round = 1;
|
|
109
|
+
record.controller = new AbortController();
|
|
110
|
+
return record;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
describe("u7a 生产迁移点 → 在途推送(wiring)", () => {
|
|
114
|
+
let agentDir: string;
|
|
115
|
+
let service: SubagentService;
|
|
116
|
+
let store: RecordStore;
|
|
117
|
+
let fake: FakePiEnginePort;
|
|
118
|
+
let seen: InFlightSnapshot[];
|
|
119
|
+
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
({ agentDir, service, store, fake } = setup());
|
|
122
|
+
seen = [];
|
|
123
|
+
setInFlightListener((s) => seen.push({ ...s }));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
afterEach(() => {
|
|
127
|
+
setInFlightListener(null);
|
|
128
|
+
service.dispose();
|
|
129
|
+
clearEngines();
|
|
130
|
+
_resetLifecycleState();
|
|
131
|
+
_resetCoreSpawnedChildrenMirrorForTest();
|
|
132
|
+
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("idle 相位 arm(生产路径 = handleChatRoundPhase → armChatIdleTimer):活句柄翻入保活,推送 inFlight=0", async () => {
|
|
136
|
+
const handle = await service.execute({ task: "first round", slug: "t", ctxModel: CTX_MODEL, conversation: true });
|
|
137
|
+
const run = fake.runs[0]!;
|
|
138
|
+
// 引擎侧活句柄(生产经 EngineClient 反向通道桥接投影——此处直接落 core 镜像,
|
|
139
|
+
// 桥接本体在下方 EngineClient 套件独立验证)
|
|
140
|
+
registerSpawnedChildForRecord(handle.subagentId, makeFakeChild());
|
|
141
|
+
expect(getInFlightSnapshot().inFlight).toBe(1); // 前置:在途句柄在场
|
|
142
|
+
|
|
143
|
+
seen.length = 0;
|
|
144
|
+
run.emitLifecycle({ phase: "idle" });
|
|
145
|
+
expect(hasIdleTimer(handle.subagentId)).toBe(true); // idle 相位 = timer armed
|
|
146
|
+
expect(seen.at(-1)).toEqual({ inFlight: 0 }); // 保活不算在途(D5 谓词)
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("续聊投递 disarm(生产路径 = deliverChatMessage):翻回正在执行,推送 inFlight=1", async () => {
|
|
150
|
+
const record = makeResumableRecord("sa-wiring-disarm");
|
|
151
|
+
store.register(record);
|
|
152
|
+
registerSpawnedChildForRecord(record.id, makeFakeChild());
|
|
153
|
+
|
|
154
|
+
seen.length = 0;
|
|
155
|
+
await service.chatActions.deliverChatMessage(record, "next round msg", false);
|
|
156
|
+
|
|
157
|
+
expect(fake.interacts[0]!.action).toEqual({ kind: "message", payload: "next round msg", interrupt: false });
|
|
158
|
+
expect(hasIdleTimer(record.id)).toBe(false);
|
|
159
|
+
expect(seen.at(-1)).toEqual({ inFlight: 1 }); // disarm 后该 record 计在途
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("dispose 批量回收:逐 record kill+disarm 后收敛推送终态 inFlight=0", async () => {
|
|
163
|
+
const record = makeResumableRecord("sa-wiring-dispose");
|
|
164
|
+
store.register(record);
|
|
165
|
+
registerSpawnedChildForRecord(record.id, makeFakeChild());
|
|
166
|
+
expect(getInFlightSnapshot().inFlight).toBe(1);
|
|
167
|
+
|
|
168
|
+
seen.length = 0;
|
|
169
|
+
service.dispose();
|
|
170
|
+
expect(seen.at(-1)).toEqual({ inFlight: 0 });
|
|
171
|
+
expect(getInFlightSnapshot().inFlight).toBe(0);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("u7a 数据面桥接:EngineClient 反向通道镜像 → core 镜像 + 推送", () => {
|
|
176
|
+
let dataDir: string;
|
|
177
|
+
let client: EngineClient;
|
|
178
|
+
let seen: InFlightSnapshot[];
|
|
179
|
+
|
|
180
|
+
beforeEach(() => {
|
|
181
|
+
dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "inflight-bridge-"));
|
|
182
|
+
// 不 connect(构造即被测面:镜像变更经 onChange 同步广播)——零子进程零 fs 写
|
|
183
|
+
client = new EngineClient({
|
|
184
|
+
engineId: "fake",
|
|
185
|
+
command: process.execPath,
|
|
186
|
+
args: ["-e", "process.exit(0)"],
|
|
187
|
+
hostKind: "test",
|
|
188
|
+
hostVersion: "test-host-1.0",
|
|
189
|
+
dataDir,
|
|
190
|
+
envPrefixes: [],
|
|
191
|
+
});
|
|
192
|
+
seen = [];
|
|
193
|
+
setInFlightListener((s) => seen.push({ ...s }));
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
afterEach(() => {
|
|
197
|
+
setInFlightListener(null);
|
|
198
|
+
client.dispose();
|
|
199
|
+
_resetCoreSpawnedChildrenMirrorForTest();
|
|
200
|
+
_resetLifecycleState();
|
|
201
|
+
fs.rmSync(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("childSpawned → core 镜像落项 + 推送 inFlight=1;childStateChanged(exited) → 置死 + 推送 inFlight=0", () => {
|
|
205
|
+
client.mirror.recordSpawned(4242, "sa-bridge");
|
|
206
|
+
expect(seen.at(-1)).toEqual({ inFlight: 1 });
|
|
207
|
+
expect(getInFlightSnapshot().inFlight).toBe(1); // core 镜像已投影(计数数据源)
|
|
208
|
+
|
|
209
|
+
client.mirror.recordStateChanged({ pid: 4242, recordId: "sa-bridge", state: "exited", killed: true });
|
|
210
|
+
expect(seen.at(-1)).toEqual({ inFlight: 0 });
|
|
211
|
+
expect(getInFlightSnapshot().inFlight).toBe(0);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("同 recordId 重 spawn 覆盖 + killedAll 整体置死(引擎回收语义投影)", () => {
|
|
215
|
+
client.mirror.recordSpawned(111, "sa-bridge-a");
|
|
216
|
+
client.mirror.recordSpawned(222, "sa-bridge-b");
|
|
217
|
+
expect(seen.at(-1)).toEqual({ inFlight: 2 });
|
|
218
|
+
|
|
219
|
+
client.mirror.killAll(); // 引擎 exit / killAll:逐项 killedAll 事件
|
|
220
|
+
expect(seen.at(-1)).toEqual({ inFlight: 0 });
|
|
221
|
+
expect(getInFlightSnapshot().inFlight).toBe(0);
|
|
222
|
+
});
|
|
223
|
+
});
|