@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,267 @@
1
+ /**
2
+ * atomic-write 原语测试(sink 设计 U6a / B6,验收 §4 S6 的确定性子集)。
3
+ *
4
+ * 断言面:
5
+ * - 正常写(sync/async、string/Uint8Array、encoding、父目录自动创建)
6
+ * - 半截目标文件恢复语义:torn 目标被整体替换,读者只见旧版或完整新版
7
+ * - 失败路径残留 tmp 清理语义:原错误上抛 + 自身 tmp 清除
8
+ * - 陈旧 tmp 扫描/清理:约定形态识别、maxAgeMs 在途保护窗、幂等
9
+ * - 并发写同目标不串写:rename 原子性下并发读永不见混合/半截内容
10
+ *
11
+ * 观察口径:全部经真实文件系统(临时目录),无 fs mock(对齐 TEST-STRATEGY 红线)。
12
+ */
13
+ import {
14
+ existsSync,
15
+ mkdirSync,
16
+ mkdtempSync,
17
+ readFileSync,
18
+ readdirSync,
19
+ rmSync,
20
+ utimesSync,
21
+ writeFileSync,
22
+ } from "node:fs";
23
+ import { tmpdir } from "node:os";
24
+ import { join } from "node:path";
25
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
26
+
27
+ import {
28
+ atomicTmpPathFor,
29
+ cleanupStaleTmpFiles,
30
+ listStaleTmpFiles,
31
+ parseAtomicTmpPath,
32
+ writeAtomicFile,
33
+ writeAtomicFileSync,
34
+ } from "../atomic-write.ts";
35
+
36
+ let dir: string;
37
+
38
+ beforeEach(() => {
39
+ dir = mkdtempSync(join(tmpdir(), "atomic-write-test-"));
40
+ });
41
+
42
+ afterEach(() => {
43
+ rmSync(dir, { recursive: true, force: true });
44
+ });
45
+
46
+ /** 目录内约定形态 tmp 残留计数(断言「无残留」用)。 */
47
+ function tmpResidue(targetDir: string): string[] {
48
+ return readdirSync(targetDir).filter((n) => n.includes(".tmp."));
49
+ }
50
+
51
+ describe("writeAtomicFileSync 正常写", () => {
52
+ it("字符串内容落盘,父目录缺失时自动创建(ensureDir 缺省 true)", () => {
53
+ const target = join(dir, "nested/deep/target.json");
54
+ writeAtomicFileSync(target, '{"a":1}');
55
+ expect(readFileSync(target, "utf8")).toBe('{"a":1}');
56
+ });
57
+
58
+ it("Uint8Array 内容逐字节落盘(encoding 不适用)", () => {
59
+ const target = join(dir, "bin/blob");
60
+ const bytes = Uint8Array.from([0x00, 0x01, 0xff, 0xfe, 0xe6, 0xb1, 0x89]);
61
+ writeAtomicFileSync(target, bytes);
62
+ expect(readFileSync(target)).toEqual(Buffer.from(bytes));
63
+ });
64
+
65
+ it("encoding 选项生效(latin1 单字节)", () => {
66
+ const target = join(dir, "enc.txt");
67
+ writeAtomicFileSync(target, "é", { encoding: "latin1" });
68
+ expect(readFileSync(target)).toEqual(Buffer.from([0xe9]));
69
+ });
70
+
71
+ it("ensureDir:false 且父目录缺失 → ENOENT 原样上抛", () => {
72
+ const target = join(dir, "missing/target.txt");
73
+ expect(() => writeAtomicFileSync(target, "x", { ensureDir: false })).toThrow(/ENOENT/);
74
+ });
75
+
76
+ it("写入后目录无 tmp 残留", () => {
77
+ const target = join(dir, "t.txt");
78
+ writeAtomicFileSync(target, "ok");
79
+ expect(tmpResidue(dir)).toEqual([]);
80
+ });
81
+ });
82
+
83
+ describe("writeAtomicFile 正常写(fsync 生产模式)", () => {
84
+ it("字符串内容落盘 + 父目录自动创建", async () => {
85
+ const target = join(dir, "a/b/c.json");
86
+ await writeAtomicFile(target, '{"n":1}');
87
+ expect(readFileSync(target, "utf8")).toBe('{"n":1}');
88
+ expect(tmpResidue(dir)).toEqual([]);
89
+ });
90
+
91
+ it("fsyncDir:false 可关目录 fsync(弱耐久场景)", async () => {
92
+ const target = join(dir, "fast.json");
93
+ await writeAtomicFile(target, "fast", { fsyncDir: false });
94
+ expect(readFileSync(target, "utf8")).toBe("fast");
95
+ });
96
+ });
97
+
98
+ describe("半截目标文件恢复语义(S6 确定性场景)", () => {
99
+ it("torn 目标被原子写整体替换:内容与长度精确等于新版本", () => {
100
+ const target = join(dir, "state.json");
101
+ writeFileSync(target, '{"half":tru', "utf8"); // 模拟非原子写半截产物
102
+ const full = JSON.stringify({ version: 2, entries: { a: 1, b: 2 } }, null, 2);
103
+ writeAtomicFileSync(target, full);
104
+ const after = readFileSync(target, "utf8");
105
+ expect(after).toBe(full);
106
+ expect(after.length).toBe(full.length);
107
+ expect(tmpResidue(dir)).toEqual([]);
108
+ });
109
+
110
+ it("async 写同样整体替换 torn 目标", async () => {
111
+ const target = join(dir, "state2.json");
112
+ writeFileSync(target, "{trun", "utf8");
113
+ await writeAtomicFile(target, '{"complete":true}');
114
+ expect(readFileSync(target, "utf8")).toBe('{"complete":true}');
115
+ });
116
+
117
+ it("崩溃残留 tmp 不影响新写入正确性,残留可被统一扫描+清理(恢复闭环)", () => {
118
+ const target = join(dir, "wf-state.json");
119
+ writeFileSync(target, "{old", "utf8");
120
+ // 模拟上次进程死在 tmp 阶段的残留(约定形态)
121
+ const staleTmp = atomicTmpPathFor(target);
122
+ writeFileSync(staleTmp, '{"pid-was":9,"entries":', "utf8");
123
+
124
+ // 新写入不受残留干扰:目标精确等于新内容
125
+ writeAtomicFileSync(target, '{"v":3}');
126
+ expect(readFileSync(target, "utf8")).toBe('{"v":3}');
127
+
128
+ // 残留仍可按约定反查目标路径 → 统一入口清理
129
+ const refs = listStaleTmpFiles(dir);
130
+ expect(refs).toHaveLength(1);
131
+ expect(refs[0].targetPath).toBe(target);
132
+ expect(refs[0].tmpPath).toBe(staleTmp);
133
+ const result = cleanupStaleTmpFiles(dir);
134
+ expect(result.removed).toEqual([staleTmp]);
135
+ expect(existsSync(staleTmp)).toBe(false);
136
+ expect(existsSync(target)).toBe(true);
137
+ expect(readFileSync(target, "utf8")).toBe('{"v":3}');
138
+ });
139
+ });
140
+
141
+ describe("写入失败残留 tmp 清理语义", () => {
142
+ it("sync:rename 失败(目标是目录)→ 原错误上抛 + 自身 tmp 清除", () => {
143
+ const target = join(dir, "occupied");
144
+ mkdirSync(target);
145
+ writeFileSync(join(target, "child.txt"), "block", "utf8"); // 非空目录确保 rename 失败
146
+ expect(() => writeAtomicFileSync(target, "x", { ensureDir: false })).toThrow();
147
+ expect(tmpResidue(dir)).toEqual([]);
148
+ });
149
+
150
+ it("async:rename 失败(目标是目录)→ 原错误 reject + 自身 tmp 清除", async () => {
151
+ const target = join(dir, "occupied-async");
152
+ mkdirSync(target);
153
+ writeFileSync(join(target, "child.txt"), "block", "utf8");
154
+ await expect(writeAtomicFile(target, "x", { ensureDir: false })).rejects.toThrow();
155
+ expect(tmpResidue(dir)).toEqual([]);
156
+ });
157
+ });
158
+
159
+ describe("统一 tmp 命名与扫描(B6 约定)", () => {
160
+ it("atomicTmpPathFor 命中约定标记且同进程内唯一", () => {
161
+ const target = join(dir, "x.json");
162
+ const a = atomicTmpPathFor(target);
163
+ const b = atomicTmpPathFor(target);
164
+ expect(a).toMatch(/\.tmp\.\d+\.[0-9a-z-]+$/);
165
+ expect(a.startsWith(`${target}.tmp.${process.pid}.`)).toBe(true);
166
+ expect(a).not.toBe(b);
167
+ });
168
+
169
+ it("parseAtomicTmpPath 往返解析;目标名含 .tmp. 时前缀正确还原", () => {
170
+ const ref = parseAtomicTmpPath("/data/x.json.tmp.4242.7-abc");
171
+ expect(ref).toEqual({
172
+ tmpPath: "/data/x.json.tmp.4242.7-abc",
173
+ targetPath: "/data/x.json",
174
+ pid: 4242,
175
+ });
176
+ // 目标名自身含 .tmp.:贪婪前缀还原到最近的 .tmp.<pid>.<uniq> 结构
177
+ const tricky = parseAtomicTmpPath("/data/foo.tmp.json.tmp.42.7-abc");
178
+ expect(tricky?.targetPath).toBe("/data/foo.tmp.json");
179
+ // 不合约定形态(uniq 段缺 pid 后的第二段 / pid 非数字)一律不认
180
+ expect(parseAtomicTmpPath("/data/foo.tmp.json.tmp.1-9")).toBeNull();
181
+ expect(parseAtomicTmpPath("/data/plain.json")).toBeNull();
182
+ expect(parseAtomicTmpPath("/data/x.tmp.notpid.txt")).toBeNull();
183
+ });
184
+
185
+ it("listStaleTmpFiles 只认约定形态;目录不存在返回空(宽容扫描)", () => {
186
+ const target = join(dir, "m.json");
187
+ writeFileSync(atomicTmpPathFor(target), "1", "utf8");
188
+ writeFileSync(join(dir, "unrelated.txt"), "keep", "utf8");
189
+ writeFileSync(join(dir, "m.json.tmp.notpid"), "old-format", "utf8");
190
+ const refs = listStaleTmpFiles(dir);
191
+ expect(refs).toHaveLength(1);
192
+ expect(refs[0].targetPath).toBe(target);
193
+ expect(refs[0].pid).toBe(process.pid);
194
+ expect(listStaleTmpFiles(join(dir, "no-such-dir"))).toEqual([]);
195
+ });
196
+
197
+ it("cleanupStaleTmpFiles:maxAgeMs 窗口内保留(在途保护)、窗口外清除、幂等", () => {
198
+ const now = Date.now();
199
+ const oldTmp = atomicTmpPathFor(join(dir, "old.json"));
200
+ const freshTmp = atomicTmpPathFor(join(dir, "fresh.json"));
201
+ writeFileSync(oldTmp, "old", "utf8");
202
+ writeFileSync(freshTmp, "fresh", "utf8");
203
+ // mtime 全部确定性注入(文件系统真实 mtime 不可控)
204
+ const oldPast = new Date(now - 60_000);
205
+ const freshPast = new Date(now - 1_000);
206
+ utimesSync(oldTmp, oldPast, oldPast);
207
+ utimesSync(freshTmp, freshPast, freshPast);
208
+
209
+ const first = cleanupStaleTmpFiles(dir, { maxAgeMs: 10_000, now });
210
+ expect(first.removed).toEqual([oldTmp]);
211
+ expect(first.kept).toEqual([freshTmp]);
212
+ expect(existsSync(oldTmp)).toBe(false);
213
+ expect(existsSync(freshTmp)).toBe(true);
214
+
215
+ // 第二轮:fresh 已过窗 → 清除;无 maxAgeMs = 全清
216
+ const agedPast = new Date(now - 60_000);
217
+ utimesSync(freshTmp, agedPast, agedPast);
218
+ const second = cleanupStaleTmpFiles(dir, { maxAgeMs: 10_000, now });
219
+ expect(second.removed).toEqual([freshTmp]);
220
+ const third = cleanupStaleTmpFiles(dir);
221
+ expect(third).toEqual({ removed: [], kept: [], failed: [] });
222
+ });
223
+ });
224
+
225
+ describe("并发写同目标不串写", () => {
226
+ it("24 路并发 async 写 + 并发读:每次读要么旧版完整要么新版完整,绝不混合", async () => {
227
+ const target = join(dir, "contended.json");
228
+ const baseline = '{"i":-1,"blob":""}';
229
+ writeFileSync(target, baseline, "utf8"); // 旧版基线
230
+ const payloads = Array.from({ length: 24 }, (_, i) =>
231
+ JSON.stringify({ i, blob: `x${i}`.repeat(700), tag: `payload-${i}` }),
232
+ );
233
+
234
+ // 读循环与写并发:每次让出 macrotask 让 async 写推进;快照只收集不判断
235
+ const snapshots: string[] = [];
236
+ const reader = (async () => {
237
+ for (let k = 0; k < 120; k++) {
238
+ try {
239
+ snapshots.push(readFileSync(target, "utf8"));
240
+ } catch {
241
+ // ENOENT 理论不可达(基线先于并发段存在);快照缺失由计数断言兜底
242
+ }
243
+ await new Promise((resolve) => setTimeout(resolve, 0));
244
+ }
245
+ })();
246
+
247
+ await Promise.all([...payloads.map((p) => writeAtomicFile(target, p)), reader]);
248
+ expect(snapshots.length).toBeGreaterThan(0);
249
+
250
+ // 每个快照字节级恰等于某一份完整版本(基线或 24 份 payload 之一)——串写/半截必产生集合外内容
251
+ const validFulls = new Set([baseline, ...payloads]);
252
+ for (const snap of snapshots) {
253
+ expect(validFulls.has(snap)).toBe(true);
254
+ }
255
+
256
+ const final = readFileSync(target, "utf8");
257
+ expect(payloads).toContain(final); // 终态恰为其中一份完整 payload
258
+ expect(tmpResidue(dir)).toEqual([]); // 全部 tmp 已被各自 rename 消费
259
+ });
260
+
261
+ it("sync 原语串发写:终态为最后一次写入的完整内容", () => {
262
+ const target = join(dir, "serial.json");
263
+ const payloads = Array.from({ length: 8 }, (_, i) => JSON.stringify({ seq: i }));
264
+ for (const p of payloads) writeAtomicFileSync(target, p);
265
+ expect(readFileSync(target, "utf8")).toBe(payloads[payloads.length - 1]);
266
+ });
267
+ });
@@ -0,0 +1,236 @@
1
+ /**
2
+ * boundedPrettySerialize 测试(自 pi-sw helpers-bounded-serialize.test.ts 平移
3
+ * + 字节快照,sink 设计 U6a / B7 验收②:与 pi-sw 现实现输出字节一致)。
4
+ *
5
+ * 平移说明(对照方式选型,二选一之「硬编码快照样例」):pi-sw 侧实现为
6
+ * helpers.ts 私有函数(唯一公共出口 notifyDone 固定 budget=8000,且其 import
7
+ * 链依赖 pi SDK / extension-protocol,不宜从 core 测试直连)。字节一致的锚定
8
+ * 由三层构成:
9
+ * 1. **等价参照平移**——pi-sw 既有测试的全部用例(含 legacySerialize 参照实现)
10
+ * 平移至此,在 budget=8000 与小 budget 矩阵上断言 core 实现 ≡ 参照;
11
+ * 2. **硬编码字节快照**——期望串以 JSON.stringify 规范行为基准手工固化为字面量
12
+ * (不回读被测实现),锚死具体字节形态;
13
+ * 3. 实施期实跑 pi-sw 侧 helpers-bounded-serialize.test.ts(其断言即
14
+ * pi-sw 实现 ≡ 同一参照),三方经公共参照闭环为字节一致。
15
+ */
16
+ import { describe, expect, it } from "vitest";
17
+
18
+ import { boundedPrettySerialize } from "../bounded-serialize.ts";
19
+
20
+ /** pi-sw 旧实现参照(全量序列化 + 截断;同 helpers-bounded-serialize.test.ts)。 */
21
+ function legacySerialize(x: unknown, budget = 8000): string {
22
+ let serialized: string;
23
+ try {
24
+ serialized = JSON.stringify(x, null, 2);
25
+ } catch {
26
+ serialized = String(x);
27
+ }
28
+ return serialized.length > budget
29
+ ? serialized.slice(0, budget) + "\n... (truncated)"
30
+ : serialized;
31
+ }
32
+
33
+ // ── 深嵌套大对象 fixture(平移)───────────────────────────────
34
+
35
+ function deepNested(): unknown {
36
+ const root: Record<string, unknown> = {};
37
+ let cur = root;
38
+ for (let i = 0; i < 60; i++) {
39
+ cur.header = `level-${i}-数据`;
40
+ cur.items = Array.from({ length: 12 }, (__, j) => ({
41
+ id: j,
42
+ name: `item-${i}-${j}`,
43
+ tags: ["alpha", "βeta", "γλυφ"],
44
+ nested: { deep: { deeper: { value: i * 1000 + j } } },
45
+ }));
46
+ cur.next = {};
47
+ cur = cur.next as Record<string, unknown>;
48
+ }
49
+ cur.end = "leaf";
50
+ return root;
51
+ }
52
+
53
+ describe("bounded 序列化等价锚定(平移自 pi-sw,budget=8000)", () => {
54
+ it("深嵌套大对象(>8000):与全量 stringify 后 slice+标记 逐字节一致", () => {
55
+ const x = deepNested();
56
+ expect(JSON.stringify(x, null, 2).length).toBeGreaterThan(10_000);
57
+ expect(boundedPrettySerialize(x, 8000)).toBe(legacySerialize(x));
58
+ });
59
+
60
+ it("循环引用 → String(value) 整串回退(不抛出)", () => {
61
+ const x: Record<string, unknown> = { a: 1 };
62
+ x.self = x;
63
+ expect(boundedPrettySerialize(x, 8000)).toBe(legacySerialize(x));
64
+ expect(boundedPrettySerialize(x, 8000)).toBe(String(x));
65
+ });
66
+
67
+ it("含 BigInt → String(value) 整串回退且不抛出(对齐旧整体 catch)", () => {
68
+ const x = { count: 10n, label: "big" };
69
+ expect(boundedPrettySerialize(x, 8000)).toBe(String(x));
70
+ expect(boundedPrettySerialize(x, 8000)).toBe(legacySerialize(x));
71
+ });
72
+
73
+ it("含 Date(toJSON)→ 与原生逐字节一致(带引号序列化串)", () => {
74
+ const x = { at: new Date(0), note: "ts" };
75
+ expect(boundedPrettySerialize(x, 8000)).toBe(legacySerialize(x));
76
+ expect(boundedPrettySerialize(x, 8000)).toContain('"1970-01-01T00:00:00.000Z"');
77
+ });
78
+
79
+ it("undefined/function 属性省略与原生一致;数组内 undefined/function → null", () => {
80
+ const x = {
81
+ keep: 1,
82
+ dropU: undefined,
83
+ dropF: () => 1,
84
+ arr: [undefined, 2, () => 3, null],
85
+ };
86
+ expect(boundedPrettySerialize(x, 8000)).toBe(legacySerialize(x));
87
+ expect(boundedPrettySerialize(x, 8000)).toBe(JSON.stringify(x, null, 2));
88
+ });
89
+
90
+ it("≤8000 全形态与原生逐字节一致(原语/数组/嵌套/NaN/Infinity/Unicode)", () => {
91
+ const cases: unknown[] = [
92
+ "plain string",
93
+ 42,
94
+ true,
95
+ NaN,
96
+ Infinity,
97
+ [1, [2, [3, "x"]]],
98
+ { a: { b: { c: [1, "汉", { d: null }] } } },
99
+ { emptyObj: {}, emptyArr: [] },
100
+ { unicode: "line sep pic ⌘" },
101
+ ];
102
+ for (const x of cases) {
103
+ expect(boundedPrettySerialize(x, 8000)).toBe(legacySerialize(x));
104
+ }
105
+ });
106
+ });
107
+
108
+ describe("截断边界(平移自 pi-sw,恰好 8000 / 8001 / 转义序列中间)", () => {
109
+ /** 构造 pretty 全文恰好 len 字符的对象:{\n "a": "<S>"\n} = 13 + S.length。 */
110
+ function sizedStringObject(totalLen: number): { a: string } {
111
+ return { a: "x".repeat(totalLen - 13) };
112
+ }
113
+
114
+ it("恰好 8000:不加标记,输出 === 原生全文", () => {
115
+ const x = sizedStringObject(8000);
116
+ expect(JSON.stringify(x, null, 2).length).toBe(8000);
117
+ const out = boundedPrettySerialize(x, 8000);
118
+ expect(out).toBe(JSON.stringify(x, null, 2));
119
+ expect(out).not.toContain("(truncated)");
120
+ expect(out.length).toBe(8000);
121
+ });
122
+
123
+ it("8001:截到 8000 + 标记", () => {
124
+ const x = sizedStringObject(8001);
125
+ expect(JSON.stringify(x, null, 2).length).toBe(8001);
126
+ const out = boundedPrettySerialize(x, 8000);
127
+ expect(out).toBe(JSON.stringify(x, null, 2).slice(0, 8000) + "\n... (truncated)");
128
+ expect(out.length).toBe(8000 + "\n... (truncated)".length);
129
+ });
130
+
131
+ it("截断点落在转义序列中间(\\u0001 被切半):与 slice 逐字节一致", () => {
132
+ const esc = "\u0001";
133
+ let x: unknown = null;
134
+ let full = "";
135
+ for (let pad = 0; pad < 40; pad++) {
136
+ const candidate = { pad: "p".repeat(pad), payload: Array.from({ length: 1200 }, () => esc + "汉") };
137
+ const s = JSON.stringify(candidate, null, 2);
138
+ if (s.length > 8000 && s.charCodeAt(7999) === 92) {
139
+ x = candidate;
140
+ full = s;
141
+ break;
142
+ }
143
+ }
144
+ expect(x).not.toBeNull();
145
+ expect(full.charCodeAt(7999)).toBe(92);
146
+ const out = boundedPrettySerialize(x, 8000);
147
+ expect(out).toBe(full.slice(0, 8000) + "\n... (truncated)");
148
+ expect(out.slice(0, 8000).endsWith("\\")).toBe(true);
149
+ });
150
+ });
151
+
152
+ describe("小 budget 矩阵(截断路径全覆盖:特殊值形态 × 边界刻度)", () => {
153
+ const budgets = [0, 1, 5, 13, 21, 50];
154
+ // 仅含确定性前缀形态。(b) 整串回退路径(BigInt/循环引用)不入矩阵:其回退只在
155
+ // 抛出/检环节点被实际生成时触发,截断先于该节点完成时输出结构前缀——与 pi-sw
156
+ // 逐字平移的既有行为一致,临界行为单独钉在下方用例。
157
+ const cases: unknown[] = [
158
+ { a: 1, b: [1, "汉", { d: null }] },
159
+ { at: new Date(0) },
160
+ { keep: 1, dropU: undefined, arr: [undefined, 2, null] },
161
+ [NaN, Infinity],
162
+ {},
163
+ [],
164
+ ];
165
+
166
+ it("确定性前缀形态在每个 budget 上与参照逐字节一致", () => {
167
+ for (const x of cases) {
168
+ for (const budget of budgets) {
169
+ expect(boundedPrettySerialize(x, budget)).toBe(legacySerialize(x, budget));
170
+ }
171
+ }
172
+ });
173
+
174
+ it("(b) 整串回退的触达边界:抛出节点被生成前截断则不回退(逐字平移的既有行为)", () => {
175
+ const big = { count: 10n, label: "big" };
176
+ // budget 13 = "{\n \"count\": " 恰好收完 → 抛出节点被触达 → fallback 截断(≡ 参照)
177
+ expect(boundedPrettySerialize(big, 13)).toBe(legacySerialize(big, 13));
178
+ expect(boundedPrettySerialize(big, 13)).toBe("[object Objec\n... (truncated)");
179
+ // budget 12:截断在抛出节点前完成 → 结构前缀(非 fallback,≠ 参照的 "[..." 回退)
180
+ expect(boundedPrettySerialize(big, 12)).toBe('{\n "count":\n... (truncated)');
181
+ // 循环引用同款:budget 1 时截断先于环检测 → 结构前缀 "{"
182
+ const circular: Record<string, unknown> = { a: 1 };
183
+ circular.self = circular;
184
+ expect(boundedPrettySerialize(circular, 1)).toBe("{\n... (truncated)");
185
+ expect(legacySerialize(circular, 1)).toBe("[\n... (truncated)"); // 参照为 fallback 形态
186
+ });
187
+ });
188
+
189
+ describe("硬编码字节快照(期望串固化为字面量,不回读实现)", () => {
190
+ it("小型对象全文", () => {
191
+ expect(boundedPrettySerialize({ a: 1, b: [1, "汉", { d: null }] }, 8000)).toBe(
192
+ '{\n "a": 1,\n "b": [\n 1,\n "汉",\n {\n "d": null\n }\n ]\n}',
193
+ );
194
+ });
195
+
196
+ it("Date(toJSON)序列化串", () => {
197
+ expect(boundedPrettySerialize({ at: new Date(0) }, 8000)).toBe(
198
+ '{\n "at": "1970-01-01T00:00:00.000Z"\n}',
199
+ );
200
+ });
201
+
202
+ it("undefined/function 省略 + 数组元素 null 化", () => {
203
+ expect(boundedPrettySerialize({ arr: [undefined, 2, () => 3, null] }, 8000)).toBe(
204
+ '{\n "arr": [\n null,\n 2,\n null,\n null\n ]\n}',
205
+ );
206
+ });
207
+
208
+ it("NaN/Infinity → null;空容器 compact 形态", () => {
209
+ expect(boundedPrettySerialize([NaN, Infinity], 8000)).toBe('[\n null,\n null\n]');
210
+ expect(boundedPrettySerialize({}, 8000)).toBe("{}");
211
+ expect(boundedPrettySerialize([], 8000)).toBe("[]");
212
+ });
213
+
214
+ it("循环引用 / BigInt 整串回退字面量", () => {
215
+ const circular: Record<string, unknown> = { a: 1 };
216
+ circular.self = circular;
217
+ expect(boundedPrettySerialize(circular, 8000)).toBe("[object Object]");
218
+ expect(boundedPrettySerialize({ count: 10n }, 8000)).toBe("[object Object]");
219
+ });
220
+
221
+ it("转义序列形态与三档截断的字节字面量(全文 21 字符)", () => {
222
+ // {\n "key": "\u0001"\n} = 21 字符;第 12 字符为转义起始反斜杠
223
+ const full = '{\n "key": "\\u0001"\n}';
224
+ expect(boundedPrettySerialize({ key: "\u0001" }, 8000)).toBe(full);
225
+ // 恰好 21:不加标记
226
+ expect(boundedPrettySerialize({ key: "\u0001" }, 21)).toBe(full);
227
+ // budget 20:切掉收尾 },标记另起一行(两个连续 \n)
228
+ expect(boundedPrettySerialize({ key: "\u0001" }, 20)).toBe(
229
+ '{\n "key": "\\u0001"\n' + "\n... (truncated)",
230
+ );
231
+ // budget 13:切在转义序列中间——末尾裸反斜杠,不补闭合
232
+ expect(boundedPrettySerialize({ key: "\u0001" }, 13)).toBe(
233
+ '{\n "key": "\\' + "\n... (truncated)",
234
+ );
235
+ });
236
+ });