@zhushanwen/pi-unified-hooks 0.2.4 → 0.2.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-unified-hooks",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Unified hooks extension - collect scattered hooks in one place for easy maintenance",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  ],
20
20
  "license": "MIT",
21
21
  "dependencies": {
22
- "@zhushanwen/pi-extension-logger": "0.2.2"
22
+ "@zhushanwen/pi-extension-logger": "0.3.0"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "@earendil-works/pi-coding-agent": "^0.84.1"
@@ -3,20 +3,19 @@ import { describe, expect, it, vi } from "vitest";
3
3
 
4
4
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
5
 
6
- // Mock 共享 logger,让 logger.warn 可被 spy
6
+ import { setupToolErrorHandler } from "../hooks/tool-error-handler.ts";
7
+
8
+ // 回归防护:源码已删泛化 logger.warn 双写(D5),若被重新引入测试必须红。
9
+ // mock 掉共享 logger,使 loggerMock.warn 可被断言未被调。
7
10
  const { loggerMock } = vi.hoisted(() => ({
8
- loggerMock: {
9
- debug: vi.fn(),
10
- warn: vi.fn(),
11
- error: vi.fn(),
12
- },
11
+ loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
13
12
  }));
14
13
  vi.mock("@zhushanwen/pi-extension-logger", () => ({
15
14
  getLogger: () => loggerMock,
15
+ createLogger: () => loggerMock,
16
+ setPiHandle: vi.fn(),
16
17
  }));
17
18
 
18
- import { setupToolErrorHandler } from "../hooks/tool-error-handler.ts";
19
-
20
19
  // --- helper types ---
21
20
  interface MockPi {
22
21
  on: ReturnType<typeof vi.fn>;
@@ -40,9 +39,8 @@ describe("setupToolErrorHandler", () => {
40
39
  expect(pi.on).toHaveBeenCalledWith("tool_execution_end", expect.any(Function));
41
40
  });
42
41
 
43
- it("logs via logger.warn and appendEntry with dedicated customType on isError:true", async () => {
42
+ it("appendEntry with dedicated customType on isError:true", async () => {
44
43
  const pi = createMockPi();
45
- loggerMock.warn.mockClear();
46
44
  pi.appendEntry.mockClear();
47
45
 
48
46
  setupToolErrorHandler(pi as unknown as ExtensionAPI);
@@ -50,19 +48,10 @@ describe("setupToolErrorHandler", () => {
50
48
 
51
49
  await handler({ isError: true, toolName: "read", toolCallId: "call-42" });
52
50
 
53
- // logger.warn 被调一次(内部走泛化 appendEntry customType
54
- expect(loggerMock.warn).toHaveBeenCalledTimes(1);
55
- expect(loggerMock.warn).toHaveBeenCalledWith(
56
- "[unified-hooks] read error (callId=call-42)",
57
- expect.objectContaining({
58
- toolName: "read",
59
- toolCallId: "call-42",
60
- errorText: null,
61
- }),
62
- );
63
- // 额外 appendEntry 用专属 customType "unified-hooks:tool-error",
64
- // 保留按 entry type 过滤 tool 错误的埋点契约
51
+ // 契约载体 = 专属 customType "unified-hooks:tool-error"
65
52
  expect(pi.appendEntry).toHaveBeenCalledTimes(1);
53
+ // 回归防护(D5):泛化 logger.warn 双写不得回归
54
+ expect(loggerMock.warn).not.toHaveBeenCalled();
66
55
  expect(pi.appendEntry).toHaveBeenCalledWith(
67
56
  "unified-hooks:tool-error",
68
57
  expect.objectContaining({
@@ -73,16 +62,16 @@ describe("setupToolErrorHandler", () => {
73
62
  );
74
63
  });
75
64
 
76
- it("does nothing on isError:false (no logger.warn)", async () => {
65
+ it("does nothing on isError:false (no appendEntry)", async () => {
77
66
  const pi = createMockPi();
78
- loggerMock.warn.mockClear();
67
+ pi.appendEntry.mockClear();
79
68
 
80
69
  setupToolErrorHandler(pi as unknown as ExtensionAPI);
81
70
  const handler = pi.on.mock.calls[0]![1] as (event: unknown) => Promise<void>;
82
71
 
83
72
  await handler({ isError: false, toolName: "bash", toolCallId: "call-99" });
84
73
 
85
- expect(loggerMock.warn).not.toHaveBeenCalled();
74
+ expect(pi.appendEntry).not.toHaveBeenCalled();
86
75
  });
87
76
 
88
77
  // --- edge cases ---
@@ -97,7 +86,7 @@ describe("setupToolErrorHandler", () => {
97
86
 
98
87
  it("handles concurrent error events independently", async () => {
99
88
  const pi = createMockPi();
100
- loggerMock.warn.mockClear();
89
+ pi.appendEntry.mockClear();
101
90
 
102
91
  setupToolErrorHandler(pi as unknown as ExtensionAPI);
103
92
  const handler = pi.on.mock.calls[0]![1] as (event: unknown) => Promise<void>;
@@ -108,13 +97,13 @@ describe("setupToolErrorHandler", () => {
108
97
  handler({ isError: false, toolName: "edit", toolCallId: "e3" }),
109
98
  ]);
110
99
 
111
- expect(loggerMock.warn).toHaveBeenCalledTimes(2);
112
- expect(loggerMock.warn).toHaveBeenCalledWith(
113
- "[unified-hooks] read error (callId=e1)",
100
+ expect(pi.appendEntry).toHaveBeenCalledTimes(2);
101
+ expect(pi.appendEntry).toHaveBeenCalledWith(
102
+ "unified-hooks:tool-error",
114
103
  expect.objectContaining({ toolName: "read", toolCallId: "e1" }),
115
104
  );
116
- expect(loggerMock.warn).toHaveBeenCalledWith(
117
- "[unified-hooks] bash error (callId=e2)",
105
+ expect(pi.appendEntry).toHaveBeenCalledWith(
106
+ "unified-hooks:tool-error",
118
107
  expect.objectContaining({ toolName: "bash", toolCallId: "e2" }),
119
108
  );
120
109
  });
@@ -123,7 +112,7 @@ describe("setupToolErrorHandler", () => {
123
112
 
124
113
  it("从 result.content[0].text 提取错误文本(如 'hub disposed')", async () => {
125
114
  const pi = createMockPi();
126
- loggerMock.warn.mockClear();
115
+ pi.appendEntry.mockClear();
127
116
 
128
117
  setupToolErrorHandler(pi as unknown as ExtensionAPI);
129
118
  const handler = pi.on.mock.calls[0]![1] as (event: unknown) => Promise<void>;
@@ -135,8 +124,8 @@ describe("setupToolErrorHandler", () => {
135
124
  result: { content: [{ type: "text", text: "hub disposed" }] },
136
125
  });
137
126
 
138
- expect(loggerMock.warn).toHaveBeenCalledWith(
139
- "[unified-hooks] subagent error (callId=call-disposed)",
127
+ expect(pi.appendEntry).toHaveBeenCalledWith(
128
+ "unified-hooks:tool-error",
140
129
  expect.objectContaining({
141
130
  toolName: "subagent",
142
131
  toolCallId: "call-disposed",
@@ -147,7 +136,7 @@ describe("setupToolErrorHandler", () => {
147
136
 
148
137
  it("result 缺失或无 content 时降级到无详情(不崩)", async () => {
149
138
  const pi = createMockPi();
150
- loggerMock.warn.mockClear();
139
+ pi.appendEntry.mockClear();
151
140
 
152
141
  setupToolErrorHandler(pi as unknown as ExtensionAPI);
153
142
  const handler = pi.on.mock.calls[0]![1] as (event: unknown) => Promise<void>;
@@ -160,9 +149,9 @@ describe("setupToolErrorHandler", () => {
160
149
  await handler({ isError: true, toolName: "bash", toolCallId: "x3", result: "oops" });
161
150
 
162
151
  // 三次都降级为无详情
163
- expect(loggerMock.warn.mock.calls[0]![0]).toBe("[unified-hooks] bash error (callId=x1)");
164
- expect(loggerMock.warn.mock.calls[0]![1]).toHaveProperty("errorText", null);
165
- expect(loggerMock.warn.mock.calls[1]![1]).toHaveProperty("errorText", null);
166
- expect(loggerMock.warn.mock.calls[2]![1]).toHaveProperty("errorText", null);
152
+ expect(pi.appendEntry).toHaveBeenCalledTimes(3);
153
+ expect(pi.appendEntry.mock.calls[0]![1]).toHaveProperty("errorText", null);
154
+ expect(pi.appendEntry.mock.calls[1]![1]).toHaveProperty("errorText", null);
155
+ expect(pi.appendEntry.mock.calls[2]![1]).toHaveProperty("errorText", null);
167
156
  });
168
157
  });
@@ -11,9 +11,6 @@
11
11
  */
12
12
 
13
13
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
14
- import { getLogger } from "@zhushanwen/pi-extension-logger";
15
-
16
- const logger = getLogger("unified-hooks");
17
14
 
18
15
  /**
19
16
  * Subset of `ToolExecutionEndEvent` fields used by this hook.
@@ -86,16 +83,10 @@ export function setupToolErrorHandler(pi: ExtensionAPI): void {
86
83
  // SDK 事件无 errorMessage 字段,只能从这里捞;拿不到也不阻断(降级到无详情)。
87
84
  const errorText = extractErrorText(e.result);
88
85
 
89
- // appendEntry 持久化到 session entries,供事后排查(无 UI、不泄漏)。
90
- // errorText 一起存上——事后排查能看到真实原因(如 "hub disposed")。
86
+ // 契约载体 = 专属 customType `unified-hooks:tool-error`(含 timestamp/toolName/toolCallId/errorText 全量字段)。
87
+ // 泛化 `unified-hooks:log` 冗余已删(无独有信息,全仓无消费方)。
91
88
  // 不调 ctx.ui.notify——tool error 已在对话流里(pi 原生 tool result),
92
89
  // notify 会重复显示且措辞("bash error")误导。
93
- //
94
- // 注意:除了 logger.warn(内部走泛化 `unified-hooks:log` customType),
95
- // 这里额外调一次 `pi.appendEntry("unified-hooks:tool-error", ...)`。
96
- // 原因:logger 内部的 appendEntry 用的是泛化 customType,无法区分 entry
97
- // 是否为 tool 错误;保留专属 entry type 让事后按 customType 过滤 tool
98
- // 错误的脚本/dashboard 仍可工作(埋点契约)。
99
90
  const entry = {
100
91
  timestamp: Date.now(),
101
92
  toolName: e.toolName,
@@ -103,6 +94,5 @@ export function setupToolErrorHandler(pi: ExtensionAPI): void {
103
94
  errorText: errorText ?? null,
104
95
  };
105
96
  pi.appendEntry("unified-hooks:tool-error", entry);
106
- logger.warn(`[unified-hooks] ${e.toolName} error (callId=${e.toolCallId})`, entry);
107
97
  });
108
98
  }