@heyhuynhgiabuu/pi-pretty 0.6.4 → 0.6.5
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/CHANGELOG.md +12 -0
- package/dist/autocomplete.d.ts +10 -0
- package/dist/autocomplete.d.ts.map +1 -0
- package/dist/autocomplete.js +64 -0
- package/dist/autocomplete.js.map +1 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +225 -0
- package/dist/config.js.map +1 -0
- package/dist/fff-helpers.d.ts +25 -0
- package/dist/fff-helpers.d.ts.map +1 -0
- package/dist/fff-helpers.js +83 -0
- package/dist/fff-helpers.js.map +1 -0
- package/dist/fff.d.ts +37 -0
- package/dist/fff.d.ts.map +1 -0
- package/dist/fff.js +135 -0
- package/dist/fff.js.map +1 -0
- package/dist/helpers.d.ts +21 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +133 -0
- package/dist/helpers.js.map +1 -0
- package/dist/image.d.ts +24 -0
- package/dist/image.d.ts.map +1 -0
- package/dist/image.js +144 -0
- package/dist/image.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +223 -0
- package/dist/index.js.map +1 -0
- package/dist/multi-grep-fallback.d.ts +30 -0
- package/dist/multi-grep-fallback.d.ts.map +1 -0
- package/dist/multi-grep-fallback.js +205 -0
- package/dist/multi-grep-fallback.js.map +1 -0
- package/dist/render.d.ts +25 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +366 -0
- package/dist/render.js.map +1 -0
- package/dist/tools/bash.d.ts +6 -0
- package/dist/tools/bash.d.ts.map +1 -0
- package/dist/tools/bash.js +101 -0
- package/dist/tools/bash.js.map +1 -0
- package/dist/tools/find.d.ts +6 -0
- package/dist/tools/find.d.ts.map +1 -0
- package/dist/tools/find.js +108 -0
- package/dist/tools/find.js.map +1 -0
- package/dist/tools/grep.d.ts +6 -0
- package/dist/tools/grep.d.ts.map +1 -0
- package/dist/tools/grep.js +121 -0
- package/dist/tools/grep.js.map +1 -0
- package/dist/tools/ls.d.ts +6 -0
- package/dist/tools/ls.d.ts.map +1 -0
- package/dist/tools/ls.js +63 -0
- package/dist/tools/ls.js.map +1 -0
- package/dist/tools/metrics.d.ts +10 -0
- package/dist/tools/metrics.d.ts.map +1 -0
- package/dist/tools/metrics.js +34 -0
- package/dist/tools/metrics.js.map +1 -0
- package/dist/tools/multi-grep.d.ts +7 -0
- package/dist/tools/multi-grep.d.ts.map +1 -0
- package/dist/tools/multi-grep.js +197 -0
- package/dist/tools/multi-grep.js.map +1 -0
- package/dist/tools/read.d.ts +6 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +125 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/types.d.ts +168 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +7 -1
- package/biome.json +0 -22
- package/bun.lock +0 -598
- package/media/bash-and-read.png +0 -0
- package/media/icons-and-grep.png +0 -0
- package/media/inline-image.png +0 -0
- package/pi-pretty.example.json +0 -6
- package/release-notes/v0.3.2.md +0 -34
- package/release-notes/v0.3.3.md +0 -48
- package/release-notes/v0.4.0.md +0 -58
- package/release-notes/v0.4.1.md +0 -28
- package/release-notes/v0.4.2.md +0 -36
- package/release-notes/v0.4.4.md +0 -28
- package/release-notes/v0.5.3.md +0 -29
- package/test/bash-rendering.test.ts +0 -269
- package/test/fff-integration.test.ts +0 -800
- package/test/image-rendering.test.ts +0 -153
- package/tsconfig.json +0 -19
|
@@ -1,800 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for pi-pretty FFF integration vs SDK fallback.
|
|
3
|
-
*
|
|
4
|
-
* 1. Unit tests for CursorStore + fffFormatGrepText (extracted helpers)
|
|
5
|
-
* 2. Integration tests via dependency injection (PiPrettyDeps)
|
|
6
|
-
* - SDK fallback path (no FFF)
|
|
7
|
-
* - FFF path (FFF injected)
|
|
8
|
-
* - Graceful degradation (FFF fails → SDK fallback)
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
12
|
-
import { tmpdir } from "node:os";
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
15
|
-
import { CursorStore, fffFormatGrepText } from "../src/fff-helpers.js";
|
|
16
|
-
import piPrettyExtension, { type PiPrettyDeps } from "../src/index.js";
|
|
17
|
-
import {
|
|
18
|
-
getMultiGrepRipgrepArgs,
|
|
19
|
-
parseMultiGrepConstraints,
|
|
20
|
-
runMultiGrepRipgrepFallback,
|
|
21
|
-
} from "../src/multi-grep-fallback.js";
|
|
22
|
-
|
|
23
|
-
// =========================================================================
|
|
24
|
-
// 1. Unit tests — pure functions
|
|
25
|
-
// =========================================================================
|
|
26
|
-
|
|
27
|
-
describe("CursorStore", () => {
|
|
28
|
-
it("stores and retrieves a cursor", () => {
|
|
29
|
-
const store = new CursorStore();
|
|
30
|
-
const cursor = { page: 2, offset: 50 };
|
|
31
|
-
const id = store.store(cursor);
|
|
32
|
-
expect(id).toMatch(/^fff_c\d+$/);
|
|
33
|
-
expect(store.get(id)).toBe(cursor);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("returns undefined for unknown id", () => {
|
|
37
|
-
expect(new CursorStore().get("fff_c999")).toBeUndefined();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("increments ids sequentially", () => {
|
|
41
|
-
const store = new CursorStore();
|
|
42
|
-
const n1 = Number.parseInt(store.store("a").slice(5), 10);
|
|
43
|
-
const n2 = Number.parseInt(store.store("b").slice(5), 10);
|
|
44
|
-
expect(n2).toBe(n1 + 1);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("evicts oldest when exceeding maxSize", () => {
|
|
48
|
-
const store = new CursorStore(3);
|
|
49
|
-
const id1 = store.store("a");
|
|
50
|
-
store.store("b"); store.store("c");
|
|
51
|
-
expect(store.size).toBe(3);
|
|
52
|
-
store.store("d");
|
|
53
|
-
expect(store.size).toBe(3);
|
|
54
|
-
expect(store.get(id1)).toBeUndefined();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("default maxSize is 200", () => {
|
|
58
|
-
const store = new CursorStore();
|
|
59
|
-
const ids: string[] = [];
|
|
60
|
-
for (let i = 0; i < 201; i++) ids.push(store.store(i));
|
|
61
|
-
expect(store.size).toBe(200);
|
|
62
|
-
expect(store.get(ids[0])).toBeUndefined();
|
|
63
|
-
expect(store.get(ids[200])).toBe(200);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe("fffFormatGrepText", () => {
|
|
68
|
-
it("empty → 'No matches found'", () => {
|
|
69
|
-
expect(fffFormatGrepText([], 100)).toBe("No matches found");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("single match → file:line:content", () => {
|
|
73
|
-
const items = [{ relativePath: "src/a.ts", lineNumber: 42, lineContent: "const x = 1;" }];
|
|
74
|
-
expect(fffFormatGrepText(items, 100)).toBe("src/a.ts:42:const x = 1;");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("groups by file with blank separator", () => {
|
|
78
|
-
const items = [
|
|
79
|
-
{ relativePath: "a.ts", lineNumber: 1, lineContent: "L1" },
|
|
80
|
-
{ relativePath: "a.ts", lineNumber: 5, lineContent: "L5" },
|
|
81
|
-
{ relativePath: "b.ts", lineNumber: 10, lineContent: "LB" },
|
|
82
|
-
];
|
|
83
|
-
expect(fffFormatGrepText(items, 100).split("\n")).toEqual(["a.ts:1:L1", "a.ts:5:L5", "", "b.ts:10:LB"]);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("truncates >500 char lines", () => {
|
|
87
|
-
const items = [{ relativePath: "a.ts", lineNumber: 1, lineContent: "x".repeat(600) }];
|
|
88
|
-
expect(fffFormatGrepText(items, 100)).toBe(`a.ts:1:${"x".repeat(500)}...`);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("respects limit", () => {
|
|
92
|
-
const items = [
|
|
93
|
-
{ relativePath: "a.ts", lineNumber: 1, lineContent: "one" },
|
|
94
|
-
{ relativePath: "a.ts", lineNumber: 2, lineContent: "two" },
|
|
95
|
-
{ relativePath: "a.ts", lineNumber: 3, lineContent: "three" },
|
|
96
|
-
];
|
|
97
|
-
expect(fffFormatGrepText(items, 2).split("\n")).toHaveLength(2);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("contextBefore with dash format", () => {
|
|
101
|
-
const items = [{
|
|
102
|
-
relativePath: "a.ts", lineNumber: 5, lineContent: "match",
|
|
103
|
-
contextBefore: ["before1", "before2"],
|
|
104
|
-
}];
|
|
105
|
-
const lines = fffFormatGrepText(items, 100).split("\n");
|
|
106
|
-
expect(lines[0]).toBe("a.ts-3-before1");
|
|
107
|
-
expect(lines[1]).toBe("a.ts-4-before2");
|
|
108
|
-
expect(lines[2]).toBe("a.ts:5:match");
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("contextAfter with dash format", () => {
|
|
112
|
-
const items = [{
|
|
113
|
-
relativePath: "a.ts", lineNumber: 5, lineContent: "match",
|
|
114
|
-
contextAfter: ["after1"],
|
|
115
|
-
}];
|
|
116
|
-
const lines = fffFormatGrepText(items, 100).split("\n");
|
|
117
|
-
expect(lines[0]).toBe("a.ts:5:match");
|
|
118
|
-
expect(lines[1]).toBe("a.ts-6-after1");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("sanitizes CRLF and CR without injecting grep record newlines", () => {
|
|
122
|
-
const items = [{
|
|
123
|
-
relativePath: "a.ts",
|
|
124
|
-
lineNumber: 5,
|
|
125
|
-
lineContent: "match\r\ncontinued\rtrail",
|
|
126
|
-
contextBefore: ["before\r\nline"],
|
|
127
|
-
contextAfter: ["after\rline"],
|
|
128
|
-
}];
|
|
129
|
-
const text = fffFormatGrepText(items, 100);
|
|
130
|
-
const lines = text.split("\n");
|
|
131
|
-
|
|
132
|
-
expect(lines).toEqual([
|
|
133
|
-
"a.ts-4-before\\nline",
|
|
134
|
-
"a.ts:5:match\\ncontinued\\rtrail",
|
|
135
|
-
"a.ts-6-after\\rline",
|
|
136
|
-
]);
|
|
137
|
-
expect(lines).toHaveLength(3);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("strips trailing CR from CRLF-backed FFF records", () => {
|
|
141
|
-
const items = [{ relativePath: "a.ts", lineNumber: 5, lineContent: "match\r" }];
|
|
142
|
-
expect(fffFormatGrepText(items, 100)).toBe("a.ts:5:match");
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
describe("multi_grep constraint parsing", () => {
|
|
147
|
-
it("maps complex include/exclude constraints to ripgrep globs", () => {
|
|
148
|
-
expect(parseMultiGrepConstraints("*.{ts,tsx} !test/")).toEqual({
|
|
149
|
-
ok: true,
|
|
150
|
-
tokens: ["*.{ts,tsx}", "!test/"],
|
|
151
|
-
globs: ["*.{ts,tsx}", "!**/test/**"],
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("maps directory constraints as path components", () => {
|
|
156
|
-
expect(parseMultiGrepConstraints("src/ !src/generated/")).toEqual({
|
|
157
|
-
ok: true,
|
|
158
|
-
tokens: ["src/", "!src/generated/"],
|
|
159
|
-
globs: ["**/src/**", "!**/src/generated/**"],
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("builds literal ripgrep OR arguments with every constraint glob", () => {
|
|
164
|
-
const result = getMultiGrepRipgrepArgs({
|
|
165
|
-
cwd: "/repo",
|
|
166
|
-
patterns: ["foo", "bar"],
|
|
167
|
-
path: "src",
|
|
168
|
-
constraints: "*.ts !test/",
|
|
169
|
-
ignoreCase: true,
|
|
170
|
-
limit: 100,
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
expect(result.ok).toBe(true);
|
|
174
|
-
if (!result.ok) return;
|
|
175
|
-
expect(result.args).toEqual([
|
|
176
|
-
"--line-number",
|
|
177
|
-
"--with-filename",
|
|
178
|
-
"--color=never",
|
|
179
|
-
"--hidden",
|
|
180
|
-
"--fixed-strings",
|
|
181
|
-
"--ignore-case",
|
|
182
|
-
"--glob",
|
|
183
|
-
"*.ts",
|
|
184
|
-
"--glob",
|
|
185
|
-
"!**/test/**",
|
|
186
|
-
"-e",
|
|
187
|
-
"foo",
|
|
188
|
-
"-e",
|
|
189
|
-
"bar",
|
|
190
|
-
"--",
|
|
191
|
-
"src",
|
|
192
|
-
]);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it("ripgrep fallback enforces include/exclude constraints without widening", async () => {
|
|
196
|
-
const root = mkdtempSync(join(tmpdir(), "pi-pretty-mgrep-"));
|
|
197
|
-
try {
|
|
198
|
-
mkdirSync(join(root, "src", "test"), { recursive: true });
|
|
199
|
-
mkdirSync(join(root, "test"), { recursive: true });
|
|
200
|
-
writeFileSync(join(root, "src", "keep.ts"), "needle\n");
|
|
201
|
-
writeFileSync(join(root, "src", "keep.js"), "needle\n");
|
|
202
|
-
writeFileSync(join(root, "src", "test", "drop.ts"), "needle\n");
|
|
203
|
-
writeFileSync(join(root, "test", "drop.ts"), "needle\n");
|
|
204
|
-
|
|
205
|
-
const result = await runMultiGrepRipgrepFallback({
|
|
206
|
-
cwd: root,
|
|
207
|
-
patterns: ["needle"],
|
|
208
|
-
constraints: "*.ts !test/",
|
|
209
|
-
ignoreCase: true,
|
|
210
|
-
limit: 100,
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
expect(result.text).toContain("src/keep.ts");
|
|
214
|
-
expect(result.text).not.toContain("src/keep.js");
|
|
215
|
-
expect(result.text).not.toContain("src/test/drop.ts");
|
|
216
|
-
expect(result.text).not.toContain("test/drop.ts");
|
|
217
|
-
} catch (error) {
|
|
218
|
-
if (String(error).includes("ripgrep (rg) is not available")) return;
|
|
219
|
-
throw error;
|
|
220
|
-
} finally {
|
|
221
|
-
rmSync(root, { recursive: true, force: true });
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it("rejects unsupported empty negation instead of ignoring it", () => {
|
|
226
|
-
expect(parseMultiGrepConstraints("*.ts !")).toEqual({
|
|
227
|
-
ok: false,
|
|
228
|
-
error: "empty constraint token: !",
|
|
229
|
-
});
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
// =========================================================================
|
|
234
|
-
// 2. Integration tests — via PiPrettyDeps injection
|
|
235
|
-
// =========================================================================
|
|
236
|
-
|
|
237
|
-
// Mock SDK tool factories
|
|
238
|
-
function mockToolFactory(exec: ReturnType<typeof vi.fn>) {
|
|
239
|
-
return (_cwd: string) => ({
|
|
240
|
-
name: "mock",
|
|
241
|
-
description: "mock",
|
|
242
|
-
parameters: { type: "object", properties: {} },
|
|
243
|
-
execute: exec,
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Mock FFF finder
|
|
248
|
-
function mkFinder(overrides?: Record<string, any>) {
|
|
249
|
-
return {
|
|
250
|
-
isDestroyed: false,
|
|
251
|
-
waitForScan: vi.fn().mockResolvedValue({ ok: true, value: true }),
|
|
252
|
-
fileSearch: vi.fn().mockReturnValue({
|
|
253
|
-
ok: true,
|
|
254
|
-
value: {
|
|
255
|
-
items: [{ relativePath: "src/index.ts" }, { relativePath: "src/main.ts" }],
|
|
256
|
-
totalMatched: 2,
|
|
257
|
-
},
|
|
258
|
-
}),
|
|
259
|
-
glob: vi.fn().mockReturnValue({
|
|
260
|
-
ok: true,
|
|
261
|
-
value: {
|
|
262
|
-
items: [{ relativePath: "src/index.ts" }, { relativePath: "src/main.ts" }],
|
|
263
|
-
totalMatched: 2,
|
|
264
|
-
},
|
|
265
|
-
}),
|
|
266
|
-
getBasePath: vi.fn().mockReturnValue({ ok: true, value: "/Users/test/proj" }),
|
|
267
|
-
grep: vi.fn().mockReturnValue({
|
|
268
|
-
ok: true,
|
|
269
|
-
value: {
|
|
270
|
-
items: [{ relativePath: "src/index.ts", lineNumber: 42, lineContent: "const x = 1;" }],
|
|
271
|
-
totalMatched: 1,
|
|
272
|
-
nextCursor: null,
|
|
273
|
-
},
|
|
274
|
-
}),
|
|
275
|
-
multiGrep: vi.fn().mockReturnValue({
|
|
276
|
-
ok: true,
|
|
277
|
-
value: {
|
|
278
|
-
items: [
|
|
279
|
-
{ relativePath: "src/index.ts", lineNumber: 10, lineContent: "import {foo}" },
|
|
280
|
-
{ relativePath: "src/main.ts", lineNumber: 5, lineContent: "const baz" },
|
|
281
|
-
],
|
|
282
|
-
totalMatched: 2,
|
|
283
|
-
nextCursor: null,
|
|
284
|
-
},
|
|
285
|
-
}),
|
|
286
|
-
destroy: vi.fn(),
|
|
287
|
-
...overrides,
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
describe("piPrettyExtension integration", () => {
|
|
292
|
-
let tools: Map<string, any>;
|
|
293
|
-
let events: Map<string, Function>;
|
|
294
|
-
let mockPi: any;
|
|
295
|
-
|
|
296
|
-
// SDK execute mocks
|
|
297
|
-
const findExec = vi.fn();
|
|
298
|
-
const grepExec = vi.fn();
|
|
299
|
-
const readExec = vi.fn();
|
|
300
|
-
const bashExec = vi.fn();
|
|
301
|
-
const lsExec = vi.fn();
|
|
302
|
-
const multiGrepRgExec = vi.fn();
|
|
303
|
-
|
|
304
|
-
function makeDeps(withFFF: boolean, finderOverrides?: Record<string, any>): PiPrettyDeps {
|
|
305
|
-
const finder = mkFinder(finderOverrides);
|
|
306
|
-
const fffModule = finderOverrides?.FileFinder
|
|
307
|
-
? { FileFinder: finderOverrides.FileFinder }
|
|
308
|
-
: { FileFinder: { create: vi.fn().mockReturnValue({ ok: true, value: finder }) } };
|
|
309
|
-
return {
|
|
310
|
-
sdk: {
|
|
311
|
-
createReadToolDefinition: mockToolFactory(readExec),
|
|
312
|
-
createBashToolDefinition: mockToolFactory(bashExec),
|
|
313
|
-
createLsToolDefinition: mockToolFactory(lsExec),
|
|
314
|
-
createFindToolDefinition: mockToolFactory(findExec),
|
|
315
|
-
createGrepToolDefinition: mockToolFactory(grepExec),
|
|
316
|
-
getAgentDir: () => "/tmp/pi-pretty-test",
|
|
317
|
-
},
|
|
318
|
-
TextComponent: class { private t = ""; setText(v: string) { this.t = v; } getText() { return this.t; } },
|
|
319
|
-
fffModule: withFFF ? fffModule : undefined,
|
|
320
|
-
multiGrepRipgrepFallback: multiGrepRgExec,
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
beforeEach(() => {
|
|
325
|
-
vi.useRealTimers();
|
|
326
|
-
tools = new Map();
|
|
327
|
-
events = new Map();
|
|
328
|
-
mockPi = {
|
|
329
|
-
registerTool: vi.fn((t: any) => tools.set(t.name, t)),
|
|
330
|
-
registerCommand: vi.fn((c: any) => {}),
|
|
331
|
-
on: vi.fn((e: string, h: Function) => events.set(e, h)),
|
|
332
|
-
};
|
|
333
|
-
|
|
334
|
-
for (const fn of [findExec, grepExec, readExec, bashExec, lsExec, multiGrepRgExec]) fn.mockReset();
|
|
335
|
-
findExec.mockResolvedValue({ content: [{ type: "text", text: "src/index.ts\nsrc/main.ts" }] });
|
|
336
|
-
grepExec.mockResolvedValue({ content: [{ type: "text", text: "src/index.ts:10:const x = 1;" }] });
|
|
337
|
-
readExec.mockResolvedValue({ content: [{ type: "text", text: "content" }] });
|
|
338
|
-
bashExec.mockResolvedValue({ content: [{ type: "text", text: "output" }] });
|
|
339
|
-
lsExec.mockResolvedValue({ content: [{ type: "text", text: "f1\nf2" }] });
|
|
340
|
-
multiGrepRgExec.mockResolvedValue({ text: "src/index.ts:10:const x = 1;", matchCount: 1, limitReached: false });
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
function load(withFFF = false, finderOverrides?: Record<string, any>) {
|
|
344
|
-
const deps = makeDeps(withFFF, finderOverrides);
|
|
345
|
-
piPrettyExtension(mockPi, deps);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
afterEach(() => {
|
|
349
|
-
vi.useRealTimers();
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
async function loadWithFFF(finderOverrides?: Record<string, any>) {
|
|
353
|
-
load(true, finderOverrides);
|
|
354
|
-
const start = events.get("session_start")!;
|
|
355
|
-
expect(start, "session_start not registered").toBeDefined();
|
|
356
|
-
await start({}, { cwd: "/tmp/test" });
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// ---- registration --------------------------------------------------
|
|
360
|
-
|
|
361
|
-
describe("tool registration", () => {
|
|
362
|
-
it("registers core tools (find, grep, read, bash, ls)", () => {
|
|
363
|
-
load();
|
|
364
|
-
for (const n of ["find", "grep", "read", "bash", "ls"]) {
|
|
365
|
-
expect(tools.has(n), `missing: ${n}`).toBe(true);
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
it("registers multi_grep when FFF available", () => {
|
|
370
|
-
load(true);
|
|
371
|
-
expect(tools.has("multi_grep")).toBe(true);
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
it("registers multi_grep when grep SDK available", () => {
|
|
375
|
-
load(false);
|
|
376
|
-
expect(tools.has("multi_grep")).toBe(true);
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
it("registers session_start + session_shutdown", () => {
|
|
380
|
-
load();
|
|
381
|
-
expect(events.has("session_start")).toBe(true);
|
|
382
|
-
expect(events.has("session_shutdown")).toBe(true);
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
it("skips tools listed in PRETTY_DISABLE_TOOLS", () => {
|
|
386
|
-
process.env.PRETTY_DISABLE_TOOLS = "read,find";
|
|
387
|
-
load();
|
|
388
|
-
expect(tools.has("read"), "read should be disabled").toBe(false);
|
|
389
|
-
expect(tools.has("find"), "find should be disabled").toBe(false);
|
|
390
|
-
expect(tools.has("bash"), "bash should be enabled").toBe(true);
|
|
391
|
-
expect(tools.has("grep"), "grep should be enabled").toBe(true);
|
|
392
|
-
expect(tools.has("ls"), "ls should be enabled").toBe(true);
|
|
393
|
-
delete process.env.PRETTY_DISABLE_TOOLS;
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
it("skips multi_grep when listed in PRETTY_DISABLE_TOOLS", () => {
|
|
397
|
-
process.env.PRETTY_DISABLE_TOOLS = "multi_grep";
|
|
398
|
-
load(true);
|
|
399
|
-
expect(tools.has("multi_grep"), "multi_grep should be disabled").toBe(false);
|
|
400
|
-
expect(tools.has("read"), "read should still be enabled").toBe(true);
|
|
401
|
-
delete process.env.PRETTY_DISABLE_TOOLS;
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
it("handles whitespace in PRETTY_DISABLE_TOOLS", () => {
|
|
405
|
-
process.env.PRETTY_DISABLE_TOOLS = " bash , ls ";
|
|
406
|
-
load();
|
|
407
|
-
expect(tools.has("bash"), "bash should be disabled").toBe(false);
|
|
408
|
-
expect(tools.has("ls"), "ls should be disabled").toBe(false);
|
|
409
|
-
expect(tools.has("read"), "read should be enabled").toBe(true);
|
|
410
|
-
expect(tools.has("grep"), "grep should be enabled").toBe(true);
|
|
411
|
-
delete process.env.PRETTY_DISABLE_TOOLS;
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
it("empty PRETTY_DISABLE_TOOLS registers all tools", () => {
|
|
415
|
-
process.env.PRETTY_DISABLE_TOOLS = "";
|
|
416
|
-
load();
|
|
417
|
-
for (const n of ["find", "grep", "read", "bash", "ls"]) {
|
|
418
|
-
expect(tools.has(n), `missing: ${n}`).toBe(true);
|
|
419
|
-
}
|
|
420
|
-
delete process.env.PRETTY_DISABLE_TOOLS;
|
|
421
|
-
});
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
// ---- find: SDK fallback (no FFF) -----------------------------------
|
|
425
|
-
|
|
426
|
-
describe("find — SDK fallback", () => {
|
|
427
|
-
it("delegates to SDK when FFF not loaded", async () => {
|
|
428
|
-
load(false);
|
|
429
|
-
const r = await tools.get("find")!.execute("t1", { pattern: "*.ts" }, null, null, {});
|
|
430
|
-
expect(findExec).toHaveBeenCalledOnce();
|
|
431
|
-
expect(r.details._type).toBe("findResult");
|
|
432
|
-
expect(r.details.pattern).toBe("*.ts");
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
it("counts matches from SDK text", async () => {
|
|
436
|
-
findExec.mockResolvedValue({ content: [{ type: "text", text: "a.ts\nb.ts\nc.ts" }] });
|
|
437
|
-
load(false);
|
|
438
|
-
const r = await tools.get("find")!.execute("t1", { pattern: "*.ts" }, null, null, {});
|
|
439
|
-
expect(r.details.matchCount).toBe(3);
|
|
440
|
-
});
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
// ---- grep: SDK fallback (no FFF) -----------------------------------
|
|
444
|
-
|
|
445
|
-
describe("grep — SDK fallback", () => {
|
|
446
|
-
it("delegates to SDK when FFF not loaded", async () => {
|
|
447
|
-
load(false);
|
|
448
|
-
const r = await tools.get("grep")!.execute("t1", { pattern: "TODO" }, null, null, {});
|
|
449
|
-
expect(grepExec).toHaveBeenCalledOnce();
|
|
450
|
-
expect(r.details._type).toBe("grepResult");
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
it("counts ripgrep-style matches", async () => {
|
|
454
|
-
grepExec.mockResolvedValue({
|
|
455
|
-
content: [{ type: "text", text: "a.ts:1:TODO\na.ts:5:TODO\nb.ts:10:TODO" }],
|
|
456
|
-
});
|
|
457
|
-
load(false);
|
|
458
|
-
const r = await tools.get("grep")!.execute("t1", { pattern: "TODO" }, null, null, {});
|
|
459
|
-
expect(r.details.matchCount).toBe(3);
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
it("normalizes CRLF in SDK text results", async () => {
|
|
463
|
-
grepExec.mockResolvedValue({
|
|
464
|
-
content: [{ type: "text", text: "a.ts:1:TODO\r\na.ts:5:TODO\rb.ts:10:TODO" }],
|
|
465
|
-
});
|
|
466
|
-
load(false);
|
|
467
|
-
const r = await tools.get("grep")!.execute("t1", { pattern: "TODO" }, null, null, {});
|
|
468
|
-
expect(r.content[0].text).toBe("a.ts:1:TODO\na.ts:5:TODO\nb.ts:10:TODO");
|
|
469
|
-
expect(r.details.text).toBe("a.ts:1:TODO\na.ts:5:TODO\nb.ts:10:TODO");
|
|
470
|
-
expect(r.details.matchCount).toBe(3);
|
|
471
|
-
});
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
// ---- read -----------------------------------------------------------
|
|
475
|
-
|
|
476
|
-
describe("read", () => {
|
|
477
|
-
it("normalizes CRLF in read details content", async () => {
|
|
478
|
-
readExec.mockResolvedValue({
|
|
479
|
-
content: [{ type: "text", text: "line1\r\nline2\rline3" }],
|
|
480
|
-
});
|
|
481
|
-
load(false);
|
|
482
|
-
const r = await tools.get("read")!.execute("t1", { path: "file.txt" }, null, null, {});
|
|
483
|
-
expect(r.details._type).toBe("readFile");
|
|
484
|
-
expect(r.details.content).toBe("line1\nline2\nline3");
|
|
485
|
-
expect(r.details.lineCount).toBe(3);
|
|
486
|
-
});
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
// ---- find: FFF path ------------------------------------------------
|
|
490
|
-
|
|
491
|
-
describe("find — FFF path", () => {
|
|
492
|
-
it("uses FFF fileSearch when initialized", async () => {
|
|
493
|
-
await loadWithFFF();
|
|
494
|
-
const r = await tools.get("find")!.execute("t1", { pattern: "*.ts" }, null, null, {});
|
|
495
|
-
expect(findExec).not.toHaveBeenCalled();
|
|
496
|
-
expect(r.details._type).toBe("findResult");
|
|
497
|
-
expect(r.content[0].text).toContain("src/index.ts");
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
it("falls back to SDK on FFF { ok: false }", async () => {
|
|
501
|
-
await loadWithFFF({
|
|
502
|
-
glob: vi.fn().mockReturnValue({ ok: false, error: "fail" }),
|
|
503
|
-
});
|
|
504
|
-
await tools.get("find")!.execute("t1", { pattern: "*.ts" }, null, null, {});
|
|
505
|
-
expect(findExec).toHaveBeenCalledOnce();
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
it("falls back to SDK on FFF throw", async () => {
|
|
509
|
-
await loadWithFFF({
|
|
510
|
-
glob: vi.fn().mockImplementation(() => { throw new Error("crash"); }),
|
|
511
|
-
});
|
|
512
|
-
await tools.get("find")!.execute("t1", { pattern: "*.ts" }, null, null, {});
|
|
513
|
-
expect(findExec).toHaveBeenCalledOnce();
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
it("respects limit param", async () => {
|
|
517
|
-
const glob = vi.fn().mockReturnValue({
|
|
518
|
-
ok: true,
|
|
519
|
-
value: { items: Array.from({ length: 50 }, (_, i) => ({ relativePath: `f${i}.ts` })), totalMatched: 50 },
|
|
520
|
-
});
|
|
521
|
-
await loadWithFFF({ glob });
|
|
522
|
-
await tools.get("find")!.execute("t1", { pattern: "*.ts", limit: 5 }, null, null, {});
|
|
523
|
-
expect(glob).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ pageSize: 5 }));
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
it("combines path and pattern into glob pattern", async () => {
|
|
527
|
-
const glob = vi.fn().mockReturnValue({ ok: true, value: { items: [], totalMatched: 0 } });
|
|
528
|
-
await loadWithFFF({ glob });
|
|
529
|
-
await tools.get("find")!.execute("t1", { pattern: "*.ts", path: "src" }, null, null, {});
|
|
530
|
-
expect(glob).toHaveBeenCalledWith("src/**/*.ts", expect.any(Object));
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
it("shows partial-index + limit notices", async () => {
|
|
534
|
-
await loadWithFFF({
|
|
535
|
-
waitForScan: vi.fn().mockResolvedValue({ ok: true, value: false }),
|
|
536
|
-
glob: vi.fn().mockReturnValue({
|
|
537
|
-
ok: true,
|
|
538
|
-
value: { items: Array.from({ length: 200 }, (_, i) => ({ relativePath: `f${i}` })), totalMatched: 500 },
|
|
539
|
-
}),
|
|
540
|
-
});
|
|
541
|
-
const result = await tools.get("find")!.execute("t1", { pattern: "*", limit: 200 }, null, null, {});
|
|
542
|
-
const notices = (result.details as any).notices as string[];
|
|
543
|
-
expect(notices).toContain("Warning: partial file index");
|
|
544
|
-
expect(notices).toContain("200 limit reached");
|
|
545
|
-
expect(notices).toContain("500 total matches");
|
|
546
|
-
});
|
|
547
|
-
// ---- grep: FFF path ------------------------------------------------
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
// ---- grep: FFF path ------------------------------------------------
|
|
551
|
-
|
|
552
|
-
describe("grep — FFF path", () => {
|
|
553
|
-
it("uses FFF grep when initialized", async () => {
|
|
554
|
-
await loadWithFFF();
|
|
555
|
-
const r = await tools.get("grep")!.execute("t1", { pattern: "TODO" }, null, null, {});
|
|
556
|
-
expect(grepExec).not.toHaveBeenCalled();
|
|
557
|
-
expect(r.content[0].text).toContain("src/index.ts:42:const x = 1;");
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
it("sanitizes CRLF in FFF grep output without extra records", async () => {
|
|
561
|
-
await loadWithFFF({
|
|
562
|
-
grep: vi.fn().mockReturnValue({
|
|
563
|
-
ok: true,
|
|
564
|
-
value: {
|
|
565
|
-
items: [{ relativePath: "src/index.ts", lineNumber: 42, lineContent: "const x = 1;\r\nconst y = 2;" }],
|
|
566
|
-
totalMatched: 1,
|
|
567
|
-
nextCursor: null,
|
|
568
|
-
},
|
|
569
|
-
}),
|
|
570
|
-
});
|
|
571
|
-
const r = await tools.get("grep")!.execute("t1", { pattern: "const" }, null, null, {});
|
|
572
|
-
expect(r.content[0].text).toBe("src/index.ts:42:const x = 1;\\nconst y = 2;");
|
|
573
|
-
expect(r.details.text.split("\n")).toHaveLength(1);
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
it("literal=true → mode=plain", async () => {
|
|
577
|
-
const grep = vi.fn().mockReturnValue({ ok: true, value: { items: [], totalMatched: 0, nextCursor: null } });
|
|
578
|
-
await loadWithFFF({ grep });
|
|
579
|
-
await tools.get("grep")!.execute("t1", { pattern: "foo", literal: true }, null, null, {});
|
|
580
|
-
expect(grep).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ mode: "plain" }));
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
it("no literal → mode=regex", async () => {
|
|
584
|
-
const grep = vi.fn().mockReturnValue({ ok: true, value: { items: [], totalMatched: 0, nextCursor: null } });
|
|
585
|
-
await loadWithFFF({ grep });
|
|
586
|
-
await tools.get("grep")!.execute("t1", { pattern: "foo.*bar" }, null, null, {});
|
|
587
|
-
expect(grep).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ mode: "regex" }));
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
it("glob constraints bypass FFF to avoid native Unicode path panic", async () => {
|
|
591
|
-
const grep = vi.fn().mockReturnValue({ ok: true, value: { items: [], totalMatched: 0, nextCursor: null } });
|
|
592
|
-
await loadWithFFF({ grep });
|
|
593
|
-
await tools.get("grep")!.execute("t1", { pattern: "TODO", glob: "*.ts" }, null, null, {});
|
|
594
|
-
expect(grep).not.toHaveBeenCalled();
|
|
595
|
-
expect(grepExec).toHaveBeenCalledOnce();
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
it("path constraints bypass FFF to avoid native Unicode path panic", async () => {
|
|
599
|
-
const grep = vi.fn().mockReturnValue({ ok: true, value: { items: [], totalMatched: 0, nextCursor: null } });
|
|
600
|
-
await loadWithFFF({ grep });
|
|
601
|
-
await tools.get("grep")!.execute("t1", { pattern: "TODO", path: "file_reviewapp/static/app.js" }, null, null, {});
|
|
602
|
-
expect(grep).not.toHaveBeenCalled();
|
|
603
|
-
expect(grepExec).toHaveBeenCalledOnce();
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
it("falls back to SDK on throw", async () => {
|
|
607
|
-
await loadWithFFF({ grep: vi.fn().mockImplementation(() => { throw new Error("crash"); }) });
|
|
608
|
-
const r = await tools.get("grep")!.execute("t1", { pattern: "TODO" }, null, null, {});
|
|
609
|
-
expect(grepExec).toHaveBeenCalledOnce();
|
|
610
|
-
expect(r.details._type).toBe("grepResult");
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
it("cursor notice when nextCursor present", async () => {
|
|
614
|
-
await loadWithFFF({
|
|
615
|
-
grep: vi.fn().mockReturnValue({
|
|
616
|
-
ok: true,
|
|
617
|
-
value: { items: [{ relativePath: "a.ts", lineNumber: 1, lineContent: "hit" }], totalMatched: 1, nextCursor: { p: 2 } },
|
|
618
|
-
}),
|
|
619
|
-
});
|
|
620
|
-
const text = (await tools.get("grep")!.execute("t1", { pattern: "hit" }, null, null, {})).content[0].text;
|
|
621
|
-
expect(text).toContain("More results available");
|
|
622
|
-
expect(text).toMatch(/cursor="fff_c\d+"/);
|
|
623
|
-
});
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
// ---- multi_grep (FFF only) -----------------------------------------
|
|
627
|
-
|
|
628
|
-
describe("multi_grep", () => {
|
|
629
|
-
it("error for empty patterns", async () => {
|
|
630
|
-
await loadWithFFF();
|
|
631
|
-
const r = await tools.get("multi_grep")!.execute("t1", { patterns: [] }, null, null, null);
|
|
632
|
-
expect(r.content[0].text).toContain("patterns array must have at least 1 element");
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
it("falls back to SDK when FFF not initialized (no session_start)", async () => {
|
|
636
|
-
load(true);
|
|
637
|
-
const r = await tools.get("multi_grep")!.execute("t1", { patterns: ["foo"] }, null, null, null);
|
|
638
|
-
expect(grepExec).toHaveBeenCalledOnce();
|
|
639
|
-
expect(r.details._type).toBe("grepResult");
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
it("returns multiGrep results", async () => {
|
|
643
|
-
await loadWithFFF();
|
|
644
|
-
const r = await tools.get("multi_grep")!.execute("t1", { patterns: ["foo", "bar"] }, null, null, null);
|
|
645
|
-
expect(r.details._type).toBe("grepResult");
|
|
646
|
-
expect(r.content[0].text).toContain("src/index.ts");
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
it("aborted signal → Aborted", async () => {
|
|
650
|
-
await loadWithFFF();
|
|
651
|
-
const r = await tools.get("multi_grep")!.execute("t1", { patterns: ["x"] }, { aborted: true }, null, null);
|
|
652
|
-
expect(r.content[0].text).toBe("Aborted");
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
it("multiGrep failure → error text", async () => {
|
|
656
|
-
await loadWithFFF({
|
|
657
|
-
multiGrep: vi.fn().mockReturnValue({ ok: false, error: "compile failed" }),
|
|
658
|
-
});
|
|
659
|
-
const r = await tools.get("multi_grep")!.execute("t1", { patterns: ["[bad"] }, null, null, null);
|
|
660
|
-
expect(r.content[0].text).toContain("compile failed");
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
it("passes context to unconstrained FFF multiGrep", async () => {
|
|
664
|
-
const multiGrep = vi.fn().mockReturnValue({ ok: true, value: { items: [], totalMatched: 0, nextCursor: null } });
|
|
665
|
-
await loadWithFFF({ multiGrep });
|
|
666
|
-
await tools.get("multi_grep")!.execute("t1", { patterns: ["a", "b"], context: 2 }, null, null, null);
|
|
667
|
-
expect(multiGrep).toHaveBeenCalledWith(expect.objectContaining({
|
|
668
|
-
patterns: ["a", "b"], beforeContext: 2, afterContext: 2,
|
|
669
|
-
}));
|
|
670
|
-
expect(multiGrep.mock.calls[0][0]).not.toHaveProperty("constraints");
|
|
671
|
-
});
|
|
672
|
-
|
|
673
|
-
it("glob constraints bypass FFF multiGrep and use ripgrep fallback", async () => {
|
|
674
|
-
const multiGrep = vi.fn().mockReturnValue({ ok: true, value: { items: [], totalMatched: 0, nextCursor: null } });
|
|
675
|
-
await loadWithFFF({ multiGrep });
|
|
676
|
-
await tools.get("multi_grep")!.execute("t1", { patterns: ["a", "b"], constraints: "*.ts", context: 2 }, null, null, {});
|
|
677
|
-
expect(multiGrep).not.toHaveBeenCalled();
|
|
678
|
-
expect(grepExec).not.toHaveBeenCalled();
|
|
679
|
-
expect(multiGrepRgExec).toHaveBeenCalledWith(expect.objectContaining({
|
|
680
|
-
patterns: ["a", "b"], constraints: "*.ts", context: 2, ignoreCase: true,
|
|
681
|
-
}));
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
it("path and constraints together bypass FFF multiGrep", async () => {
|
|
685
|
-
const multiGrep = vi.fn().mockReturnValue({ ok: true, value: { items: [], totalMatched: 0, nextCursor: null } });
|
|
686
|
-
await loadWithFFF({ multiGrep });
|
|
687
|
-
await tools.get("multi_grep")!.execute(
|
|
688
|
-
"t1",
|
|
689
|
-
{ patterns: ["a", "b"], path: "src", constraints: "*.ts" },
|
|
690
|
-
null,
|
|
691
|
-
null,
|
|
692
|
-
{},
|
|
693
|
-
);
|
|
694
|
-
expect(multiGrep).not.toHaveBeenCalled();
|
|
695
|
-
expect(grepExec).not.toHaveBeenCalled();
|
|
696
|
-
expect(multiGrepRgExec).toHaveBeenCalledWith(expect.objectContaining({
|
|
697
|
-
patterns: ["a", "b"], path: "src", constraints: "*.ts",
|
|
698
|
-
}));
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
it("falls back to SDK when path is provided", async () => {
|
|
702
|
-
const multiGrep = vi.fn().mockReturnValue({ ok: true, value: { items: [], totalMatched: 0, nextCursor: null } });
|
|
703
|
-
await loadWithFFF({ multiGrep });
|
|
704
|
-
await tools.get("multi_grep")!.execute("t1", { patterns: ["foo", "bar"], path: "src" }, null, null, {});
|
|
705
|
-
expect(multiGrep).not.toHaveBeenCalled();
|
|
706
|
-
expect(grepExec).toHaveBeenCalledWith(
|
|
707
|
-
"t1",
|
|
708
|
-
expect.objectContaining({ pattern: "foo|bar", path: "src", ignoreCase: true }),
|
|
709
|
-
null,
|
|
710
|
-
null,
|
|
711
|
-
{},
|
|
712
|
-
);
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
it("uses path-backed ripgrep fallback for simple path constraints", async () => {
|
|
716
|
-
const multiGrep = vi.fn().mockReturnValue({ ok: true, value: { items: [], totalMatched: 0, nextCursor: null } });
|
|
717
|
-
await loadWithFFF({ multiGrep });
|
|
718
|
-
await tools.get("multi_grep")!.execute("t1", { patterns: ["foo", "bar"], constraints: "src" }, null, null, {});
|
|
719
|
-
expect(multiGrep).not.toHaveBeenCalled();
|
|
720
|
-
expect(grepExec).not.toHaveBeenCalled();
|
|
721
|
-
expect(multiGrepRgExec).toHaveBeenCalledWith(expect.objectContaining({
|
|
722
|
-
patterns: ["foo", "bar"], path: "src", constraints: undefined,
|
|
723
|
-
}));
|
|
724
|
-
});
|
|
725
|
-
|
|
726
|
-
it("preserves complex constraints in ripgrep fallback", async () => {
|
|
727
|
-
await loadWithFFF();
|
|
728
|
-
const result = await tools.get("multi_grep")!.execute(
|
|
729
|
-
"t1",
|
|
730
|
-
{ patterns: ["foo", "bar"], path: "src", constraints: "*.ts !test/" },
|
|
731
|
-
null,
|
|
732
|
-
null,
|
|
733
|
-
{},
|
|
734
|
-
);
|
|
735
|
-
expect(grepExec).not.toHaveBeenCalled();
|
|
736
|
-
expect(multiGrepRgExec).toHaveBeenCalledWith(expect.objectContaining({
|
|
737
|
-
patterns: ["foo", "bar"], path: "src", constraints: "*.ts !test/",
|
|
738
|
-
}));
|
|
739
|
-
expect(result.content[0].text).not.toContain("ignored unsupported constraints");
|
|
740
|
-
});
|
|
741
|
-
|
|
742
|
-
it("uses case-sensitive SDK fallback when any pattern contains uppercase", async () => {
|
|
743
|
-
await loadWithFFF();
|
|
744
|
-
await tools.get("multi_grep")!.execute("t1", { patterns: ["foo", "Bar"], path: "src" }, null, null, {});
|
|
745
|
-
expect(grepExec).toHaveBeenCalledWith(
|
|
746
|
-
"t1",
|
|
747
|
-
expect.objectContaining({ pattern: "foo|Bar", ignoreCase: false }),
|
|
748
|
-
null,
|
|
749
|
-
null,
|
|
750
|
-
{},
|
|
751
|
-
);
|
|
752
|
-
});
|
|
753
|
-
});
|
|
754
|
-
|
|
755
|
-
// ---- session lifecycle ---------------------------------------------
|
|
756
|
-
|
|
757
|
-
describe("session lifecycle", () => {
|
|
758
|
-
it("stores FFF data under a pi-pretty-specific directory", async () => {
|
|
759
|
-
const create = vi.fn().mockReturnValue({ ok: true, value: mkFinder() });
|
|
760
|
-
load(true, { FileFinder: { create } });
|
|
761
|
-
const start = events.get("session_start")!;
|
|
762
|
-
expect(start, "session_start not registered").toBeDefined();
|
|
763
|
-
await start({}, { cwd: "/tmp/test" });
|
|
764
|
-
expect(create).toHaveBeenCalledWith(expect.objectContaining({
|
|
765
|
-
frecencyDbPath: "/tmp/pi-pretty-test/pi-pretty/fff/frecency.mdb",
|
|
766
|
-
historyDbPath: "/tmp/pi-pretty-test/pi-pretty/fff/history.mdb",
|
|
767
|
-
}));
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
it("delayed FFF status clear does not read a stale session ctx", async () => {
|
|
771
|
-
vi.useFakeTimers();
|
|
772
|
-
const setStatus = vi.fn();
|
|
773
|
-
let stale = false;
|
|
774
|
-
const ctx = {
|
|
775
|
-
cwd: "/tmp/test",
|
|
776
|
-
get ui() {
|
|
777
|
-
if (stale) throw new Error("stale ctx");
|
|
778
|
-
return { setStatus };
|
|
779
|
-
},
|
|
780
|
-
};
|
|
781
|
-
|
|
782
|
-
load(true);
|
|
783
|
-
const start = events.get("session_start")!;
|
|
784
|
-
await start({}, ctx);
|
|
785
|
-
stale = true;
|
|
786
|
-
|
|
787
|
-
vi.advanceTimersByTime(3000);
|
|
788
|
-
|
|
789
|
-
expect(setStatus).toHaveBeenNthCalledWith(1, "fff", "FFF indexed");
|
|
790
|
-
expect(setStatus).toHaveBeenNthCalledWith(2, "fff", undefined);
|
|
791
|
-
});
|
|
792
|
-
|
|
793
|
-
it("shutdown → subsequent find falls back to SDK", async () => {
|
|
794
|
-
await loadWithFFF();
|
|
795
|
-
await events.get("session_shutdown")!();
|
|
796
|
-
await tools.get("find")!.execute("t1", { pattern: "*.ts" }, null, null, {});
|
|
797
|
-
expect(findExec).toHaveBeenCalledOnce();
|
|
798
|
-
});
|
|
799
|
-
});
|
|
800
|
-
});
|