@zhushanwen/pi-base-tool-enhance 0.9.0 → 0.10.1

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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # @zhushanwen/pi-base-tool-enhance
2
2
 
3
- 同名 override pi 内置 bash 工具的增强层:前台行为 100% 委托 pi 官方工厂(`createBashToolDefinition`),增量提供 background 模式、强制后台白名单与双模式可配置超时。能力面:测试类命令 force-test 白名单命中自动转后台(模式登记于 force-patterns.ts)、前台挂死保护(可配置前台默认超时,对一切未显式填 timeout 的前台命令生效——典型挂死场景是网络类命令)、工具报错审计(tool_error 审计 hook,entry customType 为 `unified-hooks:tool-error`)。
3
+ 同名 override pi 内置 bash 工具的增强层:前台行为 100% 委托 pi 官方工厂(`createBashToolDefinition`),增量提供 background 模式、强制后台白名单与双模式可配置超时。能力侧:测试类命令 force-test 白名单命中自动转后台(模式登记于 force-patterns.ts)、前台挂死保护(可配置前台默认超时,对一切未显式填 timeout 的前台命令生效——典型挂死场景是网络类命令)、工具报错审计(tool_error 审计 hook,entry customType 为 `unified-hooks:tool-error`)。
4
4
 
5
- 设计决策沉淀在包内源码注释与本文档。
5
+ 设计决策记录在案于包内源码注释与本文档。
6
6
 
7
7
  ## 安装
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-base-tool-enhance",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "Base tool enhancement - override builtin bash tool: foreground delegates to pi official factory, incremental background mode (design decisions documented in source comments)",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -28,11 +28,12 @@
28
28
  "skills/"
29
29
  ],
30
30
  "dependencies": {
31
- "@zhushanwen/extension-protocol": "0.21.0",
31
+ "@zhushanwen/extension-protocol": "0.23.0",
32
+ "@zhushanwen/pi-delivery-port": "0.2.1",
32
33
  "@zhushanwen/pi-ext-guards": "0.5.0",
33
34
  "@zhushanwen/pi-extension-logger": "0.6.2",
34
35
  "@zhushanwen/pi-file-lock": "0.4.1",
35
- "@zhushanwen/pi-llm-shared": "0.11.3"
36
+ "@zhushanwen/pi-llm-shared": "0.12.0"
36
37
  },
37
38
  "devDependencies": {
38
39
  "@vitest/coverage-v8": "^4.1.9",
@@ -42,7 +43,7 @@
42
43
  "@earendil-works/pi-coding-agent": "^1.0.0",
43
44
  "@earendil-works/pi-tui": "^1.0.0",
44
45
  "typebox": "1.3.27",
45
- "@zhushanwen/pi-pending-notifications": "0.9.0"
46
+ "@zhushanwen/pi-pending-notifications": "0.10.1"
46
47
  },
47
48
  "peerDependenciesMeta": {
48
49
  "@earendil-works/pi-tui": {
@@ -61,7 +61,7 @@ description: "使用或排查 @zhushanwen/pi-base-tool-enhance 的 bash 后台/
61
61
 
62
62
  用户正则**自动加命令位置锚定前缀**(`CMD_ANCHOR`:行首,或 `;` / `&&` / `||` / `|` / 换行之后的命令起始位,后随可选空白)——与内置条目统一匹配语义:
63
63
 
64
- - **无需也不应自带 `^`**:写 `pnpm\s+typecheck` 即可。自带 `^` 反而收窄匹配(`^` 只钉死整条命令第一段,`cmd1 && pnpm typecheck` 中的第二段就匹配不到了)。
64
+ - **无需也不应自带 `^`**:写 `pnpm\s+typecheck` 即可。自带 `^` 反而收窄匹配(`^` 只固定整条命令第一段,`cmd1 && pnpm typecheck` 中的第二段就匹配不到了)。
65
65
  - **不做裸子串匹配(防误伤)**:参数文本里的命令词不会命中——`git commit -m "fix: npm test"` 不触发后台(unified-hooks 时代的 `\s` 前缀会误伤,本包已收紧为命令位置锚定)。
66
66
  - **wrapper 局限(漏报方向)**:`sudo npm test` / `timeout 300 npm test` / `xargs npm test` 这类 wrapper 形态不命中(wrapper 名占命令位置)。漏报无害——force 命中本就是非破坏性的,模型可显式传 `background: true` 兜底。
67
67
 
@@ -11,12 +11,8 @@ import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vites
11
11
  vi.setConfig({ testTimeout: 20000 });
12
12
 
13
13
  // getAgentDir → 测试临时数据目录(工具 execute 内部调用,不写真 ~/.pi/agent)。
14
- // hoisted 回调里不能引用顶层 import(初始化顺序),用可变引用在 import 完成后注入
15
- const { dataDirRef } = vi.hoisted(() => ({ dataDirRef: { dir: "" } }));
16
- vi.mock("@earendil-works/pi-coding-agent", () => ({
17
- getAgentDir: () => dataDirRef.dir,
18
- }));
19
-
14
+ // [session-store-layout-inversion 域 5] 工具的 dataDir 来源已换为 resolveTaijiDataDir()
15
+ // (env 注入)——getAgentDir mock 随换根退役,改 beforeEach stubEnv + afterEach 还原。
20
16
  // 进程原语(ext-simplify-13 后单点在 protocol 子出口)转发真实杀 + 记录调用
21
17
  //(timeout 分支断言 killProcessTree 被调)
22
18
  const { killTreeCalls } = vi.hoisted(() => ({ killTreeCalls: [] as number[] }));
@@ -46,11 +42,15 @@ import { clearTaskStoreForTest, getActiveTasks, getTask, markKillingIntent } fro
46
42
  import { reapBackgroundTasksNow, resetProcessExitGuardForTest } from "../background/process-exit-guard.ts";
47
43
 
48
44
  const DATA_DIR = mkdtempSync(join(tmpdir(), "bte-data-"));
49
- dataDirRef.dir = DATA_DIR;
50
45
 
51
46
  const SESSION_ID = "sess-lifecycle";
52
47
  const REGISTRY_PATH = getRegistryPath(DATA_DIR, SESSION_ID);
53
48
 
49
+ beforeEach(() => {
50
+ // 工具 execute 的 dataDir 来源 = resolveTaijiDataDir()(env,域 5 换根)
51
+ vi.stubEnv("TAIJI_AGENT_DATA_DIR", DATA_DIR);
52
+ });
53
+
54
54
  function makeCtx(sessionId: string = SESSION_ID): { cwd: string; sessionManager: { getSessionId: () => string } } {
55
55
  return { cwd: process.cwd(), sessionManager: { getSessionId: () => sessionId } };
56
56
  }
@@ -124,6 +124,8 @@ beforeEach(() => {
124
124
  });
125
125
 
126
126
  afterEach(() => {
127
+ // env stub 逐用例还原(防同 worker 跨文件残留)
128
+ vi.unstubAllEnvs();
127
129
  // 残留活跃任务先杀干净再清表(防句柄/进程泄漏影响后续测试)
128
130
  for (const task of getActiveTasks()) {
129
131
  try {
@@ -6,7 +6,7 @@ import { join } from "node:path";
6
6
 
7
7
  import { afterEach, describe, expect, it, vi } from "vitest";
8
8
 
9
- // mock pi 官方工厂:前台行为是委托(方案 B),测试断言「透传面」而非真实 spawn
9
+ // mock pi 官方工厂:前台行为是委托(方案 B),测试断言「透传侧」而非真实 spawn
10
10
  // 行为——真实行为由 pi 上游保证 + 探针 P2 实测。
11
11
  const { createBashToolDefinitionMock, officialExecuteMock, agentDirRef } = vi.hoisted(() => ({
12
12
  createBashToolDefinitionMock: vi.fn(),
@@ -68,6 +68,8 @@ afterEach(() => {
68
68
  }
69
69
  agentDirRef.dir = "/tmp/bte-fake-agent-dir";
70
70
  clearConfigCache();
71
+ // background 分支的 dataDir 来源是 env(resolveTaijiDataDir),逐用例 stub 后在此还原
72
+ vi.unstubAllEnvs();
71
73
  });
72
74
 
73
75
  function createOfficialFactoryResult(cwd: string) {
@@ -166,7 +168,7 @@ describe("createBashOverrideToolDefinition", () => {
166
168
  const tool = createBashOverrideToolDefinition();
167
169
  // 引用级相等证明 delegate 的 render 闭包原样透传(未覆写未丢弃)——官方
168
170
  // renderCall(命令格式化)/ renderResult(elapsed 计时/富结果组件)是 TUI
169
- // 渲染面,独立 pi 用户安装本包后不降级为通用组件
171
+ // 渲染范围,独立 pi 用户安装本包后不降级为通用组件
170
172
  expect(tool.renderCall).toBe(renderCall);
171
173
  expect(tool.renderResult).toBe(renderResult);
172
174
  });
@@ -245,6 +247,9 @@ describe("createBashOverrideToolDefinition", () => {
245
247
  describe("background branch routing (M2)", () => {
246
248
  it("background:true routes to spawnBackgroundTask with ctx-derived paths and returns task_id message", async () => {
247
249
  setupFactory();
250
+ // dataDir 来源 = resolveTaijiDataDir()(env 注入,session-store-layout-inversion 域 5);
251
+ // 断言值与 stub 值同字面(getAgentDir mock 已随换根退役)
252
+ vi.stubEnv("TAIJI_AGENT_DATA_DIR", "/tmp/bte-fake-agent-dir");
248
253
  spawnBackgroundTaskMock.mockReturnValue({
249
254
  ok: true,
250
255
  task: {
@@ -0,0 +1,145 @@
1
+ // src/__tests__/delivery-port-assembly.test.ts —— 统一投递端口装配点行为锁
2
+ //(unified-delivery 波 2 前半,设计 §3.7 装配点:base-tool-enhance 的 session_start)。
3
+ // 锁三类装配行为:
4
+ // ① session_start 触发端口装配且经进程级槽可达(getDeliveryPort);
5
+ // ② 装配后端口是活的投递通路(deliver → pi.appendEntry outbox + pi.sendMessage
6
+ // 直达唯一队列——用真 deliver 链验证 host 接线正确,非仅槽非空);
7
+ // ③ 重复 session_start 重绑(旧实例 dispose 拒收)+ 差集重放触发。
8
+ import { afterEach, describe, expect, it, vi } from "vitest";
9
+
10
+ const { createBashToolDefinitionMock, dataDirRef } = vi.hoisted(() => ({
11
+ createBashToolDefinitionMock: vi.fn(),
12
+ dataDirRef: { dir: "/tmp/bte-fake-agent-dir-delivery-port" },
13
+ }));
14
+ vi.mock("@earendil-works/pi-coding-agent", () => ({
15
+ createBashToolDefinition: createBashToolDefinitionMock,
16
+ getAgentDir: () => dataDirRef.dir,
17
+ }));
18
+
19
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
20
+
21
+ import { _resetDeliveryPortForTest, getDeliveryPort } from "@zhushanwen/pi-delivery-port";
22
+ import { resetNotifyForTest } from "../background/notify.ts";
23
+ import { clearTaskStoreForTest } from "../background/task-store.ts";
24
+ import baseToolEnhanceExtension from "../index.ts";
25
+
26
+ function setupOfficialFactory(): void {
27
+ // bash 同名 override 依赖官方工厂 mock(index.test.ts setupOfficialFactory 同款);
28
+ // 本文件只走 session_start 链,工厂 mock 仅为 registerTool 路径不炸。
29
+ createBashToolDefinitionMock.mockReset();
30
+ createBashToolDefinitionMock.mockReturnValue({
31
+ name: "bash",
32
+ label: "bash",
33
+ description: "official",
34
+ parameters: {},
35
+ execute: vi.fn(),
36
+ });
37
+ }
38
+
39
+ function createMockPi(entries: unknown[] = []) {
40
+ return {
41
+ registerTool: vi.fn(),
42
+ registerCommand: vi.fn(),
43
+ // pi 1.0.0 契约:on 返回退订函数(types.d.ts `): () => void`)——port dispose 兑现
44
+ on: vi.fn(() => () => {}),
45
+ // appendEntry 落 entries(真 pi 持久化 session JSONL 的 mock 对应——重放扫描读它)
46
+ appendEntry: vi.fn((customType: string, data: unknown) => {
47
+ entries.push({ type: "custom", customType, data });
48
+ }),
49
+ events: { emit: vi.fn(), on: vi.fn() },
50
+ sendMessage: vi.fn(),
51
+ };
52
+ }
53
+
54
+ function createMockCtx(entries: unknown[]): ExtensionContext {
55
+ return {
56
+ mode: "rpc",
57
+ sessionManager: {
58
+ getSessionId: () => "sess-1",
59
+ getEntries: () => entries,
60
+ },
61
+ isIdle: () => true,
62
+ } as unknown as ExtensionContext;
63
+ }
64
+
65
+ /** 从 mock pi 提取 session_start handler 并触发(startup reason)。 */
66
+ function fireSessionStart(pi: ReturnType<typeof createMockPi>, ctx: ExtensionContext): void {
67
+ const call = pi.on.mock.calls.find((c) => c[0] === "session_start");
68
+ expect(call).toBeDefined();
69
+ (call![1] as (event: unknown, ctx: ExtensionContext) => unknown)(
70
+ { type: "session_start", reason: "startup" },
71
+ ctx,
72
+ );
73
+ }
74
+
75
+ describe("session_start 装配统一投递端口(unified-delivery §3.7)", () => {
76
+ it("session_start 后端口经进程级槽可达", () => {
77
+ setupOfficialFactory();
78
+ const pi = createMockPi();
79
+ baseToolEnhanceExtension(pi as unknown as ExtensionAPI);
80
+ expect(getDeliveryPort()).toBeUndefined(); // 装配前缺席
81
+
82
+ fireSessionStart(pi, createMockCtx([]));
83
+ expect(getDeliveryPort()).toBeDefined(); // 装配后可达
84
+ _resetDeliveryPortForTest();
85
+ });
86
+
87
+ it("装配的端口是活的投递通路:deliver 落 outbox + sendMessage 直达唯一队列", () => {
88
+ setupOfficialFactory();
89
+ const pi = createMockPi();
90
+ baseToolEnhanceExtension(pi as unknown as ExtensionAPI);
91
+ fireSessionStart(pi, createMockCtx([]));
92
+
93
+ const port = getDeliveryPort()!;
94
+ const receipt = port.deliver({
95
+ id: "task-1:0",
96
+ payload: { kind: "custom", customType: "workflow-result", content: "workflow done", display: true, details: { runId: "r-1" } },
97
+ });
98
+ expect(receipt).toEqual({ kind: "accepted", id: "task-1:0" });
99
+ // 受理前落盘(经 pi.appendEntry 接线)
100
+ expect(pi.appendEntry).toHaveBeenCalledWith(
101
+ "delivery-port-outbox",
102
+ expect.objectContaining({ v: 2, id: "task-1:0", timing: "steer" }),
103
+ );
104
+ // 直达唯一队列(经 pi.sendMessage 接线,deliveryId 注入前写入 details)
105
+ expect(pi.sendMessage).toHaveBeenCalledWith(
106
+ expect.objectContaining({
107
+ customType: "workflow-result",
108
+ details: expect.objectContaining({ deliveryId: "task-1:0" }),
109
+ }),
110
+ { deliverAs: "steer", triggerTurn: true },
111
+ );
112
+ _resetDeliveryPortForTest();
113
+ });
114
+
115
+ it("重复 session_start 重绑:旧实例 dispose(deliver 拒收)、新实例差集重放", () => {
116
+ setupOfficialFactory();
117
+ const entries: unknown[] = [];
118
+ const pi = createMockPi(entries);
119
+ baseToolEnhanceExtension(pi as unknown as ExtensionAPI);
120
+ fireSessionStart(pi, createMockCtx(entries));
121
+
122
+ const first = getDeliveryPort()!;
123
+ // 落一条账(已注入未送达——无 custom_message entry 无 ack,差集合法重投)
124
+ first.deliver({ id: "w-1", payload: { kind: "custom", customType: "workflow-result", content: "x", display: true } });
125
+ (pi.sendMessage as ReturnType<typeof vi.fn>).mockClear();
126
+
127
+ // 新 session_start(同 entries——同 session 文件语义):重绑 + recover
128
+ fireSessionStart(pi, createMockCtx(entries));
129
+ const second = getDeliveryPort()!;
130
+ expect(second).not.toBe(first);
131
+ expect(first.deliver({ id: "w-2", payload: { kind: "custom", customType: "workflow-result", content: "y", display: true } }).kind).toBe("rejected");
132
+ // 差集重放:w-1 未送达未处理完毕 → 重投(经新实例 sendMessage)
133
+ expect(pi.sendMessage).toHaveBeenCalledWith(
134
+ expect.objectContaining({ customType: "workflow-result", details: expect.objectContaining({ deliveryId: "w-1" }) }),
135
+ expect.anything(),
136
+ );
137
+ _resetDeliveryPortForTest();
138
+ });
139
+ });
140
+
141
+ afterEach(() => {
142
+ _resetDeliveryPortForTest();
143
+ resetNotifyForTest();
144
+ clearTaskStoreForTest();
145
+ });
@@ -12,7 +12,6 @@ const { createBashToolDefinitionMock, dataDirRef } = vi.hoisted(() => ({
12
12
  }));
13
13
  vi.mock("@earendil-works/pi-coding-agent", () => ({
14
14
  createBashToolDefinition: createBashToolDefinitionMock,
15
- getAgentDir: () => dataDirRef.dir,
16
15
  }));
17
16
 
18
17
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
@@ -91,12 +90,14 @@ describe("baseToolEnhanceExtension entry", () => {
91
90
  });
92
91
  });
93
92
 
94
- describe("__taiji_bg_reconcile__ command: runtime-resume trigger face (bg-task-notify-durability)", () => {
93
+ describe("__taiji_bg_reconcile__ command: runtime-resume trigger face", () => {
95
94
  it("handler runs the same maintenance chain as session_start (zombie settle via appendEntry on the SAME pi reference)", async () => {
96
95
  setupOfficialFactory();
97
- // 独立临时 dataDir(同 session_start 用例:维护链只读不扫),registry 预置终态僵尸条目
96
+ // 独立临时 dataDir(同 session_start 用例:维护链只读不扫),registry 预置终态僵尸条目。
97
+ // 维护链 dataDir 来源 = resolveTaijiDataDir()(env,session-store-layout-inversion 域 5)
98
98
  const dataDir = mkdtempSync(join(tmpdir(), "bte-bgcmd-"));
99
99
  dataDirRef.dir = dataDir;
100
+ vi.stubEnv("TAIJI_AGENT_DATA_DIR", dataDir);
100
101
  try {
101
102
  const sessionId = "sess-bgcmd";
102
103
  writeRegistryEntry(getRegistryPath(dataDir, sessionId), {
@@ -138,6 +139,7 @@ describe("__taiji_bg_reconcile__ command: runtime-resume trigger face (bg-task-n
138
139
  } finally {
139
140
  rmSync(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
140
141
  dataDirRef.dir = "/tmp/bte-fake-agent-dir";
142
+ vi.unstubAllEnvs();
141
143
  resetNotifyForTest();
142
144
  }
143
145
  });
@@ -146,9 +148,11 @@ describe("__taiji_bg_reconcile__ command: runtime-resume trigger face (bg-task-n
146
148
  describe("session_start chain: reconcile (M3, reap sunk into runtime by u-bte-remove)", () => {
147
149
  it("settles a zombie register via appendEntry on the SAME pi reference", async () => {
148
150
  setupOfficialFactory();
149
- // 独立临时 dataDir(维护链只读不扫:收殓已下沉 runtime),registry 预置终态僵尸条目
151
+ // 独立临时 dataDir(维护链只读不扫:收殓已下沉 runtime),registry 预置终态僵尸条目。
152
+ // 维护链 dataDir 来源 = resolveTaijiDataDir()(env,session-store-layout-inversion 域 5)
150
153
  const dataDir = mkdtempSync(join(tmpdir(), "bte-index-"));
151
154
  dataDirRef.dir = dataDir;
155
+ vi.stubEnv("TAIJI_AGENT_DATA_DIR", dataDir);
152
156
  try {
153
157
  const sessionId = "sess-index";
154
158
  writeRegistryEntry(getRegistryPath(dataDir, sessionId), {
@@ -197,6 +201,7 @@ describe("session_start chain: reconcile (M3, reap sunk into runtime by u-bte-re
197
201
  } finally {
198
202
  rmSync(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
199
203
  dataDirRef.dir = "/tmp/bte-fake-agent-dir";
204
+ vi.unstubAllEnvs();
200
205
  resetNotifyForTest();
201
206
  }
202
207
  });
@@ -3,17 +3,17 @@
3
3
  // reap 类操作不再执行):
4
4
  // - reconcilePendingEntries 是 session 级豁免类,每 session_start 派发都执行
5
5
  // (startup/resume/new 多派发 ×N——含 factory 二调 handler 累积的真实形态)
6
- // - 触发面消失:reaper.ts 模块文件已删除(文件级断言,见首条用例)+ 维护链不再触发
6
+ // - 触发点消失:reaper.ts 模块文件已删除(文件级断言,见首条用例)+ 维护链不再触发
7
7
  // 全局文件锁(原 reapOrphanedTasks 的 reaper.lock 路径随模块删除一并消失)
8
8
  // - 入口无条件 debug 日志按派发次数出现,detail 仅含 reason(reapSkipped 字段
9
9
  // 随 reap 调用移除,S6 观测通道语义更新)
10
10
  // 断言方式:reconcile / logger 间谍注入观察调用次数与参数;file-lock 为 mock
11
- // 缺键守卫 + withFileLockSync throw 哨兵(不计数)。
11
+ // 缺键检查 + withFileLockSync throw 哨兵(不计数)。
12
12
  import { existsSync } from "node:fs";
13
13
  import { dirname, join } from "node:path";
14
14
  import { fileURLToPath } from "node:url";
15
15
 
16
- import { describe, expect, it, vi, beforeEach } from "vitest";
16
+ import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
17
17
 
18
18
  const { reconcileMock, loggerMock, officialBashFactoryMock, dataDirRef } =
19
19
  vi.hoisted(() => ({
@@ -25,12 +25,11 @@ const { reconcileMock, loggerMock, officialBashFactoryMock, dataDirRef } =
25
25
 
26
26
  vi.mock("@earendil-works/pi-coding-agent", () => ({
27
27
  createBashToolDefinition: officialBashFactoryMock,
28
- getAgentDir: () => dataDirRef.dir,
29
28
  }));
30
29
  vi.mock("@zhushanwen/pi-extension-logger", () => ({
31
30
  getLogger: () => loggerMock,
32
31
  }));
33
- // 守卫形态 = mock 工厂缺键:包入口已删除的 async 锁导出不在工厂注册——维护链若
32
+ // 检查形态 = mock 工厂缺键:包入口已删除的 async 锁导出不在工厂注册——维护链若
34
33
  // 误回流,对已删导出的任何访问(调用与 typeof 均计)即抛错(vitest mock 缺键语义;
35
34
  // 模块导入本身不抛)。withFileLockSync 哨兵保留:registry 写路径误入维护链即抛
36
35
  vi.mock("@zhushanwen/pi-file-lock", () => ({
@@ -47,10 +46,12 @@ import type { ExtensionAPI, SessionStartEvent } from "@earendil-works/pi-coding-
47
46
  function createMockPi() {
48
47
  return {
49
48
  registerTool: vi.fn(),
50
- // __taiji_bg_reconcile__ 第二触发面(runtime 切回补投)注册桩——本文件只验证
51
- // session_start 入口,命令注册面断言在 index.test.ts
49
+ // __taiji_bg_reconcile__ 第二触发点(runtime 切回补投)注册桩——本文件只验证
50
+ // session_start 入口,命令注册点断言在 index.test.ts
52
51
  registerCommand: vi.fn(),
53
- on: vi.fn(),
52
+ // pi 1.0.0 契约:on 返回退订函数(delivery-port 装配在 session_start 重绑时
53
+ // dispose 旧实例兑现退订,mock 需符合真契约)
54
+ on: vi.fn(() => () => {}),
54
55
  appendEntry: vi.fn(),
55
56
  events: { emit: vi.fn(), on: vi.fn() },
56
57
  sendMessage: vi.fn(),
@@ -60,7 +61,7 @@ function createMockPi() {
60
61
  function getSessionStartHandler(pi: ReturnType<typeof createMockPi>) {
61
62
  const handler = pi.on.mock.calls.find((call) => call[0] === "session_start")?.[1];
62
63
  expect(handler).toBeTypeOf("function");
63
- // handler 返回 promise(补投 await sendMessage 使维护链 async 化,bg-task-notify-durability)
64
+ // handler 返回 promise(pi runner emit await handler 消费,维护链 async 形态——unified-delivery 波 3 口径)
64
65
  return handler as (event: SessionStartEvent, ctx: unknown) => Promise<void>;
65
66
  }
66
67
 
@@ -80,6 +81,9 @@ async function loadExtension() {
80
81
 
81
82
  beforeEach(() => {
82
83
  reconcileMock.mockReset();
84
+ // 维护链 dataDir 来源 = resolveTaijiDataDir()(env,session-store-layout-inversion 域 5):
85
+ // getAgentDir mock 已随换根退役,改 stub env(dataDirRef.dir 仍是断言与 stub 的同一值源)
86
+ vi.stubEnv("TAIJI_AGENT_DATA_DIR", dataDirRef.dir);
83
87
  // 官方 bash 工厂返回最小工具定义(override 定义 spread initial.name)
84
88
  officialBashFactoryMock.mockReset();
85
89
  officialBashFactoryMock.mockReturnValue({
@@ -94,11 +98,16 @@ beforeEach(() => {
94
98
  loggerMock.error.mockClear();
95
99
  });
96
100
 
101
+ afterEach(() => {
102
+ // env stub 逐用例还原(防同 worker 跨文件残留)
103
+ vi.unstubAllEnvs();
104
+ });
105
+
97
106
  describe("session_start maintenance chain after reap sink (u-bte-remove)", () => {
98
107
  it("no longer ships a reaper: the module is gone from the extension (trigger surface removed)", () => {
99
108
  // 等价改写(fallow unresolved-import):原断言动态 import 已删除模块期待
100
109
  // rejects——静态分析无法理解故意性。模块文件不存在 ⟹ import 必 rejects,
101
- // 蕴含成立,断言价值不变(触发面消失),文件级判定更直接。路径用 join 逐段
110
+ // 蕴含成立,断言价值不变(触发点消失),文件级判定更直接。路径用 join 逐段
102
111
  // 拼装:fallow 会把 ../reaper.ts 形态的相对路径字符串按 import specifier 扫描
103
112
  const reaperModulePath = join(dirname(fileURLToPath(import.meta.url)), "..", "reaper.ts");
104
113
  expect(existsSync(reaperModulePath)).toBe(false);
@@ -120,7 +129,7 @@ describe("session_start maintenance chain after reap sink (u-bte-remove)", () =>
120
129
  expect(reconcileMock).toHaveBeenCalledTimes(3);
121
130
  expect(reconcileMock).toHaveBeenNthCalledWith(1, expect.anything(), dataDirRef.dir, "sid-once", []);
122
131
  expect(reconcileMock).toHaveBeenLastCalledWith(expect.anything(), dataDirRef.dir, "sid-once", []);
123
- // 无全局扫描回流:误触发已删锁导出的任何访问会被 mock 缺键直接抛错(守卫见工厂注释),无告警
132
+ // 无全局扫描回流:误触发已删锁导出的任何访问会被 mock 缺键直接抛错(检查见工厂注释),无告警
124
133
  expect(loggerMock.warn).not.toHaveBeenCalled();
125
134
  });
126
135
 
@@ -147,7 +156,7 @@ describe("session_start maintenance chain after reap sink (u-bte-remove)", () =>
147
156
  });
148
157
 
149
158
  it("keeps reconcile failures non-fatal: handler promise resolves, warn logged", async () => {
150
- // async 化后的非致命语义(bg-task-notify-durability U3 防假绿改写):维护链已
159
+ // async 化后的非致命语义(unified-delivery 防假绿改写):维护链已
151
160
  // async——同步 not.toThrow() 断言恒真,改为显式断言「handler 的 promise resolve
152
161
  //(错误在链内被 async 捕获,不向上传播成 unhandled rejection)+ warn 日志确实
153
162
  // 记录」。两个断言缺一即红:吞掉后不打日志 = 观测丢失;打日志但 reject = fire
@@ -1,16 +1,13 @@
1
- // src/__tests__/notify-reconcile-judgement.test.ts —— 补投判据纯函数(设计 §5 U1):
2
- // ① collectDeliveredTaskIds 五形态(正常消息/fork 副本/缺 details 旧消息/非 bash
3
- // 通知/killed 无痕迹任务)+ content 双落盘形态容错;② 补投标记 entry 读写形态
4
- // (customType background-bash:reconciled,data {taskId, reconciledAt})
1
+ // src/__tests__/notify-reconcile-judgement.test.ts —— 收殓终态补投的送达痕迹提取
2
+ // (unified-delivery D3 裁决 1):collectDeliveredTaskIds 五形态(正常消息/fork 副本/
3
+ // 缺 details 旧消息/非 bash 通知/killed 无痕迹任务)+ content 双落盘形态容错。
4
+ // 旧补投标记(background-bash:reconciled)随波 3 补投机制退役:防双投由 port 幂等
5
+ // 接管,marker 判据/读写形态及其测试一并删除(反向锁见 pending-reconcile.test.ts
6
+ // 「legacy reconciled marker present WITHOUT trace → still re-notified」用例)
5
7
  import { describe, expect, it } from "vitest";
6
8
 
7
9
  import { BACKGROUND_BASH_CUSTOM_TYPE } from "../background/notify.ts";
8
- import {
9
- buildReconciledMarkerData,
10
- collectDeliveredTaskIds,
11
- collectReconciledTaskIds,
12
- RECONCILED_MARKER_CUSTOM_TYPE,
13
- } from "../background/notify-reconcile-judgement.ts";
10
+ import { collectDeliveredTaskIds } from "../background/notify-reconcile-judgement.ts";
14
11
 
15
12
  const TASK_ID = "bt-1700000000-abcd12";
16
13
  const OTHER_TASK_ID = "bt-1700000000-efgh34";
@@ -58,7 +55,7 @@ describe("collectDeliveredTaskIds: five entry shapes (design §5 U1)", () => {
58
55
  it("non-bash notification (different customType, content mentions bt- token): does NOT hit", () => {
59
56
  const ids = collectDeliveredTaskIds([
60
57
  deliveredEntry({ customType: "subagent-bg-notify" }),
61
- deliveredEntry({ customType: "background-bash:reconciled", details: undefined }),
58
+ deliveredEntry({ customType: "managed-session-notify", details: undefined }),
62
59
  ]);
63
60
  expect(ids.size).toBe(0);
64
61
  });
@@ -107,42 +104,3 @@ describe("collectDeliveredTaskIds: content落盘形态与脏数据容错", () =>
107
104
  expect([...ids].sort()).toEqual([TASK_ID, OTHER_TASK_ID].sort());
108
105
  });
109
106
  });
110
-
111
- describe("reconciled marker entry: write/read shape contract (design decision 1)", () => {
112
- it("buildReconciledMarkerData produces {taskId, reconciledAt}", () => {
113
- expect(buildReconciledMarkerData(TASK_ID, 1_700_000_000_000)).toEqual({
114
- taskId: TASK_ID,
115
- reconciledAt: 1_700_000_000_000,
116
- });
117
- });
118
-
119
- it("marker customType is the plain-entry channel, distinct from the delivery customType", () => {
120
- // 通道纪律:账务走 plain custom entry(appendEntry),送达消息走 sendMessage——
121
- // 两者 customType 不得混用(NotifyLedger 同款纪律)
122
- expect(RECONCILED_MARKER_CUSTOM_TYPE).toBe("background-bash:reconciled");
123
- expect(RECONCILED_MARKER_CUSTOM_TYPE).not.toBe(BACKGROUND_BASH_CUSTOM_TYPE);
124
- });
125
-
126
- it("collectReconciledTaskIds picks marker entries and tolerates malformed data", () => {
127
- const ids = collectReconciledTaskIds([
128
- { type: "custom", customType: RECONCILED_MARKER_CUSTOM_TYPE, data: { taskId: TASK_ID, reconciledAt: 1 } },
129
- { type: "custom", customType: RECONCILED_MARKER_CUSTOM_TYPE }, // data 缺失
130
- { type: "custom", customType: RECONCILED_MARKER_CUSTOM_TYPE, data: { reconciledAt: 2 } }, // taskId 缺
131
- { type: "custom", customType: RECONCILED_MARKER_CUSTOM_TYPE, data: { taskId: 42 } }, // 非字符串
132
- { type: "custom_message", customType: RECONCILED_MARKER_CUSTOM_TYPE, data: { taskId: OTHER_TASK_ID } }, // 误入消息通道的形态仍按 data 读
133
- deliveredEntry(), // 送达消息不是标记
134
- null,
135
- 7,
136
- ]);
137
- expect([...ids].sort()).toEqual([TASK_ID, OTHER_TASK_ID].sort());
138
- });
139
-
140
- it("delivered and reconciled collectors partition the two anchors without cross-talk", () => {
141
- const entries = [
142
- deliveredEntry(), // TASK_ID 送达痕迹在
143
- { type: "custom", customType: RECONCILED_MARKER_CUSTOM_TYPE, data: { taskId: OTHER_TASK_ID, reconciledAt: 1 } },
144
- ];
145
- expect(collectDeliveredTaskIds(entries).has(OTHER_TASK_ID)).toBe(false);
146
- expect(collectReconciledTaskIds(entries).has(TASK_ID)).toBe(false);
147
- });
148
- });