@zhushanwen/pi-rename-session 0.5.2 → 0.5.3

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-rename-session",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "xyz-agent": {
@@ -18,7 +18,8 @@
18
18
  "pi-package"
19
19
  ],
20
20
  "dependencies": {
21
- "@zhushanwen/pi-llm-shared": "0.4.0"
21
+ "@zhushanwen/pi-extension-logger": "0.3.0",
22
+ "@zhushanwen/pi-llm-shared": "0.4.1"
22
23
  },
23
24
  "devDependencies": {
24
25
  "@vitest/coverage-v8": "^4.1.9",
@@ -1,5 +1,15 @@
1
1
  /* eslint-disable taste/no-unsafe-cast */
2
2
 
3
+ // Mock 共享 logger,让 logger.warn/error 可被 spy(源码已从 console 改为 logger)
4
+ const { loggerMock } = vi.hoisted(() => ({
5
+ loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
6
+ }));
7
+ vi.mock("@zhushanwen/pi-extension-logger", () => ({
8
+ getLogger: () => loggerMock,
9
+ createLogger: () => loggerMock,
10
+ setPiHandle: vi.fn(),
11
+ }));
12
+
3
13
  import type { Api, Model } from "@earendil-works/pi-ai";
4
14
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
5
15
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -135,19 +145,20 @@ function fire(setup: MockSetup, ctx: ExtensionContext, message?: unknown): Promi
135
145
  ) as Promise<void>;
136
146
  }
137
147
 
138
- /** 开 debug 开关 + 静音 warn,返回 spy 供日志断言(还原由顶层 afterEach 统一负责)。 */
139
- function debugWarnSpy(): ReturnType<typeof vi.spyOn> {
148
+ /** 开 debug 开关 + 清空 warn mock,返回 loggerMock.warn 供日志断言(还原由顶层 afterEach 统一负责)。 */
149
+ function debugWarnSpy(): ReturnType<typeof vi.fn> {
140
150
  vi.stubEnv("XYZ_AGENT_DEBUG", "1");
141
- return vi.spyOn(console, "warn").mockImplementation(() => {});
151
+ loggerMock.warn.mockClear();
152
+ return loggerMock.warn;
142
153
  }
143
154
 
144
155
  /** warn spy 的全部调用文本行(debug 日志断言用)。 */
145
- function warnLines(warnSpy: ReturnType<typeof vi.spyOn>): string[] {
156
+ function warnLines(warnSpy: ReturnType<typeof vi.fn>): string[] {
146
157
  return warnSpy.mock.calls.map((c) => String(c[0]));
147
158
  }
148
159
 
149
160
  /** 拼接 warn spy 的全部调用为单行文本(debug 日志断言用)。 */
150
- function warnText(warnSpy: ReturnType<typeof vi.spyOn>): string {
161
+ function warnText(warnSpy: ReturnType<typeof vi.fn>): string {
151
162
  return warnLines(warnSpy).join("\n");
152
163
  }
153
164
 
@@ -183,9 +194,9 @@ describe("renameSessionExtension", () => {
183
194
  expect(setup.setSessionNameMock).not.toHaveBeenCalled();
184
195
  });
185
196
 
186
- it("TC15: getEntries 抛错时 handler 不抛(catch console.error)", async () => {
197
+ it("TC15: getEntries 抛错时 handler 不抛(catch logger.error)", async () => {
187
198
  vi.mocked(loadRenameConfig).mockReturnValue(ENABLED_CONFIG);
188
- const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
199
+ loggerMock.error.mockClear();
189
200
  const ctx = {
190
201
  sessionManager: {
191
202
  getEntries: () => {
@@ -199,9 +210,8 @@ describe("renameSessionExtension", () => {
199
210
 
200
211
  await expect(fire(setup, ctx)).resolves.toBeUndefined();
201
212
 
202
- expect(errorSpy).toHaveBeenCalled();
213
+ expect(loggerMock.error).toHaveBeenCalled();
203
214
  expect(setup.setSessionNameMock).not.toHaveBeenCalled();
204
- errorSpy.mockRestore();
205
215
  });
206
216
 
207
217
  // ────────────────────────────────────────────────────
@@ -221,7 +231,7 @@ describe("renameSessionExtension", () => {
221
231
  const expected = `turnIndex=${FIRE_TURN_INDEX} skip: stopReason=${String(r)}`;
222
232
  expect(warnText(warnSpy)).toContain(expected);
223
233
  const line = warnLines(warnSpy).find((l) => l.includes(`skip: stopReason=${String(r)}`));
224
- expect(line).toMatch(/^\[rename-session\] t=\d{4}-\d{2}-\d{2}T/);
234
+ expect(line).toMatch(/^t=\d{4}-\d{2}-\d{2}T/);
225
235
  }
226
236
 
227
237
  expect(resolveModel).not.toHaveBeenCalled();
@@ -360,13 +370,12 @@ describe("renameSessionExtension", () => {
360
370
  vi.mocked(loadRenameConfig).mockReturnValue(ENABLED_CONFIG);
361
371
  vi.mocked(resolveModel).mockReturnValue(STUB_MODEL);
362
372
  vi.mocked(callLLM).mockRejectedValue(new Error("llm down"));
363
- const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
373
+ loggerMock.error.mockClear();
364
374
 
365
375
  await expect(fire(setup, createMockCtx())).resolves.toBeUndefined();
366
376
  await vi.waitFor(() => expect(callLLM).toHaveBeenCalledTimes(1));
367
377
 
368
378
  expect(setup.setSessionNameMock).not.toHaveBeenCalled();
369
- errorSpy.mockRestore();
370
379
  });
371
380
 
372
381
  it("LTC13: 首 turn 判定(成功 assistant 回复数 !== 1)→ 不调 resolveModel", async () => {
@@ -419,9 +428,9 @@ describe("renameSessionExtension", () => {
419
428
 
420
429
  const renamedIdx = warnLines(warnSpy).findIndex((l) => l.includes('renamed to "自动生成的标题"'));
421
430
  expect(renamedIdx).toBeGreaterThanOrEqual(0);
422
- // handler 侧日志契约(C3):[rename-session] + t=<ISO> + turnIndex=<n> + renamed to "<title>"
431
+ // handler 侧日志契约(C3):t=<ISO> + turnIndex=<n> + renamed to "<title>"([rename-session] 前缀由共享 logger 自动补)
423
432
  expect(String(warnSpy.mock.calls[renamedIdx][0])).toMatch(
424
- new RegExp(`^\\[rename-session\\] t=\\d{4}-\\d{2}-\\d{2}T.* turnIndex=${FIRE_TURN_INDEX} renamed to "自动生成的标题"$`),
433
+ new RegExp(`^t=\\d{4}-\\d{2}-\\d{2}T.* turnIndex=${FIRE_TURN_INDEX} renamed to "自动生成的标题"$`),
425
434
  );
426
435
  // 移位契约(时序):日志调用序晚于 setSessionName——先落库后报捷
427
436
  expect(warnSpy.mock.invocationCallOrder[renamedIdx]).toBeGreaterThan(
@@ -436,7 +445,8 @@ describe("renameSessionExtension", () => {
436
445
  it("TC9: XYZ_AGENT_DEBUG 未设 → 全流程 7 条 debug 契约文案零输出(既有 A1 日志除外)", async () => {
437
446
  // 显式清除(防宿主环境泄漏 XYZ_AGENT_DEBUG 影响判定)
438
447
  vi.stubEnv("XYZ_AGENT_DEBUG", undefined);
439
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
448
+ loggerMock.warn.mockClear();
449
+ const warnSpy = loggerMock.warn;
440
450
  vi.mocked(loadRenameConfig).mockReturnValue(ENABLED_CONFIG);
441
451
  vi.mocked(resolveModel).mockReturnValue(STUB_MODEL);
442
452
 
@@ -1,5 +1,15 @@
1
1
  /* eslint-disable taste/no-unsafe-cast */
2
2
 
3
+ // Mock 共享 logger,让 logger.warn/error 可被 spy(源码已从 console 改为 logger)
4
+ const { loggerMock } = vi.hoisted(() => ({
5
+ loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
6
+ }));
7
+ vi.mock("@zhushanwen/pi-extension-logger", () => ({
8
+ getLogger: () => loggerMock,
9
+ createLogger: () => loggerMock,
10
+ setPiHandle: vi.fn(),
11
+ }));
12
+
3
13
  import type { Api, Model } from "@earendil-works/pi-ai";
4
14
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
5
15
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -245,14 +255,15 @@ describe("RENAME_SYSTEM_PROMPT / RENAME_INSTRUCTION", () => {
245
255
  // callRenameLLM(mock resolveModel + callLLM @ llm-shared 边界)
246
256
  // ────────────────────────────────────────────────────
247
257
 
248
- /** 开 debug 开关 + 静音 warn,返回 spy 供日志断言(还原由顶层 afterEach 统一负责)。 */
249
- function debugWarnSpy(): ReturnType<typeof vi.spyOn> {
258
+ /** 开 debug 开关 + 清空 warn mock,返回 loggerMock.warn 供日志断言(还原由顶层 afterEach 统一负责)。 */
259
+ function debugWarnSpy(): ReturnType<typeof vi.fn> {
250
260
  vi.stubEnv("XYZ_AGENT_DEBUG", "1");
251
- return vi.spyOn(console, "warn").mockImplementation(() => {});
261
+ loggerMock.warn.mockClear();
262
+ return loggerMock.warn;
252
263
  }
253
264
 
254
265
  /** warn spy 的全部调用文本行(debug 日志断言用)。 */
255
- function warnLines(warnSpy: ReturnType<typeof vi.spyOn>): string[] {
266
+ function warnLines(warnSpy: ReturnType<typeof vi.fn>): string[] {
256
267
  return warnSpy.mock.calls.map((c) => String(c[0]));
257
268
  }
258
269
 
@@ -480,9 +491,9 @@ describe("callRenameLLM", () => {
480
491
 
481
492
  expect(result).toBeNull();
482
493
  expect(callLLM).not.toHaveBeenCalled();
483
- // C3 契约:[rename-session] + t=<ISO>(llm.ts 侧不含 turnIndex
494
+ // C3 契约:t=<ISO>(llm.ts 侧不含 turnIndex;[rename-session] 前缀由共享 logger prefixMsg 自动补,spy 捕获原始入参)
484
495
  const line = warnLines(warnSpy).find((l) => l.includes("skip: no user prompt"));
485
- expect(line).toMatch(/^\[rename-session\] t=\d{4}-\d{2}-\d{2}T/);
496
+ expect(line).toMatch(/^t=\d{4}-\d{2}-\d{2}T/);
486
497
  });
487
498
 
488
499
  it("TC7: callLLM ok:false(超时/模型错误)→ 返回 null 不抛错(失败归一静默跳过)", async () => {
@@ -501,7 +512,7 @@ describe("callRenameLLM", () => {
501
512
  expect(result).toBeNull();
502
513
  // 该日志在 cleanTitle 清洗为空、返回 null 前打出(7 条契约文案之一)
503
514
  const line = warnLines(warnSpy).find((l) => l.includes("skip: title empty"));
504
- expect(line).toMatch(/^\[rename-session\] t=\d{4}-\d{2}-\d{2}T/);
515
+ expect(line).toMatch(/^t=\d{4}-\d{2}-\d{2}T/);
505
516
  });
506
517
 
507
518
  it("maxTitleLength 截断生效(config.maxTitleLength 透传给 cleanTitle)", async () => {
@@ -548,65 +559,50 @@ describe("callRenameLLM A1 日志", () => {
548
559
  });
549
560
 
550
561
  it("TC1: resolveModel null → console 输出 '[rename-session] model not available, skipping',返回 null", async () => {
551
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
552
- try {
553
- vi.mocked(resolveModel).mockReturnValue(null);
554
- const result = await callRenameLLM(createCtx(), BASE_CONFIG, FINAL_MESSAGE);
555
- expect(result).toBeNull();
556
- expect(warnSpy).toHaveBeenCalledWith("[rename-session] model not available, skipping");
557
- } finally {
558
- warnSpy.mockRestore();
559
- }
562
+ loggerMock.warn.mockClear();
563
+ vi.mocked(resolveModel).mockReturnValue(null);
564
+ const result = await callRenameLLM(createCtx(), BASE_CONFIG, FINAL_MESSAGE);
565
+ expect(result).toBeNull();
566
+ expect(loggerMock.warn).toHaveBeenCalledWith("model not available, skipping");
560
567
  });
561
568
 
562
569
  it("TC2: callLLM {ok:false} → console 输出 '[rename-session] rename LLM call failed: <error>',返回 null", async () => {
563
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
564
- try {
565
- vi.mocked(resolveModel).mockReturnValue(STUB_MODEL);
566
- vi.mocked(callLLM).mockResolvedValue({ ok: false, error: "boom", recoverable: true });
567
- const result = await callRenameLLM(createCtx(), BASE_CONFIG, FINAL_MESSAGE);
568
- expect(result).toBeNull();
569
- expect(warnSpy).toHaveBeenCalledWith("[rename-session] rename LLM call failed: boom");
570
- } finally {
571
- warnSpy.mockRestore();
572
- }
570
+ loggerMock.warn.mockClear();
571
+ vi.mocked(resolveModel).mockReturnValue(STUB_MODEL);
572
+ vi.mocked(callLLM).mockResolvedValue({ ok: false, error: "boom", recoverable: true });
573
+ const result = await callRenameLLM(createCtx(), BASE_CONFIG, FINAL_MESSAGE);
574
+ expect(result).toBeNull();
575
+ expect(loggerMock.warn).toHaveBeenCalledWith("rename LLM call failed", { error: "boom" });
573
576
  });
574
577
 
575
578
  it("TC2b: callLLM {ok:false} → 不输出 'rename with model' 成功日志(B2 位置修正:成功日志已移到 result.ok 分支后),但仍输出 failed 日志", async () => {
576
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
577
- try {
578
- vi.mocked(resolveModel).mockReturnValue(STUB_MODEL);
579
- vi.mocked(callLLM).mockResolvedValue({ ok: false, error: "boom", recoverable: true });
580
- const result = await callRenameLLM(createCtx(), BASE_CONFIG, FINAL_MESSAGE);
581
- expect(result).toBeNull();
582
- // 失败分支仍输出 failed 日志
583
- expect(warnSpy).toHaveBeenCalledWith(
584
- "[rename-session] rename LLM call failed: boom",
585
- );
586
- // 失败分支不应输出成功日志(B2:成功日志移到 result.ok=true 之后)
587
- const successLogCalls = warnSpy.mock.calls.filter((c) =>
588
- String(c[0]).includes("rename with model"),
589
- );
590
- expect(successLogCalls).toHaveLength(0);
591
- } finally {
592
- warnSpy.mockRestore();
593
- }
579
+ loggerMock.warn.mockClear();
580
+ vi.mocked(resolveModel).mockReturnValue(STUB_MODEL);
581
+ vi.mocked(callLLM).mockResolvedValue({ ok: false, error: "boom", recoverable: true });
582
+ const result = await callRenameLLM(createCtx(), BASE_CONFIG, FINAL_MESSAGE);
583
+ expect(result).toBeNull();
584
+ // 失败分支仍输出 failed 日志
585
+ expect(loggerMock.warn).toHaveBeenCalledWith(
586
+ "rename LLM call failed",
587
+ { error: "boom" },
588
+ );
589
+ // 失败分支不应输出成功日志(B2:成功日志移到 result.ok=true 之后)
590
+ const successLogCalls = loggerMock.warn.mock.calls.filter((c) =>
591
+ String(c[0]).includes("rename with model"),
592
+ );
593
+ expect(successLogCalls).toHaveLength(0);
594
594
  });
595
595
 
596
596
  it("TC3: callLLM 成功 → 默认不输出 'rename with model'(避免常开日志污染 Pi 输入框),返回标题", async () => {
597
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
598
- try {
599
- vi.mocked(resolveModel).mockReturnValue(STUB_MODEL);
600
- vi.mocked(callLLM).mockResolvedValue({ ok: true, content: "修复登录bug" });
601
- const result = await callRenameLLM(createCtx(), BASE_CONFIG, FINAL_MESSAGE);
602
- expect(result).toBe("修复登录bug");
603
- const successLogCalls = warnSpy.mock.calls.filter((c) =>
604
- String(c[0]).includes("rename with model"),
605
- );
606
- expect(successLogCalls).toHaveLength(0);
607
- } finally {
608
- warnSpy.mockRestore();
609
- }
597
+ loggerMock.warn.mockClear();
598
+ vi.mocked(resolveModel).mockReturnValue(STUB_MODEL);
599
+ vi.mocked(callLLM).mockResolvedValue({ ok: true, content: "修复登录bug" });
600
+ const result = await callRenameLLM(createCtx(), BASE_CONFIG, FINAL_MESSAGE);
601
+ expect(result).toBe("修复登录bug");
602
+ const successLogCalls = loggerMock.warn.mock.calls.filter((c) =>
603
+ String(c[0]).includes("rename with model"),
604
+ );
605
+ expect(successLogCalls).toHaveLength(0);
610
606
  });
611
607
 
612
608
  it("TC3b: debug 开启 + callLLM 成功 → 输出 'rename with model <provider>/<modelId>'(B3 带 provider 前缀),返回标题", async () => {
@@ -617,7 +613,7 @@ describe("callRenameLLM A1 日志", () => {
617
613
  expect(result).toBe("修复登录bug");
618
614
  const line = warnLines(warnSpy).find((l) => l.includes("rename with model"));
619
615
  expect(line).toMatch(
620
- /^\[rename-session\] t=\d{4}-\d{2}-\d{2}T.*rename with model stub\/stub-model/,
616
+ /^t=\d{4}-\d{2}-\d{2}T.*rename with model stub\/stub-model/,
621
617
  );
622
618
  });
623
619
  });
package/src/index.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { getLogger } from "@zhushanwen/pi-extension-logger";
2
3
 
3
4
  import { registerAutoRenameCommand } from "./commands.js";
4
5
  import { callRenameLLM, debugLog as llmDebugLog, isSubagentSession } from "./llm.js";
5
6
  import { countSuccessfulAssistantReplies, loadRenameConfig } from "./pure.js";
6
7
 
8
+ const logger = getLogger("rename-session");
9
+
7
10
  /**
8
11
  * turn_end 事件的宽松类型(参考 pi extensions/types.ts 的 TurnEndEvent)。
9
12
  * message 收紧为带 stopReason 的宽松结构(D2 快速路径读取),不依赖 pi 类型导出。
@@ -73,13 +76,13 @@ export default function renameSessionExtension(pi: ExtensionAPI): void {
73
76
  // 防覆盖 return 在前,竞态命中时本日志不出现,避免「日志称 renamed 但未落库」)
74
77
  debugLog(`renamed to "${title}"`);
75
78
  })
76
- .catch((e) => console.error("[pi-rename-session] rename LLM failed:", e));
79
+ .catch((e) => logger.error("rename LLM failed", { error: String(e) }));
77
80
  // rename 是 best-effort,任何 LLM 失败(网络/提取/auth/model 不可用)都静默跳过保留原 label,
78
81
  // 不进 session history。
79
82
  } catch (e) {
80
83
  // best-effort 降级:turn_end handler 同步部分(开关/subagent/判定)抛错时记录但不阻断
81
84
  // agent 循环——rename 是非关键副作用,任何失败不得干扰主对话。
82
- console.error("[pi-rename-session] failed:", e);
85
+ logger.error("failed", { error: String(e) });
83
86
  }
84
87
  });
85
88
  }
package/src/llm.ts CHANGED
@@ -2,10 +2,13 @@ import path from "node:path";
2
2
 
3
3
  import type { AssistantMessage, Message } from "@earendil-works/pi-ai";
4
4
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
5
+ import { getLogger } from "@zhushanwen/pi-extension-logger";
5
6
  import { callLLM, resolveModel } from "@zhushanwen/pi-llm-shared";
6
7
 
7
8
  import { type RenameSessionConfig, cleanTitle } from "./pure.js";
8
9
 
10
+ const logger = getLogger("rename-session");
11
+
9
12
  // ──────────────────────── prompt 常量 ────────────────────────
10
13
 
11
14
  /**
@@ -161,7 +164,7 @@ export function isRenameDebugEnabled(): boolean {
161
164
  */
162
165
  export function debugLog(message: string): void {
163
166
  if (isRenameDebugEnabled()) {
164
- console.warn(`[rename-session] t=${new Date().toISOString()} ${message}`);
167
+ logger.warn(`t=${new Date().toISOString()} ${message}`);
165
168
  }
166
169
  }
167
170
 
@@ -221,7 +224,7 @@ export async function callRenameLLM(
221
224
  const model = resolveModel(ctx, config.model);
222
225
  if (!model) {
223
226
  // A1 日志:不可用不静默(可排查)
224
- console.warn("[rename-session] model not available, skipping");
227
+ logger.warn("model not available, skipping");
225
228
  return null;
226
229
  }
227
230
 
@@ -266,7 +269,7 @@ export async function callRenameLLM(
266
269
  });
267
270
  if (!result.ok) {
268
271
  // A1 失败路径日志:调用失败不静默(不抛错,靠日志留痕)。
269
- console.warn(`[rename-session] rename LLM call failed: ${result.error ?? "unknown error"}`);
272
+ logger.warn("rename LLM call failed", { error: result.error ?? "unknown error" });
270
273
  return null;
271
274
  }
272
275