@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.
Files changed (166) hide show
  1. package/README.md +39 -12
  2. package/agents/analyst.md +61 -0
  3. package/agents/coder.md +70 -0
  4. package/agents/debugger.md +67 -0
  5. package/agents/doc-reviewer.md +3 -3
  6. package/agents/explorer.md +50 -18
  7. package/agents/general-purpose.md +19 -8
  8. package/agents/orchestrator.md +37 -32
  9. package/agents/planner.md +45 -11
  10. package/agents/researcher.md +53 -11
  11. package/agents/reviewer.md +74 -0
  12. package/package.json +1 -1
  13. package/skills/workflow-script-format/SKILL.md +1 -1
  14. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
  15. package/src/execution/__tests__/agent-registry.test.ts +13 -11
  16. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
  17. package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
  18. package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
  19. package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
  20. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
  21. package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
  22. package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
  23. package/src/execution/__tests__/crash-recovery.test.ts +8 -2
  24. package/src/execution/__tests__/delivery-methods.test.ts +385 -0
  25. package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
  26. package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
  27. package/src/execution/__tests__/execute-nesting.test.ts +20 -72
  28. package/src/execution/__tests__/execution-record.test.ts +199 -0
  29. package/src/execution/__tests__/finalize-record.test.ts +170 -14
  30. package/src/execution/__tests__/format.test.ts +131 -7
  31. package/src/execution/__tests__/gc-timer.test.ts +184 -0
  32. package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
  33. package/src/execution/__tests__/helpers/spawn-mock.ts +25 -7
  34. package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
  35. package/src/execution/__tests__/index-session-start.test.ts +257 -5
  36. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
  37. package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
  38. package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
  39. package/src/execution/__tests__/list-component.test.ts +59 -5
  40. package/src/execution/__tests__/list-fields.test.ts +109 -0
  41. package/src/execution/__tests__/model-resolver.test.ts +38 -1
  42. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
  43. package/src/execution/__tests__/nested-visibility.test.ts +325 -0
  44. package/src/execution/__tests__/notifier-flush.test.ts +209 -7
  45. package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
  46. package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
  47. package/src/execution/__tests__/record-store.test.ts +158 -52
  48. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
  49. package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
  50. package/src/execution/__tests__/resource-policy.test.ts +109 -0
  51. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
  52. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
  53. package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
  54. package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
  55. package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
  56. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
  57. package/src/execution/__tests__/session-pending.test.ts +61 -2
  58. package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
  59. package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
  60. package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
  61. package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
  62. package/src/execution/__tests__/spawn-args.test.ts +127 -49
  63. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
  64. package/src/execution/__tests__/spawned-children.test.ts +92 -0
  65. package/src/execution/__tests__/status-refactor.test.ts +345 -0
  66. package/src/execution/__tests__/stdin-writer.test.ts +97 -0
  67. package/src/execution/__tests__/subagent-service-message-close.test.ts +598 -0
  68. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
  69. package/src/execution/__tests__/subagent-service.test.ts +49 -11
  70. package/src/execution/__tests__/timeout-integration.test.ts +27 -13
  71. package/src/execution/__tests__/tool-action.test.ts +12 -10
  72. package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
  73. package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
  74. package/src/execution/__tests__/worktree-manager.test.ts +292 -89
  75. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +13 -12
  76. package/src/execution/argv-mirror.ts +21 -2
  77. package/src/execution/execution-record.ts +126 -9
  78. package/src/execution/finalize-record.ts +90 -13
  79. package/src/execution/host-mode.ts +1 -1
  80. package/src/execution/lifecycle-manager.ts +484 -0
  81. package/src/execution/lifecycle-predicates.ts +65 -0
  82. package/src/execution/manifest-store.ts +61 -16
  83. package/src/execution/model-resolver.ts +26 -5
  84. package/src/execution/notifier.ts +69 -12
  85. package/src/execution/pi-invocation.ts +21 -1
  86. package/src/execution/record-store.ts +554 -107
  87. package/src/execution/session-pending.ts +116 -45
  88. package/src/execution/session-reconstructor.ts +224 -7
  89. package/src/execution/session-runner.ts +290 -75
  90. package/src/execution/sessions-index.ts +304 -0
  91. package/src/execution/stdin-writer.ts +93 -7
  92. package/src/execution/stream-sink.ts +20 -3
  93. package/src/execution/subagent-service.ts +867 -138
  94. package/src/execution/turn-limiter.ts +14 -0
  95. package/src/execution/types.ts +204 -22
  96. package/src/execution/worktree-manager.ts +128 -49
  97. package/src/execution/worktree-registry.ts +13 -2
  98. package/src/index.ts +277 -19
  99. package/src/injectors/subagent-list-injector.ts +26 -8
  100. package/src/injectors/workflow-list-injector.ts +25 -8
  101. package/src/interface/__tests__/subagent-tool-prompt.test.ts +18 -5
  102. package/src/interface/__tests__/tool-render.test.ts +15 -13
  103. package/src/interface/bg-notify-render.ts +32 -8
  104. package/src/interface/command-actions.ts +26 -7
  105. package/src/interface/commands.ts +21 -22
  106. package/src/interface/format.ts +44 -17
  107. package/src/interface/gui-mappers.ts +6 -8
  108. package/src/interface/helpers.ts +170 -10
  109. package/src/interface/list-component.ts +53 -14
  110. package/src/interface/subagent-actions.ts +235 -17
  111. package/src/interface/subagent-tool.ts +82 -17
  112. package/src/interface/subagents.ts +2 -1
  113. package/src/interface/tool-render.ts +11 -24
  114. package/src/interface/tool-workflow.ts +20 -35
  115. package/src/interface/views/WorkflowsView.ts +89 -32
  116. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
  117. package/src/interface/views/__tests__/detail-content-session-file.test.ts +1 -1
  118. package/src/interface/views/format.ts +3 -3
  119. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +325 -0
  120. package/src/orchestration/__tests__/args-validator.test.ts +1 -1
  121. package/src/orchestration/__tests__/config-loader.test.ts +38 -0
  122. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
  123. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
  124. package/src/orchestration/__tests__/execute-agent-call.test.ts +95 -0
  125. package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +657 -18
  126. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
  127. package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
  128. package/src/orchestration/__tests__/skill-discovery.test.ts +157 -0
  129. package/src/orchestration/__tests__/test-mocks.ts +191 -0
  130. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +98 -0
  131. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
  132. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
  133. package/src/orchestration/__tests__/workflows-e2e.test.ts +1 -1
  134. package/src/orchestration/agent-opts-resolver.ts +4 -1
  135. package/src/orchestration/args-validator.ts +2 -2
  136. package/src/orchestration/config-loader.ts +30 -1
  137. package/src/orchestration/error-recovery.ts +133 -29
  138. package/src/orchestration/execute-agent-call.ts +31 -7
  139. package/src/orchestration/jsonl-run-store.ts +287 -40
  140. package/src/orchestration/launcher.ts +7 -1
  141. package/src/orchestration/lifecycle.ts +135 -132
  142. package/src/orchestration/models/__tests__/trace.test.ts +408 -0
  143. package/src/orchestration/models/budget.ts +1 -1
  144. package/src/orchestration/models/run-runtime.ts +15 -17
  145. package/src/orchestration/models/run-spec.ts +2 -2
  146. package/src/orchestration/models/run-state.ts +3 -3
  147. package/src/orchestration/models/trace.ts +95 -15
  148. package/src/orchestration/models/types.ts +8 -9
  149. package/src/orchestration/models/workflow-run.ts +50 -71
  150. package/src/orchestration/models/workflow-script.ts +32 -1
  151. package/src/orchestration/skill-discovery.ts +30 -0
  152. package/src/orchestration/worker-handle.ts +1 -1
  153. package/src/orchestration/worker-host.ts +1 -1
  154. package/src/orchestration/worker-script-builder.ts +29 -10
  155. package/src/shared/__tests__/agent-ref.test.ts +34 -0
  156. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
  157. package/src/shared/__tests__/resource-discovery.test.ts +55 -0
  158. package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
  159. package/src/shared/agent-ref.ts +16 -0
  160. package/src/shared/resource-discovery.ts +147 -58
  161. package/src/shared/schema-jsonify.ts +53 -0
  162. package/workflows/README.md +4 -4
  163. package/agents/code-reviewer.md +0 -47
  164. package/agents/context-builder.md +0 -21
  165. package/agents/oracle.md +0 -34
  166. package/agents/worker.md +0 -20
@@ -0,0 +1,371 @@
1
+ // src/execution/__tests__/index-session-start-identity.test.ts
2
+ //
3
+ // [M4 / V2 决策 5] identity 子进程写入测试。
4
+ //
5
+ // 验证 index.ts session_start hook 的 identity 写入职责:
6
+ // 1. 子进程(PI_SUBAGENT_SELF_RECORD_ID 存在):调 pi.appendEntry 写
7
+ // customType="subagent-identity" 的 custom entry,data 字段与 env 组装的
8
+ // SubagentIdentityData 一致(id/agent/mode/task/slug/startedAt/rootSessionId/
9
+ // parentRecordId/depth/forkDepth/chatMode)。
10
+ // 2. 主进程(无 PI_SUBAGENT_SELF_RECORD_ID):不写 identity custom entry。
11
+ // 3. 可选字段缺失(chatMode/slug/parentRecordId/forkDepth 未注入):identity 仍写入,
12
+ // 可选字段为 undefined / false,不抛错。
13
+ //
14
+ // 修复背景:旧实现父进程 fs.appendFileSync 补写的 custom entry 缺 id/parentId →
15
+ // 污染 pi _buildIndex leafId → message tree 断成两棵 → 多轮对话丢上下文。
16
+ // 改由子进程(session 文件所有者)在 session_start 用 pi.appendEntry 写,
17
+ // pi 自动生成 id/parentId → message tree 连续。
18
+ //
19
+ // mock 模式复用 index-session-start.test.ts(隔离真实 SDK 顶层副作用),差异:
20
+ // appendEntry 改 vi.fn() 捕获调用(断言 identity custom entry)。
21
+
22
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
23
+
24
+ // ── mock modules(路径相对 src/execution/__tests/)──
25
+ vi.mock("@earendil-works/pi-coding-agent", () => ({
26
+ getAgentDir: () => "/home/user/.pi/agent",
27
+ }));
28
+ vi.mock("@earendil-works/pi-ai", () => ({
29
+ StringEnum: (values: string[]) => ({ type: "string", enum: values }),
30
+ }));
31
+ vi.mock("typebox", () => ({
32
+ Type: {
33
+ Object: (props: Record<string, unknown>) => ({ type: "object", properties: props }),
34
+ Optional: (schema: unknown) => ({ ...(schema as object), optional: true }),
35
+ String: () => ({ type: "string" }),
36
+ Boolean: () => ({ type: "boolean" }),
37
+ Number: () => ({ type: "number" }),
38
+ Array: (items: unknown) => ({ type: "array", items }),
39
+ Record: (key: unknown, value: unknown) => ({ type: "object", additionalProperties: value, key }),
40
+ Unknown: () => ({ type: "unknown" }),
41
+ Union: (members: unknown[]) => ({ type: "union", members }),
42
+ Literal: (value: unknown) => ({ type: "literal", value }),
43
+ },
44
+ }));
45
+
46
+ // ── hoisted mock 实例 ──
47
+ const { mockSetUiRequestHandler, mockInitSession, mockLoadAll, mockRecoverManifestTmpFiles } =
48
+ vi.hoisted(() => ({
49
+ mockSetUiRequestHandler: vi.fn(),
50
+ mockInitSession: vi.fn(),
51
+ mockLoadAll: vi.fn(async () => []),
52
+ mockRecoverManifestTmpFiles: vi.fn(async () => ({ deleted: 0, recovered: 0 })),
53
+ }));
54
+
55
+ vi.mock("../subagent-service.ts", () => ({
56
+ SubagentService: class {
57
+ initSession = mockInitSession;
58
+ setUiRequestHandler = mockSetUiRequestHandler;
59
+ recoverManifestTmpFiles = mockRecoverManifestTmpFiles;
60
+ startGcTimer = vi.fn();
61
+ getStreamSink = () => null;
62
+ dispose = vi.fn();
63
+ },
64
+ getSubagentService: () => null,
65
+ setSubagentService: vi.fn(),
66
+ }));
67
+
68
+ vi.mock("../model-config-service.ts", () => ({
69
+ ModelConfigService: class {
70
+ initModel = vi.fn();
71
+ setCtxModel = vi.fn();
72
+ },
73
+ getModelConfigService: () => null,
74
+ setModelConfigService: vi.fn(),
75
+ }));
76
+
77
+ vi.mock("../worktree-manager.ts", () => ({
78
+ WorktreeManager: class {
79
+ constructor(_agentDir: string) {
80
+ /* mock */
81
+ }
82
+ scan = vi.fn();
83
+ cleanup = vi.fn();
84
+ create = vi.fn();
85
+ collectPatch = vi.fn();
86
+ registerPid = vi.fn();
87
+ },
88
+ }));
89
+
90
+ vi.mock("../session-file-gc.ts", () => ({
91
+ maybeCleanupExpiredSessionFiles: vi.fn(),
92
+ }));
93
+
94
+ // dispose/flushPendingSaves:session_shutdown handler 会调 state.store.dispose()(W2C5),
95
+ // 防御性补齐(identity 语义与 dispose 无关,不加新用例)。
96
+ vi.mock("../../orchestration/jsonl-run-store.ts", () => ({
97
+ JsonlRunStore: class {
98
+ loadAll = mockLoadAll;
99
+ save = vi.fn(async () => {});
100
+ dispose = vi.fn(async () => {});
101
+ flushPendingSaves = vi.fn(async () => {});
102
+ },
103
+ }));
104
+
105
+ vi.mock("../../interface/subagent-tool.ts", () => ({ registerSubagentTool: vi.fn() }));
106
+ vi.mock("../../interface/subagents.ts", () => ({ registerSubagentsCommand: vi.fn() }));
107
+ vi.mock("../../interface/bg-notify-render.ts", () => ({ renderBgNotifyMessage: vi.fn() }));
108
+ // registerWorkflowsCommand 用 hoisted spy——W3TC9 需在 it 内读 mock.calls 捕获
109
+ // runs getter(对齐 index-session-start.test.ts 的 mockStoreDispose 先例)。
110
+ const { mockRegisterWorkflowsCommand } = vi.hoisted(() => ({
111
+ mockRegisterWorkflowsCommand: vi.fn(),
112
+ }));
113
+ vi.mock("../../interface/tool-workflow.ts", () => ({ registerWorkflowTool: vi.fn() }));
114
+ vi.mock("../../interface/tool-workflow-script.ts", () => ({
115
+ registerWorkflowScriptTool: vi.fn(),
116
+ }));
117
+ vi.mock("../../interface/commands.ts", () => ({
118
+ registerWorkflowsCommand: mockRegisterWorkflowsCommand,
119
+ }));
120
+
121
+ // ── import 被测工厂 ──
122
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
123
+
124
+ import subagentsExtension from "../../index.ts";
125
+ import { Budget } from "../../orchestration/models/budget.ts";
126
+ import { Trace } from "../../orchestration/models/trace.ts";
127
+ import type { WorkflowRun } from "../../orchestration/models/workflow-run.ts";
128
+ import { WorkflowRun as WorkflowRunCtor } from "../../orchestration/models/workflow-run.ts";
129
+ import { IDENTITY_CUSTOM_TYPE } from "../session-reconstructor.ts";
130
+
131
+ // ── helpers ──
132
+
133
+ const IDENTITY_ENV_KEYS = [
134
+ "PI_SUBAGENT_SELF_RECORD_ID",
135
+ "PI_SUBAGENT_AGENT",
136
+ "PI_SUBAGENT_MODE",
137
+ "PI_SUBAGENT_TASK",
138
+ "PI_SUBAGENT_SLUG",
139
+ "PI_SUBAGENT_STARTED_AT",
140
+ "PI_SUBAGENT_ROOT_SESSION_ID",
141
+ "PI_SUBAGENT_PARENT_RECORD_ID",
142
+ "PI_SUBAGENT_DEPTH",
143
+ "PI_SUBAGENT_FORK_DEPTH",
144
+ "PI_SUBAGENT_CHAT_MODE",
145
+ "PI_SUBAGENT_WORKTREE",
146
+ ] as const;
147
+
148
+ function clearIdentityEnv(): void {
149
+ for (const k of IDENTITY_ENV_KEYS) delete process.env[k];
150
+ }
151
+
152
+ /** 创建可观察 appendEntry 的 mock ExtensionAPI,捕获 session_start handler。 */
153
+ function createMockPi(): {
154
+ pi: ExtensionAPI;
155
+ getSessionStartHandler: () => ((event: unknown, ctx: unknown) => Promise<void>) | undefined;
156
+ appendEntrySpy: ReturnType<typeof vi.fn>;
157
+ } {
158
+ let sessionStartHandler: ((event: unknown, ctx: unknown) => Promise<void>) | undefined;
159
+ const appendEntrySpy = vi.fn();
160
+ const events = { emit: vi.fn() };
161
+ const noop = (): void => {
162
+ /* mock */
163
+ };
164
+ const pi = new Proxy<ExtensionAPI>({} as ExtensionAPI, {
165
+ get(_target, prop: string | symbol): unknown {
166
+ if (prop === "on") {
167
+ return (event: string, handler: (...args: unknown[]) => unknown) => {
168
+ if (event === "session_start") {
169
+ sessionStartHandler = handler as (event: unknown, ctx: unknown) => Promise<void>;
170
+ }
171
+ };
172
+ }
173
+ if (prop === "events") return events;
174
+ if (prop === "appendEntry") return appendEntrySpy;
175
+ if (prop === "registerMessageRenderer") return noop;
176
+ return noop;
177
+ },
178
+ });
179
+ return { pi, getSessionStartHandler: () => sessionStartHandler, appendEntrySpy };
180
+ }
181
+
182
+ /** 最小 ExtensionContext mock(tui mode,足够走完 session_start 装配)。 */
183
+ function createMockCtx(): Record<string, unknown> {
184
+ const sessionManager = {
185
+ getSessionId: () => "session-identity-1",
186
+ getSessionFile: () => "/home/user/.pi/agent/sessions/session-identity-1.jsonl",
187
+ getSessionDir: () => "/home/user/.pi/agent/sessions",
188
+ getCwd: () => "/home/user/project",
189
+ getEntries: () => [],
190
+ getBranch: () => [],
191
+ getLeafId: () => null,
192
+ getLeafEntry: () => undefined,
193
+ getEntry: () => undefined,
194
+ getHeader: () => null,
195
+ getTree: () => [],
196
+ getSessionName: () => undefined,
197
+ };
198
+ return {
199
+ cwd: "/home/user/project",
200
+ mode: "tui",
201
+ modelRegistry: {
202
+ getAvailable: () => [],
203
+ find: () => undefined,
204
+ hasConfiguredAuth: () => false,
205
+ },
206
+ model: undefined,
207
+ sessionManager,
208
+ ui: undefined,
209
+ };
210
+ }
211
+
212
+ beforeEach(() => {
213
+ vi.clearAllMocks();
214
+ mockLoadAll.mockResolvedValue([]);
215
+ clearIdentityEnv();
216
+ });
217
+
218
+ afterEach(() => {
219
+ clearIdentityEnv();
220
+ });
221
+
222
+ // ── tests ──
223
+
224
+ describe("session_start identity 子进程写入(M4 / V2 决策 5)", () => {
225
+ it("子进程(PI_SUBAGENT_SELF_RECORD_ID 存在):appendEntry 写完整 identity custom entry", async () => {
226
+ process.env.PI_SUBAGENT_SELF_RECORD_ID = "rec-child-1";
227
+ process.env.PI_SUBAGENT_AGENT = "worker";
228
+ process.env.PI_SUBAGENT_MODE = "background";
229
+ process.env.PI_SUBAGENT_TASK = "fix the bug";
230
+ process.env.PI_SUBAGENT_SLUG = "fix-bug";
231
+ process.env.PI_SUBAGENT_STARTED_AT = "1700000000000";
232
+ process.env.PI_SUBAGENT_ROOT_SESSION_ID = "root-session-9";
233
+ process.env.PI_SUBAGENT_PARENT_RECORD_ID = "rec-parent-0";
234
+ process.env.PI_SUBAGENT_DEPTH = "2";
235
+ process.env.PI_SUBAGENT_FORK_DEPTH = "1";
236
+ process.env.PI_SUBAGENT_CHAT_MODE = "true";
237
+ process.env.PI_SUBAGENT_WORKTREE = "true";
238
+
239
+ const { pi, getSessionStartHandler, appendEntrySpy } = createMockPi();
240
+ subagentsExtension(pi);
241
+
242
+ const handler = getSessionStartHandler();
243
+ expect(handler).toBeDefined();
244
+ await handler!({ type: "session_start" }, createMockCtx());
245
+
246
+ // 找到 identity 的 appendEntry 调用(customType = IDENTITY_CUSTOM_TYPE)
247
+ const identityCall = appendEntrySpy.mock.calls.find(
248
+ (c: unknown[]) => c[0] === IDENTITY_CUSTOM_TYPE,
249
+ );
250
+ expect(identityCall).toBeDefined();
251
+ // customType 第一参数
252
+ expect(identityCall![0]).toBe(IDENTITY_CUSTOM_TYPE);
253
+ // data 第二参数:与 env 组装的 SubagentIdentityData 一致
254
+ const data = identityCall![1] as Record<string, unknown>;
255
+ expect(data).toMatchObject({
256
+ id: "rec-child-1",
257
+ agent: "worker",
258
+ mode: "background",
259
+ task: "fix the bug",
260
+ slug: "fix-bug",
261
+ startedAt: 1700000000000,
262
+ rootSessionId: "root-session-9",
263
+ parentRecordId: "rec-parent-0",
264
+ depth: 2,
265
+ forkDepth: 1,
266
+ chatMode: true,
267
+ // [review round2] worktree 隔离标志经 env 贯穿写入 identity entry(跨重启重建
268
+ // 拒绝续聊的数据源)
269
+ worktree: true,
270
+ });
271
+ });
272
+
273
+ it("主进程(无 PI_SUBAGENT_SELF_RECORD_ID):不写 identity custom entry", async () => {
274
+ // 不设 SELF_RECORD_ID(主进程环境)
275
+ const { pi, getSessionStartHandler, appendEntrySpy } = createMockPi();
276
+ subagentsExtension(pi);
277
+
278
+ const handler = getSessionStartHandler();
279
+ expect(handler).toBeDefined();
280
+ await handler!({ type: "session_start" }, createMockCtx());
281
+
282
+ // 无 identity 的 appendEntry 调用(logger.warn 等可能调 workflow:log,需过滤)
283
+ const identityCall = appendEntrySpy.mock.calls.find(
284
+ (c: unknown[]) => c[0] === IDENTITY_CUSTOM_TYPE,
285
+ );
286
+ expect(identityCall).toBeUndefined();
287
+ });
288
+
289
+ it("可选字段缺失(chatMode/slug/parentRecordId/forkDepth/worktree 未注入):identity 仍写入,可选字段为默认", async () => {
290
+ process.env.PI_SUBAGENT_SELF_RECORD_ID = "rec-child-2";
291
+ process.env.PI_SUBAGENT_AGENT = "explorer";
292
+ process.env.PI_SUBAGENT_MODE = "background";
293
+ process.env.PI_SUBAGENT_TASK = "scan code";
294
+ process.env.PI_SUBAGENT_STARTED_AT = "1700000000002";
295
+ process.env.PI_SUBAGENT_ROOT_SESSION_ID = "root-9";
296
+ process.env.PI_SUBAGENT_DEPTH = "1";
297
+ // 不设 SLUG / PARENT_RECORD_ID / FORK_DEPTH / CHAT_MODE
298
+
299
+ const { pi, getSessionStartHandler, appendEntrySpy } = createMockPi();
300
+ subagentsExtension(pi);
301
+
302
+ const handler = getSessionStartHandler();
303
+ await handler!({ type: "session_start" }, createMockCtx());
304
+
305
+ const identityCall = appendEntrySpy.mock.calls.find(
306
+ (c: unknown[]) => c[0] === IDENTITY_CUSTOM_TYPE,
307
+ );
308
+ expect(identityCall).toBeDefined();
309
+ const data = identityCall![1] as Record<string, unknown>;
310
+ // 必填字段正常
311
+ expect(data.id).toBe("rec-child-2");
312
+ expect(data.agent).toBe("explorer");
313
+ expect(data.mode).toBe("background");
314
+ expect(data.startedAt).toBe(1700000000002);
315
+ // 可选字段缺失 → undefined / false
316
+ expect(data.chatMode).toBe(false);
317
+ expect(data.slug).toBeUndefined();
318
+ expect(data.parentRecordId).toBeUndefined();
319
+ expect(data.forkDepth).toBeUndefined();
320
+ expect(data.worktree).toBe(false);
321
+ });
322
+
323
+ it("W3TC9: 主进程分支 session_start 裁剪接线——21 done 经 loadAll 裁到 20(identity 与淘汰共存)", async () => {
324
+ // identity handler 前段(appendEntry)与裁剪接线(loadAll 循环后)共存于同一
325
+ // session_start handler——主进程分支(PI_SUBAGENT_SELF_RECORD_ID 未设,beforeEach
326
+ // 已清)验证裁剪同样生效。
327
+ const T0 = Date.parse("2020-01-01T00:00:00.000Z");
328
+ const doneRuns = Array.from({ length: 21 }, (_, i) =>
329
+ WorkflowRunCtor.reconstruct(
330
+ `wf-id-${i}`,
331
+ {
332
+ scriptSource: "execute() {}",
333
+ args: {},
334
+ scriptName: "test",
335
+ scriptPath: "/fake/test.js",
336
+ },
337
+ {
338
+ status: "done",
339
+ reason: "completed",
340
+ budget: new Budget({ maxTokens: 1000 }),
341
+ calls: new Map(),
342
+ trace: new Trace(),
343
+ errorLogs: [],
344
+ },
345
+ {
346
+ startedAt: new Date(T0 + i * 60_000).toISOString(),
347
+ completedAt: new Date(T0 + i * 60_000).toISOString(),
348
+ },
349
+ ));
350
+ mockLoadAll.mockResolvedValue(doneRuns);
351
+
352
+ const { pi, getSessionStartHandler } = createMockPi();
353
+ subagentsExtension(pi);
354
+ const handler = getSessionStartHandler();
355
+ expect(handler).toBeDefined();
356
+ await handler!({ type: "session_start" }, createMockCtx());
357
+
358
+ // runs Map 观察:registerWorkflowsCommand 第二参数的 runs getter
359
+ const getRuns = mockRegisterWorkflowsCommand.mock.calls[0]?.[1] as
360
+ | (() => Map<string, WorkflowRun>)
361
+ | undefined;
362
+ expect(typeof getRuns).toBe("function");
363
+ const runs = getRuns!();
364
+ // 21 done 裁 1:最旧(wf-id-0)被裁
365
+ expect(runs.size).toBe(20);
366
+ expect(runs.has("wf-id-0")).toBe(false);
367
+ for (let i = 1; i < 21; i++) {
368
+ expect(runs.has(`wf-id-${i}`)).toBe(true);
369
+ }
370
+ });
371
+ });