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:
Dmitriy Kovalenko
2026-08-16 08:57:22 -07:00
committed by GitHub
parent 0d8c257a5d
commit f565d37175
14 changed files with 363 additions and 85 deletions
+1 -1
View File
@@ -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
View File
@@ -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)
+1
View File
@@ -35,6 +35,7 @@
"@ff-labs/fff-node": "*",
},
"devDependencies": {
"@types/bun": "^1.3.8",
"@types/node": "^22.0.0",
"typescript": "^5.0.0",
},
+17 -5
View File
@@ -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.
+1
View File
@@ -49,6 +49,7 @@
"@sinclair/typebox": "*"
},
"devDependencies": {
"@types/bun": "^1.3.8",
"@types/node": "^22.0.0",
"typescript": "^5.0.0"
}
+4 -18
View File
@@ -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;
}
+73
View File
@@ -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;
}
}
+24 -35
View File
@@ -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;
+58
View File
@@ -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;
}
}
+16 -2
View File
@@ -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]);
+69 -8
View File
@@ -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",
);
});
});
+84
View File
@@ -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;
}
});
+11 -9
View File
@@ -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,
+2 -2
View File
@@ -7,7 +7,7 @@
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true,
"types": ["node"]
"types": ["node", "bun"]
},
"include": ["src"]
"include": ["src", "test"]
}