@zhushanwen/subagent-core 0.2.0 → 0.4.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 (195) hide show
  1. package/README.md +45 -2
  2. package/agents/analyst.md +60 -0
  3. package/agents/coder.md +69 -0
  4. package/agents/debugger.md +66 -0
  5. package/agents/doc-reviewer.md +49 -0
  6. package/agents/explorer.md +63 -0
  7. package/agents/general-purpose.md +32 -0
  8. package/agents/orchestrator.md +61 -0
  9. package/agents/planner.md +53 -0
  10. package/agents/researcher.md +67 -0
  11. package/agents/reviewer.md +73 -0
  12. package/dist/chunk-4KN4TTG7.js +240 -0
  13. package/dist/chunk-APZY4IME.js +27 -0
  14. package/dist/chunk-X7SZ5HLQ.js +66 -0
  15. package/dist/execution/engine/engines/zcode/constants.cjs +65 -0
  16. package/dist/execution/engine/engines/zcode/constants.d.cts +93 -1
  17. package/dist/execution/engine/engines/zcode/constants.d.ts +93 -1
  18. package/dist/execution/engine/engines/zcode/constants.js +43 -1
  19. package/dist/execution/engine/engines/zcode/reader.d.cts +1 -1
  20. package/dist/execution/engine/engines/zcode/reader.d.ts +1 -1
  21. package/dist/execution/engine/engines/zcode/reader.js +4 -234
  22. package/dist/execution/engine/paths.js +7 -19
  23. package/dist/index.cjs +14709 -100
  24. package/dist/index.d.cts +4053 -200
  25. package/dist/index.d.ts +4053 -200
  26. package/dist/index.js +14358 -102
  27. package/dist/{types-BxyAidGf.d.cts → types-DpUO16pj.d.cts} +464 -1
  28. package/dist/{types-BxyAidGf.d.ts → types-DpUO16pj.d.ts} +464 -1
  29. package/package.json +6 -3
  30. package/src/__tests__/review-fix-loop-script.test.ts +90 -2
  31. package/src/__tests__/review-fix-loop-utils.test.ts +21 -0
  32. package/src/__tests__/smoke.test.ts +9 -2
  33. package/src/execution/__tests__/agent-profile.test.ts +232 -0
  34. package/src/execution/__tests__/agents-assembly.test.ts +219 -0
  35. package/src/execution/__tests__/chat-engine-routing.test.ts +71 -0
  36. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +1 -0
  37. package/src/execution/__tests__/create-concurrency-pool.test.ts +194 -0
  38. package/src/execution/__tests__/delivery-methods.test.ts +2 -0
  39. package/src/execution/__tests__/descendant-sweep.test.ts +269 -0
  40. package/src/execution/__tests__/dialog-queue.test.ts +199 -1
  41. package/src/execution/__tests__/epipe-fallback.test.ts +2 -0
  42. package/src/execution/__tests__/execution-runtime-face.test.ts +272 -0
  43. package/src/execution/__tests__/finalize-record.test.ts +106 -0
  44. package/src/execution/__tests__/gc-timer.test.ts +2 -0
  45. package/src/execution/__tests__/get-record-for-action-restart.test.ts +2 -0
  46. package/src/execution/__tests__/get-state-handshake.test.ts +127 -0
  47. package/src/execution/__tests__/keep-alive-no-progress.test.ts +378 -0
  48. package/src/execution/__tests__/kill-all-escalation.test.ts +196 -0
  49. package/src/execution/__tests__/lifecycle-manager-idle-timer-identity.test.ts +117 -0
  50. package/src/execution/__tests__/lifecycle-manager.test.ts +39 -0
  51. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +99 -0
  52. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +113 -0
  53. package/src/execution/__tests__/notify-ledger.test.ts +294 -0
  54. package/src/execution/__tests__/record-store-orphan-revive.test.ts +159 -0
  55. package/src/execution/__tests__/record-store.test.ts +83 -0
  56. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +4 -0
  57. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +1 -0
  58. package/src/execution/__tests__/run-spawn-edges.test.ts +407 -17
  59. package/src/execution/__tests__/run-spawn-integration.test.ts +1 -1
  60. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +12 -4
  61. package/src/execution/__tests__/service-kill-escalation.test.ts +89 -0
  62. package/src/execution/__tests__/session-pending.test.ts +228 -0
  63. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +110 -0
  64. package/src/execution/__tests__/session-runner-close-prune.test.ts +181 -0
  65. package/src/execution/__tests__/session-runner-epipe.test.ts +1 -0
  66. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +226 -0
  67. package/src/execution/__tests__/settled-watchdog.test.ts +289 -0
  68. package/src/execution/__tests__/spawned-children.test.ts +153 -10
  69. package/src/execution/__tests__/subagent-actions-core.test.ts +997 -0
  70. package/src/execution/__tests__/subagent-service-message-close.test.ts +71 -10
  71. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +174 -0
  72. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +266 -0
  73. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +3 -0
  74. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +316 -0
  75. package/src/execution/__tests__/timeout-integration.test.ts +7 -7
  76. package/src/execution/__tests__/ui-request-handler-factory.test.ts +95 -1
  77. package/src/execution/__tests__/ui-request-queue.test.ts +25 -0
  78. package/src/execution/__tests__/worktree-git-ops.test.ts +571 -0
  79. package/src/execution/__tests__/worktree-reconcile-aging.test.ts +204 -0
  80. package/src/execution/agent-registry.ts +225 -1
  81. package/src/execution/agents-assembly.ts +88 -0
  82. package/src/execution/concurrency-pool.ts +71 -9
  83. package/src/execution/dialog-queue.ts +101 -3
  84. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +49 -2
  85. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +87 -1
  86. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +26 -0
  87. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +83 -1
  88. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +131 -0
  89. package/src/execution/engine/__tests__/registry.test.ts +90 -1
  90. package/src/execution/engine/engine-discovery.ts +12 -16
  91. package/src/execution/engine/engines/pi/task-spec-mapper.ts +3 -3
  92. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +274 -0
  93. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +36 -0
  94. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +472 -0
  95. package/src/execution/engine/engines/zcode/__tests__/preparer-appserver.test.ts +387 -0
  96. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +780 -0
  97. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +815 -0
  98. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +462 -0
  99. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +119 -1
  100. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +136 -6
  101. package/src/execution/engine/engines/zcode/appserver-home.ts +442 -0
  102. package/src/execution/engine/engines/zcode/appserver-probe.ts +141 -0
  103. package/src/execution/engine/engines/zcode/connection.ts +585 -0
  104. package/src/execution/engine/engines/zcode/constants.ts +129 -0
  105. package/src/execution/engine/engines/zcode/golden-sample.ts +54 -8
  106. package/src/execution/engine/engines/zcode/preparer.ts +25 -22
  107. package/src/execution/engine/engines/zcode/session-channel.ts +656 -0
  108. package/src/execution/engine/engines/zcode/zcode-engine.ts +909 -41
  109. package/src/execution/engine/host-task-spec.ts +3 -3
  110. package/src/execution/engine/port.ts +41 -1
  111. package/src/execution/engine/registry.ts +53 -0
  112. package/src/execution/finalize-record.ts +63 -5
  113. package/src/execution/get-state-handshake.ts +85 -10
  114. package/src/execution/lifecycle-manager.ts +27 -3
  115. package/src/execution/manifest-store.ts +53 -62
  116. package/src/execution/notifier.ts +10 -10
  117. package/src/execution/notify-ledger.ts +117 -16
  118. package/src/execution/record-entry.ts +8 -2
  119. package/src/execution/record-store.ts +29 -15
  120. package/src/execution/session-pending.ts +213 -62
  121. package/src/execution/session-runner.ts +974 -128
  122. package/src/execution/sessions-index.ts +10 -55
  123. package/src/execution/settled-watchdog.ts +99 -0
  124. package/src/execution/subagent-actions-core.ts +686 -0
  125. package/src/execution/subagent-service.ts +396 -49
  126. package/src/execution/ui-request-handler-factory.ts +27 -4
  127. package/src/execution/worktree-git-ops.ts +397 -0
  128. package/src/execution/worktree-manager.ts +72 -10
  129. package/src/execution/worktree-registry.ts +6 -12
  130. package/src/index.ts +413 -6
  131. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +42 -3
  132. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -4
  133. package/src/orchestration/__tests__/args-meta.test.ts +358 -0
  134. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +1 -8
  135. package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +312 -0
  136. package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +382 -0
  137. package/src/orchestration/__tests__/file-run-store-prune.test.ts +168 -0
  138. package/src/orchestration/__tests__/file-run-store-throttle.test.ts +168 -0
  139. package/src/orchestration/__tests__/file-run-store.test.ts +389 -0
  140. package/src/orchestration/__tests__/helpers/flush-microtasks.ts +13 -0
  141. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +34 -0
  142. package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +357 -0
  143. package/src/orchestration/__tests__/lifecycle-recover-crashed.test.ts +289 -0
  144. package/src/orchestration/__tests__/lifecycle.test.ts +77 -0
  145. package/src/orchestration/__tests__/run-snapshot.test.ts +353 -0
  146. package/src/orchestration/__tests__/script-generate.test.ts +314 -0
  147. package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +275 -0
  148. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +78 -1
  149. package/src/orchestration/__tests__/workflow-files.test.ts +186 -0
  150. package/src/orchestration/__tests__/workflow-run-summary.test.ts +119 -0
  151. package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +124 -0
  152. package/src/orchestration/agent-opts-resolver.ts +7 -7
  153. package/src/orchestration/args-meta.ts +198 -0
  154. package/src/orchestration/error-recovery.ts +416 -146
  155. package/src/orchestration/execute-agent-call.ts +9 -9
  156. package/src/orchestration/file-run-store.ts +327 -0
  157. package/src/orchestration/launcher.ts +35 -26
  158. package/src/orchestration/lifecycle.ts +355 -79
  159. package/src/orchestration/models/agent-call.ts +7 -7
  160. package/src/orchestration/models/budget.ts +5 -5
  161. package/src/orchestration/models/run-runtime.ts +13 -13
  162. package/src/orchestration/models/trace.ts +8 -8
  163. package/src/orchestration/models/workflow-run.ts +27 -27
  164. package/src/orchestration/models/workflow-script.ts +4 -4
  165. package/src/orchestration/run-snapshot.ts +266 -0
  166. package/src/orchestration/script-generate.ts +154 -0
  167. package/src/orchestration/script-lint.ts +33 -33
  168. package/src/orchestration/worker-handle.ts +10 -10
  169. package/src/orchestration/worker-host.ts +10 -10
  170. package/src/orchestration/worker-script-builder.ts +401 -346
  171. package/src/orchestration/workflow-files.ts +37 -11
  172. package/src/orchestration/workflow-run-summary.ts +69 -0
  173. package/src/orchestration/workflow-script-registry-impl.ts +31 -9
  174. package/src/shared/__tests__/agent-ref.test.ts +209 -2
  175. package/src/shared/__tests__/atomic-write.test.ts +267 -0
  176. package/src/shared/__tests__/bounded-serialize.test.ts +236 -0
  177. package/src/shared/__tests__/injection-render.test.ts +518 -0
  178. package/src/shared/__tests__/resource-discovery-host-roots.test.ts +474 -0
  179. package/src/shared/__tests__/resource-discovery.test.ts +3 -2
  180. package/src/shared/agent-ref.ts +143 -2
  181. package/src/shared/atomic-write.ts +320 -0
  182. package/src/shared/bounded-serialize.ts +154 -0
  183. package/src/shared/injection-render.ts +279 -0
  184. package/src/shared/meta-parser.ts +41 -1
  185. package/src/shared/resource-discovery.ts +104 -37
  186. package/src/shared/resource-meta.ts +14 -0
  187. package/src/shared/xml-injection.ts +9 -9
  188. package/workflows/README.md +9 -9
  189. package/workflows/chain.js +4 -2
  190. package/workflows/map-reduce.js +5 -3
  191. package/workflows/parallel.js +5 -3
  192. package/workflows/review-fix-loop-utils.cjs +14 -3
  193. package/workflows/review-fix-loop.js +18 -7
  194. package/workflows/scatter-gather.js +4 -2
  195. package/dist/chunk-3VOERJPJ.js +0 -22
@@ -0,0 +1,168 @@
1
+ // file-run-store-throttle.test.ts —— FileRunStore save 节流(OR-5 单 run 快照 O(n²) 修复 ⑥a)。
2
+ //
3
+ // 语义锁定:
4
+ // - running 中间态:同一 runId 两次落盘间隔 < saveMinIntervalMs → 跳过 append
5
+ // (高频 save 落盘次数有界 = ceil(run 时长 / 间隔));
6
+ // - 首写永不节流(新 run 至少一条快照,loadAll 可发现);
7
+ // - 终态(done)永不节流(最终状态必落盘,节流窗口内到达也不丢);
8
+ // - runId 之间互不影响;saveMinIntervalMs=0 = 禁用节流;
9
+ // - 默认间隔 DEFAULT_SAVE_MIN_INTERVAL_MS = 60s。
10
+ //
11
+ // 时间推进用 vitest fake timers(默认 fake Date.now——save 节流判据的时间源);
12
+ // fs 真实 IO 不受 fake timers 影响。
13
+
14
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
15
+ import { tmpdir } from "node:os";
16
+ import { join } from "node:path";
17
+
18
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
19
+
20
+ import { configureCore, resetCoreForTests, type HostServices } from "../../core/host-services.ts";
21
+ import { Budget } from "../models/budget.ts";
22
+ import { Trace } from "../models/trace.ts";
23
+ import { WorkflowRun } from "../models/workflow-run.ts";
24
+ import { DEFAULT_SAVE_MIN_INTERVAL_MS, FileRunStore } from "../file-run-store.ts";
25
+
26
+ let dataRoot: string;
27
+
28
+ beforeEach(() => {
29
+ resetCoreForTests();
30
+ dataRoot = mkdtempSync(join(tmpdir(), "file-run-store-throttle-"));
31
+ const host: HostServices = {
32
+ dataRoot: () => dataRoot,
33
+ log: () => {},
34
+ };
35
+ configureCore(host);
36
+ vi.useFakeTimers();
37
+ });
38
+
39
+ afterEach(() => {
40
+ vi.useRealTimers();
41
+ resetCoreForTests();
42
+ rmSync(dataRoot, { recursive: true, force: true });
43
+ });
44
+
45
+ /** 构造可持久化的 WorkflowRun(对齐 file-run-store.test.ts makeRun 模式)。 */
46
+ function makeRun(runId: string, status: "running" | "done" = "running"): WorkflowRun {
47
+ return WorkflowRun.reconstruct(
48
+ runId,
49
+ {
50
+ scriptSource: "export function execute() { return 'ok'; }",
51
+ args: { topic: "demo" },
52
+ scriptName: "test-script",
53
+ scriptPath: "/fake/test.js",
54
+ parameters: { type: "object" },
55
+ budgetTokens: 1000,
56
+ },
57
+ {
58
+ status,
59
+ ...(status === "done" ? { reason: "completed" as const } : {}),
60
+ budget: new Budget({ maxTokens: 1000, usedTokens: 1, usedCost: 0, totalCallCount: 1 }),
61
+ calls: new Map(),
62
+ trace: new Trace(),
63
+ errorLogs: [],
64
+ },
65
+ { startedAt: "2026-09-01T00:00:00.000Z" },
66
+ );
67
+ }
68
+
69
+ /** 某 runId 的 state 文件行数 = 实际落盘快照次数。 */
70
+ function snapshotLineCount(runId: string): number {
71
+ const content = readFileSync(join(dataRoot, "workflow-state", `${runId}.jsonl`), "utf8");
72
+ return content.split("\n").filter((l) => l.trim() !== "").length;
73
+ }
74
+
75
+ describe("FileRunStore save 节流(OR-5 ⑥a)", () => {
76
+ it("高频 running save:节流窗口内只落盘 1 次(落盘次数有界)", async () => {
77
+ const store = new FileRunStore({ saveMinIntervalMs: 1000 });
78
+ const run = makeRun("wf-throttle-1");
79
+
80
+ for (let i = 0; i < 10; i++) {
81
+ await store.save(run); // 不推进时间——全部命中节流窗口
82
+ }
83
+ expect(snapshotLineCount("wf-throttle-1")).toBe(1);
84
+ });
85
+
86
+ it("窗口过期后恢复落盘:次数 = 窗口数 + 1(首写)", async () => {
87
+ const store = new FileRunStore({ saveMinIntervalMs: 1000 });
88
+ const run = makeRun("wf-throttle-2");
89
+
90
+ await store.save(run); // 首写(窗口 0 时刻)
91
+ vi.advanceTimersByTime(600);
92
+ await store.save(run); // 窗口内:跳过
93
+ vi.advanceTimersByTime(400); // 累计 1000ms = 窗口到期
94
+ await store.save(run); // 落盘第 2 行
95
+ vi.advanceTimersByTime(999);
96
+ await store.save(run); // 差 1ms:跳过
97
+ expect(snapshotLineCount("wf-throttle-2")).toBe(2);
98
+
99
+ vi.advanceTimersByTime(1);
100
+ await store.save(run); // 恰好到期:落盘第 3 行
101
+ expect(snapshotLineCount("wf-throttle-2")).toBe(3);
102
+ });
103
+
104
+ it("终态强制落盘:节流窗口内到达的 done 不被跳过,loadAll 恢复最终状态", async () => {
105
+ const store = new FileRunStore({ saveMinIntervalMs: 1000 });
106
+ const runId = "wf-throttle-3";
107
+
108
+ await store.save(makeRun(runId, "running"));
109
+ vi.advanceTimersByTime(100);
110
+ await store.save(makeRun(runId, "running")); // 窗口内:跳过
111
+ expect(snapshotLineCount(runId)).toBe(1);
112
+
113
+ // 同 runId 终态到达(不推进时间——永不节流)
114
+ await store.save(makeRun(runId, "done"));
115
+ expect(snapshotLineCount(runId)).toBe(2);
116
+
117
+ const runs = await store.loadAll();
118
+ expect(runs).toHaveLength(1);
119
+ expect(runs[0]?.state.status).toBe("done");
120
+ expect(runs[0]?.state.reason).toBe("completed");
121
+ });
122
+
123
+ it("首写永不节流:新 runId 第一次 save 必落盘(loadAll 可发现)", async () => {
124
+ const store = new FileRunStore({ saveMinIntervalMs: 1000 });
125
+
126
+ await store.save(makeRun("wf-throttle-4"));
127
+ const runs = await store.loadAll();
128
+ expect(runs).toHaveLength(1);
129
+ expect(runs[0]?.runId).toBe("wf-throttle-4");
130
+ });
131
+
132
+ it("runId 互不影响:runA 节流窗口内 runB 正常落盘", async () => {
133
+ const store = new FileRunStore({ saveMinIntervalMs: 1000 });
134
+
135
+ await store.save(makeRun("wf-throttle-a"));
136
+ await store.save(makeRun("wf-throttle-b"));
137
+ await store.save(makeRun("wf-throttle-a")); // A 窗口内跳过
138
+ await store.save(makeRun("wf-throttle-b")); // B 窗口内跳过
139
+
140
+ expect(snapshotLineCount("wf-throttle-a")).toBe(1);
141
+ expect(snapshotLineCount("wf-throttle-b")).toBe(1);
142
+ });
143
+
144
+ it("saveMinIntervalMs=0 禁用节流:每次 save 都落盘", async () => {
145
+ const store = new FileRunStore({ saveMinIntervalMs: 0 });
146
+ const run = makeRun("wf-throttle-5");
147
+
148
+ for (let i = 0; i < 3; i++) {
149
+ await store.save(run);
150
+ }
151
+ expect(snapshotLineCount("wf-throttle-5")).toBe(3);
152
+ });
153
+
154
+ it("默认构造:间隔 = DEFAULT_SAVE_MIN_INTERVAL_MS(60s)", async () => {
155
+ expect(DEFAULT_SAVE_MIN_INTERVAL_MS).toBe(60_000);
156
+ const store = new FileRunStore(); // 缺省构造即生产形态
157
+ const run = makeRun("wf-throttle-6");
158
+
159
+ await store.save(run);
160
+ vi.advanceTimersByTime(DEFAULT_SAVE_MIN_INTERVAL_MS - 1);
161
+ await store.save(run);
162
+ expect(snapshotLineCount("wf-throttle-6")).toBe(1);
163
+
164
+ vi.advanceTimersByTime(1);
165
+ await store.save(run);
166
+ expect(snapshotLineCount("wf-throttle-6")).toBe(2);
167
+ });
168
+ });
@@ -0,0 +1,389 @@
1
+ // file-run-store.test.ts —— RunStore port 的宿主无关文件实现(D2 设计件)。
2
+ //
3
+ // 四视角:
4
+ // ①使用者——save/loadAll 往返一致(聚合根字段全量保真:spec/state/budget/calls/trace/meta);
5
+ // ②隔离者——多 run 各落各文件,互不串扰;
6
+ // ③幸存者——append-only + 损坏行容错(半行写入崩溃后取最后一条「有效」行;整文件损坏不炸 loadAll);
7
+ // ④接线者——stateFilePath 路径形状(<dataRoot>/workflow-state/<runId>.jsonl)与
8
+ // 未 configureCore 的 fail-loud(core_host_not_configured,§3.4 错误规格)。
9
+ //
10
+ // dataRoot 经 configureCore(tmp) 注入 + resetCoreForTests 复位(对齐
11
+ // core/__tests__/host-services.test.ts 的配置态隔离模式);warn 断言经宿主 log
12
+ // 端口 spy 捕获(logger facade 每次调用动态解析宿主实现,logger.ts 契约)。
13
+
14
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync } from "node:fs";
15
+ import { tmpdir } from "node:os";
16
+ import { join } from "node:path";
17
+
18
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
19
+
20
+ import { configureCore, resetCoreForTests, type HostServices } from "../../core/host-services.ts";
21
+ import { Budget } from "../models/budget.ts";
22
+ import { Trace } from "../models/trace.ts";
23
+ import { WorkflowRun } from "../models/workflow-run.ts";
24
+ import { FileRunStore } from "../file-run-store.ts";
25
+
26
+ let dataRoot: string;
27
+ let logSpy: ReturnType<typeof vi.fn>;
28
+ let store: FileRunStore;
29
+
30
+ beforeEach(() => {
31
+ resetCoreForTests();
32
+ dataRoot = mkdtempSync(join(tmpdir(), "file-run-store-"));
33
+ logSpy = vi.fn();
34
+ const host: HostServices = {
35
+ dataRoot: () => dataRoot,
36
+ log: logSpy,
37
+ };
38
+ configureCore(host);
39
+ store = new FileRunStore();
40
+ });
41
+
42
+ afterEach(() => {
43
+ resetCoreForTests();
44
+ rmSync(dataRoot, { recursive: true, force: true });
45
+ });
46
+
47
+ /** 构造可持久化的 WorkflowRun(对齐 lifecycle.test.ts makeEvictableRun 模式)。 */
48
+ function makeRun(runId: string, opts: { status?: "running" | "done" } = {}): WorkflowRun {
49
+ const status = opts.status ?? "running";
50
+ return WorkflowRun.reconstruct(
51
+ runId,
52
+ {
53
+ scriptSource: "export function execute() { return 'ok'; }",
54
+ args: { topic: "demo", count: 2 },
55
+ scriptName: "test-script",
56
+ scriptPath: "/fake/test.js",
57
+ parameters: { type: "object" },
58
+ budgetTokens: 1000,
59
+ },
60
+ {
61
+ status,
62
+ ...(status === "done" ? { reason: "completed" as const } : {}),
63
+ budget: new Budget({ maxTokens: 1000, usedTokens: 42, usedCost: 0.5, totalCallCount: 3 }),
64
+ calls: new Map(),
65
+ trace: new Trace(),
66
+ errorLogs: [],
67
+ scriptResult: status === "done" ? { summary: "done-value" } : undefined,
68
+ },
69
+ { startedAt: "2026-08-30T00:00:00.000Z" },
70
+ );
71
+ }
72
+
73
+ /** warn 级日志消息集合(log 端口 (level, component, message) 签名过滤)。 */
74
+ function warnMessages(): string[] {
75
+ return logSpy.mock.calls
76
+ .filter((c) => c[0] === "warn")
77
+ .map((c) => String(c[2]));
78
+ }
79
+
80
+ /** 直接往 workflow-state 目录写预置文件(损坏行场景——不经 save 路径建目录)。 */
81
+ function writeStateFile(name: string, content: string): void {
82
+ const dir = join(dataRoot, "workflow-state");
83
+ mkdirSync(dir, { recursive: true });
84
+ writeFileSync(join(dir, name), content);
85
+ }
86
+
87
+ describe("FileRunStore — save/loadAll 往返一致", () => {
88
+ it("running 快照往返:runId/spec/budget/trace/meta 字段保真", async () => {
89
+ const run = makeRun("wf-rt-1");
90
+ await store.save(run);
91
+
92
+ const loaded = await store.loadAll();
93
+ expect(loaded).toHaveLength(1);
94
+ const back = loaded[0];
95
+
96
+ expect(back.runId).toBe("wf-rt-1");
97
+ expect(back.state.status).toBe("running");
98
+ expect(back.spec.scriptName).toBe("test-script");
99
+ expect(back.spec.args).toEqual({ topic: "demo", count: 2 });
100
+ expect(back.spec.parameters).toEqual({ type: "object" });
101
+ // Budget 重水合为实例且消耗累积保真(后续预算判定不归零)
102
+ expect(back.state.budget).toBeInstanceOf(Budget);
103
+ expect(back.state.budget.maxTokens).toBe(1000);
104
+ expect(back.state.budget.usedTokens).toBe(42);
105
+ expect(back.state.budget.usedCost).toBe(0.5);
106
+ expect(back.state.budget.totalCallCount).toBe(3);
107
+ // Trace 重水合为实例;runtime 不落盘(跨进程必死,reconstruct 语义)
108
+ expect(back.state.trace).toBeInstanceOf(Trace);
109
+ expect(back.runtime).toBeUndefined();
110
+ expect(back.meta.startedAt).toBe("2026-08-30T00:00:00.000Z");
111
+ });
112
+
113
+ it("done 快照往返:reason/scriptResult/error 保真", async () => {
114
+ const run = makeRun("wf-rt-2", { status: "done" });
115
+ await store.save(run);
116
+
117
+ const back = (await store.loadAll())[0];
118
+ expect(back.state.status).toBe("done");
119
+ expect(back.state.reason).toBe("completed");
120
+ expect(back.state.scriptResult).toEqual({ summary: "done-value" });
121
+ });
122
+
123
+ it("append-only:同 run 多次 save 追加多行,loadAll 取最后一条(状态演进不丢)", async () => {
124
+ const run = makeRun("wf-rt-3");
125
+ await store.save(run);
126
+ run.transition("done", "failed");
127
+ await store.save(run);
128
+
129
+ const raw = readFileSync(store.stateFilePath("wf-rt-3"), "utf8");
130
+ expect(raw.trim().split("\n")).toHaveLength(2);
131
+
132
+ const back = (await store.loadAll())[0];
133
+ expect(back.state.status).toBe("done");
134
+ expect(back.state.reason).toBe("failed");
135
+ });
136
+
137
+ it("含 calls 的快照往返:calls Map 逐项保真 + traceNode 回链 Trace 副本", async () => {
138
+ const run = makeRun("wf-rt-4");
139
+ // 经公开 append 路径构造 trace 节点(Trace 值对象,禁止外部打洞 nodes 数组;
140
+ // append 返回 void,节点对象由调用方持有——正是 D-10「call 与 trace 共享引用」的入口)
141
+ const node = {
142
+ stepIndex: 0,
143
+ agent: "coder",
144
+ task: "do work",
145
+ model: "test-model",
146
+ status: "completed" as const,
147
+ };
148
+ run.state.trace.append(node);
149
+ const calls = run.state.calls as Map<number, import("../models/agent-call.ts").AgentCall>;
150
+ const AgentCallMod = await import("../models/agent-call.ts");
151
+ const call = new AgentCallMod.AgentCall(0, { prompt: "do work" }, node);
152
+ call.status = "done";
153
+ call.attempts = 1;
154
+ run.state.calls.set(0, call);
155
+
156
+ await store.save(run);
157
+ const back = (await store.loadAll())[0];
158
+
159
+ expect(back.state.calls.size).toBe(1);
160
+ const restored = back.state.calls.get(0)!;
161
+ expect(restored.id).toBe(0);
162
+ expect(restored.opts.prompt).toBe("do work");
163
+ expect(restored.status).toBe("done");
164
+ expect(restored.attempts).toBe(1);
165
+ // D-10 尽力恢复:重水合 call.traceNode 与 trace.nodes 副本共享引用
166
+ expect(back.state.trace.toArray()[0].stepIndex).toBe(0);
167
+ expect(restored.traceNode.stepIndex).toBe(0);
168
+ expect(restored.traceNode).toBe(back.state.trace.toArray()[0]);
169
+ });
170
+ });
171
+
172
+ describe("FileRunStore — 多 run 隔离", () => {
173
+ it("两个 run 各落各文件,loadAll 全量返回且互不覆盖", async () => {
174
+ const a = makeRun("wf-iso-a");
175
+ const b = makeRun("wf-iso-b", { status: "done" });
176
+ await store.save(a);
177
+ await store.save(b);
178
+
179
+ const loaded = await store.loadAll();
180
+ expect(loaded.map((r) => r.runId).sort()).toEqual(["wf-iso-a", "wf-iso-b"]);
181
+ const backA = loaded.find((r) => r.runId === "wf-iso-a")!;
182
+ const backB = loaded.find((r) => r.runId === "wf-iso-b")!;
183
+ expect(backA.state.status).toBe("running");
184
+ expect(backB.state.status).toBe("done");
185
+ });
186
+ });
187
+
188
+ describe("FileRunStore — 损坏行容错", () => {
189
+ it("文件尾损坏行(半行写入)→ 取更早的最后有效行 + warn", async () => {
190
+ const run = makeRun("wf-corrupt-1");
191
+ await store.save(run);
192
+ // 模拟崩溃半行:追加一段非法 JSON(无换行截断形态)
193
+ const path = store.stateFilePath("wf-corrupt-1");
194
+ const before = readFileSync(path, "utf8");
195
+ writeFileSync(path, before + '{"runId": "wf-corrupt-1", "state": {"sta');
196
+
197
+ const back = (await store.loadAll())[0];
198
+ expect(back?.runId).toBe("wf-corrupt-1");
199
+ expect(back?.state.status).toBe("running");
200
+ expect(warnMessages().some((m) => m.includes("wf-corrupt-1.jsonl"))).toBe(true);
201
+ });
202
+
203
+ it("形状合法但字段残缺的快照(缺 runId/state)→ 判损坏跳过 + warn", async () => {
204
+ writeStateFile("wf-corrupt-2.jsonl", '{"foo": 1}\n{"also": "bad"}\n');
205
+
206
+ const loaded = await store.loadAll();
207
+ expect(loaded).toHaveLength(0);
208
+ expect(warnMessages().some((m) => m.includes("malformed snapshot"))).toBe(true);
209
+ });
210
+
211
+ it("整文件全损坏 → run 跳过不炸 loadAll(其余 run 正常恢复)", async () => {
212
+ writeStateFile("wf-corrupt-3.jsonl", "not json at all\n{broken\n");
213
+ const good = makeRun("wf-corrupt-4");
214
+ await store.save(good);
215
+
216
+ const loaded = await store.loadAll();
217
+ expect(loaded.map((r) => r.runId)).toEqual(["wf-corrupt-4"]);
218
+ });
219
+
220
+ it("非 .jsonl 文件与空文件不参与加载", async () => {
221
+ writeStateFile("README.txt", "hello");
222
+ writeStateFile("wf-corrupt-5.jsonl", "\n\n");
223
+
224
+ const loaded = await store.loadAll();
225
+ expect(loaded).toHaveLength(0);
226
+ });
227
+
228
+ it("loadAll 目录不存在(干净环境首启)→ 空数组不抛错", async () => {
229
+ // beforeEach 只建了 dataRoot 本身,workflow-state 尚未创建(未 save 过)
230
+ const loaded = await store.loadAll();
231
+ expect(loaded).toEqual([]);
232
+ });
233
+ });
234
+
235
+ describe("FileRunStore — 版本衔接与 live-strip(U8 / ⛔5,D4 裁决)", () => {
236
+ /** core 存量形态行(U8 前:无 v 字段、live 未 strip 直接落盘)。 */
237
+ const LEGACY_LINE = JSON.stringify({
238
+ runId: "wf-legacy-1",
239
+ spec: {
240
+ scriptSource: "export function execute() { return 'legacy'; }",
241
+ args: { topic: "legacy" },
242
+ scriptName: "test-script",
243
+ scriptPath: "/fake/test.js",
244
+ },
245
+ state: {
246
+ status: "running",
247
+ budget: { maxTokens: 1000, usedTokens: 7, usedCost: 0.1, totalCallCount: 1 },
248
+ calls: [
249
+ {
250
+ id: 0,
251
+ opts: { prompt: "legacy work" },
252
+ status: "running",
253
+ attempts: 1,
254
+ traceNode: {
255
+ stepIndex: 0,
256
+ agent: "coder",
257
+ task: "legacy work",
258
+ model: "test-model",
259
+ status: "running",
260
+ live: { turns: [] },
261
+ },
262
+ },
263
+ ],
264
+ trace: [
265
+ {
266
+ stepIndex: 0,
267
+ agent: "coder",
268
+ task: "legacy work",
269
+ model: "test-model",
270
+ status: "running",
271
+ live: { turns: [] },
272
+ },
273
+ ],
274
+ errorLogs: [],
275
+ },
276
+ meta: { startedAt: "2026-08-29T00:00:00.000Z" },
277
+ });
278
+
279
+ it("⛔5 无 v 存量行(core 旧格式,含未 strip 的 live)宽容读取不丢数据", async () => {
280
+ writeStateFile("wf-legacy-1.jsonl", LEGACY_LINE + "\n");
281
+
282
+ const loaded = await store.loadAll();
283
+ expect(loaded).toHaveLength(1);
284
+ const back = loaded[0];
285
+ expect(back.runId).toBe("wf-legacy-1");
286
+ expect(back.state.status).toBe("running");
287
+ expect(back.state.budget.usedTokens).toBe(7);
288
+ expect(back.spec.args).toEqual({ topic: "legacy" });
289
+ // 宽容读不是降级路径:不产生 warn(版本不匹配才 warn)
290
+ expect(warnMessages().some((m) => m.includes("wf-legacy-1"))).toBe(false);
291
+ });
292
+
293
+ it("⛔5 写入补 v 字段(快照行携带 wf-run-v2)+ 宽容读行写回后自然迁移", async () => {
294
+ const run = makeRun("wf-vwrite-1");
295
+ await store.save(run);
296
+
297
+ const raw = readFileSync(store.stateFilePath("wf-vwrite-1"), "utf8").trim();
298
+ expect(JSON.parse(raw).v).toBe("wf-run-v2");
299
+
300
+ // 存量行宽容读 → 再 save:落盘行升级为带 v 当前版本(不做自动迁移的
301
+ // 渐进收敛——D4 裁决②「写入时补 v」)
302
+ writeStateFile("wf-legacy-2.jsonl", LEGACY_LINE + "\n");
303
+ const legacy = (await store.loadAll()).find((r) => r.runId === "wf-legacy-1")!;
304
+ await store.save(legacy);
305
+ const migrated = readFileSync(store.stateFilePath("wf-legacy-1"), "utf8").trim();
306
+ expect(JSON.parse(migrated).v).toBe("wf-run-v2");
307
+ expect(migrated.includes('"live"')).toBe(false);
308
+ });
309
+
310
+ it("⛔5 未知更高版本行跳过 + warn(消息含实际版本值,可定位)", async () => {
311
+ const future = JSON.parse(LEGACY_LINE);
312
+ future.runId = "wf-future-1";
313
+ future.v = "wf-run-v3";
314
+ const current = JSON.parse(LEGACY_LINE);
315
+ current.runId = "wf-current-1";
316
+ current.v = "wf-run-v2";
317
+ // 从尾向头扫描序:尾损坏行(继续向前)→ 高版本行(warn 跳过,继续向前)
318
+ // → 当前版本行(恢复)——单行版本不匹配不拖垮同文件其余行,warn 可见
319
+ writeStateFile(
320
+ "wf-vermix.jsonl",
321
+ JSON.stringify(current) + "\n" + JSON.stringify(future) + "\n" + '{"trunc',
322
+ );
323
+
324
+ const loaded = await store.loadAll();
325
+ expect(loaded.map((r) => r.runId)).toEqual(["wf-current-1"]);
326
+ expect(warnMessages().some((m) => m.includes("unsupported version") && m.includes("wf-run-v3"))).toBe(true);
327
+ });
328
+
329
+ it("⛔5 live 字段 strip 后落盘(落盘行无 live 键,内存 run 不受影响)", async () => {
330
+ const run = makeRun("wf-livestrip-1");
331
+ const node = {
332
+ stepIndex: 0,
333
+ agent: "coder",
334
+ task: "do work",
335
+ model: "test-model",
336
+ status: "running" as const,
337
+ };
338
+ run.state.trace.append(node);
339
+ const AgentCallMod = await import("../models/agent-call.ts");
340
+ const call = new AgentCallMod.AgentCall(0, { prompt: "do work" }, node);
341
+ run.state.calls.set(0, call);
342
+ node.live = { id: "run-0", turns: [] } as unknown as import("../../execution/types.ts").ExecutionRecord;
343
+
344
+ await store.save(run);
345
+
346
+ const raw = readFileSync(store.stateFilePath("wf-livestrip-1"), "utf8");
347
+ expect(raw.includes('"live"')).toBe(false);
348
+ // strip 产出快照副本:内存中 run 的 live 保留(save 后 run 可继续跑)
349
+ expect(node.live).toBeDefined();
350
+ });
351
+
352
+ it("pi 现网形态行(带 v)经 store 端到端恢复", async () => {
353
+ const piForm = JSON.parse(LEGACY_LINE);
354
+ piForm.runId = "wf-pi-e2e-1";
355
+ piForm.v = "wf-run-v2";
356
+ delete piForm.state.calls[0].traceNode.live;
357
+ delete piForm.state.trace[0].live;
358
+ writeStateFile("wf-pi-e2e-1.jsonl", JSON.stringify(piForm) + "\n");
359
+
360
+ const back = (await store.loadAll())[0];
361
+ expect(back.runId).toBe("wf-pi-e2e-1");
362
+ expect(back.state.calls.size).toBe(1);
363
+ });
364
+ });
365
+
366
+ describe("FileRunStore — stateFilePath / 端口语义", () => {
367
+ it("路径形状 = <dataRoot>/workflow-state/<runId>.jsonl(纯计算不建目录)", () => {
368
+ expect(store.stateFilePath("wf-x-1")).toBe(
369
+ join(dataRoot, "workflow-state", "wf-x-1.jsonl"),
370
+ );
371
+ });
372
+
373
+ it("宿主覆盖 configureCore 后路径现取新 dataRoot(不缓存路径)", () => {
374
+ const altRoot = mkdtempSync(join(tmpdir(), "file-run-store-alt-"));
375
+ try {
376
+ configureCore({ dataRoot: () => altRoot, log: () => {} });
377
+ expect(store.stateFilePath("wf-x-2")).toBe(
378
+ join(altRoot, "workflow-state", "wf-x-2.jsonl"),
379
+ );
380
+ } finally {
381
+ rmSync(altRoot, { recursive: true, force: true });
382
+ }
383
+ });
384
+
385
+ it("未 configureCore 即消费 dataRoot → core_host_not_configured(§3.4 fail-loud)", () => {
386
+ resetCoreForTests();
387
+ expect(() => store.stateFilePath("wf-x-3")).toThrowError("core_host_not_configured");
388
+ });
389
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * 排空微任务队列:固定 ticks 次 `await Promise.resolve()`,把 fire-and-forget
3
+ * promise 链推进到稳定态。递归而非 for 循环——固定 tick 排空不是逐项串行等待,
4
+ * no-await-in-loop 豁免形态统一收敛到本函数(项目纪律禁行内 disable)。
5
+ *
6
+ * 不用 vi.waitFor 的场景:反向断言(「X 未发生」)下 waitFor 会一满足即返回,
7
+ * 必须先跑满固定 tick 再静态断言。
8
+ */
9
+ export async function flushMicrotasks(ticks = 10): Promise<void> {
10
+ if (ticks <= 0) return;
11
+ await Promise.resolve();
12
+ await flushMicrotasks(ticks - 1);
13
+ }
@@ -598,3 +598,37 @@ describe("executeNestedWorkflow", () => {
598
598
  });
599
599
  });
600
600
  });
601
+
602
+ // ── runAndWait model 透传(host-surface:zsw CLI --model 的同步面入口)──
603
+ // 契约:显式 model → spec.model(Option B:workerData → $MODEL → agent() fallback);
604
+ // 未传 → undefined(不覆盖脚本内 agent() 显式参数 / agent .md / 引擎默认的解析序)。
605
+ describe("runAndWait model 透传", () => {
606
+ it("显式 model → spec.model 透传", async () => {
607
+ const childRun = makeDoneChildRun({ reason: "completed", scriptResult: "ok" });
608
+ const deps = makeDeps({ script: makeScript(), childRun });
609
+ setupRunWorkflow(childRun);
610
+
611
+ const result = await runAndWait(
612
+ "child-wf",
613
+ {},
614
+ deps,
615
+ undefined,
616
+ undefined,
617
+ "zai-coding-cn/glm-5.3-flash",
618
+ );
619
+ expect(result.reason).toBe("completed");
620
+ expect(vi.mocked(runWorkflow)).toHaveBeenCalledTimes(1);
621
+ const spec = vi.mocked(runWorkflow).mock.calls[0][0] as RunSpec;
622
+ expect(spec.model).toBe("zai-coding-cn/glm-5.3-flash");
623
+ });
624
+
625
+ it("未传 model → spec.model 保持 undefined(不覆盖既有解析序)", async () => {
626
+ const childRun = makeDoneChildRun({ reason: "completed", scriptResult: "ok" });
627
+ const deps = makeDeps({ script: makeScript(), childRun });
628
+ setupRunWorkflow(childRun);
629
+
630
+ await runAndWait("child-wf", {}, deps);
631
+ const spec = vi.mocked(runWorkflow).mock.calls[0][0] as RunSpec;
632
+ expect(spec.model).toBeUndefined();
633
+ });
634
+ });