@zhushanwen/pi-subagent-workflow 8.4.0 → 8.6.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 (171) hide show
  1. package/package.json +22 -7
  2. package/relay/relay.mjs +390 -0
  3. package/skills/subagent-ext-config/SKILL.md +80 -0
  4. package/src/execution/__tests__/agent-registry.test.ts +110 -0
  5. package/src/execution/__tests__/bg-notify-render.test.ts +73 -0
  6. package/src/execution/__tests__/chat-engine-routing.test.ts +601 -0
  7. package/src/execution/__tests__/delivery-methods.test.ts +38 -1
  8. package/src/execution/__tests__/execute-options-mapper.test.ts +11 -0
  9. package/src/execution/__tests__/execution-record.test.ts +237 -1
  10. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +171 -0
  11. package/src/execution/__tests__/format-schema-instruction.test.ts +63 -32
  12. package/src/execution/__tests__/helpers/spawn-mock.ts +4 -0
  13. package/src/execution/__tests__/index-session-start.test.ts +86 -7
  14. package/src/execution/__tests__/lifecycle-manager.test.ts +46 -0
  15. package/src/execution/__tests__/list-fields.test.ts +45 -14
  16. package/src/execution/__tests__/model-resolver.test.ts +57 -5
  17. package/src/execution/__tests__/notifier-flush.test.ts +64 -26
  18. package/src/execution/__tests__/notify-ledger.test.ts +826 -0
  19. package/src/execution/__tests__/output-collector.test.ts +299 -2
  20. package/src/execution/__tests__/pi-invocation.test.ts +62 -1
  21. package/src/execution/__tests__/relay-agent.test.ts +448 -0
  22. package/src/execution/__tests__/relay-env.test.ts +42 -0
  23. package/src/execution/__tests__/rpc-mode.test.ts +1 -1
  24. package/src/execution/__tests__/run-spawn-edges.test.ts +44 -1
  25. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +199 -0
  26. package/src/execution/__tests__/session-runner-schema-env.test.ts +39 -0
  27. package/src/execution/__tests__/spawn-args.test.ts +37 -26
  28. package/src/execution/__tests__/start-sync-model-guard.test.ts +150 -0
  29. package/src/execution/__tests__/startup-config-declaration.test.ts +35 -0
  30. package/src/execution/__tests__/stream-sink-retirement.test.ts +261 -0
  31. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +310 -0
  32. package/src/execution/__tests__/subprocess-agent-runner.test.ts +147 -6
  33. package/src/execution/__tests__/timeout-integration.test.ts +220 -2
  34. package/src/execution/__tests__/tool-action.test.ts +92 -1
  35. package/src/execution/agent-registry.ts +16 -0
  36. package/src/execution/argv-mirror.ts +5 -1
  37. package/src/execution/concurrency-pool.ts +1 -1
  38. package/src/execution/config.ts +25 -2
  39. package/src/execution/engine/__tests__/common/data-dir.test.ts +53 -0
  40. package/src/execution/engine/__tests__/common/errors.test.ts +132 -0
  41. package/src/execution/engine/__tests__/common/event-journal.test.ts +177 -0
  42. package/src/execution/engine/__tests__/common/kill-chain.test.ts +192 -0
  43. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +81 -0
  44. package/src/execution/engine/__tests__/common/persona-router.test.ts +123 -0
  45. package/src/execution/engine/__tests__/common/pool-manager.test.ts +154 -0
  46. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +128 -0
  47. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +28 -0
  48. package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +141 -0
  49. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +109 -0
  50. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +101 -0
  51. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +77 -0
  52. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +104 -0
  53. package/src/execution/engine/__tests__/conformance/contract.relay.test.ts +342 -0
  54. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +201 -0
  55. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +76 -0
  56. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +79 -0
  57. package/src/execution/engine/__tests__/engine-discovery.test.ts +87 -0
  58. package/src/execution/engine/__tests__/engines-declaration.test.ts +36 -0
  59. package/src/execution/engine/__tests__/model-prompt.test.ts +85 -0
  60. package/src/execution/engine/__tests__/paths.test.ts +39 -0
  61. package/src/execution/engine/__tests__/registry.test.ts +120 -0
  62. package/src/execution/engine/__tests__/routing.test.ts +231 -0
  63. package/src/execution/engine/common/data-dir.ts +62 -0
  64. package/src/execution/engine/common/errors.ts +183 -0
  65. package/src/execution/engine/common/event-journal.ts +254 -0
  66. package/src/execution/engine/common/journal-replay.ts +62 -0
  67. package/src/execution/engine/common/kill-chain.ts +221 -0
  68. package/src/execution/engine/common/nesting-guard.ts +50 -0
  69. package/src/execution/engine/common/persona-router.ts +108 -0
  70. package/src/execution/engine/common/pool-manager.ts +226 -0
  71. package/src/execution/engine/common/schema-emulation.ts +189 -0
  72. package/src/execution/engine/common/session-view-projection.ts +51 -0
  73. package/src/execution/engine/engine-discovery.ts +65 -0
  74. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +469 -0
  75. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +155 -0
  76. package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +164 -0
  77. package/src/execution/engine/engines/pi/pi-engine.ts +415 -0
  78. package/src/execution/engine/engines/pi/reader.ts +48 -0
  79. package/src/execution/engine/engines/pi/registration.ts +35 -0
  80. package/src/execution/engine/engines/pi/task-spec-mapper.ts +100 -0
  81. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +39 -0
  82. package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +150 -0
  83. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +246 -0
  84. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +228 -0
  85. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +210 -0
  86. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +64 -0
  87. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +127 -0
  88. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +580 -0
  89. package/src/execution/engine/engines/zcode/constants.ts +43 -0
  90. package/src/execution/engine/engines/zcode/golden-sample.ts +39 -0
  91. package/src/execution/engine/engines/zcode/launcher.ts +161 -0
  92. package/src/execution/engine/engines/zcode/parser.ts +436 -0
  93. package/src/execution/engine/engines/zcode/preparer.ts +363 -0
  94. package/src/execution/engine/engines/zcode/reader.ts +381 -0
  95. package/src/execution/engine/engines/zcode/registration.ts +37 -0
  96. package/src/execution/engine/engines/zcode/zcode-engine.ts +658 -0
  97. package/src/execution/engine/host-task-spec.ts +47 -0
  98. package/src/execution/engine/model-prompt.ts +59 -0
  99. package/src/execution/engine/paths.ts +42 -0
  100. package/src/execution/engine/port.ts +153 -0
  101. package/src/execution/engine/registry.ts +123 -0
  102. package/src/execution/engine/routing.ts +218 -0
  103. package/src/execution/engine/types.ts +309 -0
  104. package/src/execution/execute-options-mapper.ts +13 -8
  105. package/src/execution/execution-record.ts +66 -1
  106. package/src/execution/lifecycle-manager.ts +23 -1
  107. package/src/execution/model-config-service.ts +16 -1
  108. package/src/execution/model-resolver.ts +37 -59
  109. package/src/execution/notifier.ts +105 -35
  110. package/src/execution/notify-ledger.ts +580 -0
  111. package/src/execution/output-collector.ts +143 -3
  112. package/src/execution/pi-invocation.ts +32 -2
  113. package/src/execution/record-entry.ts +14 -0
  114. package/src/execution/record-store.ts +34 -0
  115. package/src/execution/relay-env.ts +37 -0
  116. package/src/execution/session-runner.ts +328 -71
  117. package/src/execution/stream-sink.ts +26 -0
  118. package/src/execution/subagent-service.ts +273 -13
  119. package/src/execution/subprocess-agent-runner.ts +210 -14
  120. package/src/execution/types.ts +124 -5
  121. package/src/execution/ui-request-queue.ts +14 -4
  122. package/src/index.ts +99 -1
  123. package/src/interface/__tests__/subagent-tool-path-guard.test.ts +157 -0
  124. package/src/interface/__tests__/subagent-tool-prompt.test.ts +12 -0
  125. package/src/interface/bg-notify-render.ts +33 -12
  126. package/src/interface/helpers.ts +2 -2
  127. package/src/interface/subagent-actions.ts +29 -9
  128. package/src/interface/subagent-tool-schema.ts +156 -0
  129. package/src/interface/subagent-tool.ts +56 -119
  130. package/src/interface/subagents.ts +2 -2
  131. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +19 -3
  132. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +0 -6
  133. package/src/orchestration/__tests__/agent-call-stream.test.ts +0 -5
  134. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +89 -4
  135. package/src/orchestration/__tests__/execute-agent-call.test.ts +137 -0
  136. package/src/orchestration/__tests__/jsonl-run-store-corrupt-entry.test.ts +150 -0
  137. package/src/orchestration/__tests__/jsonl-run-store-retention.test.ts +202 -0
  138. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +326 -3
  139. package/src/orchestration/__tests__/lifecycle.test.ts +41 -7
  140. package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +95 -0
  141. package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +57 -3
  142. package/src/orchestration/__tests__/skill-discovery.test.ts +44 -0
  143. package/src/orchestration/__tests__/worker-exit-without-result.test.ts +368 -0
  144. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +43 -0
  145. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +22 -3
  146. package/src/orchestration/agent-opts-resolver.ts +104 -23
  147. package/src/orchestration/error-recovery.ts +189 -33
  148. package/src/orchestration/execute-agent-call.ts +39 -0
  149. package/src/orchestration/jsonl-run-store.ts +121 -7
  150. package/src/orchestration/launcher.ts +60 -15
  151. package/src/orchestration/lifecycle.ts +10 -7
  152. package/src/orchestration/models/__tests__/budget.test.ts +1 -61
  153. package/src/orchestration/models/budget.ts +5 -35
  154. package/src/orchestration/models/run-runtime.ts +24 -9
  155. package/src/orchestration/models/types.ts +16 -0
  156. package/src/orchestration/script-lint.ts +1 -1
  157. package/src/orchestration/skill-discovery.ts +31 -8
  158. package/src/orchestration/worker-script-builder.ts +19 -3
  159. package/src/shared/__tests__/model-ref.test.ts +306 -0
  160. package/src/shared/__tests__/schema-jsonify.test.ts +1 -1
  161. package/src/shared/__tests__/timer-delay.test.ts +61 -0
  162. package/src/shared/meta-parser.ts +5 -1
  163. package/src/shared/model-ref.ts +286 -0
  164. package/src/shared/resource-meta.ts +5 -0
  165. package/src/shared/schema-env.ts +44 -0
  166. package/src/shared/schema-jsonify.ts +6 -4
  167. package/src/shared/timer-delay.ts +54 -0
  168. package/workflows/review-fix-loop-utils.cjs +9 -7
  169. package/workflows/review-fix-loop.js +20 -12
  170. package/src/orchestration/__tests__/concurrency-gate.test.ts +0 -125
  171. package/src/orchestration/concurrency-gate.ts +0 -69
@@ -0,0 +1,100 @@
1
+ // src/execution/engine/engines/pi/task-spec-mapper.ts
2
+ //
3
+ // AgentTaskSpec ↔ ExecuteOptions 映射(P1 回填)。pi 专有语义的隔离点(设计 §3.3.5
4
+ // 删字段去向 + P1 行「ExecuteOptions→AgentTaskSpec 映射层」):
5
+ // - effort ↔ thinkingLevel:pi 的 7 档枚举是引擎私有语义,中立层只透传字符串——
6
+ // pi 侧恒等映射(不识别的档位与现状一致:交 resolveModel 层处理);
7
+ // - persona ↔ skillPath + appendSystemPrompt:skillPath 并入 persona 语义(D2),
8
+ // pi 侧原样还原(pi 的 persona 注入通道 = --skill/--append-system-prompt flag);
9
+ // - schemaEnv 内化:中立层无此字段,PiEngine 从 task.schema 派生(stringifySchemaCached
10
+ // compact——与 agent-opts-resolver 同一函数同一缓存,逐字节等值,D-A6 bridge 保真);
11
+ // - conversation/idleTimeoutMs:interact 控制面的 task 标志,原名透传。
12
+ //
13
+ // 往返保真(验收 A1 的映射层证据):executeOptionsToTaskSpec ∘ taskSpecToExecuteOptions
14
+ // 对全部 ExecuteOptions 字段恒等(schemaEnv 由 schema 派生后逐字节等于原值——resolver
15
+ // 与本 mapper 共用 WeakMap 缓存的同一 compact 串)。唯一归一化:appendSystemPrompt: []
16
+ // 归一为 persona 缺省(下游 spread 空数组与 undefined 均为 no-op,行为等价)。
17
+
18
+ import type { ModelInfo } from "../../../model-resolver.ts";
19
+ import type { ExecuteOptions } from "../../../types.ts";
20
+ import { stringifySchemaCached } from "../../../../shared/schema-jsonify.ts";
21
+ import type { AgentTaskSpec, PersonaSpec } from "../../types.ts";
22
+
23
+ /**
24
+ * ExecuteOptions → AgentTaskSpec(宿主侧声明 → 引擎中立声明)。
25
+ * 纯函数、无副作用——同一 opts 可重复转换(幂等)。
26
+ */
27
+ export function executeOptionsToTaskSpec(opts: ExecuteOptions): AgentTaskSpec {
28
+ // persona 归并:skillPath / appendSystemPrompt 有其一即建 persona,避免空对象噪声。
29
+ const persona: PersonaSpec | undefined =
30
+ opts.skillPath !== undefined || opts.appendSystemPrompt !== undefined
31
+ ? {
32
+ ...(opts.skillPath !== undefined ? { skillPath: opts.skillPath } : {}),
33
+ ...(opts.appendSystemPrompt !== undefined ? { appendSystemPrompt: opts.appendSystemPrompt } : {}),
34
+ }
35
+ : undefined;
36
+ return {
37
+ task: opts.task,
38
+ slug: opts.slug,
39
+ agent: opts.agent,
40
+ model: opts.model,
41
+ // pi 7 档枚举 → 引擎无关 effort(恒等:档位集合是 pi 私有语义,中立层透传)
42
+ effort: opts.thinkingLevel,
43
+ ...(persona !== undefined ? { persona } : {}),
44
+ schema: opts.schema,
45
+ maxTurns: opts.maxTurns,
46
+ graceTurns: opts.graceTurns,
47
+ fork: opts.fork,
48
+ worktree: opts.worktree,
49
+ cwd: opts.cwd,
50
+ conversation: opts.conversation,
51
+ idleTimeoutMs: opts.idleTimeoutMs,
52
+ // ExecuteOptions 不携带的运行期字段(signal/ctxModel/onComplete)归 RunContext;
53
+ // schemaEnv 由 taskSpecToExecuteOptions 从 schema 派生。
54
+ };
55
+ }
56
+
57
+ /**
58
+ * AgentTaskSpec → ExecuteOptions(pi 引擎侧还原)。
59
+ *
60
+ * @param spec 中立任务声明
61
+ * @param runCtx 运行期上下文:ctxModel 是 ExecuteOptions 的运行期字段(从 RunContext
62
+ * 回填);schemaEnvFallback 是解耦形态(有 schemaEnv 无 schema,生产
63
+ * 不可达)的兜底透传——schema 可派生时派生优先(逐字节等值)
64
+ */
65
+ export function taskSpecToExecuteOptions(
66
+ spec: AgentTaskSpec,
67
+ runCtx?: { ctxModel?: ModelInfo; schemaEnvFallback?: string },
68
+ ): ExecuteOptions {
69
+ return {
70
+ task: spec.task,
71
+ slug: spec.slug,
72
+ // 身份解析键:spec.agent 优先;persona.agentRef 是 persona 注入通道的定位符,
73
+ // 仅在 spec.agent 缺失时兜底(flag/file 通道引擎的主路径;pi 恒走 spec.agent)
74
+ agent: spec.agent ?? spec.persona?.agentRef,
75
+ model: spec.model,
76
+ // effort → pi thinkingLevel(恒等映射)
77
+ thinkingLevel: spec.effort,
78
+ // persona 还原:pi 的注入通道是 --skill / --append-system-prompt flag
79
+ skillPath: spec.persona?.skillPath,
80
+ appendSystemPrompt: spec.persona?.appendSystemPrompt,
81
+ schema: spec.schema,
82
+ // schemaEnv 内化派生(D-A6 bridge):与 agent-opts-resolver 共用 stringifySchemaCached
83
+ // compact 缓存,输出逐字节等值。schema 缺失(无法派生)且调用方持有预编码值时兜底
84
+ // 透传(解耦形态,生产不可达);无 schema 亦无兜底时不设(BC-6:childEnv 不注入)。
85
+ schemaEnv: spec.schema !== undefined
86
+ ? stringifySchemaCached(spec.schema, "compact")
87
+ : runCtx?.schemaEnvFallback,
88
+ maxTurns: spec.maxTurns,
89
+ graceTurns: spec.graceTurns,
90
+ ctxModel: runCtx?.ctxModel,
91
+ fork: spec.fork,
92
+ worktree: spec.worktree,
93
+ cwd: spec.cwd,
94
+ conversation: spec.conversation,
95
+ idleTimeoutMs: spec.idleTimeoutMs,
96
+ // signal 不入 opts(PiEngine 直接传给 executeAndAwait 第 2 参,与 SAR 现行为一致);
97
+ // onComplete 不经引擎(workflow 路径从不设置;background 完成通知走 service 内部 notifier)。
98
+ // denyTools/permissionMode:中立新字段,pi 链路暂无对应面(P2 公共层接 denylist 映射)。
99
+ };
100
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "_meta": {
3
+ "description": "zcode CLI spawn 单轮模式 golden 实录(2026-08-25 验收前置门)",
4
+ "engineVersion": "0.16.5",
5
+ "recordedAt": "2026-08-25T01:41:43Z",
6
+ "recordedOn": "macOS arm64, node v24.11.1",
7
+ "argv": [
8
+ "node",
9
+ "/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs",
10
+ "--json",
11
+ "--cwd",
12
+ "<临时目录>",
13
+ "--mode",
14
+ "yolo",
15
+ "--prompt",
16
+ "Reply with the single word: ok"
17
+ ],
18
+ "envSummary": {
19
+ "HOME": "<隔离池目录>(config.json = model.main + 目标 provider 条目,无 plugins 块)",
20
+ "stdin": "/dev/null(stdio ignore——CLI 不读 stdin,否则挂起)",
21
+ "XYZ_AGENT_SUBAGENT": "实录未注入;引擎实现注入 =1"
22
+ },
23
+ "exitCode": 0,
24
+ "durationMs": 4914,
25
+ "fieldStructure": {
26
+ "sessionId": "string,sess_<uuid>",
27
+ "traceId": "string(诊断用)",
28
+ "turnId": "string,turn_<uuid>",
29
+ "response": "string——assistant 全文",
30
+ "usage": "object:source/modelRequestCount/inputTokens/outputTokens/totalTokens/cacheReadTokens/cacheWriteTokens/reasoningTokens/webFetchRequests/webSearchRequests",
31
+ "eventCount": "number",
32
+ "projection": "object:status/turnCount/totalTokenCount/contextUsed/contextWindow"
33
+ },
34
+ "usageMapping": "inputTokens→input、outputTokens→output、cacheReadTokens→cacheRead、cacheWriteTokens→cacheWrite;cost 无来源(cost 回传 ❌ 与调研一致)",
35
+ "credentialNote": "stdout 不含凭据;凭据只存在于池内 config.json(此处不收录)。隔离 HOME 的凭据源用 ~/.zcode/v2/config.json(桌面登录态)——本机 ~/.zcode/cli/config.json 的 router provider 指向局域网网关 192.168.1.202:9981,实录时不可达",
36
+ "sigtermTiming": "SIGTERM → exit 实测 103ms(exit code 143,stderr \"Turn was cancelled\");引擎杀链 grace 取 5000ms(zsub 同构,保守)"
37
+ },
38
+ "stdoutRaw": "{\n \"sessionId\": \"sess_35852a0f-1302-4e20-9e48-87f47527abe3\",\n \"traceId\": \"9fffaebd-749e-468b-af3f-2a89e3347785\",\n \"turnId\": \"turn_99879260-1c9b-4e97-afe1-c2e7abeaf5b9\",\n \"response\": \"ok\",\n \"usage\": {\n \"source\": \"provider\",\n \"modelRequestCount\": 1,\n \"inputTokens\": 12599,\n \"outputTokens\": 17,\n \"totalTokens\": 12616,\n \"cacheReadTokens\": 512,\n \"cacheWriteTokens\": 0,\n \"reasoningTokens\": 0,\n \"webFetchRequests\": 0,\n \"webSearchRequests\": 0\n },\n \"eventCount\": 17,\n \"projection\": {\n \"status\": \"idle\",\n \"turnCount\": 1,\n \"totalTokenCount\": 12616,\n \"contextUsed\": 12616,\n \"contextWindow\": 1000000\n }\n}\n"
39
+ }
@@ -0,0 +1,150 @@
1
+ // launcher.test.ts —— argv 组装 / 字节估算超限 / env 构造 / 真实子进程杀链(用 node -e
2
+ // 代替 zcode CLI,验证 SIGTERM→grace→SIGKILL 与 exited 标志机制本身)。
3
+
4
+ import { execFileSync } from "node:child_process";
5
+
6
+ import { describe, expect, it } from "vitest";
7
+
8
+ import { DEFAULT_ARGV_BUDGET_BYTES, estimateArgvBytes } from "../../../common/persona-router.ts";
9
+ import {
10
+ assertZcodeArgvBudget,
11
+ buildZcodeArgv,
12
+ buildZcodeEnv,
13
+ launchZcodeProcess,
14
+ } from "../launcher.ts";
15
+
16
+ describe("buildZcodeArgv", () => {
17
+ it("基础形态:--json --cwd --mode yolo --prompt 尾置", () => {
18
+ const args = buildZcodeArgv({ cwd: "/tmp/w", prompt: "do it" });
19
+ expect(args).toEqual(["--json", "--cwd", "/tmp/w", "--mode", "yolo", "--prompt", "do it"]);
20
+ });
21
+
22
+ it("denyTools 逗号连接进 --disallowed-tools", () => {
23
+ const args = buildZcodeArgv({ cwd: "/w", prompt: "p", denyTools: ["bash", "edit"] });
24
+ expect(args).toContain("--disallowed-tools");
25
+ expect(args[args.indexOf("--disallowed-tools") + 1]).toBe("bash,edit");
26
+ });
27
+
28
+ it("denyTools 空/空白项过滤,全空不加 flag", () => {
29
+ expect(buildZcodeArgv({ cwd: "/w", prompt: "p", denyTools: [] })).not.toContain("--disallowed-tools");
30
+ expect(buildZcodeArgv({ cwd: "/w", prompt: "p", denyTools: [" ", ""] })).not.toContain("--disallowed-tools");
31
+ });
32
+
33
+ it("resumeSessionId 落 --resume(prompt 之前)", () => {
34
+ const args = buildZcodeArgv({ cwd: "/w", prompt: "p", resumeSessionId: "sess_x" });
35
+ expect(args.indexOf("--resume")).toBeLessThan(args.indexOf("--prompt"));
36
+ expect(args[args.indexOf("--resume") + 1]).toBe("sess_x");
37
+ });
38
+ });
39
+
40
+ describe("assertZcodeArgvBudget(对齐点②:公共 persona-router 权威预算的引擎侧消费)", () => {
41
+ it("估算含 node 与 cliPath 前导(NUL 分隔近似),中文 prompt 按字节计", () => {
42
+ // 公共 estimateArgvBytes 的口径回归:前导两元素计入 + 多字节按 UTF-8 字节
43
+ const est = estimateArgvBytes(["n", "c", "--prompt", "中文"]);
44
+ expect(est).toBe(2 + 2 + ("--prompt".length + 1) + Buffer.byteLength("中文") + 1);
45
+ });
46
+
47
+ it("超 128KB 阈值抛 prompt_too_large(含恢复指引;阈值 = DEFAULT_ARGV_BUDGET_BYTES)", () => {
48
+ const bigPrompt = "x".repeat(DEFAULT_ARGV_BUDGET_BYTES);
49
+ expect(() => assertZcodeArgvBudget("node", "/cli.cjs", ["--prompt", bigPrompt])).toThrowError(
50
+ /prompt_too_large/,
51
+ );
52
+ try {
53
+ assertZcodeArgvBudget("node", "/cli.cjs", ["--prompt", bigPrompt]);
54
+ } catch (err) {
55
+ // 公共 EngineError 形态:message 是 code 前缀 detail,恢复指引在 recovery 字段
56
+ expect((err as { code: string }).code).toBe("prompt_too_large");
57
+ expect((err as { recovery: string }).recovery).toContain("Shorten the task text");
58
+ }
59
+ });
60
+
61
+ it("阈值内不抛", () => {
62
+ expect(() => assertZcodeArgvBudget("node", "/cli.cjs", ["--prompt", "x".repeat(1024)])).not.toThrow();
63
+ });
64
+ });
65
+
66
+ describe("buildZcodeEnv", () => {
67
+ it("HOME 强制覆盖(同名键不许覆盖隔离值)+ 统一嵌套标记(经公共 nesting-guard)", () => {
68
+ const env = buildZcodeEnv("/pool/home", { HOME: "/real/home", PATH: "/usr/bin" });
69
+ expect(env["HOME"]).toBe("/pool/home");
70
+ expect(env["XYZ_AGENT_SUBAGENT"]).toBe("1");
71
+ expect(env["PATH"]).toBe("/usr/bin");
72
+ });
73
+
74
+ it("剥离引擎原生嵌套标记(D8:防孙代理误判已在嵌套层)", () => {
75
+ const env = buildZcodeEnv("/pool/home", {
76
+ ZSW_NESTED: "1",
77
+ CLAUDECODE: "1",
78
+ PI_SUBAGENT_SELF_RECORD_ID: "sa-x",
79
+ PI_SUBAGENT_DEPTH: "2",
80
+ });
81
+ expect(env["ZSW_NESTED"]).toBeUndefined();
82
+ expect(env["CLAUDECODE"]).toBeUndefined();
83
+ expect(env["PI_SUBAGENT_SELF_RECORD_ID"]).toBeUndefined();
84
+ expect(env["PI_SUBAGENT_DEPTH"]).toBeUndefined();
85
+ expect(env["XYZ_AGENT_SUBAGENT"]).toBe("1");
86
+ });
87
+ });
88
+
89
+ describe("launchZcodeProcess:真实子进程杀链(node -e 代替 CLI)", () => {
90
+ // 杀链/exited 机制与 CLI 无关(对子进程发信号 + close 事件),用 `node -e <script>`
91
+ // 验证机制本身:launchZcodeProcess 的 spawn 形态是 `node <cliPath> <args...>`,
92
+ // 测试借 cliPath 槽位传 -e、args 传脚本内容。
93
+ it("child 句柄暴露(D10 记账数据源):proc.child 即 spawn 出的进程,退出码可读", async () => {
94
+ const proc = launchZcodeProcess({
95
+ cliPath: "-e",
96
+ args: ["process.stdout.write('hi'); process.exit(0)"],
97
+ env: process.env,
98
+ });
99
+ // 句柄与 pid 同源(宿主经 RunContext.onChildSpawned 注册进 spawnedChildren)
100
+ expect(proc.child.pid).toBe(proc.pid);
101
+ expect(proc.child.killed).toBe(false);
102
+ const exited = await proc.exited;
103
+ expect(exited.code).toBe(0);
104
+ expect(proc.child.exitCode).toBe(0); // close 后 exitCode 可读(close 事件先于 exited resolve)
105
+ });
106
+
107
+ it("正常退出:exited 带退出码,stdout 可收集", async () => {
108
+ const proc = launchZcodeProcess({
109
+ cliPath: "-e",
110
+ args: ["process.stdout.write('hello'); process.exit(0)"],
111
+ env: process.env,
112
+ });
113
+ let out = "";
114
+ proc.stdout.on("data", (d: Buffer) => {
115
+ out += d.toString();
116
+ });
117
+ const exited = await proc.exited;
118
+ expect(out).toBe("hello");
119
+ expect(exited.code).toBe(0);
120
+ expect(proc.killedByUs()).toBe(false);
121
+ });
122
+
123
+ it("abort 触发杀链:SIGTERM 后进程退出,killedByUs 标记", async () => {
124
+ const proc = launchZcodeProcess({
125
+ cliPath: "-e",
126
+ args: ["setInterval(() => {}, 1000)"],
127
+ env: process.env,
128
+ });
129
+ expect(proc.pid).toBeGreaterThan(0);
130
+ // 实测 zcode 对 SIGTERM ~103ms 退出;普通 node 进程无 handler 直接终止
131
+ const abortPromise = proc.abort(3000);
132
+ const exited = await proc.exited;
133
+ await abortPromise;
134
+ expect(exited.code === null || exited.code === 143 || exited.code === 1).toBe(true);
135
+ expect(proc.killedByUs()).toBe(true);
136
+ // 无僵尸:进程已从系统进程表消失
137
+ expect(() => execFileSync("ps", ["-p", String(proc.pid)])).toThrow();
138
+ });
139
+
140
+ it("abort 幂等:重复调用不再触发第二条杀链", async () => {
141
+ const proc = launchZcodeProcess({
142
+ cliPath: "-e",
143
+ args: ["setInterval(() => {}, 1000)"],
144
+ env: process.env,
145
+ });
146
+ await proc.abort(2000);
147
+ await proc.abort(2000); // 不应抛
148
+ await proc.exited;
149
+ });
150
+ });
@@ -0,0 +1,246 @@
1
+ // parser.test.ts —— golden 样本回归(验收 2)+ 损坏 stdout 错误路径 + 缓冲有界性。
2
+ // golden 原文来自 2026-08-25 真机实录(zcode 0.16.5),采集元数据见 fixture _meta。
3
+
4
+ import { readFileSync } from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ import { describe, expect, it } from "vitest";
8
+
9
+ import {
10
+ buildRunFailedMessage,
11
+ createBoundedLineBuffer,
12
+ mapZcodeUsage,
13
+ parseZcodeStdoutJson,
14
+ parseZcodeTerminal,
15
+ synthesizeCoarseEvents,
16
+ } from "../parser.ts";
17
+
18
+ interface GoldenFixture {
19
+ stdoutRaw: string;
20
+ _meta: { engineVersion: string; exitCode: number; usageMapping: string };
21
+ }
22
+
23
+ const golden: GoldenFixture = JSON.parse(
24
+ readFileSync(fileURLToPath(new URL("./__fixtures__/zcode-golden-spawn.json", import.meta.url)), "utf8"),
25
+ );
26
+
27
+ describe("parseZcodeTerminal:golden 回归", () => {
28
+ it("实录样本解析出完整终态(sessionId/response/usage/turnCount)", () => {
29
+ const r = parseZcodeTerminal(golden.stdoutRaw);
30
+ expect(r.ok).toBe(true);
31
+ if (!r.ok) return;
32
+ expect(r.payload.sessionId).toBe("sess_35852a0f-1302-4e20-9e48-87f47527abe3");
33
+ expect(r.payload.response).toBe("ok");
34
+ expect(r.payload.usage).toEqual({ input: 12599, output: 17, cacheRead: 512, cacheWrite: 0 });
35
+ expect(r.payload.turnCount).toBe(1);
36
+ // 终态层 usage(orchestration 版):cost 显式 0(zcode 不回传)、contextTokens=
37
+ // projection.contextUsed、turns=projection.turnCount
38
+ expect(r.payload.outcomeUsage).toEqual({
39
+ input: 12599,
40
+ output: 17,
41
+ cacheRead: 512,
42
+ cacheWrite: 0,
43
+ cost: 0,
44
+ contextTokens: 12616,
45
+ turns: 1,
46
+ });
47
+ });
48
+
49
+ it("usage 字段名映射:inputTokens→input 等四项(cost 无来源不出现)", () => {
50
+ const u = mapZcodeUsage({
51
+ inputTokens: 10,
52
+ outputTokens: 20,
53
+ cacheReadTokens: 5,
54
+ cacheWriteTokens: 0,
55
+ });
56
+ expect(u).toEqual({ input: 10, output: 20, cacheRead: 5, cacheWrite: 0 });
57
+ expect(u !== undefined && "cost" in u).toBe(false);
58
+ });
59
+
60
+ it("usage 形状不完整时显式 undefined(不给残缺对象)", () => {
61
+ expect(mapZcodeUsage(undefined)).toBeUndefined();
62
+ expect(mapZcodeUsage("nope")).toBeUndefined();
63
+ expect(mapZcodeUsage({})).toBeUndefined();
64
+ });
65
+ });
66
+
67
+ describe("parseZcodeTerminal:损坏 stdout → ok:false(engine_run_failed 输入)", () => {
68
+ it("非 JSON 文本", () => {
69
+ const r = parseZcodeTerminal("Error: something broke\n");
70
+ expect(r.ok).toBe(false);
71
+ if (r.ok) return;
72
+ expect(r.reason).toContain("不是 JSON");
73
+ });
74
+
75
+ it("JSON 但缺 string 型 response(格式漂移形态)", () => {
76
+ const r = parseZcodeTerminal('{"sessionId": "sess_x", "usage": {}}');
77
+ expect(r.ok).toBe(false);
78
+ if (r.ok) return;
79
+ expect(r.reason).toContain("response");
80
+ });
81
+
82
+ it("数组/标量 JSON 拒收", () => {
83
+ expect(parseZcodeTerminal("[1,2,3]").ok).toBe(false);
84
+ expect(parseZcodeTerminal("42").ok).toBe(false);
85
+ });
86
+
87
+ it("混入日志行时首尾大括号容错提取", () => {
88
+ const mixed = '[info] boot\n{"sessionId":"sess_a","response":"done"}\n[info] exit';
89
+ const r = parseZcodeTerminal(mixed);
90
+ expect(r.ok).toBe(true);
91
+ if (!r.ok) return;
92
+ expect(r.payload.sessionId).toBe("sess_a");
93
+ expect(r.payload.response).toBe("done");
94
+ });
95
+ });
96
+
97
+ describe("parseZcodeStdoutJson:空对象形态防御", () => {
98
+ it("无大括号文本返回 null", () => {
99
+ expect(parseZcodeStdoutJson("plain text")).toBeNull();
100
+ });
101
+ });
102
+
103
+ describe("synthesizeCoarseEvents:coarse 不变量", () => {
104
+ it("message_end(含 usage)在前、turn_end 最后", () => {
105
+ const usage = { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 };
106
+ const evs = synthesizeCoarseEvents("ok", usage);
107
+ expect(evs).toEqual([{ type: "message_end", usage }, { type: "turn_end" }]);
108
+ });
109
+
110
+ it("无 usage 时 message_end 不带残缺字段", () => {
111
+ const evs = synthesizeCoarseEvents("ok");
112
+ expect(evs[0]).toEqual({ type: "message_end" });
113
+ expect("usage" in evs[0]!).toBe(false);
114
+ expect(evs[evs.length - 1]).toEqual({ type: "turn_end" });
115
+ });
116
+ });
117
+
118
+ describe("buildRunFailedMessage:错误规格(stdout 尾部 + exit code + 恢复指引)", () => {
119
+ it("含 exit code、尾部内容与三段恢复动作", () => {
120
+ const msg = buildRunFailedMessage({
121
+ exitCode: 3,
122
+ stdoutTail: "x".repeat(5000),
123
+ stderrTail: "boom",
124
+ parseReason: "stdout 不是 JSON 对象",
125
+ });
126
+ expect(msg).toContain("engine_run_failed");
127
+ expect(msg).toContain("exit code: 3");
128
+ expect(msg).toContain("解析失败");
129
+ expect(msg).toContain("stderr 尾部: boom");
130
+ // stdout 尾部截断到 2000 字
131
+ expect(msg).toContain("x".repeat(2000));
132
+ expect(msg).not.toContain("x".repeat(2001));
133
+ expect(msg).toContain("--version");
134
+ expect(msg).toContain("golden");
135
+ expect(msg).toContain("engine: pi");
136
+ });
137
+
138
+ it("null exit code 标注被信号杀死", () => {
139
+ const msg = buildRunFailedMessage({ exitCode: null, stdoutTail: "" });
140
+ expect(msg).toContain("null(被信号杀死)");
141
+ });
142
+
143
+ it("空 stdout 不产生空 part(防「stdout 尾部: <指引段>」视觉嵌套误读)", () => {
144
+ const msg = buildRunFailedMessage({ exitCode: 1, stdoutTail: " " });
145
+ expect(msg).not.toContain("stdout 尾部");
146
+ });
147
+
148
+ it("LLM API 失败(AI_APICallError)归因到端点/凭据指引,不再给 probe 指引", () => {
149
+ const msg = buildRunFailedMessage({
150
+ cliPath: "/fake/zcode.cjs",
151
+ exitCode: 1,
152
+ stdoutTail: "",
153
+ stderrTail: "AI_APICallError: 500 Internal Server Error\nError: Turn execution failed",
154
+ modelRef: "router/mimo-v2.5-pro",
155
+ configPath: "/pool/.zcode/cli/config.json",
156
+ });
157
+ expect(msg).toContain("模型 API 调用失败");
158
+ expect(msg).toContain("router/mimo-v2.5-pro");
159
+ expect(msg).toContain("/pool/.zcode/cli/config.json");
160
+ expect(msg).toContain("baseURL");
161
+ expect(msg).toContain("engine: pi");
162
+ // probe/golden 指引不出现(误导排查方向——2026-08-25 真机教训)
163
+ expect(msg).not.toContain("--version");
164
+ expect(msg).not.toContain("golden");
165
+ expect(msg).not.toContain("stdout 尾部");
166
+ });
167
+
168
+ it("401 有响应形态(无 AI_APICallError 类名)也归因,且带 HTTP 状态与凭据定向", () => {
169
+ const stderr401 = [
170
+ " 'x-mife-upstream-status': '401'",
171
+ " },",
172
+ " responseStatus: 401,",
173
+ " statusCode: 401",
174
+ "}",
175
+ "Error: Turn execution failed (traceId: a487a9b3)",
176
+ ].join("\n");
177
+ const msg = buildRunFailedMessage({
178
+ cliPath: "/fake/zcode.cjs",
179
+ exitCode: 1,
180
+ stdoutTail: "",
181
+ stderrTail: stderr401,
182
+ modelRef: "router/mimo-v2.5-pro",
183
+ configPath: "/pool/.zcode/cli/config.json",
184
+ });
185
+ expect(msg).toContain("模型 API 调用失败");
186
+ expect(msg).toContain("HTTP 401");
187
+ expect(msg).toContain("凭据无效或过期");
188
+ expect(msg).toContain("apiKey 大概率无效或过期");
189
+ expect(msg).not.toContain("--version");
190
+ expect(msg).not.toContain("golden");
191
+ });
192
+
193
+ it("LLM 归因在缺省 modelRef/configPath 时兜底可读", () => {
194
+ const msg = buildRunFailedMessage({
195
+ cliPath: "/fake/zcode.cjs",
196
+ exitCode: 1,
197
+ stdoutTail: "ok",
198
+ stderrTail: "Symbol(vercel.ai.error.AI_APICallError): true",
199
+ });
200
+ expect(msg).toContain("模型 API 调用失败");
201
+ expect(msg).toContain("provider 的 baseURL 可达性");
202
+ expect(msg).not.toContain("undefined");
203
+ });
204
+
205
+ it("stderr 的 [Object] 噪音行折叠为计数", () => {
206
+ const noisy = [
207
+ "AIError: fail",
208
+ ...Array.from({ length: 12 }, () => " [Object], [Object],"),
209
+ " statusCode: undefined,",
210
+ ].join("\n");
211
+ const msg = buildRunFailedMessage({ cliPath: "/c", exitCode: 1, stdoutTail: "", stderrTail: noisy });
212
+ expect(msg).toContain("[Object]×12");
213
+ expect(msg).not.toContain("[Object], [Object]");
214
+ });
215
+ });
216
+
217
+ describe("createBoundedLineBuffer:有界双缓冲", () => {
218
+ it("头 4K + 尾 64K 保留,中间丢弃并标记", () => {
219
+ const buf = createBoundedLineBuffer();
220
+ const line = "a".repeat(100) + "\n"; // 每行 101 字节
221
+ for (let i = 0; i < 2000; i++) buf.push(line); // ~202KB,远超 4K+64K
222
+ const text = buf.text();
223
+ expect(text.startsWith("a".repeat(100))).toBe(true); // 头部保留
224
+ expect(text).toContain("已丢弃"); // 中间丢弃标记
225
+ expect(text.endsWith("\n")).toBe(true);
226
+ // 尾部窗口 ≈ 64K:总长有界
227
+ expect(text.length).toBeLessThan(4 * 1024 + 64 * 1024 + 200);
228
+ });
229
+
230
+ it("单行超整个尾部窗口时保留行尾(错误信息在末尾)", () => {
231
+ const buf = createBoundedLineBuffer({ headLimit: 0, tailLimit: 1000 });
232
+ buf.push("B".repeat(5000) + "\n");
233
+ const text = buf.text();
234
+ // 有效载荷 ≤ 1000(text 里另有「已丢弃 N 字节」标记行)
235
+ expect(text.match(/B/g)?.length ?? 0).toBeLessThanOrEqual(1000);
236
+ expect(text.endsWith("\n")).toBe(true);
237
+ expect(text).toContain("已丢弃");
238
+ });
239
+
240
+ it("未超限时原文完整(golden 646 字节场景)", () => {
241
+ const buf = createBoundedLineBuffer();
242
+ buf.push(golden.stdoutRaw);
243
+ expect(buf.text()).toBe(golden.stdoutRaw);
244
+ expect(buf.tail(10)).toBe(golden.stdoutRaw.slice(-10));
245
+ });
246
+ });