@zhushanwen/pi-subagent-workflow 7.3.4 → 8.0.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 +39 -12
- package/agents/analyst.md +61 -0
- package/agents/coder.md +70 -0
- package/agents/debugger.md +67 -0
- package/agents/doc-reviewer.md +3 -3
- package/agents/explorer.md +50 -18
- package/agents/general-purpose.md +19 -8
- package/agents/orchestrator.md +37 -32
- package/agents/planner.md +45 -11
- package/agents/researcher.md +53 -11
- package/agents/reviewer.md +74 -0
- package/package.json +1 -1
- package/skills/workflow-script-format/SKILL.md +1 -1
- package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
- package/src/execution/__tests__/agent-registry.test.ts +13 -11
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
- package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
- package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
- package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
- package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
- package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
- package/src/execution/__tests__/crash-recovery.test.ts +8 -2
- package/src/execution/__tests__/delivery-methods.test.ts +385 -0
- package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
- package/src/execution/__tests__/execute-nesting.test.ts +20 -72
- package/src/execution/__tests__/execution-record.test.ts +199 -0
- package/src/execution/__tests__/finalize-record.test.ts +170 -14
- package/src/execution/__tests__/format.test.ts +131 -7
- package/src/execution/__tests__/gc-timer.test.ts +184 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +25 -7
- package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
- package/src/execution/__tests__/index-session-start.test.ts +257 -5
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
- package/src/execution/__tests__/list-component.test.ts +59 -5
- package/src/execution/__tests__/list-fields.test.ts +109 -0
- package/src/execution/__tests__/model-resolver.test.ts +38 -1
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
- package/src/execution/__tests__/nested-visibility.test.ts +325 -0
- package/src/execution/__tests__/notifier-flush.test.ts +209 -7
- package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
- package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
- package/src/execution/__tests__/record-store.test.ts +158 -52
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
- package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
- package/src/execution/__tests__/resource-policy.test.ts +109 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
- package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
- package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
- package/src/execution/__tests__/session-pending.test.ts +61 -2
- package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
- package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
- package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
- package/src/execution/__tests__/spawn-args.test.ts +127 -49
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
- package/src/execution/__tests__/spawned-children.test.ts +92 -0
- package/src/execution/__tests__/status-refactor.test.ts +345 -0
- package/src/execution/__tests__/stdin-writer.test.ts +97 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +598 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
- package/src/execution/__tests__/subagent-service.test.ts +49 -11
- package/src/execution/__tests__/timeout-integration.test.ts +27 -13
- package/src/execution/__tests__/tool-action.test.ts +12 -10
- package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
- package/src/execution/__tests__/worktree-manager.test.ts +292 -89
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +13 -12
- package/src/execution/argv-mirror.ts +21 -2
- package/src/execution/execution-record.ts +126 -9
- package/src/execution/finalize-record.ts +90 -13
- package/src/execution/host-mode.ts +1 -1
- package/src/execution/lifecycle-manager.ts +484 -0
- package/src/execution/lifecycle-predicates.ts +65 -0
- package/src/execution/manifest-store.ts +61 -16
- package/src/execution/model-resolver.ts +26 -5
- package/src/execution/notifier.ts +69 -12
- package/src/execution/pi-invocation.ts +21 -1
- package/src/execution/record-store.ts +554 -107
- package/src/execution/session-pending.ts +116 -45
- package/src/execution/session-reconstructor.ts +224 -7
- package/src/execution/session-runner.ts +290 -75
- package/src/execution/sessions-index.ts +304 -0
- package/src/execution/stdin-writer.ts +93 -7
- package/src/execution/stream-sink.ts +20 -3
- package/src/execution/subagent-service.ts +867 -138
- package/src/execution/turn-limiter.ts +14 -0
- package/src/execution/types.ts +204 -22
- package/src/execution/worktree-manager.ts +128 -49
- package/src/execution/worktree-registry.ts +13 -2
- package/src/index.ts +277 -19
- package/src/injectors/subagent-list-injector.ts +26 -8
- package/src/injectors/workflow-list-injector.ts +25 -8
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +18 -5
- package/src/interface/__tests__/tool-render.test.ts +15 -13
- package/src/interface/bg-notify-render.ts +32 -8
- package/src/interface/command-actions.ts +26 -7
- package/src/interface/commands.ts +21 -22
- package/src/interface/format.ts +44 -17
- package/src/interface/gui-mappers.ts +6 -8
- package/src/interface/helpers.ts +170 -10
- package/src/interface/list-component.ts +53 -14
- package/src/interface/subagent-actions.ts +235 -17
- package/src/interface/subagent-tool.ts +82 -17
- package/src/interface/subagents.ts +2 -1
- package/src/interface/tool-render.ts +11 -24
- package/src/interface/tool-workflow.ts +20 -35
- package/src/interface/views/WorkflowsView.ts +89 -32
- package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
- package/src/interface/views/__tests__/detail-content-session-file.test.ts +1 -1
- package/src/interface/views/format.ts +3 -3
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +325 -0
- package/src/orchestration/__tests__/args-validator.test.ts +1 -1
- package/src/orchestration/__tests__/config-loader.test.ts +38 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
- package/src/orchestration/__tests__/execute-agent-call.test.ts +95 -0
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +657 -18
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
- package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
- package/src/orchestration/__tests__/skill-discovery.test.ts +157 -0
- package/src/orchestration/__tests__/test-mocks.ts +191 -0
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +98 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
- package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
- package/src/orchestration/__tests__/workflows-e2e.test.ts +1 -1
- package/src/orchestration/agent-opts-resolver.ts +4 -1
- package/src/orchestration/args-validator.ts +2 -2
- package/src/orchestration/config-loader.ts +30 -1
- package/src/orchestration/error-recovery.ts +133 -29
- package/src/orchestration/execute-agent-call.ts +31 -7
- package/src/orchestration/jsonl-run-store.ts +287 -40
- package/src/orchestration/launcher.ts +7 -1
- package/src/orchestration/lifecycle.ts +135 -132
- package/src/orchestration/models/__tests__/trace.test.ts +408 -0
- package/src/orchestration/models/budget.ts +1 -1
- package/src/orchestration/models/run-runtime.ts +15 -17
- package/src/orchestration/models/run-spec.ts +2 -2
- package/src/orchestration/models/run-state.ts +3 -3
- package/src/orchestration/models/trace.ts +95 -15
- package/src/orchestration/models/types.ts +8 -9
- package/src/orchestration/models/workflow-run.ts +50 -71
- package/src/orchestration/models/workflow-script.ts +32 -1
- package/src/orchestration/skill-discovery.ts +30 -0
- package/src/orchestration/worker-handle.ts +1 -1
- package/src/orchestration/worker-host.ts +1 -1
- package/src/orchestration/worker-script-builder.ts +29 -10
- package/src/shared/__tests__/agent-ref.test.ts +34 -0
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
- package/src/shared/__tests__/resource-discovery.test.ts +55 -0
- package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
- package/src/shared/agent-ref.ts +16 -0
- package/src/shared/resource-discovery.ts +147 -58
- package/src/shared/schema-jsonify.ts +53 -0
- package/workflows/README.md +4 -4
- package/agents/code-reviewer.md +0 -47
- package/agents/context-builder.md +0 -21
- package/agents/oracle.md +0 -34
- package/agents/worker.md +0 -20
|
@@ -89,13 +89,19 @@ function writeSessionJsonl(
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
describe("RecordStore", () => {
|
|
92
|
+
let rootDir: string;
|
|
92
93
|
let tmpDir: string;
|
|
93
94
|
|
|
94
95
|
beforeEach(() => {
|
|
95
|
-
tmpDir
|
|
96
|
+
// [DS3] 两层布局:tmpDir 深一层(对齐生产 <enc> 段结构),sessions-index.json 落
|
|
97
|
+
// dirname(tmpDir)=rootDir 内随 afterEach 清理(含 MF-3/S-20 的嵌套 <enc> 段索引)。
|
|
98
|
+
rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "rs-test-"));
|
|
99
|
+
tmpDir = path.join(rootDir, "sessions");
|
|
100
|
+
fs.mkdirSync(tmpDir);
|
|
96
101
|
});
|
|
97
102
|
afterEach(() => {
|
|
98
|
-
|
|
103
|
+
// maxRetries:fire-and-forget 的 sessions-index 写可能与删除并发(ENOTEMPTY 竞态)
|
|
104
|
+
fs.rmSync(rootDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
99
105
|
});
|
|
100
106
|
|
|
101
107
|
// ============================================================
|
|
@@ -104,7 +110,7 @@ describe("RecordStore", () => {
|
|
|
104
110
|
describe("archive 立即移除", () => {
|
|
105
111
|
it("archive 后 record 立即从内存移除(不再 linger)", () => {
|
|
106
112
|
const store = new RecordStore(tmpDir);
|
|
107
|
-
const r = makeRecord({ id: "sync-1", mode: "sync", status: "
|
|
113
|
+
const r = makeRecord({ id: "sync-1", mode: "sync", status: "closed" });
|
|
108
114
|
store.register(r);
|
|
109
115
|
expect(store.getMutable("sync-1")).toBeDefined();
|
|
110
116
|
store.archive(r);
|
|
@@ -113,7 +119,7 @@ describe("RecordStore", () => {
|
|
|
113
119
|
|
|
114
120
|
it("background record 同样立即移除(不再 FIFO)", () => {
|
|
115
121
|
const store = new RecordStore(tmpDir);
|
|
116
|
-
const r = makeRecord({ id: "bg-1", mode: "background", status: "
|
|
122
|
+
const r = makeRecord({ id: "bg-1", mode: "background", status: "closed" });
|
|
117
123
|
store.register(r);
|
|
118
124
|
store.archive(r);
|
|
119
125
|
expect(store.getMutable("bg-1")).toBeUndefined();
|
|
@@ -131,20 +137,25 @@ describe("RecordStore", () => {
|
|
|
131
137
|
expect(ids).toContain("run-1");
|
|
132
138
|
});
|
|
133
139
|
|
|
134
|
-
it("磁盘 session.jsonl
|
|
140
|
+
it("磁盘 session.jsonl 重建的 record 出现在结果中(无 sidecar → running, SP-2 跨重启可恢复)", () => {
|
|
135
141
|
const sessionFile = path.join(tmpDir, "2026-01-01-uuid-a.jsonl");
|
|
136
142
|
writeSessionJsonl(sessionFile, {
|
|
137
143
|
id: "bg-1", agent: "worker", mode: "background", task: "do it", startedAt: 5000,
|
|
138
144
|
});
|
|
139
|
-
// 无 sidecar → 四分支兜底
|
|
145
|
+
// 无 sidecar → 四分支兜底 running(v4 B-1:旧 idle 折入 running,SP-2 跨重启可恢复)
|
|
140
146
|
const store = new RecordStore(tmpDir);
|
|
141
147
|
const found = store.collectRecords(100).find((r) => r.id === "bg-1");
|
|
142
148
|
expect(found).toBeDefined();
|
|
143
|
-
expect(found?.status).toBe("
|
|
149
|
+
expect(found?.status).toBe("running");
|
|
144
150
|
expect(found?.agent).toBe("worker");
|
|
145
|
-
|
|
146
|
-
expect(found?.
|
|
147
|
-
expect(found?.result).
|
|
151
|
+
// [perf] 列表是 light(头部 identity,详情字段缺省)——turns/tokens/result 走 getFullRecord 懒加载
|
|
152
|
+
expect(found?.turns).toBe(0);
|
|
153
|
+
expect(found?.result).toBeUndefined();
|
|
154
|
+
const full = store.getFullRecord("bg-1");
|
|
155
|
+
expect(full?.turns).toBe(1);
|
|
156
|
+
expect(full?.totalTokens).toBe(30);
|
|
157
|
+
expect(full?.result).toBe("result text");
|
|
158
|
+
expect(full?.status).toBe("running"); // 状态矩阵在全量路径同样套用
|
|
148
159
|
});
|
|
149
160
|
|
|
150
161
|
it("磁盘 session.jsonl + .finalized sidecar → done", () => {
|
|
@@ -156,10 +167,10 @@ describe("RecordStore", () => {
|
|
|
156
167
|
const store = new RecordStore(tmpDir);
|
|
157
168
|
const found = store.collectRecords(100).find((r) => r.id === "bg-2");
|
|
158
169
|
expect(found).toBeDefined();
|
|
159
|
-
expect(found?.status).toBe("
|
|
170
|
+
expect(found?.status).toBe("closed");
|
|
160
171
|
});
|
|
161
172
|
|
|
162
|
-
it("statusFilter='running'
|
|
173
|
+
it("statusFilter='running' 含内存 + 磁盘跨重启 running(v4 B-1:旧 idle 折入 running)", () => {
|
|
163
174
|
const sessionFile = path.join(tmpDir, "2026-01-01-uuid-a.jsonl");
|
|
164
175
|
writeSessionJsonl(sessionFile, {
|
|
165
176
|
id: "bg-1", agent: "worker", mode: "background", task: "do it", startedAt: 5000,
|
|
@@ -168,7 +179,8 @@ describe("RecordStore", () => {
|
|
|
168
179
|
store.register(makeRecord({ id: "run-1", mode: "background", startedAt: 1000 }));
|
|
169
180
|
const filter: StatusFilter = "running";
|
|
170
181
|
const ids = store.collectRecords(100, filter).map((r) => r.id);
|
|
171
|
-
|
|
182
|
+
// 内存 run-1 + 磁盘跨重启 bg-1 都是 running(旧 idle 折入);startedAt desc → bg-1(5000) 先
|
|
183
|
+
expect(ids).toEqual(["bg-1", "run-1"]);
|
|
172
184
|
});
|
|
173
185
|
|
|
174
186
|
it("statusFilter='all'(默认)返回内存 + 磁盘", () => {
|
|
@@ -191,7 +203,7 @@ describe("RecordStore", () => {
|
|
|
191
203
|
const store = new RecordStore(tmpDir);
|
|
192
204
|
store.register(makeRecord({ id: "dup-1", mode: "background", status: "running", startedAt: 5000 }));
|
|
193
205
|
const found = store.collectRecords(100).find((r) => r.id === "dup-1");
|
|
194
|
-
expect(found?.status).toBe("running"); // 内存 running 覆盖磁盘
|
|
206
|
+
expect(found?.status).toBe("running"); // 内存 running 覆盖磁盘 idle(SP-2)
|
|
195
207
|
});
|
|
196
208
|
|
|
197
209
|
it("[MF-3/S-20] 跨进程组合:子进程(worktree)写入 enc(ROOT) 的深层 record 被 ROOT store 重建,同 rootSessionId 全树可见、他树被排除", () => {
|
|
@@ -235,7 +247,7 @@ describe("RecordStore", () => {
|
|
|
235
247
|
// cancelled tombstone override
|
|
236
248
|
// ============================================================
|
|
237
249
|
describe("cancelled tombstone", () => {
|
|
238
|
-
it("有 .cancelled sidecar → status override 为 cancelled", () => {
|
|
250
|
+
it("有 .cancelled sidecar → status override 为 closed + closedReason=cancelled", () => {
|
|
239
251
|
const sessionFile = path.join(tmpDir, "2026-01-01-uuid-a.jsonl");
|
|
240
252
|
writeSessionJsonl(sessionFile, {
|
|
241
253
|
id: "bg-1", agent: "worker", mode: "background", task: "do it", startedAt: 5000,
|
|
@@ -245,7 +257,9 @@ describe("RecordStore", () => {
|
|
|
245
257
|
});
|
|
246
258
|
const store = new RecordStore(tmpDir);
|
|
247
259
|
const found = store.collectRecords(100).find((r) => r.id === "bg-1");
|
|
248
|
-
|
|
260
|
+
// v4 B-1:cancelled 折入 closed,closedReason='cancelled' 保留用户取消语义
|
|
261
|
+
expect(found?.status).toBe("closed");
|
|
262
|
+
expect(found?.closedReason).toBe("cancelled");
|
|
249
263
|
expect(found?.error).toBe("cancelled by user");
|
|
250
264
|
});
|
|
251
265
|
});
|
|
@@ -254,17 +268,19 @@ describe("RecordStore", () => {
|
|
|
254
268
|
// compareRecords 排序稳定性(内存 running record)
|
|
255
269
|
// ============================================================
|
|
256
270
|
describe("compareRecords 排序", () => {
|
|
257
|
-
it("status priority(running <
|
|
271
|
+
it("status priority(running < closed)", () => {
|
|
258
272
|
const store = new RecordStore(tmpDir);
|
|
259
|
-
// 内存 running record
|
|
273
|
+
// 内存 running record
|
|
260
274
|
const running = makeRecord({ id: "run-1", mode: "background", startedAt: 3000, status: "running" });
|
|
261
275
|
store.register(running);
|
|
262
|
-
// 磁盘
|
|
263
|
-
|
|
276
|
+
// 磁盘 closed record(.finalized sidecar → closed,v4 B-1 统一终态)
|
|
277
|
+
const doneFile = path.join(tmpDir, "a.jsonl");
|
|
278
|
+
writeSessionJsonl(doneFile, {
|
|
264
279
|
id: "done-1", agent: "w", mode: "background", task: "t", startedAt: 5000,
|
|
265
280
|
});
|
|
281
|
+
writeFinalized(doneFile);
|
|
266
282
|
const ids = store.collectRecords(100).map((r) => r.id);
|
|
267
|
-
expect(ids[0]).toBe("run-1"); // running
|
|
283
|
+
expect(ids[0]).toBe("run-1"); // running(0) 排在 closed(3) 前
|
|
268
284
|
});
|
|
269
285
|
|
|
270
286
|
it("同 status 时 startedAt desc(新→旧)", () => {
|
|
@@ -275,30 +291,55 @@ describe("RecordStore", () => {
|
|
|
275
291
|
writeSessionJsonl(path.join(tmpDir, "new.jsonl"), {
|
|
276
292
|
id: "new", agent: "w", mode: "background", task: "t", startedAt: 9000,
|
|
277
293
|
});
|
|
278
|
-
// 两个都是
|
|
294
|
+
// 两个都是 running(磁盘重建,无 sidecar → SP-2 兜底 running,旧 idle 折入),按 startedAt desc
|
|
279
295
|
const ids = store.collectRecords(100).map((r) => r.id);
|
|
280
296
|
expect(ids).toEqual(["new", "old"]);
|
|
281
297
|
});
|
|
282
298
|
});
|
|
283
299
|
|
|
300
|
+
// ============================================================
|
|
301
|
+
// running 态 record(M2-A chatMode 续聊;v4 B-1:旧 idle 折入 running)
|
|
302
|
+
// ============================================================
|
|
303
|
+
describe("running 态 record(旧 idle 折入,等待续聊)", () => {
|
|
304
|
+
it("getMutable 能查到 running record(不筛 status,running record 留内存不 archive)", () => {
|
|
305
|
+
const store = new RecordStore(tmpDir);
|
|
306
|
+
const r = makeRecord({ id: "idle-1", status: "running" });
|
|
307
|
+
store.register(r);
|
|
308
|
+
// running record 不 archive → getMutable 仍可查(续聊定位依赖此)
|
|
309
|
+
expect(store.getMutable("idle-1")).toBe(r);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("running record 经 collectRecords(statusFilter=all) 可见", () => {
|
|
313
|
+
const store = new RecordStore(tmpDir);
|
|
314
|
+
store.register(makeRecord({ id: "idle-2", status: "running" }));
|
|
315
|
+
const ids = store.collectRecords(100, "all").map((r) => r.id);
|
|
316
|
+
expect(ids).toContain("idle-2");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("running record 经 collectRecords(statusFilter=running) 可见(v4 B-1:旧 idle 折入 running)", () => {
|
|
320
|
+
const store = new RecordStore(tmpDir);
|
|
321
|
+
store.register(makeRecord({ id: "idle-3", status: "running" }));
|
|
322
|
+
const ids = store.collectRecords(100, "running").map((r) => r.id);
|
|
323
|
+
expect(ids).toContain("idle-3");
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
|
|
284
328
|
// ============================================================
|
|
285
329
|
// 重建缓存
|
|
286
330
|
// ============================================================
|
|
287
331
|
describe("重建缓存", () => {
|
|
288
|
-
it("
|
|
332
|
+
it("新 session.jsonl 无需内存事件即立即可见([perf] stat 校验取代 notifyChange 整体失效)", () => {
|
|
289
333
|
const store = new RecordStore(tmpDir);
|
|
290
334
|
// 首次 collect:空目录
|
|
291
335
|
expect(store.collectRecords(100)).toHaveLength(0);
|
|
292
|
-
// 写新 session.jsonl
|
|
336
|
+
// 写新 session.jsonl(模拟外部进程/子进程写入——不经本进程任何内存事件)
|
|
293
337
|
writeSessionJsonl(path.join(tmpDir, "new.jsonl"), {
|
|
294
338
|
id: "bg-1", agent: "w", mode: "background", task: "t", startedAt: 1000,
|
|
295
339
|
});
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
//
|
|
299
|
-
store.register(makeRecord({ id: "trigger", mode: "sync", startedAt: 2000 }));
|
|
300
|
-
store.archive(makeRecord({ id: "trigger", mode: "sync", startedAt: 2000 }));
|
|
301
|
-
// 现在 bg-1 可见
|
|
340
|
+
// [perf] 旧实现:reconCache 建立后只能靠 register/archive 触发 notifyChange 失效,
|
|
341
|
+
// 否则永远 stale(外部进程写入的文件对本进程不可见)。新实现:每次 collectRecords
|
|
342
|
+
// 都 readdir + stat 校验,新文件立即可见(同时修复跨进程可见性)。
|
|
302
343
|
const ids = store.collectRecords(100).map((r) => r.id);
|
|
303
344
|
expect(ids).toContain("bg-1");
|
|
304
345
|
});
|
|
@@ -336,14 +377,16 @@ describe("RecordStore", () => {
|
|
|
336
377
|
}
|
|
337
378
|
|
|
338
379
|
// ── 分支 1: .cancelled ──
|
|
339
|
-
it(".cancelled sidecar → cancelled", () => {
|
|
380
|
+
it(".cancelled sidecar → closed + closedReason=cancelled", () => {
|
|
340
381
|
const sessionFile = writeBaseSession();
|
|
341
382
|
writeCancelledTombstone(sessionFile, {
|
|
342
383
|
id: SESSION_ID, status: "cancelled", agent: "worker", startedAt: STARTED_AT, endedAt: 6000,
|
|
343
384
|
});
|
|
344
385
|
const store = new RecordStore(tmpDir);
|
|
345
386
|
const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
|
|
346
|
-
|
|
387
|
+
// v4 B-1:cancelled 折入 closed(closedReason='cancelled' 区分用户取消)
|
|
388
|
+
expect(found?.status).toBe("closed");
|
|
389
|
+
expect(found?.closedReason).toBe("cancelled");
|
|
347
390
|
expect(found?.error).toBe("cancelled by user");
|
|
348
391
|
expect(found?.endedAt).toBe(6000);
|
|
349
392
|
});
|
|
@@ -354,7 +397,7 @@ describe("RecordStore", () => {
|
|
|
354
397
|
writeFinalized(sessionFile);
|
|
355
398
|
const store = new RecordStore(tmpDir);
|
|
356
399
|
const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
|
|
357
|
-
expect(found?.status).toBe("
|
|
400
|
+
expect(found?.status).toBe("closed");
|
|
358
401
|
});
|
|
359
402
|
|
|
360
403
|
// ── 分支 2: .finalized failed ──
|
|
@@ -386,7 +429,7 @@ describe("RecordStore", () => {
|
|
|
386
429
|
writeFinalized(sessionFile);
|
|
387
430
|
const store = new RecordStore(tmpDir);
|
|
388
431
|
const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
|
|
389
|
-
expect(found?.status).toBe("
|
|
432
|
+
expect(found?.status).toBe("closed");
|
|
390
433
|
});
|
|
391
434
|
|
|
392
435
|
// ── 分支 3: .alive + 活 pid → running + externalInstance ──
|
|
@@ -401,27 +444,27 @@ describe("RecordStore", () => {
|
|
|
401
444
|
expect(found?.externalInstance).toEqual(marker);
|
|
402
445
|
});
|
|
403
446
|
|
|
404
|
-
// ── 分支 3→4: .alive + 死 pid →
|
|
405
|
-
it(".alive + 死 pid →
|
|
447
|
+
// ── 分支 3→4: .alive + 死 pid → running(SP-2:跨重启可恢复)──
|
|
448
|
+
it(".alive + 死 pid → running(SP-2 跨重启可恢复)", () => {
|
|
406
449
|
const sessionFile = writeBaseSession();
|
|
407
450
|
const marker: AliveMarker = { pid: 9999999, id: SESSION_ID, startedAt: STARTED_AT };
|
|
408
451
|
writeAliveMarker(sessionFile, marker);
|
|
409
452
|
const store = new RecordStore(tmpDir);
|
|
410
453
|
const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
|
|
411
|
-
expect(found?.status).toBe("
|
|
454
|
+
expect(found?.status).toBe("running");
|
|
412
455
|
expect(found?.externalInstance).toBeUndefined();
|
|
413
456
|
});
|
|
414
457
|
|
|
415
|
-
// ── 分支 4: 都无 sidecar →
|
|
416
|
-
it("无任何 sidecar →
|
|
458
|
+
// ── 分支 4: 都无 sidecar → running(SP-2:跨重启可恢复)──
|
|
459
|
+
it("无任何 sidecar → running(SP-2 跨重启可恢复)", () => {
|
|
417
460
|
writeBaseSession();
|
|
418
461
|
const store = new RecordStore(tmpDir);
|
|
419
462
|
const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
|
|
420
|
-
expect(found?.status).toBe("
|
|
463
|
+
expect(found?.status).toBe("running");
|
|
421
464
|
});
|
|
422
465
|
|
|
423
|
-
// ── 分支 4: >24h 软超时 →
|
|
424
|
-
it(">24h 软超时 →
|
|
466
|
+
// ── 分支 4: >24h 软超时 → running(SP-2:跨重启可恢复,无视探活)──
|
|
467
|
+
it(">24h 软超时 → running(SP-2,即使 pid 存活)", () => {
|
|
425
468
|
const sessionFile = writeBaseSession();
|
|
426
469
|
// startedAt 设为 25 小时前
|
|
427
470
|
const oldStartedAt = Date.now() - 25 * 60 * 60 * 1000;
|
|
@@ -436,7 +479,7 @@ describe("RecordStore", () => {
|
|
|
436
479
|
|
|
437
480
|
const store = new RecordStore(tmpDir);
|
|
438
481
|
const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
|
|
439
|
-
expect(found?.status).toBe("
|
|
482
|
+
expect(found?.status).toBe("running");
|
|
440
483
|
expect(found?.externalInstance).toBeUndefined();
|
|
441
484
|
});
|
|
442
485
|
|
|
@@ -449,18 +492,21 @@ describe("RecordStore", () => {
|
|
|
449
492
|
});
|
|
450
493
|
const store = new RecordStore(tmpDir);
|
|
451
494
|
const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
|
|
452
|
-
|
|
495
|
+
// v4 B-1:.cancelled 优先级不变,但 status 折入 closed(closedReason='cancelled')
|
|
496
|
+
expect(found?.status).toBe("closed");
|
|
497
|
+
expect(found?.closedReason).toBe("cancelled");
|
|
453
498
|
});
|
|
454
499
|
|
|
455
500
|
// ── 回归:旧 .cancelled 单分支行为不变 ──
|
|
456
|
-
it("旧 .cancelled
|
|
501
|
+
it("旧 .cancelled 单分支行为不变(回归,status 折入 closed)", () => {
|
|
457
502
|
const sessionFile = writeBaseSession();
|
|
458
503
|
writeCancelledTombstone(sessionFile, {
|
|
459
504
|
id: SESSION_ID, status: "cancelled", agent: "worker", startedAt: STARTED_AT, endedAt: 7000,
|
|
460
505
|
});
|
|
461
506
|
const store = new RecordStore(tmpDir);
|
|
462
507
|
const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
|
|
463
|
-
expect(found?.status).toBe("
|
|
508
|
+
expect(found?.status).toBe("closed");
|
|
509
|
+
expect(found?.closedReason).toBe("cancelled");
|
|
464
510
|
expect(found?.error).toBe("cancelled by user");
|
|
465
511
|
expect(found?.endedAt).toBe(7000);
|
|
466
512
|
});
|
|
@@ -546,7 +592,7 @@ describe("RecordStore", () => {
|
|
|
546
592
|
// endedAt 重建(问题 2 修复:终态耗时不再随墙钟增长)
|
|
547
593
|
// ============================================================
|
|
548
594
|
describe("endedAt 重建(耗时不再无限增长)", () => {
|
|
549
|
-
it(".finalized → endedAt 为最后 entry
|
|
595
|
+
it(".finalized → light endedAt 用 mtime 近似,全量 endedAt 为最后 entry 时间戳", () => {
|
|
550
596
|
const sessionFile = path.join(tmpDir, "fin.jsonl");
|
|
551
597
|
writeSessionJsonl(sessionFile, {
|
|
552
598
|
id: "bg-1", agent: "w", mode: "background", task: "t",
|
|
@@ -555,10 +601,15 @@ describe("RecordStore", () => {
|
|
|
555
601
|
writeFinalized(sessionFile);
|
|
556
602
|
const store = new RecordStore(tmpDir);
|
|
557
603
|
const found = store.collectRecords(100, "all", "sess-A").find((r) => r.id === "bg-1");
|
|
558
|
-
|
|
604
|
+
// [perf] light 分支 2:endedAt 用 jsonl mtime 近似(finalize 后文件不再变化,
|
|
605
|
+
// 与最后 entry ts 差 <1s)——不是 entry ts 9000,也不是随墙钟无限增长的 now 基准。
|
|
606
|
+
expect(found?.endedAt).toBeDefined();
|
|
607
|
+
expect(found?.endedAt).toBeGreaterThan(5000);
|
|
608
|
+
// 全量(getFullRecord):精确用最后 entry 时间戳(非 now)。
|
|
609
|
+
expect(store.getFullRecord("bg-1")?.endedAt).toBe(9000);
|
|
559
610
|
});
|
|
560
611
|
|
|
561
|
-
it("无 sidecar(
|
|
612
|
+
it("无 sidecar(running, SP-2)→ endedAt 保持 undefined(非终态)", () => {
|
|
562
613
|
const sessionFile = path.join(tmpDir, "crash.jsonl");
|
|
563
614
|
writeSessionJsonl(sessionFile, {
|
|
564
615
|
id: "bg-1", agent: "w", mode: "background", task: "t",
|
|
@@ -566,8 +617,9 @@ describe("RecordStore", () => {
|
|
|
566
617
|
});
|
|
567
618
|
const store = new RecordStore(tmpDir);
|
|
568
619
|
const found = store.collectRecords(100, "all", "sess-A").find((r) => r.id === "bg-1");
|
|
569
|
-
expect(found?.status).toBe("
|
|
570
|
-
|
|
620
|
+
expect(found?.status).toBe("running");
|
|
621
|
+
// SP-2: running 是非终态(旧 idle 折入),endedAt 保持 undefined(待续聊)
|
|
622
|
+
expect(found?.endedAt).toBeUndefined();
|
|
571
623
|
});
|
|
572
624
|
|
|
573
625
|
it(".alive running → endedAt 保持 undefined(running 耗时继续增长是正确的)", () => {
|
|
@@ -602,7 +654,7 @@ describe("RecordStore", () => {
|
|
|
602
654
|
id: "broken-1",
|
|
603
655
|
rootSessionId: "sess-current",
|
|
604
656
|
agentName: "worker",
|
|
605
|
-
status: "crashed", // 越界值(union 不含 "crashed")
|
|
657
|
+
status: "crashed", // 越界值(union 不含 "crashed",SP-1 后 valid statuses = running/closed/cancelled)
|
|
606
658
|
createdAt: 1000,
|
|
607
659
|
} as unknown as ManifestRecord;
|
|
608
660
|
}
|
|
@@ -666,4 +718,58 @@ describe("RecordStore", () => {
|
|
|
666
718
|
);
|
|
667
719
|
});
|
|
668
720
|
});
|
|
721
|
+
|
|
722
|
+
// ============================================================
|
|
723
|
+
// SP-2: reconstructAll 兜底分支输出 running(非 crashed;v4 两态后旧 idle 折入 running)
|
|
724
|
+
// ============================================================
|
|
725
|
+
describe("SP-2: reconstructAll 兜底 running(跨重启恢复)", () => {
|
|
726
|
+
// TC-1: 兜底分支输出 running(非 crashed)
|
|
727
|
+
it("TC-1: 无 sidecar + 死 pid → reconstructAll 输出 running(可冷路径 resume)", () => {
|
|
728
|
+
const sessionFile = path.join(tmpDir, "sp2-tc1.jsonl");
|
|
729
|
+
writeSessionJsonl(sessionFile, {
|
|
730
|
+
id: "sa-sp2-1", agent: "worker", mode: "background", task: "cross-restart task",
|
|
731
|
+
startedAt: 1000, rootSessionId: "sess-sp2",
|
|
732
|
+
});
|
|
733
|
+
// 无任何 sidecar marker → 分支 4 兜底
|
|
734
|
+
const store = new RecordStore(tmpDir);
|
|
735
|
+
const found = store.collectRecords(100, "all", "sess-sp2").find((r) => r.id === "sa-sp2-1");
|
|
736
|
+
expect(found).toBeDefined();
|
|
737
|
+
expect(found?.status).toBe("running");
|
|
738
|
+
// SP-2: running 是非终态(旧 idle 折入),endedAt 保持 undefined
|
|
739
|
+
expect(found?.endedAt).toBeUndefined();
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
// TC-2: 有 .finalized marker 的 record 仍归档为 done(不回归)
|
|
743
|
+
it("TC-2: .finalized marker → done(分支 1/2 不回归)", () => {
|
|
744
|
+
const sessionFile = path.join(tmpDir, "sp2-tc2.jsonl");
|
|
745
|
+
writeSessionJsonl(sessionFile, {
|
|
746
|
+
id: "sa-sp2-2", agent: "worker", mode: "background", task: "finalized task",
|
|
747
|
+
startedAt: 2000, rootSessionId: "sess-sp2",
|
|
748
|
+
});
|
|
749
|
+
writeFinalized(sessionFile);
|
|
750
|
+
const store = new RecordStore(tmpDir);
|
|
751
|
+
const found = store.collectRecords(100, "all", "sess-sp2").find((r) => r.id === "sa-sp2-2");
|
|
752
|
+
expect(found).toBeDefined();
|
|
753
|
+
// 分支 2 仍归档为 done,不受 SP-2 影响
|
|
754
|
+
expect(found?.status).toBe("closed");
|
|
755
|
+
expect(found?.endedAt).toBeDefined();
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
// TC-2b: .cancelled marker → closed+cancelled(分支 1 不回归)
|
|
759
|
+
it("TC-2b: .cancelled marker → closed+cancelled reason(分支 1 不回归)", () => {
|
|
760
|
+
const sessionFile = path.join(tmpDir, "sp2-tc2b.jsonl");
|
|
761
|
+
writeSessionJsonl(sessionFile, {
|
|
762
|
+
id: "sa-sp2-2b", agent: "worker", mode: "background", task: "cancelled task",
|
|
763
|
+
startedAt: 3000, rootSessionId: "sess-sp2",
|
|
764
|
+
});
|
|
765
|
+
writeCancelledTombstone(sessionFile, {
|
|
766
|
+
id: "sa-sp2-2b", status: "cancelled", agent: "worker", startedAt: 3000, endedAt: 4000,
|
|
767
|
+
});
|
|
768
|
+
const store = new RecordStore(tmpDir);
|
|
769
|
+
const found = store.collectRecords(100, "all", "sess-sp2").find((r) => r.id === "sa-sp2-2b");
|
|
770
|
+
expect(found).toBeDefined();
|
|
771
|
+
expect(found?.status).toBe("closed");
|
|
772
|
+
expect(found?.closedReason).toBe("cancelled");
|
|
773
|
+
});
|
|
774
|
+
});
|
|
669
775
|
});
|
|
@@ -43,7 +43,15 @@ vi.mock("node:child_process", async () => {
|
|
|
43
43
|
|
|
44
44
|
return {
|
|
45
45
|
spawn: vi.fn(() => new FakeChild()),
|
|
46
|
-
|
|
46
|
+
// buildEnvBlock 用 execFile 异步取 git branch:默认 err-first 兜底(catch → branch="")
|
|
47
|
+
execFile: vi.fn(
|
|
48
|
+
(
|
|
49
|
+
_cmd: string,
|
|
50
|
+
_args: readonly string[],
|
|
51
|
+
_opts: unknown,
|
|
52
|
+
cb: (err: Error | null, stdout?: string, stderr?: string) => void,
|
|
53
|
+
) => cb(new Error("execFile not configured in this test")),
|
|
54
|
+
),
|
|
47
55
|
};
|
|
48
56
|
});
|
|
49
57
|
|
|
@@ -103,6 +111,7 @@ vi.mock("../temp-prompt.ts", () => ({
|
|
|
103
111
|
|
|
104
112
|
import { spawn } from "node:child_process";
|
|
105
113
|
|
|
114
|
+
import { waitForSpawn } from "./helpers/spawn-mock.ts";
|
|
106
115
|
import { ModelConfigService } from "../model-config-service.ts";
|
|
107
116
|
import type { ModelInfo, ModelRegistryLike } from "../model-resolver.ts";
|
|
108
117
|
import { ManifestStore } from "../manifest-store.ts";
|
|
@@ -139,16 +148,6 @@ function getLastSpawnEnv(): Record<string, string | undefined> {
|
|
|
139
148
|
return (mockSpawn.mock.calls.at(-1)?.[2]?.env as Record<string, string | undefined>) ?? {};
|
|
140
149
|
}
|
|
141
150
|
|
|
142
|
-
async function waitForSpawn(timeoutMs = 1000): Promise<void> {
|
|
143
|
-
const start = Date.now();
|
|
144
|
-
while (mockSpawn.mock.results.length === 0) {
|
|
145
|
-
if (Date.now() - start > timeoutMs) {
|
|
146
|
-
throw new Error(`spawn was not called within ${timeoutMs}ms`);
|
|
147
|
-
}
|
|
148
|
-
await new Promise((r) => setTimeout(r, 2));
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
151
|
function sessionHeader(id = "baseline-session"): Record<string, unknown> {
|
|
153
152
|
return { type: "session", id, timestamp: "2026-08-11T00-00-00-000Z", cwd: "/tmp/test" };
|
|
154
153
|
}
|
|
@@ -283,7 +282,7 @@ describe("进程级基线兜底(ALS 断裂修复,pi 事件回调模型)",
|
|
|
283
282
|
const { service } = setup({});
|
|
284
283
|
|
|
285
284
|
const execPromise = service.execute({ task: "fork child", ctxModel, fork: true });
|
|
286
|
-
await waitForSpawn();
|
|
285
|
+
await waitForSpawn(mockSpawn);
|
|
287
286
|
const childEnv = getLastSpawnEnv();
|
|
288
287
|
|
|
289
288
|
// 742 行 parentDepth = forkDepthAls.getStore() ?? forkDepthBaseline —— ALS 断裂时基线=1,+1 → 2
|
|
@@ -23,6 +23,7 @@ vi.mock("node:child_process", async () => {
|
|
|
23
23
|
class FakeChild extends EventEmitter {
|
|
24
24
|
pid = 12345;
|
|
25
25
|
stdout = new PassThrough();
|
|
26
|
+
stdin = new PassThrough(); // runSpawn 注册 stdin EPIPE handler(session-runner),FakeChild 必须提供
|
|
26
27
|
stderr = new PassThrough();
|
|
27
28
|
killed = false;
|
|
28
29
|
killSignal: string | undefined;
|
|
@@ -35,7 +36,16 @@ vi.mock("node:child_process", async () => {
|
|
|
35
36
|
|
|
36
37
|
return {
|
|
37
38
|
spawn: vi.fn(() => new FakeChild()),
|
|
38
|
-
|
|
39
|
+
// buildEnvBlock 用 execFile 异步取 git branch:默认 err-first 兜底(catch → branch=""),
|
|
40
|
+
// 形态同 worktree-manager.test.ts 的 setupExecFile
|
|
41
|
+
execFile: vi.fn(
|
|
42
|
+
(
|
|
43
|
+
_cmd: string,
|
|
44
|
+
_args: readonly string[],
|
|
45
|
+
_opts: unknown,
|
|
46
|
+
cb: (err: Error | null, stdout?: string, stderr?: string) => void,
|
|
47
|
+
) => cb(new Error("execFile not configured in this test")),
|
|
48
|
+
),
|
|
39
49
|
};
|
|
40
50
|
});
|
|
41
51
|
|
|
@@ -71,14 +81,14 @@ vi.mock("../temp-prompt.ts", () => ({
|
|
|
71
81
|
cleanupTempPrompt: vi.fn(async () => {}),
|
|
72
82
|
}));
|
|
73
83
|
|
|
74
|
-
import {
|
|
84
|
+
import { spawn } from "node:child_process";
|
|
75
85
|
import * as fs from "node:fs";
|
|
76
86
|
|
|
87
|
+
import { waitForSpawn } from "./helpers/spawn-mock.ts";
|
|
77
88
|
import { createRecord } from "../execution-record.ts";
|
|
78
89
|
import { type RunOptions, runSpawn, type SessionRunnerContext } from "../session-runner.ts";
|
|
79
90
|
|
|
80
91
|
const mockSpawn = vi.mocked(spawn);
|
|
81
|
-
const mockExec = vi.mocked(execFileSync);
|
|
82
92
|
const mockExistsSync = vi.mocked(fs.existsSync);
|
|
83
93
|
|
|
84
94
|
interface FakeChild {
|
|
@@ -101,17 +111,6 @@ function getLastSpawnEnv(): Record<string, string | undefined> {
|
|
|
101
111
|
return (mockSpawn.mock.calls.at(-1)?.[2]?.env as Record<string, string | undefined>) ?? {};
|
|
102
112
|
}
|
|
103
113
|
|
|
104
|
-
/** 等待 runSpawn 内部调到 spawn(async,spawn 在 writePromptToTempFile await 之后才调)。 */
|
|
105
|
-
async function waitForSpawn(timeoutMs = 1000): Promise<void> {
|
|
106
|
-
const start = Date.now();
|
|
107
|
-
while (mockSpawn.mock.results.length === 0) {
|
|
108
|
-
if (Date.now() - start > timeoutMs) {
|
|
109
|
-
throw new Error(`spawn was not called within ${timeoutMs}ms`);
|
|
110
|
-
}
|
|
111
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
114
|
// ── fixture ──
|
|
116
115
|
|
|
117
116
|
function makeRecord(overrides: { id?: string; depth?: number } = {}) {
|
|
@@ -160,7 +159,6 @@ describe("runSpawn 跨进程身份 env 注入(递归可见性场景 1b)", ()
|
|
|
160
159
|
beforeEach(() => {
|
|
161
160
|
vi.clearAllMocks();
|
|
162
161
|
mockExistsSync.mockReturnValue(false);
|
|
163
|
-
mockExec.mockReturnValue("");
|
|
164
162
|
});
|
|
165
163
|
|
|
166
164
|
afterEach(() => {
|
|
@@ -173,7 +171,7 @@ describe("runSpawn 跨进程身份 env 注入(递归可见性场景 1b)", ()
|
|
|
173
171
|
const opts = makeRunOpts({ fork: false });
|
|
174
172
|
|
|
175
173
|
const resultPromise = runSpawn(record, "test task", opts, ctx);
|
|
176
|
-
await waitForSpawn();
|
|
174
|
+
await waitForSpawn(mockSpawn);
|
|
177
175
|
const childEnv = getLastSpawnEnv();
|
|
178
176
|
|
|
179
177
|
expect(childEnv.PI_SUBAGENT_ROOT_SESSION_ID).toBe("root-main");
|
|
@@ -195,7 +193,7 @@ describe("runSpawn 跨进程身份 env 注入(递归可见性场景 1b)", ()
|
|
|
195
193
|
const opts = makeRunOpts({ fork: true, parentForkDepth: 1 });
|
|
196
194
|
|
|
197
195
|
const resultPromise = runSpawn(record, "test task", opts, ctx);
|
|
198
|
-
await waitForSpawn();
|
|
196
|
+
await waitForSpawn(mockSpawn);
|
|
199
197
|
const childEnv = getLastSpawnEnv();
|
|
200
198
|
|
|
201
199
|
// 身份 env 无条件存在(决策 2)
|
|
@@ -216,7 +214,7 @@ describe("runSpawn 跨进程身份 env 注入(递归可见性场景 1b)", ()
|
|
|
216
214
|
const ctx = makeCtx({ sessionRootId: "root-topmost" });
|
|
217
215
|
|
|
218
216
|
const resultPromise = runSpawn(record, "test task", makeRunOpts(), ctx);
|
|
219
|
-
await waitForSpawn();
|
|
217
|
+
await waitForSpawn(mockSpawn);
|
|
220
218
|
const childEnv = getLastSpawnEnv();
|
|
221
219
|
|
|
222
220
|
expect(childEnv.PI_SUBAGENT_DEPTH).toBe("3");
|
|
@@ -236,7 +234,7 @@ describe("runSpawn 跨进程身份 env 注入(递归可见性场景 1b)", ()
|
|
|
236
234
|
const ctx = makeCtx({ sessionRootId: "real-root-session" });
|
|
237
235
|
|
|
238
236
|
const resultPromise = runSpawn(record, "test task", makeRunOpts(), ctx);
|
|
239
|
-
await waitForSpawn();
|
|
237
|
+
await waitForSpawn(mockSpawn);
|
|
240
238
|
const childEnv = getLastSpawnEnv();
|
|
241
239
|
|
|
242
240
|
// env 用 ctx.sessionRootId,不是 record.rootSessionId
|
|
@@ -258,7 +256,7 @@ describe("runSpawn 跨进程身份 env 注入(递归可见性场景 1b)", ()
|
|
|
258
256
|
const ctx = makeCtx({ cwd: checkoutPath, mainCwd: checkoutPath, rootCwd, agentDir });
|
|
259
257
|
|
|
260
258
|
const resultPromise = runSpawn(record, "test task", makeRunOpts(), ctx);
|
|
261
|
-
await waitForSpawn();
|
|
259
|
+
await waitForSpawn(mockSpawn);
|
|
262
260
|
const childEnv = getLastSpawnEnv();
|
|
263
261
|
const spawnArgs = mockSpawn.mock.calls.at(-1)?.[1] as string[];
|
|
264
262
|
|