@monotykamary/pi-vcc 0.3.17 → 0.4.1
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/README.md +5 -4
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/core/build-sections.ts +12 -131
- package/src/core/format.ts +4 -14
- package/src/core/summarize.ts +98 -22
- package/src/sections.ts +0 -4
- package/__tests__/helpers.ts +0 -135
- package/__tests__/root-causes.test.ts +0 -858
- package/demo.gif +0 -0
- package/knip.json +0 -4
- package/scripts/audit-sessions.ts +0 -88
- package/scripts/benchmark-real-sessions.ts +0 -25
- package/scripts/compare-before-after.ts +0 -36
- package/scripts/dump-branch-output.ts +0 -20
- package/tests/australian-government.test.ts +0 -479
- package/tests/before-compact-hook.test.ts +0 -185
- package/tests/before-compact.test.ts +0 -227
- package/tests/brief.test.ts +0 -215
- package/tests/build-sections.test.ts +0 -59
- package/tests/compile.test.ts +0 -89
- package/tests/content.test.ts +0 -66
- package/tests/deep-extraction.test.ts +0 -313
- package/tests/extract-goals.test.ts +0 -86
- package/tests/extract-preferences.test.ts +0 -30
- package/tests/filter-noise.test.ts +0 -61
- package/tests/fixtures.ts +0 -61
- package/tests/format-recall.test.ts +0 -87
- package/tests/format.test.ts +0 -73
- package/tests/government-domain.test.ts +0 -542
- package/tests/lineage.test.ts +0 -33
- package/tests/load-messages.test.ts +0 -49
- package/tests/model-threshold-hook.test.ts +0 -311
- package/tests/model-threshold.test.ts +0 -223
- package/tests/normalize.test.ts +0 -104
- package/tests/proactive-threshold.test.ts +0 -490
- package/tests/real-sessions.test.ts +0 -38
- package/tests/recall-expand.test.ts +0 -15
- package/tests/recall-scope.test.ts +0 -32
- package/tests/recall-tool-scope.test.ts +0 -67
- package/tests/render-entries.test.ts +0 -92
- package/tests/report.test.ts +0 -58
- package/tests/sanitize.test.ts +0 -24
- package/tests/search-entries.test.ts +0 -233
- package/tests/support/load-session.ts +0 -23
- package/tests/support/real-sessions.ts +0 -51
- package/tests/threshold-integration.test.ts +0 -335
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import { buildSections } from "../src/core/build-sections";
|
|
3
|
-
import type { NormalizedBlock } from "../src/types";
|
|
4
|
-
|
|
5
|
-
describe("buildSections", () => {
|
|
6
|
-
it("returns all-empty for no blocks", () => {
|
|
7
|
-
const r = buildSections({ blocks: [] });
|
|
8
|
-
expect(r.sessionGoal).toEqual([]);
|
|
9
|
-
expect(r.outstandingContext).toEqual([]);
|
|
10
|
-
expect(r.briefTranscript).toBe("");
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("populates sections from realistic blocks", () => {
|
|
14
|
-
const blocks: NormalizedBlock[] = [
|
|
15
|
-
{ kind: "user", text: "Fix the auth bug" },
|
|
16
|
-
{ kind: "tool_call", name: "Read", args: { file_path: "auth.ts" } },
|
|
17
|
-
{ kind: "tool_result", name: "Read", text: "const x = 1;", isError: false },
|
|
18
|
-
{ kind: "tool_call", name: "Edit", args: { file_path: "auth.ts" } },
|
|
19
|
-
{ kind: "tool_result", name: "Edit", text: "ok", isError: false },
|
|
20
|
-
{ kind: "assistant", text: "- run tests next" },
|
|
21
|
-
];
|
|
22
|
-
const r = buildSections({ blocks });
|
|
23
|
-
expect(r.sessionGoal).toContain("Fix the auth bug");
|
|
24
|
-
expect(r.briefTranscript).toContain('[user]');
|
|
25
|
-
expect(r.briefTranscript).toContain('* Read "auth.ts"');
|
|
26
|
-
expect(r.briefTranscript).toContain('* Edit "auth.ts"');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("captures outstanding context from errors", () => {
|
|
30
|
-
const blocks: NormalizedBlock[] = [
|
|
31
|
-
{ kind: "tool_result", name: "bash", text: "FAIL: test broken\ndetails here", isError: true },
|
|
32
|
-
];
|
|
33
|
-
const r = buildSections({ blocks });
|
|
34
|
-
expect(r.outstandingContext.length).toBeGreaterThan(0);
|
|
35
|
-
expect(r.outstandingContext[0]).toContain("FAIL");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("brief transcript hides tool results but shows errors", () => {
|
|
39
|
-
const blocks: NormalizedBlock[] = [
|
|
40
|
-
{ kind: "tool_result", name: "Read", text: "lots of code here ...", isError: false },
|
|
41
|
-
{ kind: "tool_result", name: "bash", text: "Command not found", isError: true },
|
|
42
|
-
];
|
|
43
|
-
const r = buildSections({ blocks });
|
|
44
|
-
expect(r.briefTranscript).not.toContain("lots of code");
|
|
45
|
-
expect(r.briefTranscript).toContain("[tool_error] bash");
|
|
46
|
-
expect(r.briefTranscript).toContain("Command not found");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("brief transcript merges adjacent assistant sections", () => {
|
|
50
|
-
const blocks: NormalizedBlock[] = [
|
|
51
|
-
{ kind: "assistant", text: "Part one." },
|
|
52
|
-
{ kind: "tool_call", name: "Read", args: { file_path: "a.ts" } },
|
|
53
|
-
{ kind: "assistant", text: "Part two." },
|
|
54
|
-
];
|
|
55
|
-
const r = buildSections({ blocks });
|
|
56
|
-
const matches = r.briefTranscript.match(/\[assistant\]/g);
|
|
57
|
-
expect(matches?.length).toBe(1);
|
|
58
|
-
});
|
|
59
|
-
});
|
package/tests/compile.test.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import { compile } from "../src/core/summarize";
|
|
3
|
-
import {
|
|
4
|
-
userMsg,
|
|
5
|
-
assistantText,
|
|
6
|
-
assistantWithToolCall,
|
|
7
|
-
toolResult,
|
|
8
|
-
} from "./fixtures";
|
|
9
|
-
|
|
10
|
-
describe("compile", () => {
|
|
11
|
-
it("returns empty string for no messages", () => {
|
|
12
|
-
expect(compile({ messages: [] })).toBe("");
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("produces hybrid output with header + brief transcript", () => {
|
|
16
|
-
const r = compile({
|
|
17
|
-
messages: [
|
|
18
|
-
userMsg("Fix login bug"),
|
|
19
|
-
assistantWithToolCall("Read", { path: "auth.ts" }),
|
|
20
|
-
assistantText("Found the issue.\n1. Fix validation"),
|
|
21
|
-
],
|
|
22
|
-
});
|
|
23
|
-
expect(r).toContain("[Session Goal]");
|
|
24
|
-
expect(r).toContain("Fix login bug");
|
|
25
|
-
expect(r).toContain("---");
|
|
26
|
-
expect(r).toContain("[user]\nFix login bug");
|
|
27
|
-
expect(r).toContain('* Read "auth.ts"');
|
|
28
|
-
expect(r).toContain("Found the issue.");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("merges previous summary goals", () => {
|
|
32
|
-
const r = compile({
|
|
33
|
-
messages: [userMsg("New task")],
|
|
34
|
-
previousSummary: "[Session Goal]\n- Original goal\n\n---\n\n[user]\nOriginal goal",
|
|
35
|
-
});
|
|
36
|
-
expect(r).toContain("- Original goal");
|
|
37
|
-
expect(r).toContain("- New task");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("appends brief transcript on merge", () => {
|
|
41
|
-
const previousSummary = [
|
|
42
|
-
"[Session Goal]\n- Original goal",
|
|
43
|
-
"---",
|
|
44
|
-
"[user]\nOriginal goal\n\n[assistant]\n* Read \"old.ts\"",
|
|
45
|
-
].join("\n\n");
|
|
46
|
-
const r = compile({
|
|
47
|
-
previousSummary,
|
|
48
|
-
messages: [
|
|
49
|
-
userMsg("Next step"),
|
|
50
|
-
assistantWithToolCall("Read", { path: "new.ts" }),
|
|
51
|
-
],
|
|
52
|
-
});
|
|
53
|
-
expect(r).toContain('* Read "old.ts"');
|
|
54
|
-
expect(r).toContain('* Read "new.ts"');
|
|
55
|
-
expect(r).toContain("Next step");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("outstanding context is volatile (fresh only)", () => {
|
|
59
|
-
const previousSummary = "[Outstanding Context]\n- old blocker\n\n---\n\n[user]\nhi";
|
|
60
|
-
const r = compile({
|
|
61
|
-
previousSummary,
|
|
62
|
-
messages: [userMsg("continue")],
|
|
63
|
-
});
|
|
64
|
-
expect(r).not.toContain("old blocker");
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("caps long brief transcript with rolling window", () => {
|
|
68
|
-
// Build a very long previous transcript
|
|
69
|
-
const longTranscript = Array.from({ length: 200 }, (_, i) =>
|
|
70
|
-
`[user]\nmessage ${i}`
|
|
71
|
-
).join("\n\n");
|
|
72
|
-
const previousSummary = `[Session Goal]\n- goal\n\n---\n\n${longTranscript}`;
|
|
73
|
-
const r = compile({
|
|
74
|
-
previousSummary,
|
|
75
|
-
messages: [userMsg("latest")],
|
|
76
|
-
});
|
|
77
|
-
expect(r).toContain("earlier lines omitted");
|
|
78
|
-
expect(r).toContain("latest");
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("wraps final output including recall note", () => {
|
|
82
|
-
const r = compile({
|
|
83
|
-
messages: [userMsg("check final summary wrapping")],
|
|
84
|
-
});
|
|
85
|
-
const maxLineLength = Math.max(...r.split("\n").map((line) => line.length));
|
|
86
|
-
expect(r).toContain("vcc_recall");
|
|
87
|
-
expect(maxLineLength).toBeLessThanOrEqual(120);
|
|
88
|
-
});
|
|
89
|
-
});
|
package/tests/content.test.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import { textOf, thinkingOf, clip, firstLine } from "../src/core/content";
|
|
3
|
-
|
|
4
|
-
describe("textOf", () => {
|
|
5
|
-
it("returns empty string for undefined content", () => {
|
|
6
|
-
expect(textOf(undefined as any)).toBe("");
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
it("returns empty string for null content", () => {
|
|
10
|
-
expect(textOf(null as any)).toBe("");
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("returns string content as-is", () => {
|
|
14
|
-
expect(textOf("hello")).toBe("hello");
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("extracts text parts from array content", () => {
|
|
18
|
-
const content = [
|
|
19
|
-
{ type: "text" as const, text: "first" },
|
|
20
|
-
{ type: "toolCall" as const, name: "x", id: "1", arguments: {} },
|
|
21
|
-
{ type: "text" as const, text: "second" },
|
|
22
|
-
];
|
|
23
|
-
expect(textOf(content)).toBe("first\nsecond");
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("ignores thinking parts", () => {
|
|
27
|
-
const content = [
|
|
28
|
-
{ type: "thinking" as const, thinking: "let me think" },
|
|
29
|
-
{ type: "text" as const, text: "here is the answer" },
|
|
30
|
-
];
|
|
31
|
-
expect(textOf(content)).toBe("here is the answer");
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
describe("thinkingOf", () => {
|
|
36
|
-
it("returns empty string for undefined content", () => {
|
|
37
|
-
expect(thinkingOf(undefined as any)).toBe("");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("returns empty string for string content", () => {
|
|
41
|
-
expect(thinkingOf("hello")).toBe("");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("extracts thinking parts from array content", () => {
|
|
45
|
-
const content = [
|
|
46
|
-
{ type: "thinking" as const, thinking: "let me think" },
|
|
47
|
-
{ type: "text" as const, text: "here is the answer" },
|
|
48
|
-
];
|
|
49
|
-
expect(thinkingOf(content)).toBe("let me think");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("joins multiple thinking parts", () => {
|
|
53
|
-
const content = [
|
|
54
|
-
{ type: "thinking" as const, thinking: "first thought" },
|
|
55
|
-
{ type: "text" as const, text: "answer" },
|
|
56
|
-
{ type: "thinking" as const, thinking: "second thought" },
|
|
57
|
-
];
|
|
58
|
-
expect(thinkingOf(content)).toBe("first thought\nsecond thought");
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe("textOf", () => {
|
|
63
|
-
it("returns empty string for undefined content", () => {
|
|
64
|
-
expect(textOf(undefined as any)).toBe("");
|
|
65
|
-
});
|
|
66
|
-
});
|
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import { buildSections } from "../src/core/build-sections";
|
|
3
|
-
import type { NormalizedBlock } from "../src/types";
|
|
4
|
-
|
|
5
|
-
describe("outstanding context: deep error extraction", () => {
|
|
6
|
-
it("captures bash non-zero exit codes", () => {
|
|
7
|
-
const blocks: NormalizedBlock[] = [
|
|
8
|
-
{ kind: "bash", command: "npm test", output: "1 failed", exitCode: 1 },
|
|
9
|
-
];
|
|
10
|
-
const r = buildSections({ blocks });
|
|
11
|
-
expect(r.outstandingContext.length).toBeGreaterThan(0);
|
|
12
|
-
expect(r.outstandingContext[0]).toContain("bash:exit 1");
|
|
13
|
-
expect(r.outstandingContext[0]).toContain("npm test");
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("captures bash non-zero exit code with output context", () => {
|
|
17
|
-
const blocks: NormalizedBlock[] = [
|
|
18
|
-
{ kind: "bash", command: "cargo build", output: "error: could not compile", exitCode: 101 },
|
|
19
|
-
];
|
|
20
|
-
const r = buildSections({ blocks });
|
|
21
|
-
expect(r.outstandingContext.length).toBeGreaterThan(0);
|
|
22
|
-
expect(r.outstandingContext[0]).toContain("bash:exit 101");
|
|
23
|
-
expect(r.outstandingContext[0]).toContain("cargo build");
|
|
24
|
-
expect(r.outstandingContext[0]).toContain("could not compile");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("skips bash with exit code 0", () => {
|
|
28
|
-
const blocks: NormalizedBlock[] = [
|
|
29
|
-
{ kind: "bash", command: "echo ok", output: "ok", exitCode: 0 },
|
|
30
|
-
];
|
|
31
|
-
const r = buildSections({ blocks });
|
|
32
|
-
expect(r.outstandingContext).toEqual([]);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("skips bash with undefined exit code", () => {
|
|
36
|
-
const blocks: NormalizedBlock[] = [
|
|
37
|
-
{ kind: "bash", command: "echo ok", output: "ok", exitCode: undefined },
|
|
38
|
-
];
|
|
39
|
-
const r = buildSections({ blocks });
|
|
40
|
-
expect(r.outstandingContext).toEqual([]);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("captures TypeScript compiler errors", () => {
|
|
44
|
-
const blocks: NormalizedBlock[] = [
|
|
45
|
-
{ kind: "bash", command: "tsc --noEmit", output: "src/auth.ts(12,5): error TS2322: Type 'string' is not assignable to type 'number'.", exitCode: 0 },
|
|
46
|
-
];
|
|
47
|
-
const r = buildSections({ blocks });
|
|
48
|
-
expect(r.outstandingContext.length).toBeGreaterThan(0);
|
|
49
|
-
expect(r.outstandingContext.some(c => c.includes("[tsc]"))).toBe(true);
|
|
50
|
-
expect(r.outstandingContext.some(c => c.includes("TS2322"))).toBe(true);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("captures test failures in bash output", () => {
|
|
54
|
-
const blocks: NormalizedBlock[] = [
|
|
55
|
-
{ kind: "bash", command: "bun test", output: "2 failed\nFAIL auth.test.ts", exitCode: 1 },
|
|
56
|
-
];
|
|
57
|
-
const r = buildSections({ blocks });
|
|
58
|
-
expect(r.outstandingContext.length).toBeGreaterThan(0);
|
|
59
|
-
// Should capture both the exit code AND the test failure signal
|
|
60
|
-
const hasExitCode = r.outstandingContext.some(c => c.includes("bash:exit 1"));
|
|
61
|
-
const hasTests = r.outstandingContext.some(c => c.includes("[tests]"));
|
|
62
|
-
expect(hasExitCode || hasTests).toBe(true);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("captures empty grep results", () => {
|
|
66
|
-
const blocks: NormalizedBlock[] = [
|
|
67
|
-
{ kind: "tool_call", name: "grep", args: { pattern: "verifyToken", path: "src/" } },
|
|
68
|
-
{ kind: "tool_result", name: "grep", text: "No matches found", isError: false },
|
|
69
|
-
];
|
|
70
|
-
const r = buildSections({ blocks });
|
|
71
|
-
expect(r.outstandingContext.length).toBeGreaterThan(0);
|
|
72
|
-
expect(r.outstandingContext[0]).toContain("[no matches]");
|
|
73
|
-
expect(r.outstandingContext[0]).toContain("grep");
|
|
74
|
-
expect(r.outstandingContext[0]).toContain("verifyToken");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("captures empty glob results", () => {
|
|
78
|
-
const blocks: NormalizedBlock[] = [
|
|
79
|
-
{ kind: "tool_call", name: "glob", args: { pattern: "**/*.proto" } },
|
|
80
|
-
{ kind: "tool_result", name: "glob", text: "No files matched.", isError: false },
|
|
81
|
-
];
|
|
82
|
-
const r = buildSections({ blocks });
|
|
83
|
-
expect(r.outstandingContext.length).toBeGreaterThan(0);
|
|
84
|
-
expect(r.outstandingContext[0]).toContain("[no matches]");
|
|
85
|
-
expect(r.outstandingContext[0]).toContain("glob");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("captures tsc errors from tool_result with bash error", () => {
|
|
89
|
-
const blocks: NormalizedBlock[] = [
|
|
90
|
-
{ kind: "tool_result", name: "bash", text: "src/utils.ts(5,1): error TS2305: Module has no exported member 'foo'.", isError: true },
|
|
91
|
-
];
|
|
92
|
-
const r = buildSections({ blocks });
|
|
93
|
-
expect(r.outstandingContext.length).toBeGreaterThan(0);
|
|
94
|
-
expect(r.outstandingContext.some(c => c.includes("[tsc]"))).toBe(true);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("still captures BLOCKER_RE text from assistant", () => {
|
|
98
|
-
const blocks: NormalizedBlock[] = [
|
|
99
|
-
{ kind: "assistant", text: "The deployment is still failing because of the auth configuration." },
|
|
100
|
-
];
|
|
101
|
-
const r = buildSections({ blocks });
|
|
102
|
-
expect(r.outstandingContext.length).toBeGreaterThan(0);
|
|
103
|
-
expect(r.outstandingContext[0]).toContain("still failing");
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("deduplicates error items", () => {
|
|
107
|
-
const blocks: NormalizedBlock[] = [
|
|
108
|
-
{ kind: "tool_result", name: "bash", text: "Command not found", isError: true },
|
|
109
|
-
{ kind: "tool_result", name: "bash", text: "Command not found", isError: true },
|
|
110
|
-
];
|
|
111
|
-
const r = buildSections({ blocks });
|
|
112
|
-
const matchCount = r.outstandingContext.filter(c => c.includes("Command not found")).length;
|
|
113
|
-
expect(matchCount).toBeLessThanOrEqual(1);
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
describe("files and changes: symbol annotations", () => {
|
|
118
|
-
it("annotates modified files with exported symbol names from Edit newText", () => {
|
|
119
|
-
const blocks: NormalizedBlock[] = [
|
|
120
|
-
{ kind: "tool_call", name: "Edit", args: { file_path: "auth.ts", newText: "export function login() {}\nexport function verifyToken() {}\nconst _internal = 1;" } },
|
|
121
|
-
{ kind: "tool_result", name: "Edit", text: "ok", isError: false },
|
|
122
|
-
];
|
|
123
|
-
const r = buildSections({ blocks });
|
|
124
|
-
expect(r.filesAndChanges.length).toBeGreaterThan(0);
|
|
125
|
-
expect(r.filesAndChanges[0]).toContain("Modified");
|
|
126
|
-
expect(r.filesAndChanges[0]).toContain("login");
|
|
127
|
-
expect(r.filesAndChanges[0]).toContain("verifyToken");
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it("annotates read files with exported symbol names from Read result", () => {
|
|
131
|
-
const blocks: NormalizedBlock[] = [
|
|
132
|
-
{ kind: "tool_call", name: "Read", args: { file_path: "types.ts" } },
|
|
133
|
-
{ kind: "tool_result", name: "Read", text: "export interface User { name: string; }\nexport type AuthPayload = { email: string; }", isError: false },
|
|
134
|
-
];
|
|
135
|
-
const r = buildSections({ blocks });
|
|
136
|
-
expect(r.filesAndChanges.length).toBeGreaterThan(0);
|
|
137
|
-
expect(r.filesAndChanges[0]).toContain("Read");
|
|
138
|
-
expect(r.filesAndChanges[0]).toContain("User");
|
|
139
|
-
expect(r.filesAndChanges[0]).toContain("AuthPayload");
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it("does not annotate files when no symbols are found", () => {
|
|
143
|
-
const blocks: NormalizedBlock[] = [
|
|
144
|
-
{ kind: "tool_call", name: "Read", args: { file_path: "config.json" } },
|
|
145
|
-
{ kind: "tool_result", name: "Read", text: '{ "name": "test" }', isError: false },
|
|
146
|
-
];
|
|
147
|
-
const r = buildSections({ blocks });
|
|
148
|
-
expect(r.filesAndChanges[0]).toContain("config.json");
|
|
149
|
-
expect(r.filesAndChanges[0]).not.toContain("(");
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("handles Mixed Edit+Read with combined symbols", () => {
|
|
153
|
-
const blocks: NormalizedBlock[] = [
|
|
154
|
-
{ kind: "tool_call", name: "Read", args: { file_path: "utils.ts" } },
|
|
155
|
-
{ kind: "tool_result", name: "Read", text: "export function helper() {}\nexport class Util {}", isError: false },
|
|
156
|
-
{ kind: "tool_call", name: "Edit", args: { file_path: "utils.ts", newText: "export function newHelper() {}" } },
|
|
157
|
-
{ kind: "tool_result", name: "Edit", text: "ok", isError: false },
|
|
158
|
-
];
|
|
159
|
-
const r = buildSections({ blocks });
|
|
160
|
-
expect(r.filesAndChanges[0]).toContain("Modified");
|
|
161
|
-
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");
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("annotates Python def and class exports", () => {
|
|
167
|
-
const blocks: NormalizedBlock[] = [
|
|
168
|
-
{ kind: "tool_call", name: "Read", args: { file_path: "auth.py" } },
|
|
169
|
-
{ kind: "tool_result", name: "Read", text: "def login(email, pwd):\n pass\n\nclass AuthProvider:\n pass", isError: false },
|
|
170
|
-
];
|
|
171
|
-
const r = buildSections({ blocks });
|
|
172
|
-
expect(r.filesAndChanges[0]).toContain("login");
|
|
173
|
-
expect(r.filesAndChanges[0]).toContain("AuthProvider");
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("annotates Go exported functions", () => {
|
|
177
|
-
const blocks: NormalizedBlock[] = [
|
|
178
|
-
{ kind: "tool_call", name: "Read", args: { file_path: "handler.go" } },
|
|
179
|
-
{ kind: "tool_result", name: "Read", text: "func HandleRequest(w http.ResponseWriter, r *http.Request) {\n\t// ...\n}\n\nfunc internalHelper() {}", isError: false },
|
|
180
|
-
];
|
|
181
|
-
const r = buildSections({ blocks });
|
|
182
|
-
expect(r.filesAndChanges[0]).toContain("HandleRequest");
|
|
183
|
-
// unexported Go functions (lowercase) should not be included
|
|
184
|
-
expect(r.filesAndChanges[0]).not.toContain("internalHelper");
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
describe("type catalog", () => {
|
|
189
|
-
it("extracts exported signatures from modified files", () => {
|
|
190
|
-
const blocks: NormalizedBlock[] = [
|
|
191
|
-
{ kind: "tool_call", name: "Edit", args: { file_path: "auth.ts", newText: "export function login(email: string, pwd: string): Promise<Session> {\n // impl\n}" } },
|
|
192
|
-
{ kind: "tool_result", name: "Edit", text: "ok", isError: false },
|
|
193
|
-
];
|
|
194
|
-
const r = buildSections({ blocks });
|
|
195
|
-
expect(r.typeCatalog.length).toBeGreaterThan(0);
|
|
196
|
-
expect(r.typeCatalog[0]).toContain("auth.ts");
|
|
197
|
-
expect(r.typeCatalog.some(l => l.includes("login"))).toBe(true);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it("extracts exported signatures from Read results", () => {
|
|
201
|
-
const blocks: NormalizedBlock[] = [
|
|
202
|
-
{ kind: "tool_call", name: "Read", args: { file_path: "types.ts" } },
|
|
203
|
-
{ kind: "tool_result", name: "Read", text: "export interface User {\n name: string;\n email: string;\n}\n\nexport type Session = {\n token: string;\n user: User;\n}", isError: false },
|
|
204
|
-
];
|
|
205
|
-
const r = buildSections({ blocks });
|
|
206
|
-
expect(r.typeCatalog.length).toBeGreaterThan(0);
|
|
207
|
-
expect(r.typeCatalog.some(l => l.includes("interface User"))).toBe(true);
|
|
208
|
-
expect(r.typeCatalog.some(l => l.includes("type Session"))).toBe(true);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("prioritizes modified files over read files", () => {
|
|
212
|
-
const blocks: NormalizedBlock[] = [
|
|
213
|
-
{ kind: "tool_call", name: "Read", args: { file_path: "read_only.ts" } },
|
|
214
|
-
{ kind: "tool_result", name: "Read", text: "export function readOnly() {}", isError: false },
|
|
215
|
-
{ kind: "tool_call", name: "Edit", args: { file_path: "modified.ts", newText: "export function modified() {}" } },
|
|
216
|
-
{ kind: "tool_result", name: "Edit", text: "ok", isError: false },
|
|
217
|
-
];
|
|
218
|
-
const r = buildSections({ blocks });
|
|
219
|
-
expect(r.typeCatalog.length).toBeGreaterThan(0);
|
|
220
|
-
// Modified file should appear first
|
|
221
|
-
expect(r.typeCatalog[0]).toContain("modified.ts");
|
|
222
|
-
expect(r.typeCatalog[0]).toContain("[modified]");
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it("returns empty for files with no exportable signatures", () => {
|
|
226
|
-
const blocks: NormalizedBlock[] = [
|
|
227
|
-
{ kind: "tool_call", name: "Read", args: { file_path: "data.json" } },
|
|
228
|
-
{ kind: "tool_result", name: "Read", text: '{ "key": "value" }', isError: false },
|
|
229
|
-
];
|
|
230
|
-
const r = buildSections({ blocks });
|
|
231
|
-
expect(r.typeCatalog).toEqual([]);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it("caps signatures per file to 8", () => {
|
|
235
|
-
const sigs = Array.from({ length: 15 }, (_, i) => `export function fn${i}(): void {}`).join("\n");
|
|
236
|
-
const blocks: NormalizedBlock[] = [
|
|
237
|
-
{ kind: "tool_call", name: "Read", args: { file_path: "big.ts" } },
|
|
238
|
-
{ kind: "tool_result", name: "Read", text: sigs, isError: false },
|
|
239
|
-
];
|
|
240
|
-
const r = buildSections({ blocks });
|
|
241
|
-
// The catalog should have the file but max 8 signatures
|
|
242
|
-
const sigLines = r.typeCatalog.filter(l => l.startsWith(" "));
|
|
243
|
-
expect(sigLines.length).toBeLessThanOrEqual(8);
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
describe("symbol changes", () => {
|
|
248
|
-
it("extracts symbol refs from Edit/Write tool calls", () => {
|
|
249
|
-
const blocks: NormalizedBlock[] = [
|
|
250
|
-
{ kind: "tool_call", name: "Edit", args: { file_path: "auth.ts", newText: "export function login() {}\nexport function verifyToken() {}" } },
|
|
251
|
-
{ kind: "tool_result", name: "Edit", text: "ok", isError: false },
|
|
252
|
-
];
|
|
253
|
-
const r = buildSections({ blocks });
|
|
254
|
-
expect(r.symbolChanges.length).toBeGreaterThan(0);
|
|
255
|
-
expect(r.symbolChanges.some(s => s.name === "login" && s.file === "auth.ts")).toBe(true);
|
|
256
|
-
expect(r.symbolChanges.some(s => s.name === "verifyToken" && s.kind === "function")).toBe(true);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it("marks read vs modified access correctly", () => {
|
|
260
|
-
const blocks: NormalizedBlock[] = [
|
|
261
|
-
{ kind: "tool_call", name: "Read", args: { file_path: "types.ts" } },
|
|
262
|
-
{ kind: "tool_result", name: "Read", text: "export interface User {}", isError: false },
|
|
263
|
-
{ kind: "tool_call", name: "Edit", args: { file_path: "auth.ts", newText: "export function login() {}" } },
|
|
264
|
-
{ kind: "tool_result", name: "Edit", text: "ok", isError: false },
|
|
265
|
-
];
|
|
266
|
-
const r = buildSections({ blocks });
|
|
267
|
-
const loginSym = r.symbolChanges.find(s => s.name === "login");
|
|
268
|
-
const userSym = r.symbolChanges.find(s => s.name === "User");
|
|
269
|
-
expect(loginSym?.access).toBe("modified");
|
|
270
|
-
expect(userSym?.access).toBe("read");
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it("deduplicates symbols by name+file", () => {
|
|
274
|
-
const blocks: NormalizedBlock[] = [
|
|
275
|
-
{ kind: "tool_call", name: "Read", args: { file_path: "auth.ts" } },
|
|
276
|
-
{ kind: "tool_result", name: "Read", text: "export function login() {}", isError: false },
|
|
277
|
-
{ kind: "tool_call", name: "Edit", args: { file_path: "auth.ts", newText: "export function login() {}" } },
|
|
278
|
-
{ kind: "tool_result", name: "Edit", text: "ok", isError: false },
|
|
279
|
-
];
|
|
280
|
-
const r = buildSections({ blocks });
|
|
281
|
-
const loginSymCount = r.symbolChanges.filter(s => s.name === "login" && s.file === "auth.ts").length;
|
|
282
|
-
expect(loginSymCount).toBe(1);
|
|
283
|
-
});
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
describe("format output integration", () => {
|
|
287
|
-
it("includes Type Catalog section in formatted output between Files and Commits", () => {
|
|
288
|
-
const blocks: NormalizedBlock[] = [
|
|
289
|
-
{ kind: "tool_call", name: "Read", args: { file_path: "types.ts" } },
|
|
290
|
-
{ kind: "tool_result", name: "Read", text: "export interface User { name: string; }", isError: false },
|
|
291
|
-
];
|
|
292
|
-
const r = buildSections({ blocks });
|
|
293
|
-
const { formatSummary } = require("../src/core/format");
|
|
294
|
-
const output = formatSummary(r);
|
|
295
|
-
expect(output).toContain("[Type Catalog]");
|
|
296
|
-
// Should appear after Files And Changes and before Commits
|
|
297
|
-
const filesIdx = output.indexOf("[Files And Changes]");
|
|
298
|
-
const typeIdx = output.indexOf("[Type Catalog]");
|
|
299
|
-
const commitsIdx = output.indexOf("[Commits]");
|
|
300
|
-
if (filesIdx >= 0 && typeIdx >= 0) expect(filesIdx).toBeLessThan(typeIdx);
|
|
301
|
-
// Commits may or may not be present
|
|
302
|
-
if (commitsIdx >= 0 && typeIdx >= 0) expect(typeIdx).toBeLessThan(commitsIdx);
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it("Type Catalog is volatile (fresh only) on merge", () => {
|
|
306
|
-
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()";
|
|
308
|
-
const fresh = compile({ messages: [], previousSummary: prev });
|
|
309
|
-
// Type Catalog should be fresh-only, not merged from previous
|
|
310
|
-
expect(fresh).not.toContain("old-file.ts");
|
|
311
|
-
expect(fresh).not.toContain("old()");
|
|
312
|
-
});
|
|
313
|
-
});
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import { extractGoals } from "../src/extract/goals";
|
|
3
|
-
import type { NormalizedBlock } from "../src/types";
|
|
4
|
-
|
|
5
|
-
describe("extractGoals", () => {
|
|
6
|
-
it("returns empty for no blocks", () => {
|
|
7
|
-
expect(extractGoals([])).toEqual([]);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it("returns empty when no user blocks", () => {
|
|
11
|
-
const blocks: NormalizedBlock[] = [
|
|
12
|
-
{ kind: "assistant", text: "hello" },
|
|
13
|
-
];
|
|
14
|
-
expect(extractGoals(blocks)).toEqual([]);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("extracts first user message lines as goals", () => {
|
|
18
|
-
const blocks: NormalizedBlock[] = [
|
|
19
|
-
{ kind: "user", text: "Fix login bug\nCheck auth flow" },
|
|
20
|
-
];
|
|
21
|
-
const goals = extractGoals(blocks);
|
|
22
|
-
expect(goals).toEqual(["Fix login bug", "Check auth flow"]);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("takes up to 6 lines from first user block", () => {
|
|
26
|
-
const blocks: NormalizedBlock[] = [
|
|
27
|
-
{ kind: "user", text: "fix the login bug\ncheck auth flow\nupdate the tests\nrefactor utils\nclean up" },
|
|
28
|
-
];
|
|
29
|
-
expect(extractGoals(blocks)).toHaveLength(5);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("ignores subsequent user blocks", () => {
|
|
33
|
-
const blocks: NormalizedBlock[] = [
|
|
34
|
-
{ kind: "user", text: "first goal" },
|
|
35
|
-
{ kind: "assistant", text: "ok" },
|
|
36
|
-
{ kind: "user", text: "second request" },
|
|
37
|
-
];
|
|
38
|
-
expect(extractGoals(blocks)).toEqual(["first goal"]);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("detects scope change with explicit pivot keywords", () => {
|
|
42
|
-
const blocks: NormalizedBlock[] = [
|
|
43
|
-
{ kind: "user", text: "Fix login bug" },
|
|
44
|
-
{ kind: "assistant", text: "ok" },
|
|
45
|
-
{ kind: "user", text: "Actually, instead let's refactor the auth module" },
|
|
46
|
-
];
|
|
47
|
-
const goals = extractGoals(blocks);
|
|
48
|
-
expect(goals).toContain("Fix login bug");
|
|
49
|
-
expect(goals).toContain("[Scope change]");
|
|
50
|
-
expect(goals.some((g) => g.includes("refactor"))).toBe(true);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("detects scope change from new task statements", () => {
|
|
54
|
-
const blocks: NormalizedBlock[] = [
|
|
55
|
-
{ kind: "user", text: "Fix login bug" },
|
|
56
|
-
{ kind: "assistant", text: "done" },
|
|
57
|
-
{ kind: "user", text: "Now implement the user registration flow" },
|
|
58
|
-
];
|
|
59
|
-
const goals = extractGoals(blocks);
|
|
60
|
-
expect(goals).toContain("[Scope change]");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("keeps latest scope change only", () => {
|
|
64
|
-
const blocks: NormalizedBlock[] = [
|
|
65
|
-
{ kind: "user", text: "Fix login bug" },
|
|
66
|
-
{ kind: "assistant", text: "done" },
|
|
67
|
-
{ kind: "user", text: "Actually, fix the signup page instead" },
|
|
68
|
-
{ kind: "assistant", text: "ok" },
|
|
69
|
-
{ kind: "user", text: "Change of plan, implement password reset" },
|
|
70
|
-
];
|
|
71
|
-
const goals = extractGoals(blocks);
|
|
72
|
-
const scopeIdx = goals.indexOf("[Scope change]");
|
|
73
|
-
expect(goals[scopeIdx + 1]).toContain("password reset");
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("skips noise short user messages as goals", () => {
|
|
77
|
-
const blocks: NormalizedBlock[] = [
|
|
78
|
-
{ kind: "user", text: "ok" },
|
|
79
|
-
{ kind: "assistant", text: "hello" },
|
|
80
|
-
{ kind: "user", text: "Fix the authentication module" },
|
|
81
|
-
];
|
|
82
|
-
const goals = extractGoals(blocks);
|
|
83
|
-
expect(goals[0]).toContain("Fix the authentication");
|
|
84
|
-
expect(goals.some((g) => g === "ok")).toBe(false);
|
|
85
|
-
});
|
|
86
|
-
});
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import { extractPreferences } from "../src/extract/preferences";
|
|
3
|
-
import type { NormalizedBlock } from "../src/types";
|
|
4
|
-
|
|
5
|
-
describe("extractPreferences", () => {
|
|
6
|
-
it("returns empty for no blocks", () => {
|
|
7
|
-
expect(extractPreferences([])).toEqual([]);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it("captures preference patterns from user", () => {
|
|
11
|
-
const blocks: NormalizedBlock[] = [
|
|
12
|
-
{ kind: "user", text: "I prefer TypeScript over JavaScript" },
|
|
13
|
-
];
|
|
14
|
-
expect(extractPreferences(blocks).length).toBe(1);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("ignores assistant blocks", () => {
|
|
18
|
-
const blocks: NormalizedBlock[] = [
|
|
19
|
-
{ kind: "assistant", text: "I always use best practices" },
|
|
20
|
-
];
|
|
21
|
-
expect(extractPreferences(blocks)).toEqual([]);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("captures please use pattern", () => {
|
|
25
|
-
const blocks: NormalizedBlock[] = [
|
|
26
|
-
{ kind: "user", text: "please use bun instead of node" },
|
|
27
|
-
];
|
|
28
|
-
expect(extractPreferences(blocks).length).toBe(1);
|
|
29
|
-
});
|
|
30
|
-
});
|