@ff-labs/fff-bun 0.5.2 → 0.5.3-nightly.205f9d6
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/package.json +9 -9
- package/src/git-lifecycle.test.ts +273 -201
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ff-labs/fff-bun",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3-nightly.205f9d6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "High-performance fuzzy file finder for Bun - perfect for LLM agent tools",
|
|
6
6
|
"type": "module",
|
|
@@ -62,14 +62,14 @@
|
|
|
62
62
|
},
|
|
63
63
|
"homepage": "https://github.com/dmtrKovalenko/fff.nvim#readme",
|
|
64
64
|
"optionalDependencies": {
|
|
65
|
-
"@ff-labs/fff-bin-darwin-arm64": "0.5.
|
|
66
|
-
"@ff-labs/fff-bin-darwin-x64": "0.5.
|
|
67
|
-
"@ff-labs/fff-bin-linux-x64-gnu": "0.5.
|
|
68
|
-
"@ff-labs/fff-bin-linux-arm64-gnu": "0.5.
|
|
69
|
-
"@ff-labs/fff-bin-linux-x64-musl": "0.5.
|
|
70
|
-
"@ff-labs/fff-bin-linux-arm64-musl": "0.5.
|
|
71
|
-
"@ff-labs/fff-bin-win32-x64": "0.5.
|
|
72
|
-
"@ff-labs/fff-bin-win32-arm64": "0.5.
|
|
65
|
+
"@ff-labs/fff-bin-darwin-arm64": "0.5.3-nightly.205f9d6",
|
|
66
|
+
"@ff-labs/fff-bin-darwin-x64": "0.5.3-nightly.205f9d6",
|
|
67
|
+
"@ff-labs/fff-bin-linux-x64-gnu": "0.5.3-nightly.205f9d6",
|
|
68
|
+
"@ff-labs/fff-bin-linux-arm64-gnu": "0.5.3-nightly.205f9d6",
|
|
69
|
+
"@ff-labs/fff-bin-linux-x64-musl": "0.5.3-nightly.205f9d6",
|
|
70
|
+
"@ff-labs/fff-bin-linux-arm64-musl": "0.5.3-nightly.205f9d6",
|
|
71
|
+
"@ff-labs/fff-bin-win32-x64": "0.5.3-nightly.205f9d6",
|
|
72
|
+
"@ff-labs/fff-bin-win32-arm64": "0.5.3-nightly.205f9d6"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"@types/bun": "^1.3.8",
|
|
@@ -86,7 +86,10 @@ async function waitForFileStatus(
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/** Poll until a file is gone from the index, or the timeout is exceeded. */
|
|
89
|
-
async function waitForFileGone(
|
|
89
|
+
async function waitForFileGone(
|
|
90
|
+
finder: FileFinder,
|
|
91
|
+
name: string,
|
|
92
|
+
): Promise<boolean> {
|
|
90
93
|
const start = Date.now();
|
|
91
94
|
while (Date.now() - start < WATCHER_TIMEOUT_MS) {
|
|
92
95
|
if (findFile(finder, name) === undefined) return true;
|
|
@@ -96,7 +99,10 @@ async function waitForFileGone(finder: FileFinder, name: string): Promise<boolea
|
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
/** Poll until the total file count reaches the expected value, or the timeout is exceeded. */
|
|
99
|
-
async function waitForFileCount(
|
|
102
|
+
async function waitForFileCount(
|
|
103
|
+
finder: FileFinder,
|
|
104
|
+
count: number,
|
|
105
|
+
): Promise<number> {
|
|
100
106
|
const start = Date.now();
|
|
101
107
|
while (Date.now() - start < WATCHER_TIMEOUT_MS) {
|
|
102
108
|
const result = finder.fileSearch("", { pageSize: 200 });
|
|
@@ -123,203 +129,269 @@ async function waitForGrep(
|
|
|
123
129
|
return finder.grep(pattern, options);
|
|
124
130
|
}
|
|
125
131
|
|
|
126
|
-
describe.skipIf(process.platform === "win32")(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
132
|
+
describe.skipIf(process.platform === "win32")(
|
|
133
|
+
"Git lifecycle integration",
|
|
134
|
+
() => {
|
|
135
|
+
let tmpDir: string;
|
|
136
|
+
let finder: FileFinder;
|
|
137
|
+
|
|
138
|
+
beforeAll(async () => {
|
|
139
|
+
// Create temp directory and initialise a git repo with two committed files.
|
|
140
|
+
// Use realpathSync to resolve symlinks (macOS /var -> /private/var) so
|
|
141
|
+
// that git2's resolved workdir paths match the file picker's base_path.
|
|
142
|
+
tmpDir = realpathSync(mkdtempSync(join(tmpdir(), "fff-git-test-")));
|
|
143
|
+
|
|
144
|
+
git(tmpDir, "init", "-b", "main");
|
|
145
|
+
// Need at least one commit for status to work properly
|
|
146
|
+
writeFileSync(join(tmpDir, "hello.txt"), "hello world\n");
|
|
147
|
+
writeFileSync(join(tmpDir, "readme.md"), "# Test Project\n");
|
|
148
|
+
mkdirSync(join(tmpDir, "src"));
|
|
149
|
+
writeFileSync(
|
|
150
|
+
join(tmpDir, "src", "main.rs"),
|
|
151
|
+
'fn main() { println?."hi"); }\n',
|
|
152
|
+
);
|
|
153
|
+
git(tmpDir, "add", "-A");
|
|
154
|
+
git(tmpDir, "commit", "-m", "initial commit");
|
|
155
|
+
|
|
156
|
+
// Create the FileFinder instance
|
|
157
|
+
const result = FileFinder.create({ basePath: tmpDir });
|
|
158
|
+
expect(result.ok).toBe(true);
|
|
159
|
+
if (!result.ok) throw new Error(result.error);
|
|
160
|
+
finder = result.value;
|
|
161
|
+
|
|
162
|
+
// Wait for the initial scan to finish
|
|
163
|
+
const scanResult = finder.waitForScan(10_000);
|
|
164
|
+
expect(scanResult.ok).toBe(true);
|
|
165
|
+
|
|
166
|
+
// Poll getScanProgress until the watcher is ready so that
|
|
167
|
+
// filesystem events (file creates, deletes) are detected.
|
|
168
|
+
const start = Date.now();
|
|
169
|
+
while (Date.now() - start < WATCHER_TIMEOUT_MS) {
|
|
170
|
+
const progress = finder.getScanProgress();
|
|
171
|
+
if (progress.ok && progress.value.isWatcherReady) break;
|
|
172
|
+
await sleep(POLL_INTERVAL_MS);
|
|
173
|
+
}
|
|
159
174
|
const progress = finder.getScanProgress();
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
175
|
+
expect(progress.ok).toBe(true);
|
|
176
|
+
if (progress.ok) {
|
|
177
|
+
expect(progress.value.isWatcherReady).toBe(true);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
afterAll(() => {
|
|
182
|
+
finder?.destroy();
|
|
183
|
+
if (tmpDir) {
|
|
184
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("initial scan indexes all committed files", () => {
|
|
189
|
+
const result = finder.fileSearch("", { pageSize: 200 });
|
|
190
|
+
expect(result.ok).toBe(true);
|
|
191
|
+
if (!result.ok) return;
|
|
192
|
+
|
|
193
|
+
const names = result.value.items.map((i) => i.relativePath).sort();
|
|
194
|
+
expect(names).toContain("hello.txt");
|
|
195
|
+
expect(names).toContain("readme.md");
|
|
196
|
+
expect(names).toContain("src/main.rs");
|
|
197
|
+
expect(result.value.totalFiles).toBe(3);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("committed files have clean git status", async () => {
|
|
201
|
+
const hello = await waitForFileStatus(finder, "hello.txt", "clean");
|
|
202
|
+
expect(hello).toBeDefined();
|
|
203
|
+
expect(hello?.gitStatus).toBe("clean");
|
|
204
|
+
|
|
205
|
+
const main = await waitForFileStatus(finder, "main.rs", "clean");
|
|
206
|
+
expect(main).toBeDefined();
|
|
207
|
+
expect(main?.gitStatus).toBe("clean");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("new untracked file appears with 'untracked' status", async () => {
|
|
211
|
+
writeFileSync(join(tmpDir, "new_file.ts"), "export const x = 1;\n");
|
|
212
|
+
|
|
213
|
+
const newFile = await waitForFileStatus(
|
|
214
|
+
finder,
|
|
215
|
+
"new_file.ts",
|
|
216
|
+
"untracked",
|
|
217
|
+
);
|
|
218
|
+
expect(newFile).toBeDefined();
|
|
219
|
+
expect(newFile?.gitStatus).toBe("untracked");
|
|
220
|
+
|
|
221
|
+
// Total should now be 4
|
|
222
|
+
const total = await waitForFileCount(finder, 4);
|
|
223
|
+
expect(total).toBe(4);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("staging a new file changes status to 'staged_new'", async () => {
|
|
227
|
+
git(tmpDir, "add", "new_file.ts");
|
|
228
|
+
|
|
229
|
+
const newFile = await waitForFileStatus(
|
|
230
|
+
finder,
|
|
231
|
+
"new_file.ts",
|
|
232
|
+
"staged_new",
|
|
233
|
+
);
|
|
234
|
+
expect(newFile).toBeDefined();
|
|
235
|
+
expect(newFile?.gitStatus).toBe("staged_new");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("committing makes the file 'clean'", async () => {
|
|
239
|
+
git(tmpDir, "commit", "-m", "add new_file");
|
|
240
|
+
|
|
241
|
+
const newFile = await waitForFileStatus(finder, "new_file.ts", "clean");
|
|
242
|
+
expect(newFile).toBeDefined();
|
|
243
|
+
expect(newFile?.gitStatus).toBe("clean");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("modifying a tracked file changes status to 'modified'", async () => {
|
|
247
|
+
writeFileSync(
|
|
248
|
+
join(tmpDir, "hello.txt"),
|
|
249
|
+
"hello world\nupdated content\n",
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const hello = await waitForFileStatus(finder, "hello.txt", "modified");
|
|
253
|
+
expect(hello).toBeDefined();
|
|
254
|
+
expect(hello?.gitStatus).toBe("modified");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("staging a modification changes status to 'staged_modified'", async () => {
|
|
258
|
+
git(tmpDir, "add", "hello.txt");
|
|
259
|
+
|
|
260
|
+
const hello = await waitForFileStatus(
|
|
261
|
+
finder,
|
|
262
|
+
"hello.txt",
|
|
263
|
+
"staged_modified",
|
|
264
|
+
);
|
|
265
|
+
expect(hello).toBeDefined();
|
|
266
|
+
expect(hello?.gitStatus).toBe("staged_modified");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("committing the modification returns to 'clean'", async () => {
|
|
270
|
+
git(tmpDir, "commit", "-m", "update hello");
|
|
271
|
+
|
|
272
|
+
const hello = await waitForFileStatus(finder, "hello.txt", "clean");
|
|
273
|
+
expect(hello).toBeDefined();
|
|
274
|
+
expect(hello?.gitStatus).toBe("clean");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("deleting a file removes it from the index", async () => {
|
|
278
|
+
unlinkSync(join(tmpDir, "new_file.ts"));
|
|
279
|
+
|
|
280
|
+
const gone = await waitForFileGone(finder, "new_file.ts");
|
|
281
|
+
expect(gone).toBe(true);
|
|
282
|
+
|
|
283
|
+
// Total should be back to 3
|
|
284
|
+
const total = await waitForFileCount(finder, 3);
|
|
285
|
+
expect(total).toBe(3);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("adding a file in a subdirectory works", async () => {
|
|
289
|
+
writeFileSync(join(tmpDir, "src", "utils.rs"), "pub fn helper() {}\n");
|
|
290
|
+
|
|
291
|
+
const utils = await waitForFileStatus(finder, "utils.rs", "untracked");
|
|
292
|
+
expect(utils).toBeDefined();
|
|
293
|
+
expect(utils?.relativePath).toBe("src/utils.rs");
|
|
294
|
+
expect(utils?.gitStatus).toBe("untracked");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("live grep finds content in a newly added file", async () => {
|
|
298
|
+
writeFileSync(
|
|
299
|
+
join(tmpDir, "src", "searchtarget.rs"),
|
|
300
|
+
'const UNIQUE_NEEDLE: &str = "xylophone_waterfall_97";\n',
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
await waitForFile(finder, "searchtarget.rs");
|
|
304
|
+
|
|
305
|
+
const result = await waitForGrep(
|
|
306
|
+
finder,
|
|
307
|
+
"xylophone_waterfall_97",
|
|
308
|
+
{ mode: "plain" },
|
|
309
|
+
(n) => n > 0,
|
|
310
|
+
);
|
|
311
|
+
expect(result?.ok).toBe(true);
|
|
312
|
+
if (!result?.ok) return;
|
|
313
|
+
|
|
314
|
+
expect(result.value.totalMatched).toBeGreaterThan(0);
|
|
315
|
+
const match = result.value.items.find(
|
|
316
|
+
(m) => m.relativePath === "src/searchtarget.rs",
|
|
317
|
+
);
|
|
318
|
+
expect(match).toBeDefined();
|
|
319
|
+
expect(match!.lineContent).toContain("xylophone_waterfall_97");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("live grep no longer finds content after file is deleted", async () => {
|
|
323
|
+
unlinkSync(join(tmpDir, "src", "searchtarget.rs"));
|
|
324
|
+
|
|
325
|
+
const result = await waitForGrep(
|
|
326
|
+
finder,
|
|
327
|
+
"xylophone_waterfall_97",
|
|
328
|
+
{ mode: "plain" },
|
|
329
|
+
(n) => n === 0,
|
|
330
|
+
);
|
|
331
|
+
expect(result?.ok).toBe(true);
|
|
332
|
+
if (!result?.ok) return;
|
|
333
|
+
|
|
334
|
+
expect(result.value.totalMatched).toBe(0);
|
|
335
|
+
expect(result.value.items.length).toBe(0);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("file in a newly created directory is discoverable", async () => {
|
|
339
|
+
// Create a brand-new directory that didn't exist during the initial scan,
|
|
340
|
+
// then add a file inside it. The watcher must dynamically pick up the new
|
|
341
|
+
// directory and index the file.
|
|
342
|
+
mkdirSync(join(tmpDir, "lib"));
|
|
343
|
+
writeFileSync(
|
|
344
|
+
join(tmpDir, "lib", "helpers.ts"),
|
|
345
|
+
"export function add(a: number, b: number) { return a + b; }\n",
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
const helpers = await waitForFile(finder, "helpers.ts");
|
|
349
|
+
expect(helpers).toBeDefined();
|
|
350
|
+
expect(helpers?.relativePath).toBe("lib/helpers.ts");
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("files in gitignored directories are not indexed", async () => {
|
|
354
|
+
// Commit a .gitignore rule first so it's established repo state before
|
|
355
|
+
// the ignored directory is created. This tests the watch-level filtering
|
|
356
|
+
// (is_path_ignored in the debouncer callback), not a rescan triggered
|
|
357
|
+
// by a .gitignore change.
|
|
358
|
+
writeFileSync(join(tmpDir, ".gitignore"), "build_output/\n");
|
|
359
|
+
git(tmpDir, "add", ".gitignore");
|
|
360
|
+
git(tmpDir, "commit", "-m", "add gitignore");
|
|
361
|
+
|
|
362
|
+
// Wait for the watcher to settle after the commit.
|
|
363
|
+
await waitForFile(finder, ".gitignore");
|
|
364
|
+
|
|
365
|
+
// Now create the ignored directory and add a file inside it.
|
|
366
|
+
mkdirSync(join(tmpDir, "build_output"));
|
|
367
|
+
writeFileSync(
|
|
368
|
+
join(tmpDir, "build_output", "artifact.bin"),
|
|
369
|
+
"should not appear\n",
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
// Create a non-ignored file as a synchronisation barrier — once it's
|
|
373
|
+
// indexed, the watcher has processed the same batch of events.
|
|
374
|
+
writeFileSync(join(tmpDir, "canary.txt"), "visible\n");
|
|
375
|
+
const canary = await waitForFile(finder, "canary.txt");
|
|
376
|
+
expect(canary).toBeDefined();
|
|
377
|
+
|
|
378
|
+
// The ignored file must NOT appear in the index.
|
|
379
|
+
const artifact = findFile(finder, "artifact.bin");
|
|
380
|
+
expect(artifact).toBeUndefined();
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("full add-commit cycle for subdirectory file", async () => {
|
|
384
|
+
git(tmpDir, "add", "src/utils.rs");
|
|
385
|
+
|
|
386
|
+
let utils = await waitForFileStatus(finder, "utils.rs", "staged_new");
|
|
387
|
+
expect(utils).toBeDefined();
|
|
388
|
+
expect(utils?.gitStatus).toBe("staged_new");
|
|
389
|
+
|
|
390
|
+
git(tmpDir, "commit", "-m", "add utils");
|
|
391
|
+
|
|
392
|
+
utils = await waitForFileStatus(finder, "utils.rs", "clean");
|
|
393
|
+
expect(utils).toBeDefined();
|
|
394
|
+
expect(utils?.gitStatus).toBe("clean");
|
|
395
|
+
});
|
|
396
|
+
},
|
|
397
|
+
);
|