@zhushanwen/pi-subagent-workflow 7.3.4 → 8.0.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.
- package/README.md +39 -12
- package/agents/analyst.md +61 -0
- package/agents/coder.md +70 -0
- package/agents/debugger.md +67 -0
- package/agents/doc-reviewer.md +3 -3
- package/agents/explorer.md +50 -18
- package/agents/general-purpose.md +19 -8
- package/agents/orchestrator.md +37 -32
- package/agents/planner.md +45 -11
- package/agents/researcher.md +53 -11
- package/agents/reviewer.md +74 -0
- package/package.json +1 -1
- package/skills/workflow-script-format/SKILL.md +1 -1
- package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
- package/src/execution/__tests__/agent-registry.test.ts +13 -11
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
- package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
- package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
- package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
- package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
- package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
- package/src/execution/__tests__/crash-recovery.test.ts +8 -2
- package/src/execution/__tests__/delivery-methods.test.ts +385 -0
- package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
- package/src/execution/__tests__/execute-nesting.test.ts +20 -72
- package/src/execution/__tests__/execution-record.test.ts +199 -0
- package/src/execution/__tests__/finalize-record.test.ts +170 -14
- package/src/execution/__tests__/format.test.ts +131 -7
- package/src/execution/__tests__/gc-timer.test.ts +184 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +25 -7
- package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
- package/src/execution/__tests__/index-session-start.test.ts +257 -5
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
- package/src/execution/__tests__/list-component.test.ts +59 -5
- package/src/execution/__tests__/list-fields.test.ts +109 -0
- package/src/execution/__tests__/model-resolver.test.ts +38 -1
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
- package/src/execution/__tests__/nested-visibility.test.ts +325 -0
- package/src/execution/__tests__/notifier-flush.test.ts +209 -7
- package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
- package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
- package/src/execution/__tests__/record-store.test.ts +158 -52
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
- package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
- package/src/execution/__tests__/resource-policy.test.ts +109 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
- package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
- package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
- package/src/execution/__tests__/session-pending.test.ts +61 -2
- package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
- package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
- package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
- package/src/execution/__tests__/spawn-args.test.ts +127 -49
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
- package/src/execution/__tests__/spawned-children.test.ts +92 -0
- package/src/execution/__tests__/status-refactor.test.ts +345 -0
- package/src/execution/__tests__/stdin-writer.test.ts +97 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +598 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
- package/src/execution/__tests__/subagent-service.test.ts +49 -11
- package/src/execution/__tests__/timeout-integration.test.ts +27 -13
- package/src/execution/__tests__/tool-action.test.ts +12 -10
- package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
- package/src/execution/__tests__/worktree-manager.test.ts +292 -89
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +13 -12
- package/src/execution/argv-mirror.ts +21 -2
- package/src/execution/execution-record.ts +126 -9
- package/src/execution/finalize-record.ts +90 -13
- package/src/execution/host-mode.ts +1 -1
- package/src/execution/lifecycle-manager.ts +484 -0
- package/src/execution/lifecycle-predicates.ts +65 -0
- package/src/execution/manifest-store.ts +61 -16
- package/src/execution/model-resolver.ts +26 -5
- package/src/execution/notifier.ts +69 -12
- package/src/execution/pi-invocation.ts +21 -1
- package/src/execution/record-store.ts +554 -107
- package/src/execution/session-pending.ts +116 -45
- package/src/execution/session-reconstructor.ts +224 -7
- package/src/execution/session-runner.ts +290 -75
- package/src/execution/sessions-index.ts +304 -0
- package/src/execution/stdin-writer.ts +93 -7
- package/src/execution/stream-sink.ts +20 -3
- package/src/execution/subagent-service.ts +867 -138
- package/src/execution/turn-limiter.ts +14 -0
- package/src/execution/types.ts +204 -22
- package/src/execution/worktree-manager.ts +128 -49
- package/src/execution/worktree-registry.ts +13 -2
- package/src/index.ts +277 -19
- package/src/injectors/subagent-list-injector.ts +26 -8
- package/src/injectors/workflow-list-injector.ts +25 -8
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +18 -5
- package/src/interface/__tests__/tool-render.test.ts +15 -13
- package/src/interface/bg-notify-render.ts +32 -8
- package/src/interface/command-actions.ts +26 -7
- package/src/interface/commands.ts +21 -22
- package/src/interface/format.ts +44 -17
- package/src/interface/gui-mappers.ts +6 -8
- package/src/interface/helpers.ts +170 -10
- package/src/interface/list-component.ts +53 -14
- package/src/interface/subagent-actions.ts +235 -17
- package/src/interface/subagent-tool.ts +82 -17
- package/src/interface/subagents.ts +2 -1
- package/src/interface/tool-render.ts +11 -24
- package/src/interface/tool-workflow.ts +20 -35
- package/src/interface/views/WorkflowsView.ts +89 -32
- package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
- package/src/interface/views/__tests__/detail-content-session-file.test.ts +1 -1
- package/src/interface/views/format.ts +3 -3
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +325 -0
- package/src/orchestration/__tests__/args-validator.test.ts +1 -1
- package/src/orchestration/__tests__/config-loader.test.ts +38 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
- package/src/orchestration/__tests__/execute-agent-call.test.ts +95 -0
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +657 -18
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
- package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
- package/src/orchestration/__tests__/skill-discovery.test.ts +157 -0
- package/src/orchestration/__tests__/test-mocks.ts +191 -0
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +98 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
- package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
- package/src/orchestration/__tests__/workflows-e2e.test.ts +1 -1
- package/src/orchestration/agent-opts-resolver.ts +4 -1
- package/src/orchestration/args-validator.ts +2 -2
- package/src/orchestration/config-loader.ts +30 -1
- package/src/orchestration/error-recovery.ts +133 -29
- package/src/orchestration/execute-agent-call.ts +31 -7
- package/src/orchestration/jsonl-run-store.ts +287 -40
- package/src/orchestration/launcher.ts +7 -1
- package/src/orchestration/lifecycle.ts +135 -132
- package/src/orchestration/models/__tests__/trace.test.ts +408 -0
- package/src/orchestration/models/budget.ts +1 -1
- package/src/orchestration/models/run-runtime.ts +15 -17
- package/src/orchestration/models/run-spec.ts +2 -2
- package/src/orchestration/models/run-state.ts +3 -3
- package/src/orchestration/models/trace.ts +95 -15
- package/src/orchestration/models/types.ts +8 -9
- package/src/orchestration/models/workflow-run.ts +50 -71
- package/src/orchestration/models/workflow-script.ts +32 -1
- package/src/orchestration/skill-discovery.ts +30 -0
- package/src/orchestration/worker-handle.ts +1 -1
- package/src/orchestration/worker-host.ts +1 -1
- package/src/orchestration/worker-script-builder.ts +29 -10
- package/src/shared/__tests__/agent-ref.test.ts +34 -0
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
- package/src/shared/__tests__/resource-discovery.test.ts +55 -0
- package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
- package/src/shared/agent-ref.ts +16 -0
- package/src/shared/resource-discovery.ts +147 -58
- package/src/shared/schema-jsonify.ts +53 -0
- package/workflows/README.md +4 -4
- package/agents/code-reviewer.md +0 -47
- package/agents/context-builder.md +0 -21
- package/agents/oracle.md +0 -34
- package/agents/worker.md +0 -20
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// src/execution/__tests__/parent-child-matrix.test.ts
|
|
2
|
+
//
|
|
3
|
+
// SP-4: 父子联动测试(真实 SubagentService + index.ts 事件接线)。
|
|
4
|
+
//
|
|
5
|
+
// [M2/M7 重写] 旧套件在测试内手写 tryTransition/completeRecord/recentlyCascaded 模拟
|
|
6
|
+
// (平行复刻生产逻辑),生产代码任何回归恒绿。现改为两层真实驱动:
|
|
7
|
+
// Block 1: 真实 SubagentService.onParentFork / onParentNew / disposeAllRecords
|
|
8
|
+
// → 断言 record 转 closed + closedReason + archive + pending:unregister
|
|
9
|
+
// Block 2: 真实 index.ts factory 注册的 session_before_fork / session_before_switch
|
|
10
|
+
// handler 驱动同一真实 Service → 断言事件接线与 reason 门控
|
|
11
|
+
//
|
|
12
|
+
// 事件语义依据 SDK 0.82.1(core/agent-session-runtime.js + core/agent-session.js):
|
|
13
|
+
// /fork → session_before_fork(entryId, position) → teardown("fork")
|
|
14
|
+
// /new → session_before_switch(reason:"new") → teardown("new")
|
|
15
|
+
// (/new 从不触发 session_before_tree——旧实现误挂即 M2 bug)
|
|
16
|
+
// /resume /import → session_before_switch(reason:"resume")
|
|
17
|
+
// /tree → session_before_tree(同 session 内分支导航,无 session 替换)
|
|
18
|
+
// + session_tree(导航后)——均无级联关闭语义
|
|
19
|
+
|
|
20
|
+
import * as fs from "node:fs";
|
|
21
|
+
import * as os from "node:os";
|
|
22
|
+
import * as path from "node:path";
|
|
23
|
+
|
|
24
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
25
|
+
|
|
26
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
27
|
+
|
|
28
|
+
// mock logger(import 链需要 getLogger 存在;对齐 subagent-service-parent-guard.test.ts。
|
|
29
|
+
// setPiHandle 是 index.ts factory 顶层调用(注入 pi handle 给全局 logger),需一并 stub)
|
|
30
|
+
const { loggerMock } = vi.hoisted(() => ({
|
|
31
|
+
loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn(), info: vi.fn() },
|
|
32
|
+
}));
|
|
33
|
+
vi.mock("@zhushanwen/pi-extension-logger", () => ({
|
|
34
|
+
getLogger: () => loggerMock,
|
|
35
|
+
setPiHandle: vi.fn(),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
// mock session-runner(import 链需要 runSpawn/killAllSpawnedChildren/getChildByRecord;
|
|
39
|
+
// 避免真实 kill,且 dispose 收割路径可断言)
|
|
40
|
+
const { killAllSpawnedChildrenMock } = vi.hoisted(() => ({
|
|
41
|
+
killAllSpawnedChildrenMock: vi.fn(),
|
|
42
|
+
}));
|
|
43
|
+
vi.mock("../session-runner.ts", () => ({
|
|
44
|
+
runSpawn: vi.fn(),
|
|
45
|
+
killAllSpawnedChildren: killAllSpawnedChildrenMock,
|
|
46
|
+
getChildByRecord: vi.fn(() => undefined),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
import { createRecord } from "../execution-record.ts";
|
|
50
|
+
import { ModelConfigService } from "../model-config-service.ts";
|
|
51
|
+
import { RecordStore } from "../record-store.ts";
|
|
52
|
+
import { SubagentService, setSubagentService } from "../subagent-service.ts";
|
|
53
|
+
import type { PiLike } from "../subagent-service.ts";
|
|
54
|
+
import type { ExecutionRecord } from "../types.ts";
|
|
55
|
+
import subagentsExtension from "../../index.ts";
|
|
56
|
+
|
|
57
|
+
// ── helpers ──
|
|
58
|
+
|
|
59
|
+
function makeTmpAgentDir(): string {
|
|
60
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "parent-child-matrix-"));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 最小 mock PiLike(appendEntry/events.emit/sendMessage 全部可断言)。 */
|
|
64
|
+
function makePi(): { pi: PiLike; eventsEmit: ReturnType<typeof vi.fn> } {
|
|
65
|
+
const eventsEmit = vi.fn();
|
|
66
|
+
const pi = {
|
|
67
|
+
appendEntry: vi.fn(),
|
|
68
|
+
events: { emit: eventsEmit },
|
|
69
|
+
sendMessage: vi.fn(),
|
|
70
|
+
} as unknown as PiLike;
|
|
71
|
+
return { pi, eventsEmit };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 暴露私有字段供测试注入 record(对齐 subagent-service-parent-guard.test.ts 模式)。 */
|
|
75
|
+
interface ServiceInternals {
|
|
76
|
+
store: RecordStore;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 构造一个 running 状态的 ExecutionRecord(内存态,register 进真实 store)。 */
|
|
80
|
+
function makeRunningRecord(id: string, overrides: Partial<ExecutionRecord> = {}): ExecutionRecord {
|
|
81
|
+
return createRecord(id, {
|
|
82
|
+
agent: "general-purpose",
|
|
83
|
+
model: "test/model",
|
|
84
|
+
mode: "background",
|
|
85
|
+
task: `task for ${id}`,
|
|
86
|
+
slug: `slug-${id}`,
|
|
87
|
+
startedAt: Date.now(),
|
|
88
|
+
rootSessionId: "root-session",
|
|
89
|
+
controller: new AbortController(),
|
|
90
|
+
...overrides,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 重置进程级 SubagentService 单例槽(setSubagentService 不接受 null,测试清理用)。
|
|
95
|
+
* key 与生产 getServiceSlot 的 SERVICE_SLOT_KEY(subagent-service.ts)一致。 */
|
|
96
|
+
function resetServiceSlot(): void {
|
|
97
|
+
const slot = Reflect.get(globalThis, Symbol.for("@zhushanwen/pi-subagents.service")) as
|
|
98
|
+
| { current: SubagentService | null }
|
|
99
|
+
| undefined;
|
|
100
|
+
if (slot) slot.current = null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 创建真实 SubagentService(tmp agentDir + mock pi),返回 service 与内部 store。 */
|
|
104
|
+
function setupRealService(): { service: SubagentService; store: RecordStore } {
|
|
105
|
+
const agentDir = makeTmpAgentDir();
|
|
106
|
+
const modelService = new ModelConfigService({ agentDir });
|
|
107
|
+
const service = new SubagentService({ cwd: agentDir, modelService });
|
|
108
|
+
service.initSession({ pi: makePi().pi, sessionId: "root-session" });
|
|
109
|
+
const store = (service as unknown as ServiceInternals).store;
|
|
110
|
+
return { service, store, agentDir };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============================================================
|
|
114
|
+
// Block 1: 真实 SubagentService 级联关闭
|
|
115
|
+
// ============================================================
|
|
116
|
+
|
|
117
|
+
describe("SP-4 级联关闭(真实 SubagentService)", () => {
|
|
118
|
+
let agentDir: string;
|
|
119
|
+
let service: SubagentService;
|
|
120
|
+
let store: RecordStore;
|
|
121
|
+
let eventsEmit: ReturnType<typeof vi.fn>;
|
|
122
|
+
|
|
123
|
+
beforeEach(() => {
|
|
124
|
+
agentDir = makeTmpAgentDir();
|
|
125
|
+
const modelService = new ModelConfigService({ agentDir });
|
|
126
|
+
const { pi, eventsEmit: emit } = makePi();
|
|
127
|
+
eventsEmit = emit;
|
|
128
|
+
service = new SubagentService({ cwd: agentDir, modelService });
|
|
129
|
+
service.initSession({ pi, sessionId: "root-session" });
|
|
130
|
+
store = (service as unknown as ServiceInternals).store;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
afterEach(() => {
|
|
134
|
+
service.dispose();
|
|
135
|
+
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("onParentFork:running record 转 closed + closedReason=parent-fork,并从内存归档", () => {
|
|
139
|
+
const record = makeRunningRecord("sa-fork-1");
|
|
140
|
+
store.register(record);
|
|
141
|
+
|
|
142
|
+
const count = service.onParentFork();
|
|
143
|
+
|
|
144
|
+
expect(count).toBe(1);
|
|
145
|
+
expect(record.status).toBe("closed");
|
|
146
|
+
expect(record.closedReason).toBe("parent-fork");
|
|
147
|
+
expect(record.endedAt).toBeDefined();
|
|
148
|
+
// completeRecord 冻结合成 AgentResult(disposeAllRecords 构造,无在途执行)
|
|
149
|
+
expect(record.error).toBe("closed due to parent-fork");
|
|
150
|
+
// archive 生效:终态 record 立即从内存移除(读时从 session.jsonl 重建)
|
|
151
|
+
expect(store.getMutable("sa-fork-1")).toBeUndefined();
|
|
152
|
+
expect(store.listAllActive()).toHaveLength(0);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("onParentNew:running record 转 closed + closedReason=parent-new", () => {
|
|
156
|
+
const record = makeRunningRecord("sa-new-1");
|
|
157
|
+
store.register(record);
|
|
158
|
+
|
|
159
|
+
const count = service.onParentNew();
|
|
160
|
+
|
|
161
|
+
expect(count).toBe(1);
|
|
162
|
+
expect(record.status).toBe("closed");
|
|
163
|
+
expect(record.closedReason).toBe("parent-new");
|
|
164
|
+
expect(store.getMutable("sa-new-1")).toBeUndefined();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("多个 running record 全部级联关闭,返回 count 与 record 数一致", () => {
|
|
168
|
+
const records = [
|
|
169
|
+
makeRunningRecord("sa-multi-1"),
|
|
170
|
+
makeRunningRecord("sa-multi-2"),
|
|
171
|
+
makeRunningRecord("sa-multi-3"),
|
|
172
|
+
];
|
|
173
|
+
for (const r of records) store.register(r);
|
|
174
|
+
|
|
175
|
+
const count = service.onParentNew();
|
|
176
|
+
|
|
177
|
+
expect(count).toBe(3);
|
|
178
|
+
for (const r of records) {
|
|
179
|
+
expect(r.status).toBe("closed");
|
|
180
|
+
expect(r.closedReason).toBe("parent-new");
|
|
181
|
+
}
|
|
182
|
+
expect(store.listAllActive()).toHaveLength(0);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("级联关闭对每个被关 record 发 pending:unregister(reason=closed)", () => {
|
|
186
|
+
const record = makeRunningRecord("sa-pending-1");
|
|
187
|
+
store.register(record);
|
|
188
|
+
|
|
189
|
+
service.onParentFork();
|
|
190
|
+
|
|
191
|
+
expect(eventsEmit).toHaveBeenCalledWith("pending:unregister", {
|
|
192
|
+
id: "sa-pending-1",
|
|
193
|
+
reason: "closed",
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("幂等:无活跃 record 返回 0;级联后再次调用返回 0(record 已归档)", () => {
|
|
198
|
+
expect(service.onParentNew()).toBe(0);
|
|
199
|
+
|
|
200
|
+
const record = makeRunningRecord("sa-idem-1");
|
|
201
|
+
store.register(record);
|
|
202
|
+
expect(service.onParentFork()).toBe(1);
|
|
203
|
+
expect(service.onParentFork()).toBe(0); // 已 closed + archived,不重复关
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ============================================================
|
|
208
|
+
// Block 2: index.ts 事件接线(真实 factory + 真实 Service)
|
|
209
|
+
// ============================================================
|
|
210
|
+
|
|
211
|
+
describe("SP-4 index.ts 事件接线", () => {
|
|
212
|
+
type PiHandler = (...args: unknown[]) => void | Promise<void>;
|
|
213
|
+
const registered = new Map<string, PiHandler>();
|
|
214
|
+
let processOnSpy: ReturnType<typeof vi.spyOn>;
|
|
215
|
+
let agentDir: string;
|
|
216
|
+
let service: SubagentService;
|
|
217
|
+
let store: RecordStore;
|
|
218
|
+
|
|
219
|
+
/** 最小 mock ExtensionAPI:捕获 pi.on 注册的 handler(对齐 process-shutdown-hook.test.ts 模式)。 */
|
|
220
|
+
function createMockPi(): ExtensionAPI {
|
|
221
|
+
const noop = (): void => {
|
|
222
|
+
/* mock */
|
|
223
|
+
};
|
|
224
|
+
return {
|
|
225
|
+
registerTool: vi.fn(),
|
|
226
|
+
registerCommand: vi.fn(),
|
|
227
|
+
registerMessageRenderer: vi.fn(),
|
|
228
|
+
on: (event: string, handler: PiHandler) => {
|
|
229
|
+
registered.set(event, handler);
|
|
230
|
+
},
|
|
231
|
+
appendEntry: vi.fn(),
|
|
232
|
+
events: { emit: vi.fn(), on: vi.fn() },
|
|
233
|
+
} as unknown as ExtensionAPI;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
beforeEach(() => {
|
|
237
|
+
// 捕获 process.on 注册(不真实注册,防 listener 跨用例泄漏)——factory 顶层注册
|
|
238
|
+
// SIGTERM/SIGINT/beforeExit 三个 process hook(见 process-shutdown-hook.test.ts)。
|
|
239
|
+
processOnSpy = vi.spyOn(process, "on").mockImplementation((() => process) as never);
|
|
240
|
+
|
|
241
|
+
registered.clear();
|
|
242
|
+
killAllSpawnedChildrenMock.mockReset();
|
|
243
|
+
|
|
244
|
+
const ctx = setupRealService();
|
|
245
|
+
agentDir = ctx.agentDir;
|
|
246
|
+
service = ctx.service;
|
|
247
|
+
store = ctx.store;
|
|
248
|
+
setSubagentService(service);
|
|
249
|
+
|
|
250
|
+
subagentsExtension(createMockPi());
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
afterEach(() => {
|
|
254
|
+
processOnSpy.mockRestore();
|
|
255
|
+
service.dispose();
|
|
256
|
+
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
257
|
+
resetServiceSlot();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("factory 注册 session_before_fork 与 session_before_switch handler", () => {
|
|
261
|
+
expect(registered.get("session_before_fork")).toBeTypeOf("function");
|
|
262
|
+
expect(registered.get("session_before_switch")).toBeTypeOf("function");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("[M2] 不注册 session_before_tree——/tree 同 session 分支导航不得级联杀 subagent", () => {
|
|
266
|
+
// SDK 中 session_before_tree 只由 navigateTree(/tree)触发,与 /new 无关。
|
|
267
|
+
// 旧实现误挂此事件当 /new 级联入口 → 普通 /tree 导航误杀全部活跃 subagent。
|
|
268
|
+
// 本断言是 M2 回归守卫:该事件不得再注册级联 handler。
|
|
269
|
+
expect(registered.has("session_before_tree")).toBe(false);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("[M2] session_before_switch(reason:'new') 触发 /new 级联:record 转 closed + parent-new", () => {
|
|
273
|
+
const record = makeRunningRecord("sa-wire-new-1");
|
|
274
|
+
store.register(record);
|
|
275
|
+
const onParentNewSpy = vi.spyOn(service, "onParentNew");
|
|
276
|
+
|
|
277
|
+
registered.get("session_before_switch")!(
|
|
278
|
+
{ type: "session_before_switch", reason: "new" },
|
|
279
|
+
undefined,
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
expect(onParentNewSpy).toHaveBeenCalledTimes(1);
|
|
283
|
+
expect(record.status).toBe("closed");
|
|
284
|
+
expect(record.closedReason).toBe("parent-new");
|
|
285
|
+
expect(store.getMutable("sa-wire-new-1")).toBeUndefined();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("[M2] session_before_switch(reason:'resume') 不级联:/resume 回到已有 session,record 保持 running", () => {
|
|
289
|
+
const record = makeRunningRecord("sa-wire-resume-1");
|
|
290
|
+
store.register(record);
|
|
291
|
+
const onParentNewSpy = vi.spyOn(service, "onParentNew");
|
|
292
|
+
|
|
293
|
+
registered.get("session_before_switch")!(
|
|
294
|
+
{
|
|
295
|
+
type: "session_before_switch",
|
|
296
|
+
reason: "resume",
|
|
297
|
+
targetSessionFile: "/tmp/other-session.jsonl",
|
|
298
|
+
},
|
|
299
|
+
undefined,
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
expect(onParentNewSpy).not.toHaveBeenCalled();
|
|
303
|
+
expect(record.status).toBe("running");
|
|
304
|
+
expect(store.getMutable("sa-wire-resume-1")).toBe(record);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("session_before_fork 触发 /fork 级联:record 转 closed + parent-fork", () => {
|
|
308
|
+
const record = makeRunningRecord("sa-wire-fork-1");
|
|
309
|
+
store.register(record);
|
|
310
|
+
const onParentForkSpy = vi.spyOn(service, "onParentFork");
|
|
311
|
+
|
|
312
|
+
registered.get("session_before_fork")!(
|
|
313
|
+
{ type: "session_before_fork", entryId: "entry-1", position: "before" },
|
|
314
|
+
undefined,
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
expect(onParentForkSpy).toHaveBeenCalledTimes(1);
|
|
318
|
+
expect(record.status).toBe("closed");
|
|
319
|
+
expect(record.closedReason).toBe("parent-fork");
|
|
320
|
+
expect(store.getMutable("sa-wire-fork-1")).toBeUndefined();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("service 单例未初始化(getSubagentService()=null)时 handler 安全 no-op", () => {
|
|
324
|
+
resetServiceSlot();
|
|
325
|
+
expect(() => {
|
|
326
|
+
registered.get("session_before_switch")!(
|
|
327
|
+
{ type: "session_before_switch", reason: "new" },
|
|
328
|
+
undefined,
|
|
329
|
+
);
|
|
330
|
+
registered.get("session_before_fork")!(
|
|
331
|
+
{ type: "session_before_fork", entryId: "e", position: "before" },
|
|
332
|
+
undefined,
|
|
333
|
+
);
|
|
334
|
+
}).not.toThrow();
|
|
335
|
+
});
|
|
336
|
+
});
|