@askjo/pi-reflect 1.0.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,337 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import * as assert from "node:assert/strict";
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import {
6
+ runReflection,
7
+ type ReflectTarget,
8
+ type RunReflectionDeps,
9
+ type NotifyFn,
10
+ DEFAULT_TARGET,
11
+ } from "../extensions/reflect.js";
12
+ import { makeTempDir, cleanup, SAMPLE_AGENTS_MD } from "./helpers.js";
13
+
14
+ let tmpDir: string;
15
+ let notifications: Array<{ msg: string; level: string }>;
16
+ let notify: NotifyFn;
17
+
18
+ beforeEach(() => {
19
+ tmpDir = makeTempDir();
20
+ notifications = [];
21
+ notify = (msg, level) => notifications.push({ msg, level });
22
+ });
23
+
24
+ afterEach(() => {
25
+ cleanup(tmpDir);
26
+ });
27
+
28
+ function makeTarget(overrides: Partial<ReflectTarget> = {}): ReflectTarget {
29
+ return {
30
+ ...DEFAULT_TARGET,
31
+ backupDir: path.join(tmpDir, "backups"),
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ function makeLlmResponse(edits: any[], corrections_found = 5, summary = "Test summary") {
37
+ return JSON.stringify({ corrections_found, sessions_with_corrections: 3, edits, patterns_not_added: [], summary });
38
+ }
39
+
40
+ function makeDeps(llmResponseJson: string, transcripts = "### Session: test\n\n**USER:** bro no\n**AGENT:** sorry"): RunReflectionDeps {
41
+ return {
42
+ completeSimple: async () => ({
43
+ content: [{ type: "text", text: llmResponseJson }],
44
+ }),
45
+ getModel: () => ({ provider: "test", id: "test-model" }),
46
+ collectTranscriptsFn: async () => ({
47
+ transcripts,
48
+ sessionCount: 10,
49
+ includedCount: 5,
50
+ }),
51
+ };
52
+ }
53
+
54
+ function makeModelRegistry() {
55
+ return {
56
+ find: () => ({ provider: "test", id: "test-model" }),
57
+ getApiKey: async () => "test-key",
58
+ };
59
+ }
60
+
61
+ describe("runReflection", () => {
62
+ it("returns null and notifies when target file not found", async () => {
63
+ const target = makeTarget({ path: path.join(tmpDir, "nonexistent.md") });
64
+ const result = await runReflection(target, makeModelRegistry(), notify, makeDeps("{}"));
65
+ assert.equal(result, null);
66
+ assert.ok(notifications.some(n => n.level === "error" && n.msg.includes("not found")));
67
+ });
68
+
69
+ it("returns null when target file is too small", async () => {
70
+ const fp = path.join(tmpDir, "tiny.md");
71
+ fs.writeFileSync(fp, "small");
72
+ const target = makeTarget({ path: fp });
73
+ const result = await runReflection(target, makeModelRegistry(), notify, makeDeps("{}"));
74
+ assert.equal(result, null);
75
+ assert.ok(notifications.some(n => n.level === "error" && n.msg.includes("too small")));
76
+ });
77
+
78
+ it("returns null when no transcripts found", async () => {
79
+ const fp = path.join(tmpDir, "AGENTS.md");
80
+ fs.writeFileSync(fp, SAMPLE_AGENTS_MD);
81
+ const target = makeTarget({ path: fp });
82
+ const deps: RunReflectionDeps = {
83
+ ...makeDeps("{}"),
84
+ collectTranscriptsFn: async () => ({ transcripts: "", sessionCount: 5, includedCount: 0 }),
85
+ };
86
+ const result = await runReflection(target, makeModelRegistry(), notify, deps);
87
+ assert.equal(result, null);
88
+ assert.ok(notifications.some(n => n.msg.includes("No substantive sessions")));
89
+ });
90
+
91
+ it("returns null when model not found", async () => {
92
+ const fp = path.join(tmpDir, "AGENTS.md");
93
+ fs.writeFileSync(fp, SAMPLE_AGENTS_MD);
94
+ const target = makeTarget({ path: fp, model: "nonexistent/model-xyz" });
95
+ const deps: RunReflectionDeps = {
96
+ ...makeDeps("{}"),
97
+ getModel: () => null,
98
+ };
99
+ const registry = { find: () => null, getApiKey: async () => null };
100
+ const result = await runReflection(target, registry, notify, deps);
101
+ assert.equal(result, null);
102
+ assert.ok(notifications.some(n => n.level === "error" && n.msg.includes("Model not found")));
103
+ });
104
+
105
+ it("returns null when no API key available", async () => {
106
+ const fp = path.join(tmpDir, "AGENTS.md");
107
+ fs.writeFileSync(fp, SAMPLE_AGENTS_MD);
108
+ const target = makeTarget({ path: fp });
109
+ const registry = {
110
+ find: () => ({ provider: "test", id: "test" }),
111
+ getApiKey: async () => null,
112
+ };
113
+ const result = await runReflection(target, registry, notify, makeDeps("{}"));
114
+ assert.equal(result, null);
115
+ assert.ok(notifications.some(n => n.level === "error" && n.msg.includes("No API key")));
116
+ });
117
+
118
+ it("returns run with 0 edits when LLM says no edits needed", async () => {
119
+ const fp = path.join(tmpDir, "AGENTS.md");
120
+ fs.writeFileSync(fp, SAMPLE_AGENTS_MD);
121
+ const target = makeTarget({ path: fp });
122
+ const llmResponse = makeLlmResponse([], 0, "No issues found.");
123
+ const result = await runReflection(target, makeModelRegistry(), notify, makeDeps(llmResponse));
124
+ assert.notEqual(result, null);
125
+ assert.equal(result!.editsApplied, 0);
126
+ assert.equal(result!.correctionsFound, 0);
127
+ assert.ok(result!.summary.includes("No issues"));
128
+ });
129
+
130
+ it("applies edits and creates backup", async () => {
131
+ const fp = path.join(tmpDir, "AGENTS.md");
132
+ fs.writeFileSync(fp, SAMPLE_AGENTS_MD);
133
+ const backupDir = path.join(tmpDir, "backups");
134
+ const target = makeTarget({ path: fp, backupDir });
135
+
136
+ const llmResponse = makeLlmResponse([{
137
+ type: "strengthen",
138
+ old_text: "- **Keep code DRY**: NEVER duplicate logic.",
139
+ new_text: "- **Keep code DRY (CRITICAL)**: NEVER duplicate logic. If two call sites need different behavior, add parameters.",
140
+ }]);
141
+
142
+ const result = await runReflection(target, makeModelRegistry(), notify, makeDeps(llmResponse));
143
+ assert.notEqual(result, null);
144
+ assert.equal(result!.editsApplied, 1);
145
+
146
+ // File was modified
147
+ const updated = fs.readFileSync(fp, "utf-8");
148
+ assert.ok(updated.includes("Keep code DRY (CRITICAL)"));
149
+
150
+ // Backup was created
151
+ const backups = fs.readdirSync(backupDir);
152
+ assert.equal(backups.length, 1);
153
+ assert.ok(backups[0].startsWith("AGENTS_"));
154
+ assert.ok(backups[0].endsWith(".md"));
155
+
156
+ // Backup has original content
157
+ const backupContent = fs.readFileSync(path.join(backupDir, backups[0]), "utf-8");
158
+ assert.equal(backupContent, SAMPLE_AGENTS_MD);
159
+ });
160
+
161
+ it("returns null and cleans up backup when all edits fail", async () => {
162
+ const fp = path.join(tmpDir, "AGENTS.md");
163
+ fs.writeFileSync(fp, SAMPLE_AGENTS_MD);
164
+ const backupDir = path.join(tmpDir, "backups");
165
+ const target = makeTarget({ path: fp, backupDir });
166
+
167
+ const llmResponse = makeLlmResponse([{
168
+ type: "strengthen",
169
+ old_text: "- This text does not exist in the file at all.",
170
+ new_text: "- Replacement that can never apply.",
171
+ }]);
172
+
173
+ const result = await runReflection(target, makeModelRegistry(), notify, makeDeps(llmResponse));
174
+ assert.equal(result, null);
175
+ assert.ok(notifications.some(n => n.level === "warning" && n.msg.includes("edits failed")));
176
+
177
+ // Backup should be cleaned up
178
+ if (fs.existsSync(backupDir)) {
179
+ const backups = fs.readdirSync(backupDir);
180
+ assert.equal(backups.length, 0);
181
+ }
182
+ });
183
+
184
+ it("returns null when LLM response is not valid JSON", async () => {
185
+ const fp = path.join(tmpDir, "AGENTS.md");
186
+ fs.writeFileSync(fp, SAMPLE_AGENTS_MD);
187
+ const target = makeTarget({ path: fp });
188
+ const deps: RunReflectionDeps = {
189
+ ...makeDeps("{}"),
190
+ completeSimple: async () => ({
191
+ content: [{ type: "text", text: "This is not JSON at all, just plain text." }],
192
+ }),
193
+ };
194
+ const result = await runReflection(target, makeModelRegistry(), notify, deps);
195
+ assert.equal(result, null);
196
+ assert.ok(notifications.some(n => n.level === "error" && n.msg.includes("Failed to parse")));
197
+ });
198
+
199
+ it("strips markdown code fences from LLM response", async () => {
200
+ const fp = path.join(tmpDir, "AGENTS.md");
201
+ fs.writeFileSync(fp, SAMPLE_AGENTS_MD);
202
+ const target = makeTarget({ path: fp });
203
+ const jsonStr = makeLlmResponse([{
204
+ type: "strengthen",
205
+ old_text: "- **Keep code DRY**: NEVER duplicate logic.",
206
+ new_text: "- **Keep code DRY (CRITICAL)**: NEVER duplicate logic. Always parameterize.",
207
+ }]);
208
+ const deps: RunReflectionDeps = {
209
+ ...makeDeps("{}"),
210
+ completeSimple: async () => ({
211
+ content: [{ type: "text", text: "```json\n" + jsonStr + "\n```" }],
212
+ }),
213
+ };
214
+ const result = await runReflection(target, makeModelRegistry(), notify, deps);
215
+ assert.notEqual(result, null);
216
+ assert.equal(result!.editsApplied, 1);
217
+ });
218
+
219
+ it("uses command transcript source when configured", async () => {
220
+ const fp = path.join(tmpDir, "AGENTS.md");
221
+ fs.writeFileSync(fp, SAMPLE_AGENTS_MD);
222
+ const target = makeTarget({
223
+ path: fp,
224
+ transcriptSource: { type: "command", command: "echo test {lookbackDays}" },
225
+ });
226
+
227
+ let commandCalledWith = "";
228
+ const deps: RunReflectionDeps = {
229
+ ...makeDeps("{}"),
230
+ collectTranscriptsFromCommandFn: async (cmd, days, max) => {
231
+ commandCalledWith = cmd;
232
+ return { transcripts: "### Session: cmd test\n\n**USER:** test\n", sessionCount: 1, includedCount: 1 };
233
+ },
234
+ };
235
+
236
+ const llmResponse = makeLlmResponse([], 0, "No issues.");
237
+ deps.completeSimple = async () => ({
238
+ content: [{ type: "text", text: llmResponse }],
239
+ });
240
+
241
+ await runReflection(target, makeModelRegistry(), notify, deps);
242
+ assert.equal(commandCalledWith, "echo test {lookbackDays}");
243
+ });
244
+
245
+ it("counts diff lines correctly", async () => {
246
+ const fp = path.join(tmpDir, "AGENTS.md");
247
+ fs.writeFileSync(fp, SAMPLE_AGENTS_MD);
248
+ const target = makeTarget({ path: fp });
249
+ const llmResponse = makeLlmResponse([{
250
+ type: "add",
251
+ after_text: "- **Keep code DRY**: NEVER duplicate logic.",
252
+ new_text: "- **New rule 1**: First new rule.\n- **New rule 2**: Second new rule.",
253
+ }]);
254
+
255
+ const result = await runReflection(target, makeModelRegistry(), notify, makeDeps(llmResponse));
256
+ assert.notEqual(result, null);
257
+ assert.ok(result!.diffLines > 0);
258
+ });
259
+
260
+ it("reports partial success with skipped edits", async () => {
261
+ const fp = path.join(tmpDir, "AGENTS.md");
262
+ fs.writeFileSync(fp, SAMPLE_AGENTS_MD);
263
+ const target = makeTarget({ path: fp });
264
+ const llmResponse = makeLlmResponse([
265
+ {
266
+ type: "strengthen",
267
+ old_text: "- **Keep code DRY**: NEVER duplicate logic.",
268
+ new_text: "- **Keep code DRY (CRITICAL)**: NEVER duplicate logic. Parameterize always.",
269
+ },
270
+ {
271
+ type: "strengthen",
272
+ old_text: "- This text does not exist.",
273
+ new_text: "- This will fail.",
274
+ },
275
+ ]);
276
+
277
+ const result = await runReflection(target, makeModelRegistry(), notify, makeDeps(llmResponse));
278
+ assert.notEqual(result, null);
279
+ assert.equal(result!.editsApplied, 1);
280
+ assert.ok(notifications.some(n => n.level === "warning" && n.msg.includes("1 skipped")));
281
+ });
282
+
283
+ it("does not write file when result would be suspiciously small", async () => {
284
+ // Create a very large file
285
+ const fp = path.join(tmpDir, "AGENTS.md");
286
+ const bigContent = SAMPLE_AGENTS_MD + "\n" + "x".repeat(10000);
287
+ fs.writeFileSync(fp, bigContent);
288
+ const target = makeTarget({ path: fp });
289
+
290
+ // An edit that would replace most of the content with almost nothing
291
+ const llmResponse = makeLlmResponse([{
292
+ type: "strengthen",
293
+ old_text: "x".repeat(10000),
294
+ new_text: "tiny",
295
+ }]);
296
+
297
+ const result = await runReflection(target, makeModelRegistry(), notify, makeDeps(llmResponse));
298
+
299
+ // The size check should abort — file unchanged
300
+ assert.equal(result, null);
301
+ assert.ok(notifications.some(n => n.level === "error" && n.msg.includes("suspiciously small")));
302
+ const unchanged = fs.readFileSync(fp, "utf-8");
303
+ assert.equal(unchanged, bigContent);
304
+ });
305
+
306
+ it("falls back to model registry when getModel returns null", async () => {
307
+ const fp = path.join(tmpDir, "AGENTS.md");
308
+ fs.writeFileSync(fp, SAMPLE_AGENTS_MD);
309
+ const target = makeTarget({ path: fp });
310
+ const llmResponse = makeLlmResponse([], 0, "No issues.");
311
+
312
+ let registryFindCalled = false;
313
+ const registry = {
314
+ find: () => { registryFindCalled = true; return { provider: "test", id: "test" }; },
315
+ getApiKey: async () => "test-key",
316
+ };
317
+ const deps: RunReflectionDeps = {
318
+ ...makeDeps(llmResponse),
319
+ getModel: () => null, // force fallback to registry
320
+ };
321
+
322
+ await runReflection(target, registry, notify, deps);
323
+ assert.ok(registryFindCalled);
324
+ });
325
+
326
+ it("includes session stats in notifications", async () => {
327
+ const fp = path.join(tmpDir, "AGENTS.md");
328
+ fs.writeFileSync(fp, SAMPLE_AGENTS_MD);
329
+ const target = makeTarget({ path: fp });
330
+ const llmResponse = makeLlmResponse([], 0, "Clean.");
331
+ const result = await runReflection(target, makeModelRegistry(), notify, makeDeps(llmResponse));
332
+
333
+ assert.ok(notifications.some(n => n.msg.includes("Extracting transcripts")));
334
+ assert.ok(notifications.some(n => n.msg.includes("5 sessions") && n.msg.includes("10 scanned")));
335
+ assert.ok(notifications.some(n => n.msg.includes("Analyzing with")));
336
+ });
337
+ });