@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 +3 -2
- package/src/__tests__/index.test.ts +25 -15
- package/src/__tests__/llm.test.ts +54 -58
- package/src/index.ts +5 -2
- package/src/llm.ts +6 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-rename-session",
|
|
3
|
-
"version": "0.5.
|
|
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-
|
|
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 开关 +
|
|
139
|
-
function debugWarnSpy(): ReturnType<typeof vi.
|
|
148
|
+
/** 开 debug 开关 + 清空 warn mock,返回 loggerMock.warn 供日志断言(还原由顶层 afterEach 统一负责)。 */
|
|
149
|
+
function debugWarnSpy(): ReturnType<typeof vi.fn> {
|
|
140
150
|
vi.stubEnv("XYZ_AGENT_DEBUG", "1");
|
|
141
|
-
|
|
151
|
+
loggerMock.warn.mockClear();
|
|
152
|
+
return loggerMock.warn;
|
|
142
153
|
}
|
|
143
154
|
|
|
144
155
|
/** warn spy 的全部调用文本行(debug 日志断言用)。 */
|
|
145
|
-
function warnLines(warnSpy: ReturnType<typeof vi.
|
|
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.
|
|
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
|
|
197
|
+
it("TC15: getEntries 抛错时 handler 不抛(catch logger.error)", async () => {
|
|
187
198
|
vi.mocked(loadRenameConfig).mockReturnValue(ENABLED_CONFIG);
|
|
188
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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):
|
|
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(
|
|
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
|
-
|
|
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 开关 +
|
|
249
|
-
function debugWarnSpy(): ReturnType<typeof vi.
|
|
258
|
+
/** 开 debug 开关 + 清空 warn mock,返回 loggerMock.warn 供日志断言(还原由顶层 afterEach 统一负责)。 */
|
|
259
|
+
function debugWarnSpy(): ReturnType<typeof vi.fn> {
|
|
250
260
|
vi.stubEnv("XYZ_AGENT_DEBUG", "1");
|
|
251
|
-
|
|
261
|
+
loggerMock.warn.mockClear();
|
|
262
|
+
return loggerMock.warn;
|
|
252
263
|
}
|
|
253
264
|
|
|
254
265
|
/** warn spy 的全部调用文本行(debug 日志断言用)。 */
|
|
255
|
-
function warnLines(warnSpy: ReturnType<typeof vi.
|
|
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 契约:
|
|
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(
|
|
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(
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
272
|
+
logger.warn("rename LLM call failed", { error: result.error ?? "unknown error" });
|
|
270
273
|
return null;
|
|
271
274
|
}
|
|
272
275
|
|