@zhushanwen/pi-subagent-workflow 0.1.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 (143) hide show
  1. package/agents/context-builder.md +17 -0
  2. package/agents/general-purpose.md +16 -0
  3. package/agents/oracle.md +17 -0
  4. package/agents/planner.md +17 -0
  5. package/agents/researcher.md +17 -0
  6. package/agents/reviewer.md +17 -0
  7. package/agents/scout.md +17 -0
  8. package/agents/worker.md +16 -0
  9. package/examples/README.md +43 -0
  10. package/examples/chain.example.js +92 -0
  11. package/examples/map-reduce.example.js +99 -0
  12. package/examples/parallel.example.js +82 -0
  13. package/examples/scatter-gather.example.js +106 -0
  14. package/index.ts +1 -0
  15. package/package.json +66 -0
  16. package/skills/workflow-script-format/SKILL.md +328 -0
  17. package/src/execution/__tests__/agent-registry.test.ts +164 -0
  18. package/src/execution/__tests__/agent-result-mapper.test.ts +128 -0
  19. package/src/execution/__tests__/alive-store.test.ts +147 -0
  20. package/src/execution/__tests__/bg-notify-render.test.ts +256 -0
  21. package/src/execution/__tests__/concurrency-pool.test.ts +217 -0
  22. package/src/execution/__tests__/config.test.ts +110 -0
  23. package/src/execution/__tests__/crash-recovery.test.ts +311 -0
  24. package/src/execution/__tests__/execute-nesting.test.ts +359 -0
  25. package/src/execution/__tests__/execute-options-mapper.test.ts +138 -0
  26. package/src/execution/__tests__/execution-record.test.ts +959 -0
  27. package/src/execution/__tests__/finalized-marker.test.ts +82 -0
  28. package/src/execution/__tests__/format-schema-instruction.test.ts +135 -0
  29. package/src/execution/__tests__/format.test.ts +320 -0
  30. package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
  31. package/src/execution/__tests__/list-component.test.ts +347 -0
  32. package/src/execution/__tests__/model-resolver.test.ts +356 -0
  33. package/src/execution/__tests__/output-collector.test.ts +61 -0
  34. package/src/execution/__tests__/path-encoding.test.ts +75 -0
  35. package/src/execution/__tests__/pi-invocation.test.ts +73 -0
  36. package/src/execution/__tests__/record-store.test.ts +545 -0
  37. package/src/execution/__tests__/run-spawn-edges.test.ts +439 -0
  38. package/src/execution/__tests__/run-spawn-integration.test.ts +897 -0
  39. package/src/execution/__tests__/sdk-contract.test.ts +272 -0
  40. package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
  41. package/src/execution/__tests__/session-file-gc.test.ts +247 -0
  42. package/src/execution/__tests__/session-reconstructor.test.ts +359 -0
  43. package/src/execution/__tests__/session-runner-schema-env.test.ts +314 -0
  44. package/src/execution/__tests__/session-start-reaper.test.ts +227 -0
  45. package/src/execution/__tests__/spawn-args.test.ts +244 -0
  46. package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
  47. package/src/execution/__tests__/subagent-service.test.ts +678 -0
  48. package/src/execution/__tests__/subprocess-agent-runner.test.ts +389 -0
  49. package/src/execution/__tests__/temp-prompt.test.ts +53 -0
  50. package/src/execution/__tests__/timeout-integration.test.ts +381 -0
  51. package/src/execution/__tests__/tombstone-store.test.ts +73 -0
  52. package/src/execution/__tests__/tool-action.test.ts +330 -0
  53. package/src/execution/__tests__/turn-limiter.test.ts +65 -0
  54. package/src/execution/__tests__/worktree-manager.test.ts +423 -0
  55. package/src/execution/__tests__/worktree-registry.test.ts +161 -0
  56. package/src/execution/agent-registry.ts +252 -0
  57. package/src/execution/agent-result-mapper.ts +84 -0
  58. package/src/execution/alive-store.ts +92 -0
  59. package/src/execution/best-effort.ts +30 -0
  60. package/src/execution/concurrency-pool.ts +84 -0
  61. package/src/execution/config.ts +73 -0
  62. package/src/execution/execute-options-mapper.ts +86 -0
  63. package/src/execution/execution-record.ts +778 -0
  64. package/src/execution/finalized-marker.ts +51 -0
  65. package/src/execution/model-config-service.ts +225 -0
  66. package/src/execution/model-resolver.ts +247 -0
  67. package/src/execution/notifier.ts +168 -0
  68. package/src/execution/output-collector.ts +88 -0
  69. package/src/execution/path-encoding.ts +34 -0
  70. package/src/execution/pi-invocation.ts +70 -0
  71. package/src/execution/record-store.ts +350 -0
  72. package/src/execution/session-context-resolver.ts +64 -0
  73. package/src/execution/session-file-gc.ts +98 -0
  74. package/src/execution/session-reconstructor.ts +450 -0
  75. package/src/execution/session-runner.ts +725 -0
  76. package/src/execution/spawn-event-adapter.ts +150 -0
  77. package/src/execution/subagent-service.ts +973 -0
  78. package/src/execution/subprocess-agent-runner.ts +108 -0
  79. package/src/execution/temp-prompt.ts +57 -0
  80. package/src/execution/tombstone-store.ts +72 -0
  81. package/src/execution/turn-limiter.ts +88 -0
  82. package/src/execution/types.ts +634 -0
  83. package/src/execution/worktree-manager.ts +285 -0
  84. package/src/execution/worktree-registry.ts +144 -0
  85. package/src/index.ts +454 -0
  86. package/src/interface/bg-notify-render.ts +286 -0
  87. package/src/interface/commands.ts +157 -0
  88. package/src/interface/format.ts +501 -0
  89. package/src/interface/gui-adapter.ts +136 -0
  90. package/src/interface/helpers.ts +110 -0
  91. package/src/interface/list-component.ts +643 -0
  92. package/src/interface/list-shared.ts +84 -0
  93. package/src/interface/list-view.ts +373 -0
  94. package/src/interface/reentry-guard.ts +30 -0
  95. package/src/interface/subagent-actions.ts +294 -0
  96. package/src/interface/subagent-tool.ts +294 -0
  97. package/src/interface/subagents.ts +30 -0
  98. package/src/interface/tool-render.ts +333 -0
  99. package/src/interface/tool-workflow-script.ts +351 -0
  100. package/src/interface/tool-workflow.ts +485 -0
  101. package/src/interface/views/WorkflowsView.ts +944 -0
  102. package/src/interface/views/detail-content.ts +298 -0
  103. package/src/interface/views/format.ts +320 -0
  104. package/src/orchestration/__tests__/concurrency-gate.test.ts +125 -0
  105. package/src/orchestration/__tests__/config-loader.test.ts +381 -0
  106. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +332 -0
  107. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
  108. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +248 -0
  109. package/src/orchestration/__tests__/lifecycle.test.ts +385 -0
  110. package/src/orchestration/__tests__/script-lint.test.ts +347 -0
  111. package/src/orchestration/__tests__/worker-script-builder.test.ts +42 -0
  112. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +319 -0
  113. package/src/orchestration/agent-opts-resolver.ts +128 -0
  114. package/src/orchestration/concurrency-gate.ts +69 -0
  115. package/src/orchestration/config-loader.ts +313 -0
  116. package/src/orchestration/error-recovery.ts +578 -0
  117. package/src/orchestration/execute-agent-call.ts +174 -0
  118. package/src/orchestration/jsonl-run-store.ts +292 -0
  119. package/src/orchestration/launcher.ts +368 -0
  120. package/src/orchestration/lifecycle.ts +373 -0
  121. package/src/orchestration/models/__tests__/budget.test.ts +367 -0
  122. package/src/orchestration/models/agent-call.ts +76 -0
  123. package/src/orchestration/models/budget.ts +148 -0
  124. package/src/orchestration/models/ports.ts +165 -0
  125. package/src/orchestration/models/run-runtime.ts +91 -0
  126. package/src/orchestration/models/run-spec.ts +54 -0
  127. package/src/orchestration/models/run-state.ts +44 -0
  128. package/src/orchestration/models/trace.ts +102 -0
  129. package/src/orchestration/models/types.ts +242 -0
  130. package/src/orchestration/models/workflow-run.ts +275 -0
  131. package/src/orchestration/models/workflow-script-registry.ts +32 -0
  132. package/src/orchestration/models/workflow-script.ts +90 -0
  133. package/src/orchestration/node-ops.ts +192 -0
  134. package/src/orchestration/script-lint.ts +387 -0
  135. package/src/orchestration/skill-discovery.ts +60 -0
  136. package/src/orchestration/worker-handle.ts +115 -0
  137. package/src/orchestration/worker-host.ts +93 -0
  138. package/src/orchestration/worker-script-builder.ts +281 -0
  139. package/src/orchestration/workflow-files.ts +85 -0
  140. package/src/orchestration/workflow-script-registry-impl.ts +128 -0
  141. package/src/shared/__tests__/resource-discovery.test.ts +226 -0
  142. package/src/shared/agent-event.ts +13 -0
  143. package/src/shared/resource-discovery.ts +535 -0
@@ -0,0 +1,110 @@
1
+ // src/__tests__/config.test.ts
2
+ //
3
+ // loadGlobalConfig 测试。
4
+ // 配置已退化为仅 maxConcurrent(模型解析改为「主 agent model 优先」,
5
+ // 不再有 categories/fallback/yolo/session 级覆盖)。
6
+
7
+ import * as fs from "node:fs";
8
+ import * as os from "node:os";
9
+ import * as path from "node:path";
10
+
11
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
12
+
13
+ import { getGlobalConfigPath, loadGlobalConfig } from "../config.ts";
14
+
15
+ let tmpDir: string;
16
+
17
+ beforeEach(() => {
18
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "subagents-config-test-"));
19
+ });
20
+
21
+ afterEach(() => {
22
+ fs.rmSync(tmpDir, { recursive: true, force: true });
23
+ });
24
+
25
+ /** 写入 config.json(自动创建 subagents/ 子目录)。 */
26
+ function writeConfig(agentDir: string, content: string): void {
27
+ const configPath = getGlobalConfigPath(agentDir);
28
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
29
+ fs.writeFileSync(configPath, content, "utf-8");
30
+ }
31
+
32
+ // ============================================================
33
+ // loadGlobalConfig
34
+ // ============================================================
35
+
36
+ describe("loadGlobalConfig", () => {
37
+ it("returns default config when file does not exist", () => {
38
+ const cfg = loadGlobalConfig(tmpDir);
39
+ expect(cfg.version).toBe(1);
40
+ expect(cfg.maxConcurrent).toBe(6);
41
+ });
42
+
43
+ it("returns default config when JSON is corrupt", () => {
44
+ writeConfig(tmpDir, "{not valid json");
45
+ const cfg = loadGlobalConfig(tmpDir);
46
+ expect(cfg.version).toBe(1);
47
+ expect(cfg.maxConcurrent).toBe(6);
48
+ });
49
+
50
+ it("deep-merges with defaults: missing fields fall back to defaults", () => {
51
+ writeConfig(tmpDir, JSON.stringify({ version: 1 }));
52
+ const cfg = loadGlobalConfig(tmpDir);
53
+ expect(cfg.version).toBe(1);
54
+ expect(cfg.maxConcurrent).toBe(6);
55
+ });
56
+
57
+ it("loads valid config with maxConcurrent override", () => {
58
+ writeConfig(tmpDir, JSON.stringify({ version: 1, maxConcurrent: 8 }));
59
+ const cfg = loadGlobalConfig(tmpDir);
60
+ expect(cfg.maxConcurrent).toBe(8);
61
+ });
62
+
63
+ it("ignores legacy categories/fallback/yoloByDefault fields (model resolution deprecated)", () => {
64
+ // 旧 config.json 仍可能含这些字段——读取时不报错,但 SubagentsGlobalConfig
65
+ // 类型上已不含它们,loadGlobalConfig 只取 version + maxConcurrent。
66
+ writeConfig(
67
+ tmpDir,
68
+ JSON.stringify({
69
+ version: 1,
70
+ maxConcurrent: 6,
71
+ yoloByDefault: true,
72
+ categories: { coding: { label: "C", model: "x/y" } },
73
+ fallback: { model: "anthropic/x" },
74
+ }),
75
+ );
76
+ const cfg = loadGlobalConfig(tmpDir);
77
+ expect(cfg.maxConcurrent).toBe(6);
78
+ expect(cfg.version).toBe(1);
79
+ // 仅两个字段(类型保证)
80
+ expect(Object.keys(cfg).sort()).toEqual(["maxConcurrent", "version"]);
81
+ });
82
+ });
83
+
84
+ // ============================================================
85
+ // sanitizeMaxConcurrent(经 loadGlobalConfig 间接覆盖)
86
+ // ============================================================
87
+
88
+ describe("sanitizeMaxConcurrent (via loadGlobalConfig)", () => {
89
+ it("rejects non-number maxConcurrent → default", () => {
90
+ writeConfig(tmpDir, JSON.stringify({ maxConcurrent: "eight" }));
91
+ expect(loadGlobalConfig(tmpDir).maxConcurrent).toBe(6);
92
+ });
93
+
94
+ it("rejects non-integer maxConcurrent → default", () => {
95
+ writeConfig(tmpDir, JSON.stringify({ maxConcurrent: 4.5 }));
96
+ expect(loadGlobalConfig(tmpDir).maxConcurrent).toBe(6);
97
+ });
98
+
99
+ it("rejects zero/negative maxConcurrent → default", () => {
100
+ writeConfig(tmpDir, JSON.stringify({ maxConcurrent: 0 }));
101
+ expect(loadGlobalConfig(tmpDir).maxConcurrent).toBe(6);
102
+ writeConfig(tmpDir, JSON.stringify({ maxConcurrent: -1 }));
103
+ expect(loadGlobalConfig(tmpDir).maxConcurrent).toBe(6);
104
+ });
105
+
106
+ it("accepts positive integer", () => {
107
+ writeConfig(tmpDir, JSON.stringify({ maxConcurrent: 16 }));
108
+ expect(loadGlobalConfig(tmpDir).maxConcurrent).toBe(16);
109
+ });
110
+ });
@@ -0,0 +1,311 @@
1
+ // src/execution/__tests__/crash-recovery.test.ts
2
+ //
3
+ // MF-5: session_start crash recovery 路径测试。
4
+ //
5
+ // 覆盖 store.loadAll 的 4 个分支:
6
+ // 1. loadAll 成功 + running run → 重建(transition done,failed + emit pending:unregister failed)
7
+ // 2. loadAll 成功 + 已终态 run → 直接 set 到 runs Map,不 transition
8
+ // 3. loadAll 失败 → storeHealthy=false,pi.__workflowRun 返回失败(fail-fast)
9
+ // 4. loadAll 失败后 subagent 域不受影响(subagent tool 注册仍被调用)
10
+ //
11
+ // 通过 vi.mock("../../orchestration/jsonl-run-store.ts") 注入可控的 loadAll 行为。
12
+ // 路径说明:index.ts 从 ./orchestration/jsonl-run-store.ts 导入 JsonlRunStore,
13
+ // 该文件绝对路径 src/orchestration/jsonl-run-store.ts,从本测试文件
14
+ // (src/execution/__tests/)相对路径为 ../../orchestration/jsonl-run-store.ts。
15
+
16
+ import { beforeEach, describe, expect, it, vi } from "vitest";
17
+
18
+ // ── mock modules(在 import 前声明)──
19
+
20
+ vi.mock("@mariozechner/pi-coding-agent", () => ({
21
+ getAgentDir: () => "/home/user/.pi/agent",
22
+ }));
23
+ vi.mock("@earendil-works/pi-coding-agent", () => ({
24
+ getAgentDir: () => "/home/user/.pi/agent",
25
+ }));
26
+ vi.mock("@mariozechner/pi-ai", () => ({
27
+ StringEnum: (values: string[]) => ({ type: "string", enum: values }),
28
+ }));
29
+ vi.mock("@earendil-works/pi-ai", () => ({
30
+ StringEnum: (values: string[]) => ({ type: "string", enum: values }),
31
+ }));
32
+ vi.mock("@sinclair/typebox", () => ({
33
+ Type: {
34
+ Object: (props: Record<string, unknown>) => ({ type: "object", properties: props }),
35
+ Optional: (schema: unknown) => ({ ...(schema as object), optional: true }),
36
+ String: () => ({ type: "string" }),
37
+ Boolean: () => ({ type: "boolean" }),
38
+ Number: () => ({ type: "number" }),
39
+ Array: (items: unknown) => ({ type: "array", items }),
40
+ Record: (key: unknown, value: unknown) => ({ type: "object", additionalProperties: value, key }),
41
+ Unknown: () => ({ type: "unknown" }),
42
+ Union: (members: unknown[]) => ({ type: "union", members }),
43
+ Literal: (value: unknown) => ({ type: "literal", value }),
44
+ },
45
+ }));
46
+
47
+ // ── hoisted mock 实例:store loadAll 可控 + 工具注册可观察 ──
48
+
49
+ const { mockLoadAll, mockRegisterSubagentTool, mockScan } = vi.hoisted(() => ({
50
+ mockLoadAll: vi.fn(async () => []),
51
+ mockScan: vi.fn(),
52
+ mockRegisterSubagentTool: vi.fn(),
53
+ }));
54
+
55
+ // JsonlRunStore mock:loadAll 由各 test 配置。构造参数忽略(sessionDir/pi/ctx 都 mock 掉)
56
+ vi.mock("../../orchestration/jsonl-run-store.ts", () => ({
57
+ JsonlRunStore: class {
58
+ loadAll = mockLoadAll;
59
+ save = vi.fn(async () => {});
60
+ },
61
+ }));
62
+
63
+ vi.mock("../worktree-manager.ts", () => ({
64
+ WorktreeManager: class {
65
+ constructor(_agentDir: string) {
66
+ /* mock */
67
+ }
68
+ scan = mockScan;
69
+ cleanup = vi.fn();
70
+ create = vi.fn();
71
+ collectPatch = vi.fn();
72
+ registerPid = vi.fn();
73
+ },
74
+ }));
75
+
76
+ vi.mock("../session-file-gc.ts", () => ({
77
+ maybeCleanupExpiredSessionFiles: vi.fn(),
78
+ }));
79
+
80
+ vi.mock("../model-config-service.ts", () => ({
81
+ ModelConfigService: class {
82
+ initModel = vi.fn();
83
+ getAgentRegistry = () => ({ get: () => undefined, list: () => [] });
84
+ },
85
+ getModelConfigService: () => null,
86
+ setModelConfigService: vi.fn(),
87
+ }));
88
+
89
+ vi.mock("../subagent-service.ts", () => ({
90
+ SubagentService: class {
91
+ initSession = vi.fn();
92
+ },
93
+ getSubagentService: () => null,
94
+ setSubagentService: vi.fn(),
95
+ }));
96
+
97
+ // interface 层模块:mock 掉,避免触发真实 pi.registerTool(pi 是 Proxy,真实模块访问 pi
98
+ // 属性时仍可能抛错)。路径相对 src/execution/__tests/ → ../../interface/...
99
+ vi.mock("../../interface/subagent-tool.ts", () => ({
100
+ registerSubagentTool: mockRegisterSubagentTool,
101
+ }));
102
+ vi.mock("../../interface/subagents.ts", () => ({
103
+ registerSubagentsCommand: vi.fn(),
104
+ }));
105
+ vi.mock("../../interface/bg-notify-render.ts", () => ({
106
+ renderBgNotifyMessage: vi.fn(),
107
+ }));
108
+ vi.mock("../../interface/tool-workflow.ts", () => ({
109
+ registerWorkflowTool: vi.fn(),
110
+ }));
111
+ vi.mock("../../interface/tool-workflow-script.ts", () => ({
112
+ registerWorkflowScriptTool: vi.fn(),
113
+ }));
114
+ vi.mock("../../interface/commands.ts", () => ({
115
+ registerWorkflowsCommand: vi.fn(),
116
+ }));
117
+
118
+ // ── import 被测工厂 ──
119
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
120
+
121
+ import subagentsExtension from "../../index.ts";
122
+ import { WorkflowRun } from "../../orchestration/models/workflow-run.ts";
123
+ import { Budget } from "../../orchestration/models/budget.ts";
124
+ import { Trace } from "../../orchestration/models/trace.ts";
125
+ import type { WorkflowRun as WorkflowRunType } from "../../orchestration/models/workflow-run.ts";
126
+
127
+ // ── helpers ──
128
+
129
+ /** 构造一个可重水合的 WorkflowRun(用 WorkflowRun.reconstruct 跳过 I1 校验)。 */
130
+ function makeRun(
131
+ runId: string,
132
+ status: "running" | "paused" | "done",
133
+ reason?: "completed" | "failed" | "aborted" | "budget_limited" | "time_limited",
134
+ ): WorkflowRunType {
135
+ return WorkflowRun.reconstruct(
136
+ runId,
137
+ {
138
+ scriptSource: "execute() {}",
139
+ args: {},
140
+ scriptName: "test",
141
+ scriptPath: "/fake/test.js",
142
+ },
143
+ {
144
+ status,
145
+ reason,
146
+ budget: new Budget({ maxTokens: 1000 }),
147
+ calls: new Map(),
148
+ trace: new Trace(),
149
+ errorLogs: [],
150
+ },
151
+ { startedAt: new Date().toISOString() },
152
+ );
153
+ }
154
+
155
+ /** 创建可观察 eventBus.emit 的 mock ExtensionAPI,捕获 session_start handler。 */
156
+ function createMockPi(overrides: Record<string, unknown> = {}): {
157
+ pi: ExtensionAPI;
158
+ emits: Array<{ channel: string; data: unknown }>;
159
+ getSessionStartHandler: () => ((event: unknown, ctx: unknown) => Promise<void>) | undefined;
160
+ } {
161
+ let sessionStartHandler: ((event: unknown, ctx: unknown) => Promise<void>) | undefined;
162
+ const emits: Array<{ channel: string; data: unknown }> = [];
163
+ const events = {
164
+ emit(channel: string, data: unknown): void {
165
+ emits.push({ channel, data });
166
+ },
167
+ };
168
+ const noop = (): void => {
169
+ /* mock */
170
+ };
171
+ const pi = new Proxy<ExtensionAPI>(overrides as ExtensionAPI, {
172
+ get(target, prop: string | symbol): unknown {
173
+ if (prop === "on") {
174
+ return (event: string, handler: (...args: unknown[]) => unknown) => {
175
+ if (event === "session_start") {
176
+ sessionStartHandler = handler as (event: unknown, ctx: unknown) => Promise<void>;
177
+ }
178
+ };
179
+ }
180
+ if (prop === "events") return events;
181
+ if (prop === "appendEntry") return noop;
182
+ if (prop === "registerMessageRenderer") return noop;
183
+ if (prop in target) return target[prop as keyof ExtensionAPI];
184
+ return noop;
185
+ },
186
+ });
187
+ return {
188
+ pi,
189
+ emits,
190
+ getSessionStartHandler: () => sessionStartHandler,
191
+ };
192
+ }
193
+
194
+ /** 最小 ExtensionContext mock。 */
195
+ function createMockCtx(): Record<string, unknown> {
196
+ return {
197
+ cwd: "/home/user/project",
198
+ modelRegistry: {
199
+ getAvailable: () => [],
200
+ find: () => undefined,
201
+ hasConfiguredAuth: () => false,
202
+ },
203
+ model: undefined,
204
+ sessionManager: {
205
+ getSessionId: () => "session-crash",
206
+ getSessionFile: () => "/home/user/.pi/agent/sessions/session-crash.jsonl",
207
+ getSessionDir: () => "/home/user/.pi/agent/sessions",
208
+ getCwd: () => "/home/user/project",
209
+ getEntries: () => [],
210
+ getBranch: () => [],
211
+ getLeafId: () => null,
212
+ getLeafEntry: () => undefined,
213
+ getEntry: () => undefined,
214
+ getHeader: () => null,
215
+ getTree: () => [],
216
+ getSessionName: () => undefined,
217
+ },
218
+ };
219
+ }
220
+
221
+ beforeEach(() => {
222
+ vi.clearAllMocks();
223
+ mockLoadAll.mockResolvedValue([]);
224
+ });
225
+
226
+ // ── tests ──
227
+
228
+ describe("session_start crash recovery(store.loadAll 路径)", () => {
229
+ it("loadAll 成功 + running run:transition done,failed + emit pending:unregister failed", async () => {
230
+ const runningRun = makeRun("wf-crash-1", "running");
231
+ mockLoadAll.mockResolvedValue([runningRun]);
232
+
233
+ const { pi, emits, getSessionStartHandler } = createMockPi();
234
+ subagentsExtension(pi);
235
+
236
+ const handler = getSessionStartHandler();
237
+ expect(handler).toBeDefined();
238
+ await handler!({ type: "session_start" }, createMockCtx());
239
+
240
+ // run 被转为 done,failed(crash recovery)
241
+ expect(runningRun.state.status).toBe("done");
242
+ expect(runningRun.state.reason).toBe("failed");
243
+ expect(runningRun.state.error).toContain("Process killed");
244
+
245
+ // emit pending:unregister(reason=failed)
246
+ const unregister = emits.find((e) => e.channel === "pending:unregister");
247
+ expect(unregister).toBeDefined();
248
+ expect(unregister!.data).toEqual({ id: "wf-crash-1", reason: "failed" });
249
+ });
250
+
251
+ it("loadAll 成功 + 已终态 run:直接 set 到 runs Map,不 transition", async () => {
252
+ const doneRun = makeRun("wf-done-1", "done", "completed");
253
+ const originalCompletedAt = doneRun.meta.completedAt;
254
+ mockLoadAll.mockResolvedValue([doneRun]);
255
+
256
+ const { pi, emits, getSessionStartHandler } = createMockPi();
257
+ subagentsExtension(pi);
258
+
259
+ const handler = getSessionStartHandler();
260
+ await handler!({ type: "session_start" }, createMockCtx());
261
+
262
+ // 状态不变(仍 done/completed),不重新 transition(completedAt 不变)
263
+ expect(doneRun.state.status).toBe("done");
264
+ expect(doneRun.state.reason).toBe("completed");
265
+ expect(doneRun.meta.completedAt).toBe(originalCompletedAt);
266
+
267
+ // 终态 run 不触发 pending:unregister(恢复路径只处理 status==="running")
268
+ const unregister = emits.filter((e) => e.channel === "pending:unregister");
269
+ expect(unregister).toHaveLength(0);
270
+
271
+ // run 已被 set 到 runs Map —— pi.__workflowRun 在 storeHealthy=true 时
272
+ // 不会因 store unavailable 提前返回
273
+ const result = (await (pi as unknown as {
274
+ __workflowRun: (n: string, a: Record<string, unknown>) => Promise<unknown>;
275
+ }).__workflowRun("any", {})) as { error?: string };
276
+ expect(result.error).not.toContain("store unavailable");
277
+ });
278
+
279
+ it("loadAll 失败 → storeHealthy=false:pi.__workflowRun 返回 store unavailable 失败", async () => {
280
+ mockLoadAll.mockRejectedValue(new Error("disk corruption"));
281
+
282
+ const { pi, getSessionStartHandler } = createMockPi();
283
+ subagentsExtension(pi);
284
+
285
+ const handler = getSessionStartHandler();
286
+ await handler!({ type: "session_start" }, createMockCtx());
287
+
288
+ // pi.__workflowRun 在 store 不健康时 fail-fast
289
+ const result = (await (pi as unknown as {
290
+ __workflowRun: (n: string, a: Record<string, unknown>) => Promise<unknown>;
291
+ }).__workflowRun("any", {})) as { status: string; reason: string; error: string };
292
+
293
+ expect(result.status).toBe("done");
294
+ expect(result.reason).toBe("failed");
295
+ expect(result.error).toContain("store unavailable");
296
+ expect(result.error).toContain("loadAll failed");
297
+ });
298
+
299
+ it("loadAll 失败后 subagent 域不受影响:registerSubagentTool 仍被调用", async () => {
300
+ mockLoadAll.mockRejectedValue(new Error("disk corruption"));
301
+
302
+ const { pi, getSessionStartHandler } = createMockPi();
303
+ subagentsExtension(pi);
304
+
305
+ const handler = getSessionStartHandler();
306
+ await handler!({ type: "session_start" }, createMockCtx());
307
+
308
+ // subagent tool 注册在 factory 入口(session_start 之外),与 store 健康无关
309
+ expect(mockRegisterSubagentTool).toHaveBeenCalledTimes(1);
310
+ });
311
+ });