@spzhongwin/skill-logger-plugin 1.0.11 → 1.0.13

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 (54) hide show
  1. package/dist/active-skills.js +67 -0
  2. package/dist/active-skills.test.js +29 -0
  3. package/dist/config-sync.js +439 -0
  4. package/dist/config-sync.test.js +145 -0
  5. package/dist/hooks.js +337 -0
  6. package/dist/hooks.test.js +123 -0
  7. package/dist/http.js +54 -0
  8. package/dist/identity.js +56 -0
  9. package/dist/index.js +240 -78
  10. package/dist/index.test.js +39 -0
  11. package/dist/integration.test.js +102 -0
  12. package/dist/matcher.js +362 -0
  13. package/dist/matcher.test.js +139 -0
  14. package/dist/paths.js +62 -0
  15. package/dist/paths.test.js +49 -0
  16. package/dist/reporter.js +267 -0
  17. package/dist/reporter.test.js +128 -0
  18. package/dist/semver.js +64 -0
  19. package/dist/semver.test.js +21 -0
  20. package/dist/skill-version.js +23 -0
  21. package/dist/types.js +9 -0
  22. package/dist/updater.js +352 -0
  23. package/dist/updater.test.js +212 -0
  24. package/dist/ws-client.js +484 -0
  25. package/openclaw.plugin.json +50 -50
  26. package/package.json +37 -37
  27. package/src/active-skills.test.ts +32 -32
  28. package/src/active-skills.ts +77 -77
  29. package/src/config-sync.test.ts +165 -165
  30. package/src/config-sync.ts +544 -544
  31. package/src/hooks.test.ts +251 -251
  32. package/src/hooks.ts +517 -517
  33. package/src/http.ts +61 -61
  34. package/src/identity.ts +64 -64
  35. package/src/index.test.ts +53 -53
  36. package/src/index.ts +226 -226
  37. package/src/integration.test.ts +119 -119
  38. package/src/matcher.test.ts +170 -170
  39. package/src/matcher.ts +393 -393
  40. package/src/paths.test.ts +57 -57
  41. package/src/paths.ts +84 -84
  42. package/src/reporter.test.ts +139 -139
  43. package/src/reporter.ts +298 -298
  44. package/src/sample-config.json +72 -72
  45. package/src/semver.test.ts +23 -23
  46. package/src/semver.ts +60 -60
  47. package/src/skill-version.ts +53 -53
  48. package/src/types.ts +198 -198
  49. package/src/updater.test.ts +325 -237
  50. package/src/updater.ts +549 -433
  51. package/src/ws-client.test.ts +48 -37
  52. package/src/ws-client.ts +717 -642
  53. package/test-ws.ts +17 -17
  54. package/tsconfig.json +14 -14
package/src/hooks.test.ts CHANGED
@@ -1,251 +1,251 @@
1
- import { describe, it } from "node:test";
2
- import assert from "node:assert/strict";
3
- import { Hooks } from "./hooks.ts";
4
- import { ActiveSkills } from "./active-skills.ts";
5
- import { buildIndex } from "./matcher.ts";
6
- import type { Reporter } from "./reporter.ts";
7
- import type { ConfigSync } from "./config-sync.ts";
8
- import type { SkillEvent, SkillStandardConfig } from "./types.ts";
9
-
10
- const sampleConfigs: SkillStandardConfig[] = [
11
- {
12
- skillName: "model-usage",
13
- version: "1.0.0",
14
- functions: [
15
- { id: "usage_current", name: "当前用量", match: { type: "script", script: "scripts/model_usage.py", argRules: [{ flag: "--mode", value: "current" }] } },
16
- ],
17
- },
18
- ];
19
-
20
- const tick = () => new Promise((r) => setImmediate(r));
21
-
22
- function setup() {
23
- const captured: SkillEvent[] = [];
24
- const reporter = { appendEvent: async (e: SkillEvent) => void captured.push(e) } as unknown as Reporter;
25
- const index = buildIndex(sampleConfigs);
26
- const lazy: string[] = [];
27
- const configSync = {
28
- getIndex: () => index,
29
- lazyCheck: async (name: string) => void lazy.push(name),
30
- resolveSkillName: () => undefined,
31
- getVersion: () => undefined,
32
- } as unknown as ConfigSync;
33
- const activeSkills = new ActiveSkills();
34
- const hooks = new Hooks(reporter, configSync, activeSkills, () => ({}));
35
- return { hooks, captured, lazy, activeSkills };
36
- }
37
-
38
- describe("Hooks 端到端串联", () => {
39
- it("read SKILL.md → skill_trigger + 标记激活 + 懒拉", async () => {
40
- const { hooks, captured, lazy, activeSkills } = setup();
41
- hooks.onBeforeToolCall(
42
- { toolName: "read", params: { path: "/e/skills/model-usage/SKILL.md" } },
43
- { sessionId: "s1", agentId: "main" }
44
- );
45
- await tick();
46
- assert.equal(captured.length, 1);
47
- assert.equal(captured[0].event_type, "skill_trigger");
48
- assert.equal(captured[0].skill_name, "model-usage");
49
- assert.equal(captured[0].invoke_mode, "inline");
50
- assert.deepEqual(lazy, ["model-usage"]);
51
- assert.ok(activeSkills.getActive("s1").has("model-usage"));
52
- });
53
-
54
- it("skill 工具直调(command-dispatch: tool)→ skill_trigger(tool)", async () => {
55
- const { hooks, captured, activeSkills } = setup();
56
- hooks.onBeforeToolCall({ toolName: "skill", params: { skill: "model-usage" } }, { sessionId: "s1" });
57
- await tick();
58
- assert.equal(captured.length, 1);
59
- assert.equal(captured[0].event_type, "skill_trigger");
60
- assert.equal(captured[0].invoke_mode, "tool");
61
- assert.ok(activeSkills.getActive("s1").has("model-usage"));
62
- });
63
-
64
- it("skill 工具直调有 toolCallId 时 after 报错 → 补记 function_call(error)", async () => {
65
- const { hooks, captured } = setup();
66
- hooks.onBeforeToolCall(
67
- { toolName: "skill", params: { skill: "model-usage" }, toolCallId: "skill-t1" },
68
- { sessionId: "s1" }
69
- );
70
- hooks.onAfterToolCall({ toolCallId: "skill-t1", errorMessage: "skill failed", elapsedMs: 9 });
71
- await tick();
72
-
73
- assert.equal(captured.length, 2);
74
- assert.equal(captured[0].event_type, "skill_trigger");
75
- const call = captured[1];
76
- assert.equal(call.event_type, "function_call");
77
- assert.equal(call.skill_name, "model-usage");
78
- assert.equal(call.invoke_tool, "skill");
79
- assert.equal(call.status, "error");
80
- assert.equal(call.error_message, "skill failed");
81
- assert.equal(call.duration_ms, 9);
82
- });
83
-
84
- it("after 未到达:flushAllPending 补记 unknown", async () => {
85
- const { hooks, captured } = setup();
86
- hooks.onBeforeToolCall(
87
- { toolName: "exec", params: { command: "python /e/skills/model-usage/scripts/model_usage.py --mode current" }, toolCallId: "t9" },
88
- { sessionId: "s1" }
89
- );
90
- await tick();
91
- assert.equal(captured.length, 0);
92
- await hooks.flushAllPending();
93
- await tick();
94
- assert.equal(captured.length, 1);
95
- assert.equal(captured[0].status, "unknown");
96
- assert.equal(captured[0].function_id, "usage_current");
97
- });
98
-
99
- it("session_end 补记该 session 残留 pending 并清激活", async () => {
100
- const { hooks, captured, activeSkills } = setup();
101
- hooks.onBeforeToolCall({ toolName: "read", params: { path: "/e/skills/model-usage/SKILL.md" } }, { sessionId: "s1" });
102
- hooks.onBeforeToolCall(
103
- { toolName: "exec", params: { command: "python /e/skills/model-usage/scripts/model_usage.py --mode current" }, toolCallId: "t10" },
104
- { sessionId: "s1" }
105
- );
106
- await tick();
107
- captured.length = 0;
108
- hooks.onSessionEnd({ sessionId: "s1" });
109
- await tick();
110
- assert.equal(captured.filter((e) => e.status === "unknown").length, 1);
111
- assert.deepEqual([...activeSkills.getActive("s1")], []);
112
- });
113
-
114
- it("exec 命中 → 暂存,after 成功 → function_call(success)", async () => {
115
- const { hooks, captured } = setup();
116
- hooks.onBeforeToolCall(
117
- { toolName: "exec", params: { command: "python /e/skills/model-usage/scripts/model_usage.py --mode current" }, toolCallId: "t1" },
118
- { sessionId: "s1" }
119
- );
120
- await tick();
121
- assert.equal(captured.length, 0); // 等 after
122
-
123
- hooks.onAfterToolCall({ toolCallId: "t1", durationMs: 42 });
124
- await tick();
125
- assert.equal(captured.length, 1);
126
- const ev = captured[0];
127
- assert.equal(ev.event_type, "function_call");
128
- assert.equal(ev.function_id, "usage_current");
129
- assert.equal(ev.match_type, "script");
130
- assert.equal(ev.status, "success");
131
- assert.equal(ev.duration_ms, 42);
132
- assert.equal(ev.args?.mode, "current");
133
- });
134
-
135
- it("after 带 error → function_call(error)", async () => {
136
- const { hooks, captured } = setup();
137
- hooks.onBeforeToolCall(
138
- { toolName: "exec", params: { command: "python /e/skills/model-usage/scripts/model_usage.py --mode current" }, toolCallId: "t2" },
139
- { sessionId: "s1" }
140
- );
141
- await tick();
142
- hooks.onAfterToolCall({ toolCallId: "t2", error: "boom" });
143
- await tick();
144
- assert.equal(captured[0].status, "error");
145
- assert.equal(captured[0].error_message, "boom");
146
- });
147
-
148
- it("after 的结构化错误与失败状态也会归为 error", async () => {
149
- const { hooks, captured } = setup();
150
- hooks.onBeforeToolCall(
151
- { toolName: "exec", params: { command: "python /e/skills/model-usage/scripts/model_usage.py --mode current" }, toolCallId: "t4" },
152
- { sessionId: "s1" }
153
- );
154
- await tick();
155
- hooks.onAfterToolCall({ toolCallId: "t4", status: "failed", message: "process failed", duration_ms: 7 });
156
- await tick();
157
- assert.equal(captured[0].status, "error");
158
- assert.equal(captured[0].error_message, "process failed");
159
- assert.equal(captured[0].duration_ms, 7);
160
- });
161
-
162
- it("after 的嵌套 result 失败信号会提取 stderr", async () => {
163
- const { hooks, captured } = setup();
164
- hooks.onBeforeToolCall(
165
- { toolName: "exec", params: { command: "python /e/skills/model-usage/scripts/model_usage.py --mode current" }, toolCallId: "t6" },
166
- { sessionId: "s1" }
167
- );
168
- await tick();
169
- hooks.onAfterToolCall({ toolCallId: "t6", result: { ok: false, stderr: "stderr failed" } });
170
- await tick();
171
- assert.equal(captured[0].status, "error");
172
- assert.equal(captured[0].error_message, "stderr failed");
173
- });
174
-
175
- it("未匹配 exec 但会话中只有一个激活 skill → 等 after 补记状态和错误", async () => {
176
- const { hooks, captured } = setup();
177
- hooks.onBeforeToolCall({ toolName: "read", params: { path: "/e/skills/model-usage/SKILL.md" } }, { sessionId: "s1" });
178
- await tick();
179
- captured.length = 0;
180
-
181
- hooks.onBeforeToolCall(
182
- { toolName: "exec", params: { command: "custom command --bad" }, toolCallId: "t5" },
183
- { sessionId: "s1" }
184
- );
185
- await tick();
186
- assert.equal(captured.length, 0);
187
-
188
- hooks.onAfterToolCall({ toolCallId: "t5", error: { message: "custom failed" }, durationMs: 11 });
189
- await tick();
190
- assert.equal(captured.length, 1);
191
- assert.equal(captured[0].event_type, "function_call");
192
- assert.equal(captured[0].skill_name, "model-usage");
193
- assert.equal(captured[0].invoke_tool, "exec");
194
- assert.equal(captured[0].command, "custom command --bad");
195
- assert.equal(captured[0].status, "error");
196
- assert.equal(captured[0].error_message, "custom failed");
197
- assert.equal(captured[0].duration_ms, 11);
198
- });
199
-
200
- it("report_skill_error 手动上报会记录 error 事件并携带会话 appKey", async () => {
201
- const { hooks, captured } = setup();
202
- hooks.onMessageReceived({ content: "appKey: abcdef123456" }, { sessionId: "s1" });
203
- hooks.onManualErrorRecord(
204
- {
205
- skill_name: "model-usage",
206
- tool_name: "usage_current",
207
- error_message: "manual failure",
208
- input_args: "--mode current",
209
- },
210
- { sessionId: "s1", agentId: "main", runId: "r1" }
211
- );
212
- await tick();
213
- assert.equal(captured.length, 1);
214
- assert.equal(captured[0].event_type, "function_call");
215
- assert.equal(captured[0].skill_name, "model-usage");
216
- assert.equal(captured[0].invoke_tool, "report_skill_error");
217
- assert.equal(captured[0].status, "error");
218
- assert.equal(captured[0].error_message, "manual failure");
219
- assert.equal(captured[0].app_key, "abcdef123456");
220
- assert.deepEqual(captured[0].args, { raw_args: "--mode current" });
221
- });
222
-
223
- it("未命中且默认配置 → 不记录", async () => {
224
- const { hooks, captured } = setup();
225
- hooks.onBeforeToolCall({ toolName: "exec", params: { command: "ls -la" }, toolCallId: "t3" }, { sessionId: "s1" });
226
- hooks.onAfterToolCall({ toolCallId: "t3" });
227
- await tick();
228
- assert.equal(captured.length, 0);
229
- });
230
- });
231
-
232
- describe("插件模块图可加载", () => {
233
- it("import index.ts 并 register 注册全部生命周期 hook", async () => {
234
- const mod = await import("./index.ts");
235
- const registered: string[] = [];
236
- mod.default.register({ on: (name: string) => registered.push(name) });
237
- assert.deepEqual(
238
- registered.sort(),
239
- [
240
- "after_tool_call",
241
- "before_install",
242
- "before_prompt_build",
243
- "before_tool_call",
244
- "gateway_start",
245
- "gateway_stop",
246
- "message_received",
247
- "session_end",
248
- ]
249
- );
250
- });
251
- });
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { Hooks } from "./hooks.ts";
4
+ import { ActiveSkills } from "./active-skills.ts";
5
+ import { buildIndex } from "./matcher.ts";
6
+ import type { Reporter } from "./reporter.ts";
7
+ import type { ConfigSync } from "./config-sync.ts";
8
+ import type { SkillEvent, SkillStandardConfig } from "./types.ts";
9
+
10
+ const sampleConfigs: SkillStandardConfig[] = [
11
+ {
12
+ skillName: "model-usage",
13
+ version: "1.0.0",
14
+ functions: [
15
+ { id: "usage_current", name: "当前用量", match: { type: "script", script: "scripts/model_usage.py", argRules: [{ flag: "--mode", value: "current" }] } },
16
+ ],
17
+ },
18
+ ];
19
+
20
+ const tick = () => new Promise((r) => setImmediate(r));
21
+
22
+ function setup() {
23
+ const captured: SkillEvent[] = [];
24
+ const reporter = { appendEvent: async (e: SkillEvent) => void captured.push(e) } as unknown as Reporter;
25
+ const index = buildIndex(sampleConfigs);
26
+ const lazy: string[] = [];
27
+ const configSync = {
28
+ getIndex: () => index,
29
+ lazyCheck: async (name: string) => void lazy.push(name),
30
+ resolveSkillName: () => undefined,
31
+ getVersion: () => undefined,
32
+ } as unknown as ConfigSync;
33
+ const activeSkills = new ActiveSkills();
34
+ const hooks = new Hooks(reporter, configSync, activeSkills, () => ({}));
35
+ return { hooks, captured, lazy, activeSkills };
36
+ }
37
+
38
+ describe("Hooks 端到端串联", () => {
39
+ it("read SKILL.md → skill_trigger + 标记激活 + 懒拉", async () => {
40
+ const { hooks, captured, lazy, activeSkills } = setup();
41
+ hooks.onBeforeToolCall(
42
+ { toolName: "read", params: { path: "/e/skills/model-usage/SKILL.md" } },
43
+ { sessionId: "s1", agentId: "main" }
44
+ );
45
+ await tick();
46
+ assert.equal(captured.length, 1);
47
+ assert.equal(captured[0].event_type, "skill_trigger");
48
+ assert.equal(captured[0].skill_name, "model-usage");
49
+ assert.equal(captured[0].invoke_mode, "inline");
50
+ assert.deepEqual(lazy, ["model-usage"]);
51
+ assert.ok(activeSkills.getActive("s1").has("model-usage"));
52
+ });
53
+
54
+ it("skill 工具直调(command-dispatch: tool)→ skill_trigger(tool)", async () => {
55
+ const { hooks, captured, activeSkills } = setup();
56
+ hooks.onBeforeToolCall({ toolName: "skill", params: { skill: "model-usage" } }, { sessionId: "s1" });
57
+ await tick();
58
+ assert.equal(captured.length, 1);
59
+ assert.equal(captured[0].event_type, "skill_trigger");
60
+ assert.equal(captured[0].invoke_mode, "tool");
61
+ assert.ok(activeSkills.getActive("s1").has("model-usage"));
62
+ });
63
+
64
+ it("skill 工具直调有 toolCallId 时 after 报错 → 补记 function_call(error)", async () => {
65
+ const { hooks, captured } = setup();
66
+ hooks.onBeforeToolCall(
67
+ { toolName: "skill", params: { skill: "model-usage" }, toolCallId: "skill-t1" },
68
+ { sessionId: "s1" }
69
+ );
70
+ hooks.onAfterToolCall({ toolCallId: "skill-t1", errorMessage: "skill failed", elapsedMs: 9 });
71
+ await tick();
72
+
73
+ assert.equal(captured.length, 2);
74
+ assert.equal(captured[0].event_type, "skill_trigger");
75
+ const call = captured[1];
76
+ assert.equal(call.event_type, "function_call");
77
+ assert.equal(call.skill_name, "model-usage");
78
+ assert.equal(call.invoke_tool, "skill");
79
+ assert.equal(call.status, "error");
80
+ assert.equal(call.error_message, "skill failed");
81
+ assert.equal(call.duration_ms, 9);
82
+ });
83
+
84
+ it("after 未到达:flushAllPending 补记 unknown", async () => {
85
+ const { hooks, captured } = setup();
86
+ hooks.onBeforeToolCall(
87
+ { toolName: "exec", params: { command: "python /e/skills/model-usage/scripts/model_usage.py --mode current" }, toolCallId: "t9" },
88
+ { sessionId: "s1" }
89
+ );
90
+ await tick();
91
+ assert.equal(captured.length, 0);
92
+ await hooks.flushAllPending();
93
+ await tick();
94
+ assert.equal(captured.length, 1);
95
+ assert.equal(captured[0].status, "unknown");
96
+ assert.equal(captured[0].function_id, "usage_current");
97
+ });
98
+
99
+ it("session_end 补记该 session 残留 pending 并清激活", async () => {
100
+ const { hooks, captured, activeSkills } = setup();
101
+ hooks.onBeforeToolCall({ toolName: "read", params: { path: "/e/skills/model-usage/SKILL.md" } }, { sessionId: "s1" });
102
+ hooks.onBeforeToolCall(
103
+ { toolName: "exec", params: { command: "python /e/skills/model-usage/scripts/model_usage.py --mode current" }, toolCallId: "t10" },
104
+ { sessionId: "s1" }
105
+ );
106
+ await tick();
107
+ captured.length = 0;
108
+ hooks.onSessionEnd({ sessionId: "s1" });
109
+ await tick();
110
+ assert.equal(captured.filter((e) => e.status === "unknown").length, 1);
111
+ assert.deepEqual([...activeSkills.getActive("s1")], []);
112
+ });
113
+
114
+ it("exec 命中 → 暂存,after 成功 → function_call(success)", async () => {
115
+ const { hooks, captured } = setup();
116
+ hooks.onBeforeToolCall(
117
+ { toolName: "exec", params: { command: "python /e/skills/model-usage/scripts/model_usage.py --mode current" }, toolCallId: "t1" },
118
+ { sessionId: "s1" }
119
+ );
120
+ await tick();
121
+ assert.equal(captured.length, 0); // 等 after
122
+
123
+ hooks.onAfterToolCall({ toolCallId: "t1", durationMs: 42 });
124
+ await tick();
125
+ assert.equal(captured.length, 1);
126
+ const ev = captured[0];
127
+ assert.equal(ev.event_type, "function_call");
128
+ assert.equal(ev.function_id, "usage_current");
129
+ assert.equal(ev.match_type, "script");
130
+ assert.equal(ev.status, "success");
131
+ assert.equal(ev.duration_ms, 42);
132
+ assert.equal(ev.args?.mode, "current");
133
+ });
134
+
135
+ it("after 带 error → function_call(error)", async () => {
136
+ const { hooks, captured } = setup();
137
+ hooks.onBeforeToolCall(
138
+ { toolName: "exec", params: { command: "python /e/skills/model-usage/scripts/model_usage.py --mode current" }, toolCallId: "t2" },
139
+ { sessionId: "s1" }
140
+ );
141
+ await tick();
142
+ hooks.onAfterToolCall({ toolCallId: "t2", error: "boom" });
143
+ await tick();
144
+ assert.equal(captured[0].status, "error");
145
+ assert.equal(captured[0].error_message, "boom");
146
+ });
147
+
148
+ it("after 的结构化错误与失败状态也会归为 error", async () => {
149
+ const { hooks, captured } = setup();
150
+ hooks.onBeforeToolCall(
151
+ { toolName: "exec", params: { command: "python /e/skills/model-usage/scripts/model_usage.py --mode current" }, toolCallId: "t4" },
152
+ { sessionId: "s1" }
153
+ );
154
+ await tick();
155
+ hooks.onAfterToolCall({ toolCallId: "t4", status: "failed", message: "process failed", duration_ms: 7 });
156
+ await tick();
157
+ assert.equal(captured[0].status, "error");
158
+ assert.equal(captured[0].error_message, "process failed");
159
+ assert.equal(captured[0].duration_ms, 7);
160
+ });
161
+
162
+ it("after 的嵌套 result 失败信号会提取 stderr", async () => {
163
+ const { hooks, captured } = setup();
164
+ hooks.onBeforeToolCall(
165
+ { toolName: "exec", params: { command: "python /e/skills/model-usage/scripts/model_usage.py --mode current" }, toolCallId: "t6" },
166
+ { sessionId: "s1" }
167
+ );
168
+ await tick();
169
+ hooks.onAfterToolCall({ toolCallId: "t6", result: { ok: false, stderr: "stderr failed" } });
170
+ await tick();
171
+ assert.equal(captured[0].status, "error");
172
+ assert.equal(captured[0].error_message, "stderr failed");
173
+ });
174
+
175
+ it("未匹配 exec 但会话中只有一个激活 skill → 等 after 补记状态和错误", async () => {
176
+ const { hooks, captured } = setup();
177
+ hooks.onBeforeToolCall({ toolName: "read", params: { path: "/e/skills/model-usage/SKILL.md" } }, { sessionId: "s1" });
178
+ await tick();
179
+ captured.length = 0;
180
+
181
+ hooks.onBeforeToolCall(
182
+ { toolName: "exec", params: { command: "custom command --bad" }, toolCallId: "t5" },
183
+ { sessionId: "s1" }
184
+ );
185
+ await tick();
186
+ assert.equal(captured.length, 0);
187
+
188
+ hooks.onAfterToolCall({ toolCallId: "t5", error: { message: "custom failed" }, durationMs: 11 });
189
+ await tick();
190
+ assert.equal(captured.length, 1);
191
+ assert.equal(captured[0].event_type, "function_call");
192
+ assert.equal(captured[0].skill_name, "model-usage");
193
+ assert.equal(captured[0].invoke_tool, "exec");
194
+ assert.equal(captured[0].command, "custom command --bad");
195
+ assert.equal(captured[0].status, "error");
196
+ assert.equal(captured[0].error_message, "custom failed");
197
+ assert.equal(captured[0].duration_ms, 11);
198
+ });
199
+
200
+ it("report_skill_error 手动上报会记录 error 事件并携带会话 appKey", async () => {
201
+ const { hooks, captured } = setup();
202
+ hooks.onMessageReceived({ content: "appKey: abcdef123456" }, { sessionId: "s1" });
203
+ hooks.onManualErrorRecord(
204
+ {
205
+ skill_name: "model-usage",
206
+ tool_name: "usage_current",
207
+ error_message: "manual failure",
208
+ input_args: "--mode current",
209
+ },
210
+ { sessionId: "s1", agentId: "main", runId: "r1" }
211
+ );
212
+ await tick();
213
+ assert.equal(captured.length, 1);
214
+ assert.equal(captured[0].event_type, "function_call");
215
+ assert.equal(captured[0].skill_name, "model-usage");
216
+ assert.equal(captured[0].invoke_tool, "report_skill_error");
217
+ assert.equal(captured[0].status, "error");
218
+ assert.equal(captured[0].error_message, "manual failure");
219
+ assert.equal(captured[0].app_key, "abcdef123456");
220
+ assert.deepEqual(captured[0].args, { raw_args: "--mode current" });
221
+ });
222
+
223
+ it("未命中且默认配置 → 不记录", async () => {
224
+ const { hooks, captured } = setup();
225
+ hooks.onBeforeToolCall({ toolName: "exec", params: { command: "ls -la" }, toolCallId: "t3" }, { sessionId: "s1" });
226
+ hooks.onAfterToolCall({ toolCallId: "t3" });
227
+ await tick();
228
+ assert.equal(captured.length, 0);
229
+ });
230
+ });
231
+
232
+ describe("插件模块图可加载", () => {
233
+ it("import index.ts 并 register 注册全部生命周期 hook", async () => {
234
+ const mod = await import("./index.ts");
235
+ const registered: string[] = [];
236
+ mod.default.register({ on: (name: string) => registered.push(name) });
237
+ assert.deepEqual(
238
+ registered.sort(),
239
+ [
240
+ "after_tool_call",
241
+ "before_install",
242
+ "before_prompt_build",
243
+ "before_tool_call",
244
+ "gateway_start",
245
+ "gateway_stop",
246
+ "message_received",
247
+ "session_end",
248
+ ]
249
+ );
250
+ });
251
+ });