@monotykamary/pi-vcc 0.3.17 → 0.4.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,312 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { compile } from "../src/core/summarize";
3
+ import { buildSections } from "../src/core/build-sections";
4
+ import type { NormalizedBlock } from "../src/types";
5
+ import {
6
+ userMsg,
7
+ } from "./fixtures";
8
+
9
+ describe("breadcrumbs: mergeHeaderSection", () => {
10
+ it("leaves recall breadcrumbs when Session Goal exceeds cap", () => {
11
+ const goals = Array.from({ length: 10 }, (_, i) =>
12
+ `- Implement feature ${i}: auth${i} module`
13
+ ).join("\n");
14
+ const prevSummary = `[Session Goal]\n${goals}\n\n---\n\n[user]\nhi`;
15
+ const r = compile({
16
+ messages: [userMsg("continue")],
17
+ previousSummary: prevSummary,
18
+ });
19
+ expect(r).toContain("...recall:");
20
+ expect(r).toContain("auth0");
21
+ expect(r).toContain("auth1");
22
+ expect(r).toContain("feature 9");
23
+ expect(r).not.toContain("Implement feature 0: auth0 module");
24
+ });
25
+
26
+ it("leaves no breadcrumbs when under cap", () => {
27
+ const goals = Array.from({ length: 5 }, (_, i) =>
28
+ `- Goal ${i}`
29
+ ).join("\n");
30
+ const prevSummary = `[Session Goal]\n${goals}\n\n---\n\n[user]\nhi`;
31
+ const r = compile({
32
+ messages: [userMsg("continue")],
33
+ previousSummary: prevSummary,
34
+ });
35
+ expect(r).not.toContain("...recall:");
36
+ });
37
+
38
+ it("leaves breadcrumbs for Earlier Turns when cap exceeded", () => {
39
+ const turns = Array.from({ length: 20 }, (_, i) =>
40
+ `- Fix bug ${i} → edited bug${i}.ts`
41
+ ).join("\n");
42
+ const prevSummary = `[Earlier Turns]\n${turns}\n\n---\n\n[user]\nhi`;
43
+ const r = compile({
44
+ messages: [userMsg("next")],
45
+ previousSummary: prevSummary,
46
+ });
47
+ expect(r).toContain("...recall:");
48
+ expect(r).toContain("bug0.ts");
49
+ });
50
+
51
+ it("breadcrumb extraction: picks up file paths from edited actions", () => {
52
+ const manyTurns = Array.from({ length: 18 }, (_, i) =>
53
+ `- Turn ${i} → edited file${i}.ts`
54
+ ).join("\n");
55
+ const bigPrevSummary = `[Earlier Turns]\n${manyTurns}\n\n---\n\n[user]\ngo`;
56
+ const r = compile({
57
+ messages: [userMsg("continue")],
58
+ previousSummary: bigPrevSummary,
59
+ });
60
+ expect(r).toContain("...recall:");
61
+ expect(r).toContain("file0.ts");
62
+ });
63
+
64
+ it("breadcrumb from short text uses first content words", () => {
65
+ const manyGoals = Array.from({ length: 10 }, (_, i) =>
66
+ `- Task ${i} description`
67
+ ).join("\n");
68
+ const bigPrev = `[Session Goal]\n${manyGoals}\n\n---\n\n[user]\ngo`;
69
+ const r = compile({
70
+ messages: [userMsg("continue")],
71
+ previousSummary: bigPrev,
72
+ });
73
+ expect(r).toContain("...recall:");
74
+ expect(r).toContain("Task");
75
+ });
76
+ });
77
+
78
+ describe("breadcrumbs: mergeFileLines (+recall:)", () => {
79
+ it("replaces (+N more) with +recall: listing omitted paths", () => {
80
+ const paths = Array.from({ length: 12 }, (_, i) => `src/mod${i}.ts`);
81
+ const prevSummary = [
82
+ "[Files And Changes]",
83
+ `- Modified: ${paths.join(", ")}`,
84
+ ].join("\n") + "\n\n---\n\n[user]\ngo";
85
+ const r = compile({
86
+ messages: [userMsg("continue")],
87
+ previousSummary: prevSummary,
88
+ });
89
+ expect(r).toContain("+recall:");
90
+ expect(r).toContain("mod10.ts");
91
+ expect(r).toContain("mod11.ts");
92
+ });
93
+
94
+ it("no breadcrumb when all paths fit within cap", () => {
95
+ const paths = ["src/a.ts", "src/b.ts"];
96
+ const prevSummary = [
97
+ "[Files And Changes]",
98
+ `- Modified: ${paths.join(", ")}`,
99
+ ].join("\n") + "\n\n---\n\n[user]\ngo";
100
+ const r = compile({
101
+ messages: [userMsg("continue")],
102
+ previousSummary: prevSummary,
103
+ });
104
+ expect(r).not.toContain("+recall:");
105
+ });
106
+
107
+ it("breadcrumb paths are parseable on next compaction", () => {
108
+ const prevSummary = [
109
+ "[Files And Changes]",
110
+ `- Modified: src/mod0.ts, src/mod1.ts, +recall: src/mod2.ts`,
111
+ ].join("\n") + "\n\n---\n\n[user]\ngo";
112
+ const r = compile({
113
+ messages: [userMsg("continue")],
114
+ previousSummary: prevSummary,
115
+ });
116
+ expect(r).toContain("mod2.ts");
117
+ });
118
+
119
+ it("applies +recall: cap even when only previous has files (no fresh)", () => {
120
+ const paths = Array.from({ length: 15 }, (_, i) => `src/file${i}.ts`);
121
+ const prevSummary = [
122
+ "[Files And Changes]",
123
+ `- Read: ${paths.join(", ")}`,
124
+ ].join("\n") + "\n\n---\n\n[user]\ngo";
125
+ const r = compile({
126
+ messages: [userMsg("continue")],
127
+ previousSummary: prevSummary,
128
+ });
129
+ expect(r).toContain("+recall:");
130
+ expect(r).toContain("file10.ts");
131
+ });
132
+ });
133
+
134
+ describe("breadcrumbs: capBrief", () => {
135
+ it("shows omitted count without redundant recall terms", () => {
136
+ const longTranscript = Array.from({ length: 200 }, (_, i) =>
137
+ `[user]\nmessage ${i}`
138
+ ).join("\n\n");
139
+ const previousSummary = `[Session Goal]\n- goal\n\n---\n\n${longTranscript}`;
140
+ const r = compile({
141
+ previousSummary,
142
+ messages: [userMsg("latest")],
143
+ });
144
+ expect(r).toContain("earlier lines omitted");
145
+ // capBrief should NOT include recall: terms (redundant with Files And Changes)
146
+ expect(r).not.toMatch(/\.\.\.\(\d+ earlier lines omitted, recall:/);
147
+ });
148
+ });
149
+
150
+ describe("breadcrumbs: type catalog", () => {
151
+ it("emits count-only message for omitted files (not redundant file list)", () => {
152
+ const blocks: NormalizedBlock[] = [];
153
+ for (let i = 0; i < 15; i++) {
154
+ blocks.push({ kind: "tool_call", name: "Read", args: { file_path: `src/file${i}.ts` } });
155
+ blocks.push({
156
+ kind: "tool_result",
157
+ name: "Read",
158
+ text: Array.from({ length: 3 }, (_, j) =>
159
+ `export function fn${i}_${j}(): void {}`
160
+ ).join("\n"),
161
+ isError: false,
162
+ });
163
+ }
164
+ const r = buildSections({ blocks });
165
+ // Should contain count-only message, not redundant file list
166
+ expect(r.typeCatalog.some(l => /more files with signatures omitted/.test(l))).toBe(true);
167
+ // Should NOT list specific file names in the omission line (redundant with Files And Changes)
168
+ expect(r.typeCatalog.every(l => !l.includes("recall:"))).toBe(true);
169
+ });
170
+
171
+ it("no omission line when all files fit within sig cap", () => {
172
+ const blocks: NormalizedBlock[] = [
173
+ { kind: "tool_call", name: "Read", args: { file_path: "src/a.ts" } },
174
+ {
175
+ kind: "tool_result",
176
+ name: "Read",
177
+ text: "export function fnA(): void {}",
178
+ isError: false,
179
+ },
180
+ ];
181
+ const r = buildSections({ blocks });
182
+ expect(r.typeCatalog.every(l => !l.includes("omitted"))).toBe(true);
183
+ });
184
+ });
185
+
186
+ describe("breadcrumbs: no redundancy across sections", () => {
187
+ it("Anchors section removed entirely (was redundant with Commits + Outstanding Context)", () => {
188
+ // Anchors duplicates commit hashes from [Commits] and error IDs from [Outstanding Context]
189
+ // The section has been removed entirely.
190
+ const blocks: NormalizedBlock[] = [];
191
+ for (let i = 0; i < 5; i++) {
192
+ blocks.push({ kind: "tool_call", name: "Read", args: { file_path: `src/file${i}.ts` } });
193
+ blocks.push({
194
+ kind: "tool_result",
195
+ name: "Read",
196
+ text: "export function fn(): void {}",
197
+ isError: false,
198
+ });
199
+ }
200
+ const r = buildSections({ blocks });
201
+ expect(r).not.toHaveProperty("anchors");
202
+ });
203
+
204
+ it("Current Status section removed entirely (redundant with brief transcript + Files And Changes)", () => {
205
+ // "Working on:" = last user message (brief transcript)
206
+ // "Last action:" = file path (Files And Changes)
207
+ // "Next:" = assistant text (brief transcript)
208
+ const blocks: NormalizedBlock[] = [];
209
+ const r = buildSections({ blocks });
210
+ expect(r).not.toHaveProperty("currentStatus");
211
+ });
212
+ });
213
+
214
+ describe("breadcrumbs: determinism and idempotency", () => {
215
+ it("compile produces identical output for identical inputs", () => {
216
+ const goals = Array.from({ length: 10 }, (_, i) =>
217
+ `- Goal ${i} with detail`
218
+ ).join("\n");
219
+ const prevSummary = `[Session Goal]\n${goals}\n\n---\n\n[user]\nhi`;
220
+
221
+ const input = {
222
+ messages: [userMsg("continue")],
223
+ previousSummary: prevSummary,
224
+ };
225
+
226
+ const r1 = compile(input);
227
+ const r2 = compile(input);
228
+ expect(r1).toBe(r2);
229
+ });
230
+
231
+ it("breadcrumbs are deterministic: same dropped content produces same breadcrumb", () => {
232
+ const goals = Array.from({ length: 10 }, (_, i) =>
233
+ `- Implement feature ${i}: module${i}`
234
+ ).join("\n");
235
+ const prevSummary = `[Session Goal]\n${goals}\n\n---\n\n[user]\nhi`;
236
+
237
+ const r1 = compile({ messages: [userMsg("continue")], previousSummary: prevSummary });
238
+ const r2 = compile({ messages: [userMsg("continue")], previousSummary: prevSummary });
239
+
240
+ expect(r1).toBe(r2);
241
+ const recallLine1 = r1.split("\n").find(l => l.includes("...recall:"));
242
+ const recallLine2 = r2.split("\n").find(l => l.includes("...recall:"));
243
+ expect(recallLine1).toBe(recallLine2);
244
+ });
245
+
246
+ it("prior compaction breadcrumb lines survive across compactions", () => {
247
+ const prevSummary = [
248
+ "[Session Goal]",
249
+ "- ...recall: auth0, auth1",
250
+ "- Goal 2",
251
+ "- Goal 3",
252
+ "- Goal 4",
253
+ "- Goal 5",
254
+ "- Goal 6",
255
+ "- Goal 7",
256
+ "- Goal 8",
257
+ "- Goal 9",
258
+ ].join("\n") + "\n\n---\n\n[user]\nhi";
259
+ const r = compile({
260
+ messages: [userMsg("continue")],
261
+ previousSummary: prevSummary,
262
+ });
263
+ // The breadcrumb from the prior compaction should be preserved
264
+ expect(r).toContain("auth0");
265
+ expect(r).toContain("auth1");
266
+ });
267
+
268
+ it("double-compilation of same previous summary is idempotent", () => {
269
+ const goals = Array.from({ length: 10 }, (_, i) =>
270
+ `- Goal ${i} with detail`
271
+ ).join("\n");
272
+ const prevSummary = `[Session Goal]\n${goals}\n\n---\n\n[user]\nhi`;
273
+
274
+ const intermediate = compile({
275
+ messages: [userMsg("continue")],
276
+ previousSummary: prevSummary,
277
+ });
278
+
279
+ const recallNoteIdx = intermediate.lastIndexOf("Use `vcc_recall`");
280
+ const stripped = recallNoteIdx >= 0
281
+ ? intermediate.slice(0, recallNoteIdx).replace(/\s*(?:\n\n---\n\n)?\s*$/, "").trimEnd()
282
+ : intermediate;
283
+
284
+ const r1 = compile({
285
+ messages: [userMsg("more work")],
286
+ previousSummary: stripped,
287
+ });
288
+ const r2 = compile({
289
+ messages: [userMsg("more work")],
290
+ previousSummary: stripped,
291
+ });
292
+ expect(r1).toBe(r2);
293
+ });
294
+
295
+ it("buildSections type catalog is deterministic", () => {
296
+ const blocks: NormalizedBlock[] = [];
297
+ for (let i = 0; i < 15; i++) {
298
+ blocks.push({ kind: "tool_call", name: "Read", args: { file_path: `src/file${i}.ts` } });
299
+ blocks.push({
300
+ kind: "tool_result",
301
+ name: "Read",
302
+ text: Array.from({ length: 3 }, (_, j) =>
303
+ `export function fn${i}_${j}(): void {}`
304
+ ).join("\n"),
305
+ isError: false,
306
+ });
307
+ }
308
+ const r1 = buildSections({ blocks });
309
+ const r2 = buildSections({ blocks });
310
+ expect(r1.typeCatalog).toEqual(r2.typeCatalog);
311
+ });
312
+ });
@@ -114,8 +114,8 @@ describe("outstanding context: deep error extraction", () => {
114
114
  });
115
115
  });
116
116
 
117
- describe("files and changes: symbol annotations", () => {
118
- it("annotates modified files with exported symbol names from Edit newText", () => {
117
+ describe("files and changes: no symbol annotations (moved to Type Catalog)", () => {
118
+ it("lists modified files without symbol annotations", () => {
119
119
  const blocks: NormalizedBlock[] = [
120
120
  { kind: "tool_call", name: "Edit", args: { file_path: "auth.ts", newText: "export function login() {}\nexport function verifyToken() {}\nconst _internal = 1;" } },
121
121
  { kind: "tool_result", name: "Edit", text: "ok", isError: false },
@@ -123,11 +123,15 @@ describe("files and changes: symbol annotations", () => {
123
123
  const r = buildSections({ blocks });
124
124
  expect(r.filesAndChanges.length).toBeGreaterThan(0);
125
125
  expect(r.filesAndChanges[0]).toContain("Modified");
126
- expect(r.filesAndChanges[0]).toContain("login");
127
- expect(r.filesAndChanges[0]).toContain("verifyToken");
126
+ expect(r.filesAndChanges[0]).toContain("auth.ts");
127
+ // Symbol annotations removed from Files And Changes (redundant with Type Catalog)
128
+ expect(r.filesAndChanges[0]).not.toContain("(");
129
+ // Symbols should appear in the Type Catalog instead
130
+ expect(r.typeCatalog.some(l => l.includes("login"))).toBe(true);
131
+ expect(r.typeCatalog.some(l => l.includes("verifyToken"))).toBe(true);
128
132
  });
129
133
 
130
- it("annotates read files with exported symbol names from Read result", () => {
134
+ it("lists read files without symbol annotations", () => {
131
135
  const blocks: NormalizedBlock[] = [
132
136
  { kind: "tool_call", name: "Read", args: { file_path: "types.ts" } },
133
137
  { kind: "tool_result", name: "Read", text: "export interface User { name: string; }\nexport type AuthPayload = { email: string; }", isError: false },
@@ -135,11 +139,14 @@ describe("files and changes: symbol annotations", () => {
135
139
  const r = buildSections({ blocks });
136
140
  expect(r.filesAndChanges.length).toBeGreaterThan(0);
137
141
  expect(r.filesAndChanges[0]).toContain("Read");
138
- expect(r.filesAndChanges[0]).toContain("User");
139
- expect(r.filesAndChanges[0]).toContain("AuthPayload");
142
+ expect(r.filesAndChanges[0]).toContain("types.ts");
143
+ // Symbol annotations removed (redundant with Type Catalog)
144
+ expect(r.filesAndChanges[0]).not.toContain("(");
145
+ // Symbols should appear in the Type Catalog instead
146
+ expect(r.typeCatalog.some(l => l.includes("interface User"))).toBe(true);
140
147
  });
141
148
 
142
- it("does not annotate files when no symbols are found", () => {
149
+ it("lists files without annotations when no symbols are found", () => {
143
150
  const blocks: NormalizedBlock[] = [
144
151
  { kind: "tool_call", name: "Read", args: { file_path: "config.json" } },
145
152
  { kind: "tool_result", name: "Read", text: '{ "name": "test" }', isError: false },
@@ -149,7 +156,7 @@ describe("files and changes: symbol annotations", () => {
149
156
  expect(r.filesAndChanges[0]).not.toContain("(");
150
157
  });
151
158
 
152
- it("handles Mixed Edit+Read with combined symbols", () => {
159
+ it("handles Mixed Edit+Read: Modified takes priority", () => {
153
160
  const blocks: NormalizedBlock[] = [
154
161
  { kind: "tool_call", name: "Read", args: { file_path: "utils.ts" } },
155
162
  { kind: "tool_result", name: "Read", text: "export function helper() {}\nexport class Util {}", isError: false },
@@ -159,29 +166,32 @@ describe("files and changes: symbol annotations", () => {
159
166
  const r = buildSections({ blocks });
160
167
  expect(r.filesAndChanges[0]).toContain("Modified");
161
168
  expect(r.filesAndChanges[0]).toContain("utils.ts");
162
- // Should contain symbols from both Read result and Edit newText
163
- expect(r.filesAndChanges[0]).toContain("newHelper");
169
+ // Full signatures in Type Catalog, not in Files And Changes
170
+ expect(r.typeCatalog.some(l => l.includes("newHelper"))).toBe(true);
164
171
  });
165
172
 
166
- it("annotates Python def and class exports", () => {
173
+ it("Python def and class exports appear in Type Catalog only", () => {
167
174
  const blocks: NormalizedBlock[] = [
168
175
  { kind: "tool_call", name: "Read", args: { file_path: "auth.py" } },
169
176
  { kind: "tool_result", name: "Read", text: "def login(email, pwd):\n pass\n\nclass AuthProvider:\n pass", isError: false },
170
177
  ];
171
178
  const r = buildSections({ blocks });
172
- expect(r.filesAndChanges[0]).toContain("login");
173
- expect(r.filesAndChanges[0]).toContain("AuthProvider");
179
+ expect(r.filesAndChanges[0]).toContain("auth.py");
180
+ expect(r.filesAndChanges[0]).not.toContain("(");
181
+ expect(r.typeCatalog.some(l => l.includes("login"))).toBe(true);
182
+ expect(r.typeCatalog.some(l => l.includes("AuthProvider"))).toBe(true);
174
183
  });
175
184
 
176
- it("annotates Go exported functions", () => {
185
+ it("Go exported functions appear in Type Catalog only", () => {
177
186
  const blocks: NormalizedBlock[] = [
178
187
  { kind: "tool_call", name: "Read", args: { file_path: "handler.go" } },
179
188
  { kind: "tool_result", name: "Read", text: "func HandleRequest(w http.ResponseWriter, r *http.Request) {\n\t// ...\n}\n\nfunc internalHelper() {}", isError: false },
180
189
  ];
181
190
  const r = buildSections({ blocks });
182
- expect(r.filesAndChanges[0]).toContain("HandleRequest");
191
+ expect(r.filesAndChanges[0]).toContain("handler.go");
192
+ expect(r.typeCatalog.some(l => l.includes("HandleRequest"))).toBe(true);
183
193
  // unexported Go functions (lowercase) should not be included
184
- expect(r.filesAndChanges[0]).not.toContain("internalHelper");
194
+ expect(r.typeCatalog.every(l => !l.includes("internalHelper"))).toBe(true);
185
195
  });
186
196
  });
187
197
 
@@ -219,7 +229,6 @@ describe("type catalog", () => {
219
229
  expect(r.typeCatalog.length).toBeGreaterThan(0);
220
230
  // Modified file should appear first
221
231
  expect(r.typeCatalog[0]).toContain("modified.ts");
222
- expect(r.typeCatalog[0]).toContain("[modified]");
223
232
  });
224
233
 
225
234
  it("returns empty for files with no exportable signatures", () => {
@@ -304,7 +313,7 @@ describe("format output integration", () => {
304
313
 
305
314
  it("Type Catalog is volatile (fresh only) on merge", () => {
306
315
  const { compile } = require("../src/core/summarize");
307
- const prev = "[Session Goal]\n- goal\n\n[Type Catalog]\n- old-file.ts [read]:\n export function old()";
316
+ const prev = "[Session Goal]\n- goal\n\n[Type Catalog]\n- old-file.ts:\n export function old()";
308
317
  const fresh = compile({ messages: [], previousSummary: prev });
309
318
  // Type Catalog should be fresh-only, not merged from previous
310
319
  expect(fresh).not.toContain("old-file.ts");