@zhushanwen/pi-session-manager 0.4.0 → 0.6.0

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.
@@ -0,0 +1,188 @@
1
+ /**
2
+ * tool-prompt-contract.test.ts — U6 锚词契约测试(session-manager-archive-delete)。
3
+ *
4
+ * 断言对象 = 注册数据形态(mock pi 捕获 registerTool 实参,读 description /
5
+ * promptSnippet / exposure / parameters 的运行时值——不读源码文本,格式重排与
6
+ * 转义形态不敏感)。断言范围与权威源 = 设计 §4 验收 5(契约测试锁文案双轨,
7
+ * 防约束文案被无意改掉 = 约束静默消失)。
8
+ *
9
+ * 两段门控(impl-plan §2 u-contract-test 行):
10
+ * 1. 结构断言段:始终生效(随两终局工具注册在场即绿)——8 工具注册全集、
11
+ * delete_session 的 USER-INITIATED ONLY 双锚词(description + promptSnippet)、
12
+ * 其余 7 工具两处零该锚词(防约束文案复制稀释)、delete/archive exposure=deferred。
13
+ * 2. 文案断言段:挂 u-copy-rewrite 的 commit 门——COPY_READY = 六个现有工具
14
+ * description 均含 "## Example";copy 落地前整段 skip,落地后自动生效。
15
+ * 内容:8 工具 description 含 Example 段、含 sessionId 参数的 6 工具其
16
+ * sessionId 字段说明含 list_my_sessions 来源句式、description 零 "times out"
17
+ * (P0 漂移词禁入)、环境行统一短语 "hangs forever" 在场。
18
+ *
19
+ * 框架:vitest(禁 node:test)。
20
+ */
21
+ import { describe, expect, it, vi } from "vitest";
22
+
23
+ import registerExtension from "../index.ts";
24
+
25
+ /** mock pi:捕获 registerTool 实参全字段(含 promptSnippet / exposure)。 */
26
+ function createMockPi() {
27
+ const registered: Array<Record<string, unknown>> = [];
28
+ const pi = {
29
+ registerTool: (tool: Record<string, unknown>) => {
30
+ registered.push(tool);
31
+ },
32
+ on: vi.fn(),
33
+ getAllTools: vi.fn(() => []),
34
+ setActiveTools: vi.fn(),
35
+ };
36
+ return { pi, registered };
37
+ }
38
+
39
+ const { pi, registered } = createMockPi();
40
+ registerExtension(pi as never);
41
+
42
+ /** 运行时检查(extensions taste/no-unsafe-cast 规范:窄化断言须有 guard)。 */
43
+ function isRecord(v: unknown): v is Record<string, unknown> {
44
+ return typeof v === "object" && v !== null;
45
+ }
46
+
47
+ function tool(name: string): Record<string, unknown> {
48
+ const t = registered.find((r) => r.name === name);
49
+ if (!isRecord(t)) throw new Error(`tool not registered: ${name}`);
50
+ return t;
51
+ }
52
+
53
+ function description(name: string): string {
54
+ const d = tool(name).description;
55
+ if (typeof d !== "string") throw new Error(`${name} description is not a string`);
56
+ return d;
57
+ }
58
+
59
+ /** promptSnippet 的可检视图:未配置 = 空串(负锚检测不受 undefined 影响)。 */
60
+ function promptSnippet(name: string): string {
61
+ const s = tool(name).promptSnippet;
62
+ return typeof s === "string" ? s : "";
63
+ }
64
+
65
+ /** schema 的可检视图:经 JSON 往返剥离 typebox 类型包装,只看数据形态。 */
66
+ function schemaProperties(name: string): Record<string, unknown> {
67
+ const raw: unknown = JSON.parse(JSON.stringify(tool(name).parameters));
68
+ if (!isRecord(raw)) throw new Error(`${name} parameters is not an object schema`);
69
+ if (!isRecord(raw.properties)) throw new Error(`${name} schema has no properties`);
70
+ return raw.properties;
71
+ }
72
+
73
+ // ── 工具全集(与 tool-registration.test.ts 的 8 名清单同源)──
74
+
75
+ const ALL_TOOL_NAMES = [
76
+ "create_managed_session",
77
+ "send_to_session",
78
+ "read_session_history",
79
+ "list_my_sessions",
80
+ "get_session_status",
81
+ "abort_session",
82
+ "delete_session",
83
+ "archive_session",
84
+ ] as const;
85
+
86
+ /** 含 sessionId 参数的 6 工具(id 来源句式断言范围——create 无 id 参数、list 无参数)。 */
87
+ const SESSION_ID_TOOLS = [
88
+ "send_to_session",
89
+ "read_session_history",
90
+ "get_session_status",
91
+ "abort_session",
92
+ "delete_session",
93
+ "archive_session",
94
+ ] as const;
95
+
96
+ /** 六个现有工具(u-copy-rewrite 的重写对象;COPY_READY 检测范围)。 */
97
+ const LEGACY_SIX = [
98
+ "create_managed_session",
99
+ "send_to_session",
100
+ "read_session_history",
101
+ "list_my_sessions",
102
+ "get_session_status",
103
+ "abort_session",
104
+ ] as const;
105
+
106
+ // ══════════════════════════════════════════════════════════════
107
+ // 结构断言段(始终生效)
108
+ // ══════════════════════════════════════════════════════════════
109
+
110
+ describe("结构断言段:注册全集与 delete 人身约束双锚词", () => {
111
+ it("8 工具全部注册,名字集合精确匹配(漏注册/多注册/改名即红)", () => {
112
+ expect(registered).toHaveLength(ALL_TOOL_NAMES.length);
113
+ expect(new Set(registered.map((t) => t.name))).toEqual(new Set(ALL_TOOL_NAMES));
114
+ });
115
+
116
+ it("delete_session:description 与 promptSnippet 双含 USER-INITIATED ONLY", () => {
117
+ expect(description("delete_session"), "delete description 缺人身约束锚词").toContain(
118
+ "USER-INITIATED ONLY",
119
+ );
120
+ expect(promptSnippet("delete_session"), "delete promptSnippet 缺人身约束锚词").toContain(
121
+ "USER-INITIATED ONLY",
122
+ );
123
+ });
124
+
125
+ it("delete_session:description 含词义消歧锚(设计验收 5)", () => {
126
+ expect(description("delete_session")).toContain("means archive_session, NOT this tool");
127
+ });
128
+
129
+ it("其余 7 工具:description 与 promptSnippet 两处均不含 USER-INITIATED ONLY(防约束文案复制稀释)", () => {
130
+ for (const name of ALL_TOOL_NAMES) {
131
+ if (name === "delete_session") continue;
132
+ expect(description(name), `${name} description 不应含约束锚词`).not.toContain(
133
+ "USER-INITIATED ONLY",
134
+ );
135
+ expect(promptSnippet(name), `${name} promptSnippet 不应含约束锚词`).not.toContain(
136
+ "USER-INITIATED ONLY",
137
+ );
138
+ }
139
+ });
140
+
141
+ it("两新工具 exposure 为 deferred(delete_session / archive_session)", () => {
142
+ expect(tool("delete_session").exposure, "delete_session exposure 应为 deferred").toBe(
143
+ "deferred",
144
+ );
145
+ expect(tool("archive_session").exposure, "archive_session exposure 应为 deferred").toBe(
146
+ "deferred",
147
+ );
148
+ });
149
+ });
150
+
151
+ // ══════════════════════════════════════════════════════════════
152
+ // 文案断言段(COPY_READY 门:u-copy-rewrite 文案落地前整段 skip)
153
+ // ══════════════════════════════════════════════════════════════
154
+
155
+ const COPY_READY = LEGACY_SIX.every((name) => description(name).includes("## Example"));
156
+
157
+ describe.skipIf(!COPY_READY)("文案断言段:8 工具文案契约(u-copy-rewrite 落地后自动生效)", () => {
158
+ it("8 工具 description 均含 ## Example 段", () => {
159
+ for (const name of ALL_TOOL_NAMES) {
160
+ expect(description(name), `${name} description 缺 ## Example 段`).toContain("## Example");
161
+ }
162
+ });
163
+
164
+ it("sessionId 参数 6 工具:sessionId 字段说明含 list_my_sessions 来源句式", () => {
165
+ for (const name of SESSION_ID_TOOLS) {
166
+ const sessionId = schemaProperties(name).sessionId;
167
+ if (!isRecord(sessionId)) throw new Error(`${name} schema 缺 sessionId 参数`);
168
+ expect(
169
+ sessionId.description,
170
+ `${name} sessionId 字段说明缺 id 来源句式`,
171
+ ).toContain("list_my_sessions");
172
+ }
173
+ });
174
+
175
+ it("8 工具 description 零 times out(P0 漂移词禁入)", () => {
176
+ for (const name of ALL_TOOL_NAMES) {
177
+ expect(description(name), `${name} description 残留 times out`).not.toContain("times out");
178
+ }
179
+ });
180
+
181
+ it("环境行统一短语 hangs forever 在场(8 工具)", () => {
182
+ for (const name of ALL_TOOL_NAMES) {
183
+ expect(description(name), `${name} description 缺 hangs forever 环境行`).toContain(
184
+ "hangs forever",
185
+ );
186
+ }
187
+ });
188
+ });
@@ -1,4 +1,4 @@
1
- // tool-registration.test.ts — U5-A1: 6 tools registered with correct name/description/parameters + SESSION_MANAGER_MARKER import
1
+ // tool-registration.test.ts — U5-A1: 8 tools registered with correct name/description/parameters + SESSION_MANAGER_MARKER import
2
2
 
3
3
  import { describe, it, expect, vi } from "vitest";
4
4
  import { SESSION_MANAGER_MARKER } from "@zhushanwen/extension-protocol";
@@ -25,13 +25,15 @@ const EXPECTED_TOOL_NAMES = [
25
25
  "list_my_sessions",
26
26
  "get_session_status",
27
27
  "abort_session",
28
+ "delete_session",
29
+ "archive_session",
28
30
  ] as const;
29
31
 
30
32
  describe("U5-A1 tool-registration", () => {
31
- it("registers exactly 6 tools", () => {
33
+ it("registers exactly 8 tools", () => {
32
34
  const { pi, registered } = createMockPi();
33
35
  registerExtension(pi as never);
34
- expect(registered).toHaveLength(6);
36
+ expect(registered).toHaveLength(8);
35
37
  });
36
38
 
37
39
  it.each(EXPECTED_TOOL_NAMES)('registers tool "%s"', (name) => {
@@ -3,16 +3,22 @@
3
3
  import { describe, it, expect } from "vitest";
4
4
  import registerExtension from "../index.ts";
5
5
 
6
- function getRegisteredSchemas() {
7
- const registered: Array<{ name: string; parameters: Record<string, unknown> }> = [];
6
+ function getRegisteredTools() {
7
+ const registered: Array<Record<string, unknown>> = [];
8
8
  const pi = {
9
- registerTool: (tool: { name: string; parameters: Record<string, unknown> }) => registered.push(tool),
9
+ registerTool: (tool: Record<string, unknown>) => registered.push(tool),
10
10
  on: () => {},
11
11
  getAllTools: () => [],
12
12
  setActiveTools: () => {},
13
13
  };
14
14
  registerExtension(pi as never);
15
- return Object.fromEntries(registered.map((t) => [t.name, t.parameters]));
15
+ return registered;
16
+ }
17
+
18
+ function getRegisteredSchemas() {
19
+ return Object.fromEntries(
20
+ getRegisteredTools().map((t) => [t.name, t.parameters]),
21
+ ) as Record<string, Record<string, unknown>>;
16
22
  }
17
23
 
18
24
  describe("U5-A3 tool-schema", () => {
@@ -113,4 +119,43 @@ describe("U5-A3 tool-schema", () => {
113
119
  expect(required).toContain("sessionId");
114
120
  });
115
121
  });
122
+
123
+ // session-manager-archive-delete U4:两终局工具注册存在 + exposure deferred + schema 必填性
124
+ describe("delete_session", () => {
125
+ it("is registered with exposure deferred", () => {
126
+ const tool = getRegisteredTools().find((t) => t.name === "delete_session");
127
+ expect(tool).toBeDefined();
128
+ expect(tool?.exposure).toBe("deferred");
129
+ });
130
+
131
+ it("requires sessionId (string)", () => {
132
+ const s = schemas.delete_session;
133
+ expect(s).toBeDefined();
134
+ expect(s.type).toBe("object");
135
+ const props = s.properties as Record<string, unknown>;
136
+ expect(props.sessionId).toBeDefined();
137
+ expect((props.sessionId as Record<string, unknown>).type).toBe("string");
138
+ const required = s.required as string[];
139
+ expect(required).toContain("sessionId");
140
+ });
141
+ });
142
+
143
+ describe("archive_session", () => {
144
+ it("is registered with exposure deferred", () => {
145
+ const tool = getRegisteredTools().find((t) => t.name === "archive_session");
146
+ expect(tool).toBeDefined();
147
+ expect(tool?.exposure).toBe("deferred");
148
+ });
149
+
150
+ it("requires sessionId (string)", () => {
151
+ const s = schemas.archive_session;
152
+ expect(s).toBeDefined();
153
+ expect(s.type).toBe("object");
154
+ const props = s.properties as Record<string, unknown>;
155
+ expect(props.sessionId).toBeDefined();
156
+ expect((props.sessionId as Record<string, unknown>).type).toBe("string");
157
+ const required = s.required as string[];
158
+ expect(required).toContain("sessionId");
159
+ });
160
+ });
116
161
  });
@@ -1,4 +1,4 @@
1
- // tool-summary.test.ts — details.summary 产出(六工具文案断言,走 execute 骨架)+
1
+ // tool-summary.test.ts — details.summary 产出(八工具文案断言,走 execute 骨架)+
2
2
  // renderSessionManagerResult 两态(折叠态一行摘要 / 展开态原始 content 文本)。
3
3
 
4
4
  import type { Theme } from "@earendil-works/pi-coding-agent";
@@ -36,7 +36,7 @@ function renderedText(result: unknown, expanded: boolean): string {
36
36
  return renderSessionManagerResult(result, { expanded }, mockTheme).render(400).join("\n");
37
37
  }
38
38
 
39
- describe("execute 组装 details.summary(六工具文案)", () => {
39
+ describe("execute 组装 details.summary(八工具文案)", () => {
40
40
  let harness: ReturnType<typeof createHarness>;
41
41
 
42
42
  beforeEach(() => {
@@ -88,6 +88,14 @@ describe("execute 组装 details.summary(六工具文案)", () => {
88
88
  it("abort_session:Aborted <sessionId>", async () => {
89
89
  expect(await summaryOf("abort_session", { sessionId: "s1" }, { success: true })).toBe("Aborted s1");
90
90
  });
91
+
92
+ it("delete_session:Deleted <sessionId>", async () => {
93
+ expect(await summaryOf("delete_session", { sessionId: "s1" }, { sessionId: "s1", deleted: true })).toBe("Deleted s1");
94
+ });
95
+
96
+ it("archive_session:Archived <sessionId>", async () => {
97
+ expect(await summaryOf("archive_session", { sessionId: "s1" }, { sessionId: "s1", archived: true })).toBe("Archived s1");
98
+ });
91
99
  });
92
100
 
93
101
  describe("toSummary 数据缺失分支(形状漂移退化为短文案,不抛错)", () => {
@@ -149,10 +157,10 @@ describe("renderSessionManagerResult 两态", () => {
149
157
  expect(renderedText({ content: [{ type: "text", text: "raw" }], details: { summary: 7 } }, false)).toContain("raw");
150
158
  });
151
159
 
152
- it("六工具注册的 renderResult 均存在且折叠态行为一致(共用渲染内核)", () => {
160
+ it("八工具注册的 renderResult 均存在且折叠态行为一致(共用渲染内核)", () => {
153
161
  const harness = createHarness();
154
162
  const renderFns = harness.registered.map((t) => t.renderResult);
155
- expect(renderFns).toHaveLength(6);
163
+ expect(renderFns).toHaveLength(8);
156
164
  for (const fn of renderFns) expect(typeof fn).toBe("function");
157
165
  // 抽查一个 handler:折叠态出摘要、不出原始 JSON(handler 是各 registerTool
158
166
  // 的方法包装,内核同为 renderSessionManagerResult)