fix(pi): New database files are not created (#780)
This make sure that we either use existing users's neovim databases or actually create a new database
This commit is contained in:
committed by
GitHub
parent
0d8c257a5d
commit
f565d37175
@@ -120,7 +120,7 @@ Three operating modes, switchable at runtime with `/fff-mode`:
|
||||
| `tools-only` | Only tool injection. Keeps pi's native editor autocomplete. |
|
||||
| `override` | Replaces pi's built-in `grep`, `find`, and `multi_grep` with FFF implementations. |
|
||||
|
||||
Env vars: `PI_FFF_MODE`, `FFF_FRECENCY_DB`, `FFF_HISTORY_DB`. Flags: `--fff-mode`, `--fff-frecency-db`, `--fff-history-db`.
|
||||
Env vars: `PI_FFF_MODE`, `FFF_FRECENCY_DB`, `FFF_HISTORY_DB`. Flags: `--fff-mode`, `--fff-frecency-db`, `--fff-history-db`. The databases default to your existing fff.nvim ones when present, otherwise `~/.pi/agent/fff/`.
|
||||
|
||||
### Agent-facing tools
|
||||
|
||||
|
||||
+1
-4
@@ -167,10 +167,7 @@ M.ensure_initialized = function()
|
||||
end
|
||||
end
|
||||
|
||||
local frecency_db_path = config.frecency.db_path or (vim.fn.stdpath('cache') .. '/fff_frecency')
|
||||
local history_db_path = config.history.db_path or (vim.fn.stdpath('data') .. '/fff_history')
|
||||
|
||||
local ok, result = pcall(fuzzy.init_db, frecency_db_path, history_db_path, true)
|
||||
local ok, result = pcall(fuzzy.init_db, config.frecency.db_path, config.history.db_path, true)
|
||||
if not ok then vim.notify('Failed to databases: ' .. tostring(result), vim.log.levels.WARN) end
|
||||
|
||||
setup_global_autocmds(config)
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"@ff-labs/fff-node": "*",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.8",
|
||||
"@types/node": "^22.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
},
|
||||
|
||||
@@ -132,16 +132,28 @@ Mode precedence:
|
||||
## Flags
|
||||
|
||||
- `--fff-mode <mode>` — set mode (see above)
|
||||
- `--fff-frecency-db <path>` — path to frecency database (also: `FFF_FRECENCY_DB` env)
|
||||
- `--fff-history-db <path>` — path to query history database (also: `FFF_HISTORY_DB` env)
|
||||
- `--fff-frecency-db <path>` — path to frecency database (also: `FFF_FRECENCY_DB` env). Optional; see [Data](#data) for the default.
|
||||
- `--fff-history-db <path>` — path to query history database (also: `FFF_HISTORY_DB` env). Optional; see [Data](#data) for the default.
|
||||
- `--fff-enable-root-scan` — allow indexing when launched from `/` (also: `FFF_ENABLE_ROOT_SCAN=1` env). FFF refuses to init at the filesystem root by default.
|
||||
- `--fff-enable-home-scan` — index the home directory when launched from `$HOME` (also: `FFF_ENABLE_HOME_SCAN` env). Enabled by default. Disable with `--fff-enable-home-scan=false` or `FFF_ENABLE_HOME_SCAN=0` if your `$HOME` contains huge trees (toolchains, kernel sources, build outputs) that make the background index run for a long time. When launched from `$HOME` with this enabled, pi shows a warning that the whole home tree is being indexed.
|
||||
|
||||
## Data
|
||||
|
||||
When database paths are provided, FFF stores:
|
||||
- frecency database — file access frequency/recency
|
||||
- history database — query-to-file selection history
|
||||
FFF uses two LMDB databases:
|
||||
- frecency database - file access frequency/recency, used to rank results
|
||||
- history database - query-to-file selection history
|
||||
|
||||
Each path is resolved independently, in this order:
|
||||
|
||||
1. CLI flag — `--fff-frecency-db` / `--fff-history-db`
|
||||
2. Env var — `FFF_FRECENCY_DB` / `FFF_HISTORY_DB`
|
||||
3. An existing [fff.nvim](https://github.com/dmtrKovalenko/fff.nvim) database, so pi reuses the frecency you built up in your editor:
|
||||
- frecency: `$XDG_CACHE_HOME/nvim/fff_nvim`
|
||||
- history: `$XDG_DATA_HOME/nvim/fff_queries`
|
||||
- `XDG_CACHE_HOME` defaults to `~/.cache` and `XDG_DATA_HOME` to `~/.local/share`; on Windows both fall back under `%LOCALAPPDATA%\nvim-data`. Only directories count — a plain file at those paths is ignored.
|
||||
4. pi-local directory, created on demand — `$PI_CODING_AGENT_DIR/fff/{frecency,history}`, defaulting to `~/.pi/agent/fff/{frecency,history}`
|
||||
|
||||
The extension only reads these databases; it never records the agent's own searches into your Neovim history. If a database cannot be opened, the finder starts without persistence and pi shows a warning instead of failing.
|
||||
|
||||
No project files are uploaded anywhere by this extension. It runs locally and only uses the configured LLM through pi itself.
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"@sinclair/typebox": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.8",
|
||||
"@types/node": "^22.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { FileFinderApi } from "@ff-labs/fff-node";
|
||||
import type { FilePickerFactory } from "./file-picker";
|
||||
import { HOME_DIR } from "./paths";
|
||||
import { loadSdk, SCAN_TIMEOUT_MS } from "./sdk";
|
||||
|
||||
export const MAX_AUX = 3;
|
||||
export const IDLE_TTL_MS = 5 * 60 * 1000;
|
||||
@@ -16,10 +16,9 @@ interface AuxPicker {
|
||||
export interface AuxOpts {
|
||||
enableFsRootScanning: boolean;
|
||||
enableHomeDirScanning?: boolean;
|
||||
pickers: FilePickerFactory;
|
||||
// Called before a newly spawned aux picker starts a scan that covers $HOME.
|
||||
onHomeDirScan?: (root: string) => void;
|
||||
frecencyDbPath?: string;
|
||||
historyDbPath?: string;
|
||||
}
|
||||
|
||||
export class AuxFinderPool {
|
||||
@@ -101,26 +100,13 @@ export class AuxFinderPool {
|
||||
this.opts.onHomeDirScan?.(root);
|
||||
}
|
||||
|
||||
const { FileFinder } = await loadSdk();
|
||||
const result = FileFinder.create({
|
||||
const finder = await this.opts.pickers.create({
|
||||
basePath: root,
|
||||
frecencyDbPath: this.opts.frecencyDbPath,
|
||||
historyDbPath: this.opts.historyDbPath,
|
||||
aiMode: true,
|
||||
enableHomeDirScanning,
|
||||
enableFsRootScanning: this.opts.enableFsRootScanning,
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
throw new Error(`Failed to create aux file finder for ${root}: ${result.error}`);
|
||||
}
|
||||
|
||||
await result.value.waitForScan(SCAN_TIMEOUT_MS);
|
||||
const entry: AuxPicker = {
|
||||
root,
|
||||
finder: result.value,
|
||||
lastUsed: Date.now(),
|
||||
};
|
||||
const entry: AuxPicker = { root, finder, lastUsed: Date.now() };
|
||||
this.entries.push(entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { FileFinderApi, InitOptions, Result } from "@ff-labs/fff-node";
|
||||
import { type FileFinderStatic, loadSdk, SCAN_TIMEOUT_MS } from "./sdk";
|
||||
|
||||
export interface PickerOptions {
|
||||
basePath: string;
|
||||
enableHomeDirScanning?: boolean;
|
||||
enableFsRootScanning?: boolean;
|
||||
}
|
||||
|
||||
/** Opens every picker in this pi process — the cwd picker and the aux pickers —
|
||||
* on the same frecency/history databases. */
|
||||
export class FilePickerFactory {
|
||||
private dbDisabled = false;
|
||||
private readonly frecencyDbPath: string;
|
||||
private readonly historyDbPath: string;
|
||||
private readonly onDbFailure?: (error: string) => void;
|
||||
|
||||
constructor(opts: {
|
||||
frecencyDbPath: string;
|
||||
historyDbPath: string;
|
||||
onDbFailure?: (error: string) => void;
|
||||
}) {
|
||||
this.frecencyDbPath = opts.frecencyDbPath;
|
||||
this.historyDbPath = opts.historyDbPath;
|
||||
this.onDbFailure = opts.onDbFailure;
|
||||
}
|
||||
|
||||
/** True once the databases were given up on, so pickers open without them. */
|
||||
get databasesDisabled(): boolean {
|
||||
return this.dbDisabled;
|
||||
}
|
||||
|
||||
/** Opens a scanned, ready-to-use picker. Throws if it cannot be created. */
|
||||
async create(options: PickerOptions): Promise<FileFinderApi> {
|
||||
const { FileFinder } = await loadSdk();
|
||||
const result = this.openWithDbFallback(FileFinder, options);
|
||||
|
||||
if (!result.ok) {
|
||||
throw new Error(
|
||||
`Failed to create FFF file picker for ${options.basePath}: ${result.error}`,
|
||||
);
|
||||
}
|
||||
|
||||
// waitForScan() also resolves on timeout, so this bounds startup rather
|
||||
// than guaranteeing a complete index.
|
||||
await result.value.waitForScan(SCAN_TIMEOUT_MS);
|
||||
return result.value;
|
||||
}
|
||||
|
||||
private openWithDbFallback(
|
||||
FileFinder: FileFinderStatic,
|
||||
options: PickerOptions,
|
||||
): Result<FileFinderApi> {
|
||||
const init: InitOptions = { ...options, aiMode: true };
|
||||
if (this.dbDisabled) return FileFinder.create(init);
|
||||
|
||||
const result = FileFinder.create({
|
||||
...init,
|
||||
frecencyDbPath: this.frecencyDbPath,
|
||||
historyDbPath: this.historyDbPath,
|
||||
});
|
||||
if (result.ok) return result;
|
||||
|
||||
// A failure here is usually transient (broken lock, corruption) and self-heals
|
||||
// on restart, so drop the databases instead of leaving pi without a picker
|
||||
const dbLess = FileFinder.create(init);
|
||||
if (!dbLess.ok) return result; // db error is the more useful one to report
|
||||
|
||||
this.dbDisabled = true;
|
||||
this.onDbFailure?.(result.error);
|
||||
return dbLess;
|
||||
}
|
||||
}
|
||||
@@ -22,9 +22,9 @@ import type {
|
||||
} from "@ff-labs/fff-node";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { AuxFinderPool, routePathConstraint } from "./aux-finders";
|
||||
import { FilePickerFactory } from "./file-picker";
|
||||
import { isHomeDir, resolveDbPaths } from "./paths";
|
||||
import { buildQuery } from "./query";
|
||||
import { isHomeDir } from "./paths";
|
||||
import { loadSdk, SCAN_TIMEOUT_MS } from "./sdk";
|
||||
|
||||
export { SCAN_TIMEOUT_MS } from "./sdk";
|
||||
|
||||
@@ -162,16 +162,7 @@ export function fffFileAnnotation(item: {
|
||||
return "";
|
||||
}
|
||||
|
||||
// fff-core native definition classifier (byte-level scanner in Rust) is enabled
|
||||
// via GrepOptions.classifyDefinitions. Each GrepMatch carries isDefinition for
|
||||
// downstream consumers; pi-fff does NOT use it to re-sort.
|
||||
//
|
||||
// Ordering policy: NO CUSTOM SORTING. The engine already returns items in
|
||||
// frecency order (most-accessed files first). pi-fff only groups consecutive
|
||||
// matches into per-file blocks and preserves whatever order the engine
|
||||
// provided — inside a file we keep matches in source-line order because the
|
||||
// engine emits them that way.
|
||||
|
||||
// DO NOT ATTEMPT TO RESORT OUTPUT HERE IT ONLY CONFUSES MODELS
|
||||
function formatGrepOutput(result: GrepResult): string {
|
||||
if (result.items.length === 0) return "No matches found";
|
||||
|
||||
@@ -179,7 +170,6 @@ function formatGrepOutput(result: GrepResult): string {
|
||||
// This preserves native frecency ordering across files without re-sorting.
|
||||
const lines: string[] = [];
|
||||
let currentFile = "";
|
||||
let shown = 0;
|
||||
|
||||
for (const match of result.items) {
|
||||
if (match.relativePath !== currentFile) {
|
||||
@@ -194,7 +184,6 @@ function formatGrepOutput(result: GrepResult): string {
|
||||
});
|
||||
|
||||
lines.push(` ${match.lineNumber}: ${truncateLine(match.lineContent)}`);
|
||||
shown++;
|
||||
|
||||
match.contextAfter?.forEach((line: string, i: number) => {
|
||||
const lineNum = match.lineNumber + 1 + i;
|
||||
@@ -318,15 +307,14 @@ export default function fffExtension(pi: ExtensionAPI) {
|
||||
|
||||
const toolNames = resolveToolNames(currentMode);
|
||||
|
||||
// DB path resolution: flag > env > undefined (no persistent DBs)
|
||||
const frecencyDbPath =
|
||||
// DB path resolution: flag > env > existing fff.nvim db > pi-local data dir.
|
||||
const resolvedDbPaths = resolveDbPaths({
|
||||
frecency:
|
||||
(pi.getFlag("fff-frecency-db") as string | undefined) ??
|
||||
process.env.FFF_FRECENCY_DB ??
|
||||
undefined;
|
||||
const historyDbPath =
|
||||
(pi.getFlag("fff-history-db") as string | undefined) ??
|
||||
process.env.FFF_HISTORY_DB ??
|
||||
undefined;
|
||||
process.env.FFF_FRECENCY_DB,
|
||||
history:
|
||||
(pi.getFlag("fff-history-db") as string | undefined) ?? process.env.FFF_HISTORY_DB,
|
||||
});
|
||||
|
||||
// flag (boolean) > env ("1"/"true", or "0"/"false") > default.
|
||||
function resolveBoolOpt(flagName: string, envName: string, fallback = false): boolean {
|
||||
@@ -380,12 +368,21 @@ export default function fffExtension(pi: ExtensionAPI) {
|
||||
);
|
||||
}
|
||||
|
||||
const pickers = new FilePickerFactory({
|
||||
frecencyDbPath: resolvedDbPaths.frecency,
|
||||
historyDbPath: resolvedDbPaths.history,
|
||||
onDbFailure: (error) =>
|
||||
uiCtx?.ui.notify(
|
||||
`(fff): Failed to open frecency/history database (${error}). Continuing without frecency persistence.`,
|
||||
"error",
|
||||
),
|
||||
});
|
||||
|
||||
const auxPool = new AuxFinderPool({
|
||||
enableFsRootScanning,
|
||||
enableHomeDirScanning,
|
||||
onHomeDirScan: warnHomeDirScan,
|
||||
frecencyDbPath,
|
||||
historyDbPath,
|
||||
pickers,
|
||||
});
|
||||
|
||||
// in case cwd changes we need to figure this out
|
||||
@@ -402,22 +399,14 @@ export default function fffExtension(pi: ExtensionAPI) {
|
||||
finderCwd = null;
|
||||
}
|
||||
|
||||
const { FileFinder } = await loadSdk();
|
||||
const result = FileFinder.create({
|
||||
// if the dbs can't be opened the factory falls back to a db-less picker,
|
||||
// e.g. when some other process corrupts the lock
|
||||
mainFinder = await pickers.create({
|
||||
basePath: cwd,
|
||||
frecencyDbPath,
|
||||
historyDbPath,
|
||||
aiMode: true,
|
||||
enableHomeDirScanning,
|
||||
enableFsRootScanning,
|
||||
});
|
||||
|
||||
if (!result.ok)
|
||||
throw new Error(`Failed to create FFF file finder: ${result.error}`);
|
||||
|
||||
mainFinder = result.value;
|
||||
finderCwd = cwd;
|
||||
await mainFinder.waitForScan(SCAN_TIMEOUT_MS);
|
||||
return mainFinder;
|
||||
})().finally(() => {
|
||||
finderPromise = null;
|
||||
|
||||
@@ -1,9 +1,67 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
// Resolved once per process: os.homedir() hits the env/passwd on every call.
|
||||
export const HOME_DIR = path.resolve(os.homedir());
|
||||
|
||||
// fff.nvim db dir names (`frecency.db_path` / `history.db_path` in lua/fff/conf.lua).
|
||||
const NVIM_FRECENCY_DIR = "fff_nvim";
|
||||
const NVIM_HISTORY_DIR = "fff_queries";
|
||||
|
||||
export interface DbPaths {
|
||||
frecency: string;
|
||||
history: string;
|
||||
}
|
||||
|
||||
export function isHomeDir(dir: string): boolean {
|
||||
return path.resolve(dir) === HOME_DIR;
|
||||
}
|
||||
|
||||
// Resolution order: explicit override > existing fff.nvim db > pi-local data dir.
|
||||
// Reusing the nvim db lets pi rank files by the frecency the user built in their editor.
|
||||
export function resolveDbPaths(overrides: {
|
||||
frecency?: string;
|
||||
history?: string;
|
||||
}): DbPaths {
|
||||
return {
|
||||
frecency:
|
||||
overrides.frecency ??
|
||||
existingDir(nvimCacheDir(), NVIM_FRECENCY_DIR) ??
|
||||
path.join(piDataDir(), "fff", "frecency"),
|
||||
history:
|
||||
overrides.history ??
|
||||
existingDir(nvimDataDir(), NVIM_HISTORY_DIR) ??
|
||||
path.join(piDataDir(), "fff", "history"),
|
||||
};
|
||||
}
|
||||
|
||||
function nvimCacheDir(): string {
|
||||
const xdg = process.env.XDG_CACHE_HOME;
|
||||
if (xdg) return path.join(xdg, "nvim");
|
||||
if (process.platform === "win32" && process.env.LOCALAPPDATA)
|
||||
return path.join(process.env.LOCALAPPDATA, "nvim-data", "cache");
|
||||
return path.join(HOME_DIR, ".cache", "nvim");
|
||||
}
|
||||
|
||||
function nvimDataDir(): string {
|
||||
const xdg = process.env.XDG_DATA_HOME;
|
||||
if (xdg) return path.join(xdg, "nvim");
|
||||
if (process.platform === "win32" && process.env.LOCALAPPDATA)
|
||||
return path.join(process.env.LOCALAPPDATA, "nvim-data");
|
||||
return path.join(HOME_DIR, ".local", "share", "nvim");
|
||||
}
|
||||
|
||||
function piDataDir(): string {
|
||||
return process.env.PI_CODING_AGENT_DIR ?? path.join(HOME_DIR, ".pi", "agent");
|
||||
}
|
||||
|
||||
// LMDB environments are directories, so a stray file at the same path is not a db.
|
||||
function existingDir(parent: string, name: string): string | undefined {
|
||||
const candidate = path.join(parent, name);
|
||||
try {
|
||||
return fs.statSync(candidate).isDirectory() ? candidate : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,11 +36,22 @@ mock.module("@ff-labs/fff-node", () => finderModule);
|
||||
mock.module("@ff-labs/fff-bun", () => finderModule);
|
||||
|
||||
const { AuxFinderPool } = await import("../src/aux-finders");
|
||||
const { FilePickerFactory } = await import("../src/file-picker");
|
||||
|
||||
function makePickers() {
|
||||
return new FilePickerFactory({
|
||||
frecencyDbPath: "/dbs/frecency",
|
||||
historyDbPath: "/dbs/history",
|
||||
});
|
||||
}
|
||||
|
||||
describe("AuxFinderPool concurrent dedup (#746)", () => {
|
||||
test("two concurrent acquires for same root share one finder", async () => {
|
||||
created.length = 0;
|
||||
const pool = new AuxFinderPool({ enableFsRootScanning: false });
|
||||
const pool = new AuxFinderPool({
|
||||
enableFsRootScanning: false,
|
||||
pickers: makePickers(),
|
||||
});
|
||||
const [a, b] = await Promise.all([
|
||||
pool.acquire("/Users/x"),
|
||||
pool.acquire("/Users/x"),
|
||||
@@ -51,7 +62,10 @@ describe("AuxFinderPool concurrent dedup (#746)", () => {
|
||||
|
||||
test("sequential acquire after in-flight one resolves still reuses", async () => {
|
||||
created.length = 0;
|
||||
const pool = new AuxFinderPool({ enableFsRootScanning: false });
|
||||
const pool = new AuxFinderPool({
|
||||
enableFsRootScanning: false,
|
||||
pickers: makePickers(),
|
||||
});
|
||||
const first = pool.acquire("/Users/x");
|
||||
const second = pool.acquire("/Users/x");
|
||||
await Promise.all([first, second]);
|
||||
|
||||
@@ -25,10 +25,19 @@ function createMockFinder(basePath: string): MockFinder {
|
||||
return finder;
|
||||
}
|
||||
|
||||
// Set to make db-backed creates fail, mimicking a corrupt/locked LMDB.
|
||||
let failDbCreates = false;
|
||||
// Set to make every create fail, db-backed or not.
|
||||
let failAllCreates = false;
|
||||
|
||||
const finderModule = {
|
||||
FileFinder: {
|
||||
create: (options: Record<string, unknown>) => {
|
||||
createOptions.push(options);
|
||||
if (failAllCreates || (failDbCreates && options.frecencyDbPath !== undefined)) {
|
||||
return { ok: false as const, error: "db locked" };
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
value: createMockFinder(options.basePath as string),
|
||||
@@ -41,11 +50,26 @@ mock.module("@ff-labs/fff-node", () => finderModule);
|
||||
mock.module("@ff-labs/fff-bun", () => finderModule);
|
||||
|
||||
const { AuxFinderPool } = await import("../src/aux-finders");
|
||||
const { FilePickerFactory } = await import("../src/file-picker");
|
||||
|
||||
function makePool(opts: Record<string, unknown> = {}) {
|
||||
created.length = 0;
|
||||
createOptions.length = 0;
|
||||
return new AuxFinderPool({ enableFsRootScanning: false, ...opts });
|
||||
failDbCreates = false;
|
||||
failAllCreates = false;
|
||||
return new AuxFinderPool({
|
||||
enableFsRootScanning: false,
|
||||
pickers: makePickers(),
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
|
||||
function makePickers(onDbFailure?: (error: string) => void) {
|
||||
return new FilePickerFactory({
|
||||
frecencyDbPath: "/dbs/frecency",
|
||||
historyDbPath: "/dbs/history",
|
||||
onDbFailure,
|
||||
});
|
||||
}
|
||||
|
||||
describe("AuxFinderPool covering reuse", () => {
|
||||
@@ -97,7 +121,7 @@ describe("AuxFinderPool covering reuse", () => {
|
||||
// Regression for #743: the agent spawning an aux picker over $HOME must warn
|
||||
// the user every time, not silently walk the home tree.
|
||||
test("notifies on every aux picker that covers $HOME", async () => {
|
||||
const onHomeDirScan = mock(() => undefined);
|
||||
const onHomeDirScan = mock((_root: string) => undefined);
|
||||
const pool = makePool({ onHomeDirScan });
|
||||
const home = os.homedir();
|
||||
|
||||
@@ -122,10 +146,7 @@ describe("AuxFinderPool covering reuse", () => {
|
||||
// #700 is fixed by the process-wide LMDB env pool: same-path opens share one
|
||||
// env, so aux finders now reuse the session's frecency/history DBs.
|
||||
test("aux finders receive the pool's frecency/history db paths", async () => {
|
||||
const pool = makePool({
|
||||
frecencyDbPath: "/dbs/frecency",
|
||||
historyDbPath: "/dbs/history",
|
||||
});
|
||||
const pool = makePool();
|
||||
await pool.acquire("/a/b/c");
|
||||
await pool.acquire("/x/y");
|
||||
expect(createOptions.length).toBe(2);
|
||||
@@ -135,10 +156,50 @@ describe("AuxFinderPool covering reuse", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("aux finders stay db-less when the session has no db paths", async () => {
|
||||
const pool = makePool();
|
||||
test("aux finder falls back to no dbs when opening them fails", async () => {
|
||||
const failures: string[] = [];
|
||||
const pool = makePool({ pickers: makePickers((e) => failures.push(e)) });
|
||||
failDbCreates = true;
|
||||
|
||||
const entry = await pool.acquire("/a/b/c");
|
||||
|
||||
expect(entry.root).toBe("/a/b/c");
|
||||
expect(createOptions.length).toBe(2);
|
||||
expect(createOptions[0].frecencyDbPath).toBe("/dbs/frecency");
|
||||
expect(createOptions[1].frecencyDbPath).toBeUndefined();
|
||||
expect(failures).toEqual(["db locked"]);
|
||||
});
|
||||
|
||||
test("a db failure on the main finder keeps later aux finders db-less", async () => {
|
||||
const failures: string[] = [];
|
||||
const pickers = makePickers((e) => failures.push(e));
|
||||
const pool = makePool({ pickers });
|
||||
failDbCreates = true;
|
||||
|
||||
// Stands in for the main cwd picker hitting the broken db first.
|
||||
// The SDK is mocked, so create() hands back a MockFinder, not a real finder.
|
||||
const main = (await pickers.create({
|
||||
basePath: "/workspace",
|
||||
})) as unknown as MockFinder;
|
||||
expect(main.basePath).toBe("/workspace");
|
||||
expect(pickers.databasesDisabled).toBe(true);
|
||||
|
||||
createOptions.length = 0;
|
||||
await pool.acquire("/a/b/c");
|
||||
|
||||
// No retry: the factory already gave up on the dbs, so one db-less create.
|
||||
expect(createOptions.length).toBe(1);
|
||||
expect(createOptions[0].frecencyDbPath).toBeUndefined();
|
||||
expect(createOptions[0].historyDbPath).toBeUndefined();
|
||||
expect(failures).toEqual(["db locked"]);
|
||||
});
|
||||
|
||||
test("create throws when the picker cannot be opened at all", async () => {
|
||||
makePool();
|
||||
failAllCreates = true;
|
||||
|
||||
expect(makePickers().create({ basePath: "/nope" })).rejects.toThrow(
|
||||
"Failed to create FFF file picker for /nope: db locked",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { resolveDbPaths } from "../src/paths";
|
||||
|
||||
const ENV_KEYS = ["XDG_CACHE_HOME", "XDG_DATA_HOME", "PI_CODING_AGENT_DIR"] as const;
|
||||
|
||||
describe("resolveDbPaths", () => {
|
||||
let tmpRoot: string;
|
||||
let piDir: string;
|
||||
let saved: Record<string, string | undefined>;
|
||||
|
||||
beforeEach(() => {
|
||||
saved = {};
|
||||
for (const key of ENV_KEYS) saved[key] = process.env[key];
|
||||
|
||||
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "fff-db-paths-"));
|
||||
piDir = path.join(tmpRoot, "pi-agent");
|
||||
|
||||
process.env.XDG_CACHE_HOME = path.join(tmpRoot, "cache");
|
||||
process.env.XDG_DATA_HOME = path.join(tmpRoot, "data");
|
||||
process.env.PI_CODING_AGENT_DIR = piDir;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const key of ENV_KEYS) {
|
||||
const value = saved[key];
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
}
|
||||
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("overrides win over discovery and fallback", () => {
|
||||
mkNvimDir("cache", "fff_nvim");
|
||||
mkNvimDir("data", "fff_queries");
|
||||
|
||||
const paths = resolveDbPaths({
|
||||
frecency: "/explicit/frecency",
|
||||
history: "/explicit/history",
|
||||
});
|
||||
|
||||
expect(paths.frecency).toBe("/explicit/frecency");
|
||||
expect(paths.history).toBe("/explicit/history");
|
||||
});
|
||||
|
||||
test("picks existing fff.nvim databases", () => {
|
||||
const frecency = mkNvimDir("cache", "fff_nvim");
|
||||
const history = mkNvimDir("data", "fff_queries");
|
||||
|
||||
expect(resolveDbPaths({})).toEqual({ frecency, history });
|
||||
});
|
||||
|
||||
test("uses the pi data dir when no nvim databases exist", () => {
|
||||
expect(resolveDbPaths({})).toEqual({
|
||||
frecency: path.join(piDir, "fff", "frecency"),
|
||||
history: path.join(piDir, "fff", "history"),
|
||||
});
|
||||
});
|
||||
|
||||
test("ignores a plain file at the nvim candidate path", () => {
|
||||
const cacheDir = path.join(tmpRoot, "cache", "nvim");
|
||||
fs.mkdirSync(cacheDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(cacheDir, "fff_nvim"), "not a db");
|
||||
|
||||
expect(resolveDbPaths({}).frecency).toBe(path.join(piDir, "fff", "frecency"));
|
||||
});
|
||||
|
||||
test("resolves each database independently", () => {
|
||||
const history = mkNvimDir("data", "fff_queries");
|
||||
const paths = resolveDbPaths({ frecency: "/explicit/frecency" });
|
||||
|
||||
expect(paths.frecency).toBe("/explicit/frecency");
|
||||
expect(paths.history).toBe(history);
|
||||
});
|
||||
|
||||
function mkNvimDir(kind: "cache" | "data", name: string): string {
|
||||
const dir = path.join(tmpRoot, kind, "nvim", name);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
});
|
||||
@@ -87,7 +87,7 @@ mock.module("@sinclair/typebox", () => ({
|
||||
properties,
|
||||
options,
|
||||
}),
|
||||
Optional: (value: unknown) => ({ ...value, optional: true }),
|
||||
Optional: (value: Record<string, unknown>) => ({ ...value, optional: true }),
|
||||
String: schema("string"),
|
||||
Union: (items: unknown[], options?: unknown) => ({ type: "union", items, options }),
|
||||
},
|
||||
@@ -120,11 +120,12 @@ function createPi(mode?: string) {
|
||||
function createContext(cwd = "/tmp/workspace") {
|
||||
return {
|
||||
cwd,
|
||||
// Signatures mirror the real pi UI surface so mock.calls stays typed.
|
||||
ui: {
|
||||
addAutocompleteProvider: mock(() => undefined),
|
||||
notify: mock(() => undefined),
|
||||
addAutocompleteProvider: mock((_factory: (current: any) => any) => undefined),
|
||||
notify: mock((_message: string, _level?: string) => undefined),
|
||||
setEditorComponent: mock(() => undefined),
|
||||
setStatus: mock(() => undefined),
|
||||
setStatus: mock((_key: string, _text?: string) => undefined),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -211,9 +212,9 @@ describe("pi-fff $HOME scan warning", () => {
|
||||
});
|
||||
const setup = await start(undefined, os.homedir());
|
||||
|
||||
const [key, text] = setup.ctx.ui.setStatus.mock.calls.at(-1) as [string, string];
|
||||
expect(key).toBe("fff");
|
||||
expect(text).toContain("12345 files");
|
||||
const lastStatus = setup.ctx.ui.setStatus.mock.calls.at(-1);
|
||||
expect(lastStatus?.[0]).toBe("fff");
|
||||
expect(lastStatus?.[1]).toContain("12345 files");
|
||||
|
||||
// session_shutdown must stop the poller and clear the footer.
|
||||
await shutdown(setup);
|
||||
@@ -239,8 +240,9 @@ describe("pi-fff autocomplete registration", () => {
|
||||
expect(createCalls).toEqual([
|
||||
{
|
||||
basePath: "/tmp/workspace",
|
||||
frecencyDbPath: undefined,
|
||||
historyDbPath: undefined,
|
||||
// Resolved defaults are host-dependent; covered by test/db-paths.test.ts.
|
||||
frecencyDbPath: expect.any(String),
|
||||
historyDbPath: expect.any(String),
|
||||
aiMode: true,
|
||||
enableHomeDirScanning: true,
|
||||
enableFsRootScanning: false,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"types": ["node"]
|
||||
"types": ["node", "bun"]
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": ["src", "test"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user