@zhushanwen/pi-subagent-workflow 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/agents/context-builder.md +17 -0
  2. package/agents/general-purpose.md +16 -0
  3. package/agents/oracle.md +17 -0
  4. package/agents/planner.md +17 -0
  5. package/agents/researcher.md +17 -0
  6. package/agents/reviewer.md +17 -0
  7. package/agents/scout.md +17 -0
  8. package/agents/worker.md +16 -0
  9. package/examples/README.md +43 -0
  10. package/examples/chain.example.js +92 -0
  11. package/examples/map-reduce.example.js +99 -0
  12. package/examples/parallel.example.js +82 -0
  13. package/examples/scatter-gather.example.js +106 -0
  14. package/index.ts +1 -0
  15. package/package.json +66 -0
  16. package/skills/workflow-script-format/SKILL.md +328 -0
  17. package/src/execution/__tests__/agent-registry.test.ts +164 -0
  18. package/src/execution/__tests__/agent-result-mapper.test.ts +128 -0
  19. package/src/execution/__tests__/alive-store.test.ts +147 -0
  20. package/src/execution/__tests__/bg-notify-render.test.ts +256 -0
  21. package/src/execution/__tests__/concurrency-pool.test.ts +217 -0
  22. package/src/execution/__tests__/config.test.ts +110 -0
  23. package/src/execution/__tests__/crash-recovery.test.ts +311 -0
  24. package/src/execution/__tests__/execute-nesting.test.ts +359 -0
  25. package/src/execution/__tests__/execute-options-mapper.test.ts +138 -0
  26. package/src/execution/__tests__/execution-record.test.ts +959 -0
  27. package/src/execution/__tests__/finalized-marker.test.ts +82 -0
  28. package/src/execution/__tests__/format-schema-instruction.test.ts +135 -0
  29. package/src/execution/__tests__/format.test.ts +320 -0
  30. package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
  31. package/src/execution/__tests__/list-component.test.ts +347 -0
  32. package/src/execution/__tests__/model-resolver.test.ts +356 -0
  33. package/src/execution/__tests__/output-collector.test.ts +61 -0
  34. package/src/execution/__tests__/path-encoding.test.ts +75 -0
  35. package/src/execution/__tests__/pi-invocation.test.ts +73 -0
  36. package/src/execution/__tests__/record-store.test.ts +545 -0
  37. package/src/execution/__tests__/run-spawn-edges.test.ts +439 -0
  38. package/src/execution/__tests__/run-spawn-integration.test.ts +897 -0
  39. package/src/execution/__tests__/sdk-contract.test.ts +272 -0
  40. package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
  41. package/src/execution/__tests__/session-file-gc.test.ts +247 -0
  42. package/src/execution/__tests__/session-reconstructor.test.ts +359 -0
  43. package/src/execution/__tests__/session-runner-schema-env.test.ts +314 -0
  44. package/src/execution/__tests__/session-start-reaper.test.ts +227 -0
  45. package/src/execution/__tests__/spawn-args.test.ts +244 -0
  46. package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
  47. package/src/execution/__tests__/subagent-service.test.ts +678 -0
  48. package/src/execution/__tests__/subprocess-agent-runner.test.ts +389 -0
  49. package/src/execution/__tests__/temp-prompt.test.ts +53 -0
  50. package/src/execution/__tests__/timeout-integration.test.ts +381 -0
  51. package/src/execution/__tests__/tombstone-store.test.ts +73 -0
  52. package/src/execution/__tests__/tool-action.test.ts +330 -0
  53. package/src/execution/__tests__/turn-limiter.test.ts +65 -0
  54. package/src/execution/__tests__/worktree-manager.test.ts +423 -0
  55. package/src/execution/__tests__/worktree-registry.test.ts +161 -0
  56. package/src/execution/agent-registry.ts +252 -0
  57. package/src/execution/agent-result-mapper.ts +84 -0
  58. package/src/execution/alive-store.ts +92 -0
  59. package/src/execution/best-effort.ts +30 -0
  60. package/src/execution/concurrency-pool.ts +84 -0
  61. package/src/execution/config.ts +73 -0
  62. package/src/execution/execute-options-mapper.ts +86 -0
  63. package/src/execution/execution-record.ts +778 -0
  64. package/src/execution/finalized-marker.ts +51 -0
  65. package/src/execution/model-config-service.ts +225 -0
  66. package/src/execution/model-resolver.ts +247 -0
  67. package/src/execution/notifier.ts +168 -0
  68. package/src/execution/output-collector.ts +88 -0
  69. package/src/execution/path-encoding.ts +34 -0
  70. package/src/execution/pi-invocation.ts +70 -0
  71. package/src/execution/record-store.ts +350 -0
  72. package/src/execution/session-context-resolver.ts +64 -0
  73. package/src/execution/session-file-gc.ts +98 -0
  74. package/src/execution/session-reconstructor.ts +450 -0
  75. package/src/execution/session-runner.ts +725 -0
  76. package/src/execution/spawn-event-adapter.ts +150 -0
  77. package/src/execution/subagent-service.ts +973 -0
  78. package/src/execution/subprocess-agent-runner.ts +108 -0
  79. package/src/execution/temp-prompt.ts +57 -0
  80. package/src/execution/tombstone-store.ts +72 -0
  81. package/src/execution/turn-limiter.ts +88 -0
  82. package/src/execution/types.ts +634 -0
  83. package/src/execution/worktree-manager.ts +285 -0
  84. package/src/execution/worktree-registry.ts +144 -0
  85. package/src/index.ts +454 -0
  86. package/src/interface/bg-notify-render.ts +286 -0
  87. package/src/interface/commands.ts +157 -0
  88. package/src/interface/format.ts +501 -0
  89. package/src/interface/gui-adapter.ts +136 -0
  90. package/src/interface/helpers.ts +110 -0
  91. package/src/interface/list-component.ts +643 -0
  92. package/src/interface/list-shared.ts +84 -0
  93. package/src/interface/list-view.ts +373 -0
  94. package/src/interface/reentry-guard.ts +30 -0
  95. package/src/interface/subagent-actions.ts +294 -0
  96. package/src/interface/subagent-tool.ts +294 -0
  97. package/src/interface/subagents.ts +30 -0
  98. package/src/interface/tool-render.ts +333 -0
  99. package/src/interface/tool-workflow-script.ts +351 -0
  100. package/src/interface/tool-workflow.ts +485 -0
  101. package/src/interface/views/WorkflowsView.ts +944 -0
  102. package/src/interface/views/detail-content.ts +298 -0
  103. package/src/interface/views/format.ts +320 -0
  104. package/src/orchestration/__tests__/concurrency-gate.test.ts +125 -0
  105. package/src/orchestration/__tests__/config-loader.test.ts +381 -0
  106. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +332 -0
  107. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
  108. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +248 -0
  109. package/src/orchestration/__tests__/lifecycle.test.ts +385 -0
  110. package/src/orchestration/__tests__/script-lint.test.ts +347 -0
  111. package/src/orchestration/__tests__/worker-script-builder.test.ts +42 -0
  112. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +319 -0
  113. package/src/orchestration/agent-opts-resolver.ts +128 -0
  114. package/src/orchestration/concurrency-gate.ts +69 -0
  115. package/src/orchestration/config-loader.ts +313 -0
  116. package/src/orchestration/error-recovery.ts +578 -0
  117. package/src/orchestration/execute-agent-call.ts +174 -0
  118. package/src/orchestration/jsonl-run-store.ts +292 -0
  119. package/src/orchestration/launcher.ts +368 -0
  120. package/src/orchestration/lifecycle.ts +373 -0
  121. package/src/orchestration/models/__tests__/budget.test.ts +367 -0
  122. package/src/orchestration/models/agent-call.ts +76 -0
  123. package/src/orchestration/models/budget.ts +148 -0
  124. package/src/orchestration/models/ports.ts +165 -0
  125. package/src/orchestration/models/run-runtime.ts +91 -0
  126. package/src/orchestration/models/run-spec.ts +54 -0
  127. package/src/orchestration/models/run-state.ts +44 -0
  128. package/src/orchestration/models/trace.ts +102 -0
  129. package/src/orchestration/models/types.ts +242 -0
  130. package/src/orchestration/models/workflow-run.ts +275 -0
  131. package/src/orchestration/models/workflow-script-registry.ts +32 -0
  132. package/src/orchestration/models/workflow-script.ts +90 -0
  133. package/src/orchestration/node-ops.ts +192 -0
  134. package/src/orchestration/script-lint.ts +387 -0
  135. package/src/orchestration/skill-discovery.ts +60 -0
  136. package/src/orchestration/worker-handle.ts +115 -0
  137. package/src/orchestration/worker-host.ts +93 -0
  138. package/src/orchestration/worker-script-builder.ts +281 -0
  139. package/src/orchestration/workflow-files.ts +85 -0
  140. package/src/orchestration/workflow-script-registry-impl.ts +128 -0
  141. package/src/shared/__tests__/resource-discovery.test.ts +226 -0
  142. package/src/shared/agent-event.ts +13 -0
  143. package/src/shared/resource-discovery.ts +535 -0
@@ -0,0 +1,347 @@
1
+ // src/__tests__/list-component.test.ts
2
+ //
3
+ // SubagentsListComponent 单元测试(循环依赖消除后的新结构)。
4
+ //
5
+ // 被测组件 list-component.ts 不再 import list-view——按键处理经第 7 个构造函数参数
6
+ // keyHandler 注入(list-view factory 的 processKey),状态经第 4 参数 ViewState 注入。
7
+ // 本测试覆盖 render 三分支调度 / hasRunning / 左列视口窗口 / 右列预览兜底链 /
8
+ // handleInput exit|changed|none 分支 / render 缓存 / detailMode 切换。
9
+ //
10
+ // Mock 策略:theme 透传为纯文本(断言业务文本而非 ANSI 码),service 只 stub collectRecords,
11
+ // keyHandler 由各用例注入返回 KeyResult。spinner 帧 Date.now() 驱动 → fake timers 锁定。
12
+
13
+ import { afterEach,beforeEach, describe, expect, it, vi } from "vitest";
14
+
15
+ import type { SubagentService } from "../subagent-service.ts";
16
+ import type { ThemeLike } from "../../interface/format.ts";
17
+ import { SubagentsListComponent } from "../../interface/list-component.ts";
18
+ import type { KeyHandler, KeyResult, TuiLike, ViewState } from "../../interface/list-shared.ts";
19
+ import type { SubagentRecord } from "../types.ts";
20
+
21
+ // ── KeyResult 常量(语义清晰,避到处写字面量对象) ──
22
+
23
+ const KEY_NONE: KeyResult = { changed: false, exit: false };
24
+ const KEY_CHANGED: KeyResult = { changed: true, exit: false };
25
+ const KEY_EXIT: KeyResult = { changed: false, exit: true };
26
+
27
+ // ── stub 工厂 ──
28
+
29
+ /** 透传 theme(list-component 经 format.ts 调用 fg/bold,ThemeLike 要求 4 方法齐全)。 */
30
+ function makeTheme(): ThemeLike {
31
+ return {
32
+ fg: (_tag: string, text: string) => text,
33
+ bg: (_color: string, text: string) => text,
34
+ bold: (text: string) => text,
35
+ underline: (text: string) => text,
36
+ } as ThemeLike;
37
+ }
38
+
39
+ /** service stub:list-component 只调 collectRecords(limit) 单参数。
40
+ * 与 tool-action.test.ts 同模式:部分对象直接断言为 SubagentService(duck-type)。 */
41
+ function makeService(records: SubagentRecord[] = []): SubagentService {
42
+ return {
43
+ collectRecords: vi.fn(() => records),
44
+ } as SubagentService;
45
+ }
46
+
47
+ /** record fixture(参考 tool-action.test.ts,字段见 types.ts SubagentRecord)。 */
48
+ function makeRecord(over: Partial<SubagentRecord> = {}): SubagentRecord {
49
+ return {
50
+ id: "run-1",
51
+ agent: "worker",
52
+ task: "do the thing",
53
+ status: "done",
54
+ mode: "sync",
55
+ startedAt: 1000,
56
+ endedAt: 2000,
57
+ rootSessionId: undefined,
58
+ parentRecordId: undefined,
59
+ depth: 0,
60
+ turns: 1,
61
+ totalTokens: 10,
62
+ model: "test/model",
63
+ thinkingLevel: undefined,
64
+ eventLog: [],
65
+ displayItems: [],
66
+ result: "ok",
67
+ error: undefined,
68
+ sessionFile: undefined,
69
+ ...over,
70
+ };
71
+ }
72
+
73
+ /** 构造 N 条 records(agent/task 含索引便于断言可见性)。 */
74
+ function makeRecords(n: number): SubagentRecord[] {
75
+ return Array.from({ length: n }, (_, i) =>
76
+ makeRecord({ id: `run-${i}`, agent: `agent-${i}`, task: `task-${i}` }),
77
+ );
78
+ }
79
+
80
+ interface MakeOpts {
81
+ records?: SubagentRecord[];
82
+ rows?: number;
83
+ selectedIdx?: number;
84
+ detailMode?: boolean;
85
+ keyHandler?: KeyHandler;
86
+ }
87
+
88
+ /** 构造组件 + 暴露 service/tui/state 供断言。 */
89
+ function makeComponent(opts: MakeOpts = {}) {
90
+ const records = opts.records ?? [];
91
+ const service = makeService(records);
92
+ const theme = makeTheme();
93
+ const tui = {
94
+ requestRender: vi.fn(),
95
+ terminal: { rows: opts.rows ?? 24 },
96
+ };
97
+ const state: ViewState = {
98
+ selectedIdx: opts.selectedIdx ?? 0,
99
+ scrollOffset: 0,
100
+ filterText: "",
101
+ detailMode: opts.detailMode ?? false,
102
+ disposed: false,
103
+ syncCancelHint: false,
104
+ };
105
+ const keyHandler: KeyHandler = opts.keyHandler ?? (() => KEY_NONE);
106
+ const comp = new SubagentsListComponent(
107
+ service,
108
+ theme,
109
+ tui as TuiLike,
110
+ state,
111
+ () => {},
112
+ () => {},
113
+ keyHandler,
114
+ );
115
+ return { comp, service, theme, tui, state };
116
+ }
117
+
118
+ // ============================================================
119
+ // SubagentsListComponent
120
+ // ============================================================
121
+ describe("SubagentsListComponent", () => {
122
+ beforeEach(() => {
123
+ // spinner 帧由 Math.floor(Date.now()/250) 选取,锁定时间避免 flaky。
124
+ vi.useFakeTimers();
125
+ vi.setSystemTime(new Date("2025-01-01T00:00:00Z"));
126
+ });
127
+ afterEach(() => {
128
+ vi.useRealTimers();
129
+ });
130
+
131
+ // ── render 三分支调度 ──────────────────────────────────
132
+ describe("render 分支调度", () => {
133
+ it("rows < MIN_TERM_ROWS(8) → too small 提示", () => {
134
+ const { comp } = makeComponent({ records: [], rows: 5 });
135
+ const joined = comp.render(80).join("\n");
136
+ expect(joined).toContain("too small");
137
+ expect(joined).toContain("need >=");
138
+ });
139
+
140
+ it("空 records → emptyBox(含 (no subagent records) 文案)", () => {
141
+ const { comp } = makeComponent({ records: [], rows: 24 });
142
+ const joined = comp.render(80).join("\n");
143
+ expect(joined).toContain("(no subagent records)");
144
+ // 紧凑小框不渲染分屏分区线
145
+ expect(joined).not.toContain("Records");
146
+ });
147
+
148
+ it("有 records → splitBox(含 record 的 task + agent 文本)", () => {
149
+ const rec = makeRecord({ agent: "researcher", task: "investigate bug" });
150
+ const { comp } = makeComponent({ records: [rec], rows: 24 });
151
+ const joined = comp.render(80).join("\n");
152
+ expect(joined).toContain("researcher"); // 左列 agent
153
+ expect(joined).toContain("investigate bug"); // 右列 task 首行
154
+ expect(joined).toContain("Records"); // 分区线标题
155
+ expect(joined).toContain("Detail");
156
+ });
157
+ });
158
+
159
+ // ── hasRunning ────────────────────────────────────────
160
+ describe("hasRunning", () => {
161
+ it("records 全是 done → false", () => {
162
+ const { comp } = makeComponent({
163
+ records: [makeRecord({ status: "done" }), makeRecord({ status: "failed" })],
164
+ });
165
+ expect(comp.hasRunning()).toBe(false);
166
+ });
167
+
168
+ it("records 含一个 running → true", () => {
169
+ const { comp } = makeComponent({
170
+ records: [
171
+ makeRecord({ id: "a", status: "done" }),
172
+ makeRecord({ id: "b", status: "running" }),
173
+ ],
174
+ });
175
+ expect(comp.hasRunning()).toBe(true);
176
+ });
177
+ });
178
+
179
+ // ── 左列视口窗口 ──────────────────────────────────────
180
+ describe("左列视口窗口", () => {
181
+ it("records 数 > bodyH → render 行数 ≤ rows(不溢出终端)", () => {
182
+ // rows=24 → 内框高 innerRows = 24 - PAD_ROWS(2) = 22;bodyH = 22 - SPLIT_FIXED_LINES(6) = 16。
183
+ // 30 条 records 远超 bodyH,选中行在中间。断言输出总行数 == rows(overlay 填满全屏),
184
+ // 且左列只渲染 bodyH 条(不溢出导致底框被推出终端)。
185
+ const records = makeRecords(30);
186
+ const { comp } = makeComponent({ records, rows: 24, selectedIdx: 15 });
187
+ const lines = comp.render(80);
188
+ expect(lines.length).toBe(24); // 填满全屏,不溢出
189
+ // 中间 record 的 agent 不在视口窗口外应仍可见(窗口居中显示 selectedIdx 附近)。
190
+ const joined = lines.join("\n");
191
+ expect(joined).toContain("agent-15"); // 选中行在视口内
192
+ });
193
+
194
+ it("selectedIdx 在尾部 → 视口贴底(最后一条可见)", () => {
195
+ const records = makeRecords(30);
196
+ const { comp } = makeComponent({ records, rows: 24, selectedIdx: 29 });
197
+ const joined = comp.render(80).join("\n");
198
+ expect(joined).toContain("agent-29"); // 尾部 record 仍可见(视口贴底)
199
+ });
200
+ });
201
+
202
+ // ── 右列预览兜底链 ────────────────────────────────────
203
+ describe("右列预览兜底链 (renderRightPreview)", () => {
204
+ it("record 有 displayItems → 输出含 displayItems 内容", () => {
205
+ const rec = makeRecord({
206
+ displayItems: [{ type: "text", text: "partial analysis output" }],
207
+ });
208
+ const { comp } = makeComponent({ records: [rec], rows: 24 });
209
+ const joined = comp.render(80).join("\n");
210
+ expect(joined).toContain("partial analysis output");
211
+ });
212
+
213
+ it("record 无 displayItems 但有 eventLog → 回退 eventLog(输出含 event 标签)", () => {
214
+ const rec = makeRecord({
215
+ displayItems: [],
216
+ eventLog: [{ type: "tool_start", label: "Read file.ts", ts: 1500 }],
217
+ });
218
+ const { comp } = makeComponent({ records: [rec], rows: 24 });
219
+ const joined = comp.render(80).join("\n");
220
+ // formatEventLine 的 tool_start 输出 "tool: <label>"
221
+ expect(joined).toContain("tool:");
222
+ expect(joined).toContain("Read file.ts");
223
+ });
224
+
225
+ it("record 两者都无 → 输出 (no output) 兜底文案", () => {
226
+ const rec = makeRecord({ displayItems: [], eventLog: [] });
227
+ const { comp } = makeComponent({ records: [rec], rows: 24 });
228
+ const joined = comp.render(80).join("\n");
229
+ expect(joined).toContain("(no output)");
230
+ });
231
+ });
232
+
233
+ // ── handleInput ───────────────────────────────────────
234
+ describe("handleInput", () => {
235
+ it("disposed → 直接返回,不调用 keyHandler", () => {
236
+ const keyHandler = vi.fn(() => KEY_NONE);
237
+ const { comp, state } = makeComponent({ keyHandler });
238
+ state.disposed = true;
239
+ comp.handleInput("x");
240
+ expect(keyHandler).not.toHaveBeenCalled();
241
+ });
242
+
243
+ it("keyHandler 返回 exit → 调用 closeFn 关闭 overlay", () => {
244
+ const keyHandler = vi.fn(() => KEY_EXIT);
245
+ const { comp } = makeComponent({ keyHandler });
246
+ const closeFn = vi.fn();
247
+ comp.setCloseFn(closeFn);
248
+ comp.handleInput("\x1b"); // Esc
249
+ expect(closeFn).toHaveBeenCalledTimes(1);
250
+ });
251
+
252
+ it("keyHandler 返回 changed → invalidate + requestRender", () => {
253
+ const keyHandler = vi.fn(() => KEY_CHANGED);
254
+ const { comp, tui } = makeComponent({ keyHandler });
255
+ comp.handleInput("\x1b[B"); // Down
256
+ expect(tui.requestRender).toHaveBeenCalledTimes(1);
257
+ });
258
+
259
+ it("keyHandler 返回 none → 不 invalidate / 不 close / 不 requestRender", () => {
260
+ const keyHandler = vi.fn(() => KEY_NONE);
261
+ const closeFn = vi.fn();
262
+ const { comp, tui } = makeComponent({ keyHandler });
263
+ comp.setCloseFn(closeFn);
264
+ comp.handleInput("x");
265
+ expect(closeFn).not.toHaveBeenCalled();
266
+ expect(tui.requestRender).not.toHaveBeenCalled();
267
+ });
268
+
269
+ it("exit 优先于 changed(exit=true 时只 close,不 requestRender)", () => {
270
+ const keyHandler = vi.fn(() => ({ changed: true, exit: true }));
271
+ const closeFn = vi.fn();
272
+ const { comp, tui } = makeComponent({ keyHandler });
273
+ comp.setCloseFn(closeFn);
274
+ comp.handleInput("\x1b");
275
+ expect(closeFn).toHaveBeenCalledTimes(1);
276
+ expect(tui.requestRender).not.toHaveBeenCalled();
277
+ });
278
+ });
279
+
280
+ // ── render 缓存 ───────────────────────────────────────
281
+ describe("render 缓存", () => {
282
+ it("相同 width 连续两次 render → 返回相同结果(缓存命中)", () => {
283
+ const records = [makeRecord({ agent: "cached-agent" })];
284
+ const { comp } = makeComponent({ records, rows: 24 });
285
+ const first = comp.render(80);
286
+ const second = comp.render(80);
287
+ // 缓存命中:返回同一引用(buildLines 未重新执行)。
288
+ expect(second).toBe(first);
289
+ });
290
+
291
+ it("invalidate 后再 render → 重建(结果内容相同但引用不同)", () => {
292
+ const records = [makeRecord()];
293
+ const { comp } = makeComponent({ records, rows: 24 });
294
+ const first = comp.render(80);
295
+ comp.invalidate();
296
+ const second = comp.render(80);
297
+ expect(second).not.toBe(first); // 引用不同 → 重建
298
+ expect(second).toEqual(first); // 内容相同
299
+ });
300
+
301
+ it("不同 width → 不命中缓存(重新构建)", () => {
302
+ const records = [makeRecord()];
303
+ const { comp } = makeComponent({ records, rows: 24 });
304
+ const w80 = comp.render(80);
305
+ const w100 = comp.render(100);
306
+ // 宽度不同 → 不同 key → 重建。行数应随宽度变化内容(至少引用不同)。
307
+ expect(w100).not.toBe(w80);
308
+ });
309
+ });
310
+
311
+ // ── detailMode 切换 ───────────────────────────────────
312
+ describe("detailMode 切换", () => {
313
+ it("detailMode=true → footer 含 Esc back + 右侧锚定提示 Pinned", () => {
314
+ const rec = makeRecord({ agent: "pinned-agent" });
315
+ const { comp } = makeComponent({ records: [rec], rows: 24, detailMode: true });
316
+ const joined = comp.render(80).join("\n");
317
+ expect(joined).toContain("Esc back to list");
318
+ expect(joined).toContain("Pinned:");
319
+ expect(joined).toContain("pinned-agent");
320
+ });
321
+
322
+ it("detailMode=false → footer 含 navigate / Enter detail(非锚定文案)", () => {
323
+ const rec = makeRecord();
324
+ const { comp } = makeComponent({ records: [rec], rows: 24, detailMode: false });
325
+ const joined = comp.render(80).join("\n");
326
+ expect(joined).toContain("Enter detail");
327
+ expect(joined).not.toContain("Pinned:");
328
+ });
329
+
330
+ it("detailMode 下完整详情含 result + sessionFile(预览阶段不显示 sessionFile)", () => {
331
+ const rec = makeRecord({
332
+ result: "final report content",
333
+ sessionFile: "/tmp/session-abc.jsonl",
334
+ });
335
+ // 预览阶段
336
+ const previewComp = makeComponent({ records: [rec], rows: 24, detailMode: false });
337
+ const previewJoined = previewComp.comp.render(80).join("\n");
338
+ expect(previewJoined).toContain("Enter for full detail"); // 预览阶段提示
339
+ // 详情阶段
340
+ const detailComp = makeComponent({ records: [rec], rows: 24, detailMode: true });
341
+ const detailJoined = detailComp.comp.render(80).join("\n");
342
+ expect(detailJoined).toContain("final report content"); // Result 段
343
+ expect(detailJoined).toContain("Result:");
344
+ expect(detailJoined).toContain("session-abc.jsonl"); // sessionFile
345
+ });
346
+ });
347
+ });