1b279d4d10
Lua CI / lua-language-server type check (push) Failing after 1s
Lua CI / luacheck lint (push) Failing after 1s
Python CI / Python bindings (ubuntu-latest) (push) Failing after 1s
Build & Publish / Build Neovim aarch64-linux-android (push) Failing after 3s
Build & Publish / Build Neovim aarch64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build Neovim aarch64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build Neovim x86_64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build Neovim x86_64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build C FFI aarch64-linux-android (push) Failing after 1s
Build & Publish / Build C FFI aarch64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build C FFI aarch64-unknown-linux-musl (push) Failing after 0s
Build & Publish / Build C FFI x86_64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build C FFI x86_64-unknown-linux-musl (push) Failing after 3s
Build & Publish / Build MCP aarch64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build MCP aarch64-unknown-linux-musl (push) Failing after 0s
Build & Publish / Build MCP x86_64-unknown-linux-gnu (push) Failing after 2s
Rust CI / Fuzz Tests (ubuntu-latest) (push) Failing after 1s
Rust CI / Build i686-unknown-linux-gnu (push) Failing after 1s
Rust CI / cargo fmt (push) Failing after 1s
Build & Publish / Build MCP x86_64-unknown-linux-musl (push) Failing after 4s
Build & Publish / Build Python wheels aarch64 (ubuntu-latest) (push) Failing after 1s
Build & Publish / Build Python wheels x86_64 (ubuntu-latest) (push) Failing after 1s
Build & Publish / Build Python sdist (push) Failing after 0s
Rust CI / Test (ubuntu-latest) (push) Failing after 1s
Rust CI / cargo clippy (push) Failing after 1s
Spelling / Spell Check with Typos (push) Failing after 1s
Stylua / Check lua files using Stylua (push) Failing after 1s
e2e Tests / e2e (ubuntu-latest) (push) Failing after 4s
e2e Tests / e2e (alpine-musl) (push) Successful in 58m54s
Build & Publish / Build C FFI aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build C FFI aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build C FFI x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build C FFI x86_64-pc-windows-msvc (push) Has been cancelled
e2e Tests / e2e (macos-latest) (push) Has been cancelled
e2e Tests / e2e (windows-latest) (push) Has been cancelled
Nix CI / check (push) Has been cancelled
Python CI / Python bindings (macos-latest) (push) Has been cancelled
Python CI / Python bindings (windows-latest) (push) Has been cancelled
Build & Publish / Build Neovim aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build Neovim aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Neovim x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build Neovim x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build MCP aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build MCP aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build MCP x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build MCP x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Python wheels aarch64 (macos-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (macos-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (windows-latest) (push) Has been cancelled
Build & Publish / Release (push) Has been cancelled
Build & Publish / Publish Python wheels to PyPI (push) Has been cancelled
Build & Publish / Publish Rust crates (push) Has been cancelled
Build & Publish / Publish npm packages (push) Has been cancelled
Rust CI / Test (macos-latest) (push) Has been cancelled
Rust CI / Test (windows-latest) (push) Has been cancelled
Rust CI / Fuzz Tests (macos-latest) (push) Has been cancelled
Rust CI / Fuzz Tests (windows-latest) (push) Has been cancelled
504 lines
16 KiB
TypeScript
504 lines
16 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { findBinary } from "../src/download";
|
|
import { FileFinder } from "../src/index";
|
|
import { getLibExtension, getLibFilename, getTriple } from "../src/platform";
|
|
|
|
// Cross-platform path normalization helpers
|
|
const normalizePath = (path: string | null | undefined): string | null => {
|
|
if (!path) return null;
|
|
// Convert backslashes to forward slashes for consistent comparison
|
|
return path.replace(/\\/g, "/");
|
|
};
|
|
|
|
const testDir = process.cwd();
|
|
|
|
describe("Platform Detection", () => {
|
|
test("getTriple returns valid triple", () => {
|
|
const triple = getTriple();
|
|
expect(triple).toMatch(
|
|
/^(x86_64|aarch64|arm)-(apple-darwin|unknown-linux-(gnu|musl)|pc-windows-msvc)$/,
|
|
);
|
|
});
|
|
|
|
test("getLibExtension returns correct extension", () => {
|
|
const ext = getLibExtension();
|
|
const platform = process.platform;
|
|
|
|
if (platform === "darwin") {
|
|
expect(ext).toBe("dylib");
|
|
} else if (platform === "win32") {
|
|
expect(ext).toBe("dll");
|
|
} else {
|
|
expect(ext).toBe("so");
|
|
}
|
|
});
|
|
|
|
test("getLibFilename returns correct filename", () => {
|
|
const filename = getLibFilename();
|
|
const ext = getLibExtension();
|
|
|
|
if (process.platform === "win32") {
|
|
expect(filename).toBe(`fff_c.${ext}`);
|
|
} else {
|
|
expect(filename).toBe(`libfff_c.${ext}`);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("Binary Detection", () => {
|
|
test("findBinary returns a path", () => {
|
|
const path = findBinary();
|
|
expect(path).not.toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("FileFinder - Health Check", () => {
|
|
test("healthCheckStatic works without an instance", () => {
|
|
const result = FileFinder.healthCheckStatic();
|
|
expect(result.ok).toBe(true);
|
|
|
|
if (result.ok) {
|
|
expect(result.value.version).toBeDefined();
|
|
expect(result.value.git.available).toBe(true);
|
|
expect(result.value.filePicker.initialized).toBe(false);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("FileFinder - Full Lifecycle", () => {
|
|
let finder: FileFinder;
|
|
|
|
beforeAll(() => {
|
|
const result = FileFinder.create({ basePath: testDir });
|
|
expect(result.ok).toBe(true);
|
|
if (result.ok) {
|
|
finder = result.value;
|
|
}
|
|
});
|
|
|
|
afterAll(() => {
|
|
finder?.destroy();
|
|
});
|
|
|
|
test("create succeeds with valid path", () => {
|
|
expect(finder).toBeDefined();
|
|
expect(finder.isDestroyed).toBe(false);
|
|
});
|
|
|
|
test("isScanning returns a boolean", () => {
|
|
const scanning = finder.isScanning();
|
|
expect(typeof scanning).toBe("boolean");
|
|
});
|
|
|
|
test("getScanProgress returns valid data", () => {
|
|
const result = finder.getScanProgress();
|
|
expect(result.ok).toBe(true);
|
|
|
|
if (result.ok) {
|
|
expect(typeof result.value.scannedFilesCount).toBe("number");
|
|
expect(typeof result.value.isScanning).toBe("boolean");
|
|
}
|
|
});
|
|
|
|
test("waitForScanBlocking completes", () => {
|
|
// Small timeout - scan should be fast or already done
|
|
const result = finder.waitForScanBlocking(500);
|
|
expect(result.ok).toBe(true);
|
|
});
|
|
|
|
test("search with empty query returns all files", () => {
|
|
// First check scan progress to see if files were indexed
|
|
const progress = finder.getScanProgress();
|
|
if (progress.ok) {
|
|
}
|
|
|
|
const result = finder.fileSearch("");
|
|
expect(result.ok).toBe(true);
|
|
|
|
if (result.ok) {
|
|
if (result.value.items.length > 0) {
|
|
// Log first few paths to see format on Windows
|
|
// Items are strings (file paths), not objects
|
|
const _samplePaths = result.value.items
|
|
.slice(0, 3)
|
|
.map((item) =>
|
|
normalizePath(typeof item === "string" ? item : item.relativePath),
|
|
);
|
|
}
|
|
// Empty query should return files (frecency-sorted)
|
|
expect(result.value.totalFiles).toBeGreaterThan(0);
|
|
} else {
|
|
}
|
|
});
|
|
|
|
test("search returns a valid result structure", () => {
|
|
const result = finder.fileSearch("Cargo.toml");
|
|
expect(result.ok).toBe(true);
|
|
|
|
if (result.ok) {
|
|
expect(typeof result.value.totalMatched).toBe("number");
|
|
expect(typeof result.value.totalFiles).toBe("number");
|
|
expect(Array.isArray(result.value.items)).toBe(true);
|
|
expect(Array.isArray(result.value.scores)).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("search returns empty for non-matching query", () => {
|
|
const result = finder.fileSearch("xyznonexistentfilenamexyz123456");
|
|
expect(result.ok).toBe(true);
|
|
|
|
if (result.ok) {
|
|
expect(result.value.totalMatched).toBe(0);
|
|
expect(result.value.items.length).toBe(0);
|
|
}
|
|
});
|
|
|
|
test("search respects pageSize option", () => {
|
|
const result = finder.fileSearch("ts", { pageSize: 3 });
|
|
expect(result.ok).toBe(true);
|
|
|
|
if (result.ok) {
|
|
expect(result.value.items.length).toBeLessThanOrEqual(3);
|
|
}
|
|
});
|
|
|
|
test("glob filters by extension via raw pattern", () => {
|
|
const result = finder.glob("**/*.ts", { pageSize: 50 });
|
|
expect(result.ok).toBe(true);
|
|
if (result.ok) {
|
|
expect(result.value.items.length).toBeGreaterThan(0);
|
|
for (const item of result.value.items) {
|
|
expect(item.relativePath.endsWith(".ts")).toBe(true);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("glob returns empty result for non-matching pattern", () => {
|
|
const result = finder.glob("**/this-extension-does-not-exist-anywhere.zzz");
|
|
expect(result.ok).toBe(true);
|
|
if (result.ok) {
|
|
expect(result.value.items.length).toBe(0);
|
|
}
|
|
});
|
|
|
|
test("glob rejects empty pattern", () => {
|
|
const result = finder.glob("");
|
|
expect(result.ok).toBe(false);
|
|
});
|
|
|
|
test("glob respects pageSize", () => {
|
|
const result = finder.glob("**/*.ts", { pageSize: 2 });
|
|
expect(result.ok).toBe(true);
|
|
if (result.ok) {
|
|
expect(result.value.items.length).toBeLessThanOrEqual(2);
|
|
}
|
|
});
|
|
|
|
test("glob pageIndex offsets results", () => {
|
|
// pageIndex is a raw item offset (not a page-count multiplier). Verify
|
|
// by skipping the first item and checking the second result begins
|
|
// where page0[1] left off.
|
|
const page0 = finder.glob("**/*.ts", { pageSize: 5, pageIndex: 0 });
|
|
const page1 = finder.glob("**/*.ts", { pageSize: 5, pageIndex: 1 });
|
|
expect(page0.ok).toBe(true);
|
|
expect(page1.ok).toBe(true);
|
|
if (
|
|
page0.ok &&
|
|
page1.ok &&
|
|
page0.value.items.length > 1 &&
|
|
page1.value.items.length > 0
|
|
) {
|
|
expect(page1.value.items[0]!.relativePath).toBe(page0.value.items[1]!.relativePath);
|
|
}
|
|
});
|
|
|
|
test("glob directory-prefix pattern matches only that subtree", () => {
|
|
const result = finder.glob("src/**/*.ts", { pageSize: 100 });
|
|
expect(result.ok).toBe(true);
|
|
if (result.ok) {
|
|
for (const item of result.value.items) {
|
|
expect(item.relativePath.startsWith("src/")).toBe(true);
|
|
expect(item.relativePath.endsWith(".ts")).toBe(true);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("glob result items carry expected fields", () => {
|
|
const result = finder.glob("**/*.ts", { pageSize: 1 });
|
|
expect(result.ok).toBe(true);
|
|
if (result.ok && result.value.items.length > 0) {
|
|
const item = result.value.items[0];
|
|
expect(typeof item.relativePath).toBe("string");
|
|
expect(typeof item.fileName).toBe("string");
|
|
expect(item.relativePath.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
test("glob literal extension pattern (no leading **) still filters", () => {
|
|
const result = finder.glob("*.ts", { pageSize: 100 });
|
|
expect(result.ok).toBe(true);
|
|
// Don't assert non-zero — depends on whether top-level .ts files exist.
|
|
// Just assert all returned items match.
|
|
if (result.ok) {
|
|
for (const item of result.value.items) {
|
|
expect(item.relativePath.endsWith(".ts")).toBe(true);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("grep plain text returns matching lines", () => {
|
|
const result = finder.grep("fff-core", {
|
|
mode: "plain",
|
|
});
|
|
expect(result.ok).toBe(true);
|
|
|
|
if (result.ok) {
|
|
if (result.value.items.length > 0) {
|
|
// Log sample match to verify content on Windows
|
|
const first = result.value.items[0];
|
|
const _normalizedPath = normalizePath(first.relativePath);
|
|
}
|
|
|
|
expect(result.value.totalMatched).toBeGreaterThan(0);
|
|
expect(result.value.items.length).toBeGreaterThan(0);
|
|
|
|
const first = result.value.items[0];
|
|
expect(typeof first.relativePath).toBe("string");
|
|
// Normalize path for cross-platform validation
|
|
const normalizedFirstPath = normalizePath(first.relativePath);
|
|
expect(normalizedFirstPath).toBeTruthy();
|
|
expect(typeof first.lineNumber).toBe("number");
|
|
expect(first.lineNumber).toBeGreaterThan(0);
|
|
expect(typeof first.lineContent).toBe("string");
|
|
expect(first.lineContent.toLowerCase()).toContain("fff-core");
|
|
expect(Array.isArray(first.matchRanges)).toBe(true);
|
|
expect(first.matchRanges.length).toBeGreaterThan(0);
|
|
|
|
expect(typeof result.value.totalFilesSearched).toBe("number");
|
|
expect(typeof result.value.totalFiles).toBe("number");
|
|
expect(typeof result.value.filteredFileCount).toBe("number");
|
|
} else {
|
|
}
|
|
});
|
|
|
|
test("grep respects pageSize option", () => {
|
|
// Cap to one match per file so pageSize bounds the total deterministically.
|
|
const unbounded = finder.grep("import", {
|
|
mode: "plain",
|
|
maxMatchesPerFile: 1,
|
|
});
|
|
expect(unbounded.ok).toBe(true);
|
|
if (!unbounded.ok) return;
|
|
expect(unbounded.value.items.length).toBeGreaterThan(2);
|
|
|
|
const limited = finder.grep("import", {
|
|
mode: "plain",
|
|
maxMatchesPerFile: 1,
|
|
pageSize: 2,
|
|
});
|
|
expect(limited.ok).toBe(true);
|
|
if (!limited.ok) return;
|
|
expect(limited.value.items.length).toBeLessThanOrEqual(2);
|
|
expect(limited.value.items.length).toBeLessThan(unbounded.value.items.length);
|
|
expect(limited.value.nextCursor).not.toBeNull();
|
|
});
|
|
|
|
test("grep fuzzy mode returns results with scores", () => {
|
|
// Intentional typo: "depdnency" instead of "dependency" to exercise fuzzy matching
|
|
const result = finder.grep("depdnency", {
|
|
mode: "fuzzy",
|
|
});
|
|
expect(result.ok).toBe(true);
|
|
|
|
if (result.ok) {
|
|
expect(result.value.totalMatched).toBeGreaterThan(0);
|
|
expect(result.value.items.length).toBeGreaterThan(0);
|
|
|
|
const first = result.value.items[0];
|
|
expect(typeof first.relativePath).toBe("string");
|
|
// Normalize path for cross-platform validation
|
|
const normalizedFirstPath = normalizePath(first.relativePath);
|
|
expect(normalizedFirstPath).toBeTruthy();
|
|
expect(typeof first.lineNumber).toBe("number");
|
|
expect(typeof first.lineContent).toBe("string");
|
|
// Fuzzy mode should produce a fuzzyScore on each match
|
|
expect(typeof first.fuzzyScore).toBe("number");
|
|
}
|
|
});
|
|
|
|
test("healthCheck shows initialized state", () => {
|
|
const result = finder.healthCheck();
|
|
expect(result.ok).toBe(true);
|
|
|
|
if (result.ok) {
|
|
expect(result.value.filePicker.initialized).toBe(true);
|
|
expect(result.value.filePicker.basePath).toBeDefined();
|
|
// Normalize basePath for cross-platform comparison
|
|
const normalizedBasePath = normalizePath(result.value.filePicker.basePath || "");
|
|
const normalizedTestDir = normalizePath(testDir);
|
|
expect(normalizedBasePath).toBe(normalizedTestDir);
|
|
expect(typeof result.value.filePicker.indexedFiles).toBe("number");
|
|
}
|
|
});
|
|
|
|
test("healthCheck detects git repository", () => {
|
|
const result = finder.healthCheck(testDir);
|
|
expect(result.ok).toBe(true);
|
|
|
|
if (result.ok) {
|
|
expect(result.value.git.available).toBe(true);
|
|
expect(typeof result.value.git.repositoryFound).toBe("boolean");
|
|
}
|
|
});
|
|
|
|
test("destroy and re-create works", () => {
|
|
finder.destroy();
|
|
expect(finder.isDestroyed).toBe(true);
|
|
|
|
const result = FileFinder.create({ basePath: testDir });
|
|
expect(result.ok).toBe(true);
|
|
if (result.ok) {
|
|
finder = result.value;
|
|
}
|
|
expect(finder.isDestroyed).toBe(false);
|
|
});
|
|
|
|
test("multiple instances can coexist", () => {
|
|
const result2 = FileFinder.create({ basePath: testDir });
|
|
expect(result2.ok).toBe(true);
|
|
|
|
if (result2.ok) {
|
|
const finder2 = result2.value;
|
|
|
|
// Both should work independently
|
|
const search1 = finder.fileSearch("Cargo");
|
|
const search2 = finder2.fileSearch("Cargo");
|
|
|
|
expect(search1.ok).toBe(true);
|
|
expect(search2.ok).toBe(true);
|
|
|
|
// Destroying one should not affect the other
|
|
finder2.destroy();
|
|
|
|
const search3 = finder.fileSearch("Cargo");
|
|
expect(search3.ok).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("FileFinder - Directory Search", () => {
|
|
let finder: FileFinder;
|
|
const tmpDir = path.join(testDir, "__test_dirs__");
|
|
const sep = path.sep;
|
|
|
|
beforeAll(() => {
|
|
fs.mkdirSync(path.join(tmpDir, "alpha", "nested"), { recursive: true });
|
|
fs.mkdirSync(path.join(tmpDir, "beta"), { recursive: true });
|
|
fs.writeFileSync(path.join(tmpDir, "alpha", "file.txt"), "x");
|
|
fs.writeFileSync(path.join(tmpDir, "alpha", "nested", "deep.txt"), "x");
|
|
fs.writeFileSync(path.join(tmpDir, "beta", "file.txt"), "x");
|
|
|
|
const result = FileFinder.create({ basePath: testDir });
|
|
expect(result.ok).toBe(true);
|
|
if (result.ok) {
|
|
finder = result.value;
|
|
}
|
|
finder.waitForScanBlocking(5000);
|
|
});
|
|
|
|
afterAll(() => {
|
|
finder?.destroy();
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test("known directories are returned with correct paths", () => {
|
|
const result = finder.directorySearch("__test_dirs__");
|
|
expect(result.ok).toBe(true);
|
|
if (!result.ok) return;
|
|
|
|
const paths = result.value.items.map((i) => i.relativePath);
|
|
expect(paths).toContain(`__test_dirs__${sep}alpha${sep}`);
|
|
expect(paths).toContain(`__test_dirs__${sep}beta${sep}`);
|
|
expect(paths).toContain(`__test_dirs__${sep}alpha${sep}nested${sep}`);
|
|
});
|
|
|
|
test("nested directory uses native separators and correct dirName", () => {
|
|
const result = finder.directorySearch("nested");
|
|
expect(result.ok).toBe(true);
|
|
if (!result.ok) return;
|
|
|
|
const nested = result.value.items.find((i) => i.relativePath.includes("nested"));
|
|
expect(nested).toBeDefined();
|
|
expect(nested!.relativePath).toBe(`__test_dirs__${sep}alpha${sep}nested${sep}`);
|
|
expect(nested!.dirName).toBe(`nested${sep}`);
|
|
});
|
|
});
|
|
|
|
describe("FileFinder - Error Handling", () => {
|
|
test("search fails on destroyed instance", () => {
|
|
const createResult = FileFinder.create({ basePath: testDir });
|
|
expect(createResult.ok).toBe(true);
|
|
if (!createResult.ok) return;
|
|
|
|
const f = createResult.value;
|
|
f.destroy();
|
|
|
|
const result = f.fileSearch("test");
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) {
|
|
expect(result.error).toContain("destroyed");
|
|
}
|
|
});
|
|
|
|
test("getScanProgress fails on destroyed instance", () => {
|
|
const createResult = FileFinder.create({ basePath: testDir });
|
|
expect(createResult.ok).toBe(true);
|
|
if (!createResult.ok) return;
|
|
|
|
const f = createResult.value;
|
|
f.destroy();
|
|
|
|
const result = f.getScanProgress();
|
|
expect(result.ok).toBe(false);
|
|
});
|
|
|
|
test("create fails with invalid path", () => {
|
|
// Use a cross-platform invalid path
|
|
const invalidPath =
|
|
process.platform === "win32"
|
|
? "C:\\nonexistent\\path\\that\\does\\not\\exist"
|
|
: "/nonexistent/path/that/does/not/exist";
|
|
|
|
const result = FileFinder.create({
|
|
basePath: invalidPath,
|
|
});
|
|
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) {
|
|
expect(result.error).toContain("Failed");
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("Result Type Helpers", () => {
|
|
test("ok helper creates success result", async () => {
|
|
const { ok } = await import("../src/fff-api");
|
|
const result = ok(42);
|
|
expect(result.ok).toBe(true);
|
|
if (result.ok) {
|
|
expect(result.value).toBe(42);
|
|
}
|
|
});
|
|
|
|
test("err helper creates error result", async () => {
|
|
const { err } = await import("../src/fff-api");
|
|
const result = err<number>("something went wrong");
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) {
|
|
expect(result.error).toBe("something went wrong");
|
|
}
|
|
});
|
|
});
|