mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
* fix(lbug): pin Ladybug core so Dependabot cannot ship a skewed FTS artifact The extension version is a separate upstream constant. Ignore daily core bumps and fail the pairing gate when the committed manifest does not name the installed core. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): make doctor and CI FTS gates resolve the packaged artifact Doctor and the REQUIRE_FTS file gates still treated an empty ~/.lbdb as unavailable, which would turn three CI jobs red once analyze stops installing into that tree. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): name native-abort and tuple-missing so analyze cannot mis-advise The CLI summary's trailing else treated every unknown skip reason as a missing extension. New crash and platform causes must get their own remedies, not a network-install hint. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): delete the dead read-path FTS index create ensureFTSIndex had no production callers and swallowed read-only CREATE_FTS_INDEX failures, which hid the only signal that a reader tried to write. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): vendor per-platform FTS artifacts so analyze needs no host install Keyword search depended on a CDN fetch into ~/.lbdb. Shipping the five published tuples inside the package makes air-gapped and ignore-scripts installs load the same artifact the publish gate checksums. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): load the packaged FTS artifact before any network install Analyze still required a CDN fetch into ~/.lbdb even when the package already shipped the file. FTS now path-loads the vendored tuple first and records source labels so a later truncated home copy cannot steal the diagnosis. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): diagnose a core/extension version skew instead of a missing runtime A structurally valid FTS artifact whose path version disagrees with the packaged pin must name both versions, not prescribe VC++ or OpenSSL. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): stamp an FTS phase so repair stays usable after an in-place abort A native CREATE_FTS_INDEX abort leaves no skip reason; the next run infers it from the dirty flag, and --repair-fts must not treat that phase as a half-written graph. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): park an in-place FTS crash WAL without wiping the graph An FTS abort after a successful checkpoint must reopen the live index on macOS, Windows, and Linux. Staging never parks the live WAL; readers keep today's large-WAL refusal. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): refuse read-only opens of an FTS-poisoned WAL MCP and serve cannot repair a leftover in-place abort. Fail before the native open and name --repair-fts, on macOS, Windows, and Linux. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): name a vendor-neutral Windows OpenSSL prerequisite OQ1 is unanswered here so GitNexus does not ship OpenSSL DLLs. Windows FTS now asks for a system OpenSSL 3 runtime instead of Git Bash PATH. Co-authored-by: Cursor <cursoragent@cursor.com> * test(lbug): inject the FTS vendor root and redact it on HTTP and MCP Path-loaded artifacts no longer vary with HOME. Tests pass an injected vendor tree and assert search warnings never leak a filesystem path. Co-authored-by: Cursor <cursoragent@cursor.com> * docs(lbug): document load-only as the global FTS install default Analyze still overrides to auto. Packaged per-platform artifacts load before any network install on macOS, Windows, and Linux. Co-authored-by: Cursor <cursoragent@cursor.com> * docs(lbug): format the FTS install-policy README table Prettier does not run on Markdown in pre-commit, so the U10 table wrap needs its own formatting commit. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): skip FTS CREATE after a persisted native abort A recovered analyze run was retrying CREATE_FTS_INDEX from skipReason alone. Keep that skip until --repair-fts, fail closed on unsupported tuples, and honor the checkpoint warrant for park/repair. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): honor checkpoint flushed warrant and align FTS tests with packaged vendor A no-op CHECKPOINT must not satisfy the FTS park warrant, and CI still asserted HOME-only FTS isolation after analyze started path-LOADing the packaged artifact. Co-authored-by: Cursor <cursoragent@cursor.com> * test(lbug): accept a nonempty incremental write set in the #2790 recovery check FTS-phase recovery can incremental-add files (changed=0, added=1). That is not the #2790 empty-diff wipe skip. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): compare FTS home versions to the core pin and tighten the publish filename gate Ladybug's ~/.lbdb/extension directory is the runtime/core version; treating it as the artifact version false-diagnosed skew. The publish guard now rejects a path-escaping filename the same way the fetch script does. Co-authored-by: Cursor <cursoragent@cursor.com> * test(lbug): seed FTS e2e fixtures from the packaged vendor artifact A machine with no ~/.lbdb copy should still run the vendor-survivorship cases; the seed no longer depends on HOME or a network install. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3274) Keep in-place FTS abort evidence after persist so a second CREATE abort cannot fail-open readers, and close the CLI, loader, embed, and e2e gaps the review called out. Note: full npm test hit Ladybug worker-pool startup failures under memory pressure; tsc and 180 targeted unit tests passed. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3274) Run the vendored-path symlink guard on the OS matrix, put e2e HOME fixtures on Ladybug's real extension layout, pin the embed crash-WAL gate before the writable open, and let analyze writers park through missing-shadow recovery. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): keep --repair-fts CI green after vendored-first FTS Never-installed warning fixtures must not inspect a packaged vendor binary, and a failed dirty restamp must not abort an otherwise successful --repair-fts run. Co-authored-by: Cursor <cursoragent@cursor.com> * test(cli): give the #1169 analyze e2e the same 90s Windows budget as its sibling The first #1169 persist-meta case was still on a 60s spawn/it budget and was killed banner-only on windows-latest after the FTS warning fixture no longer failed the shard first. Co-authored-by: Cursor <cursoragent@cursor.com> * test(ci): reweight Windows shards after the FTS e2e grew Vendored-first HOME fixtures pushed fts-extension-e2e to ~6 minutes on windows-latest, so the old 146s weight packed it with skills-e2e and blew the 20-minute watchdog. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
370 lines
15 KiB
TypeScript
370 lines
15 KiB
TypeScript
/**
|
|
* P1 Integration Tests: FTS extension lifecycle end-to-end (#2374)
|
|
*
|
|
* Everything real, nothing mocked: each test spawns the actual CLI entry as a
|
|
* child process, LadybugDB loads the actual extension shared library from
|
|
* disk. The packaged vendor artifact is the first LOAD path; HOME copies
|
|
* are no longer required for a green FTS run.
|
|
*
|
|
* Isolation: GITNEXUS_HOME isolates the registry (#829). LadybugDB still
|
|
* resolves `~/.lbdb` from HOME, and every scenario owns a hermetic fake home
|
|
* so the machine's real ~/.lbdb is never written. Analyze now path-LOADs the
|
|
* packaged vendor artifact first, so a broken or missing HOME copy is no
|
|
* longer an FTS outage when the packaged file is present. Vendor-broken
|
|
* coverage lives in `fts-vendored-root-seam.test.ts` (injected vendorRoot).
|
|
*
|
|
* Scenario matrix:
|
|
* - happy: valid HOME copy, offline (load-only) — vendor or HOME loads
|
|
* - packaged vendor survives a broken or missing HOME copy
|
|
* - #2841: HOME copy vanishes between runs — incremental stays incremental
|
|
* - auto: same vendor survivorship under the install policy
|
|
*/
|
|
import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'vitest';
|
|
import { CLI_SPAWN_PREFIX } from '../helpers/cli-entry.js';
|
|
import { spawnSync } from 'child_process';
|
|
import path from 'path';
|
|
import fs from 'fs';
|
|
import os from 'os';
|
|
|
|
import { getExtensionInstallChildProcessArgs } from '../../src/core/lbug/extension-loader.js';
|
|
import {
|
|
defaultVendorRoot,
|
|
nodePlatformTuple,
|
|
resolveVendoredFtsPath,
|
|
} from '../../src/core/lbug/vendored-extension-path.js';
|
|
import { cleanupTempDirSync } from '../helpers/test-db.js';
|
|
import { findInstalledFtsExtension } from '../helpers/fts-availability.js';
|
|
|
|
/** `.lbdb/extension/<version>/<platform>/fts/libfts.lbug_extension`, discovered not hardcoded. */
|
|
let extensionRelPath: string;
|
|
/** Canonical valid extension bytes (path to a known-good file). */
|
|
let seedExtensionFile: string | null = null;
|
|
|
|
const REQUIRE_FTS = process.env.GITNEXUS_REQUIRE_FTS === '1';
|
|
const tmpDirs: string[] = [];
|
|
|
|
const makeTmpDir = (label: string): string => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `gn-fts-e2e-${label}-`));
|
|
tmpDirs.push(dir);
|
|
return dir;
|
|
};
|
|
|
|
/**
|
|
* Locate a known-good extension file for HOME-copy fixtures.
|
|
* Prefers the packaged vendor artifact (offline, no HOME/network), then a
|
|
* copy already installed under the machine's real home, then one real
|
|
* out-of-process install into a probe home.
|
|
*/
|
|
/** Ladybug HOME layout: `~/.lbdb/extension/<coreVersion>/<upstreamPlatform>/fts/<file>`. */
|
|
const ladybugHomeExtensionRelPath = (filename: string): string | null => {
|
|
try {
|
|
const raw = JSON.parse(
|
|
fs.readFileSync(path.join(defaultVendorRoot(), 'lbug-fts', 'manifest.json'), 'utf8'),
|
|
) as {
|
|
coreVersion?: string;
|
|
tuples?: Array<{ tuple: string; upstreamPlatform: string }>;
|
|
};
|
|
const upstream = raw.tuples?.find(
|
|
(entry) => entry.tuple === nodePlatformTuple(),
|
|
)?.upstreamPlatform;
|
|
if (!raw.coreVersion || !upstream) return null;
|
|
return path.join('.lbdb', 'extension', raw.coreVersion, upstream, 'fts', filename);
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const resolveSeedExtension = (): void => {
|
|
const packaged = resolveVendoredFtsPath();
|
|
if (packaged) {
|
|
extensionRelPath =
|
|
ladybugHomeExtensionRelPath(path.basename(packaged)) ??
|
|
path.join('.lbdb', 'extension', 'vendor-seed', 'fts', path.basename(packaged));
|
|
seedExtensionFile = packaged;
|
|
return;
|
|
}
|
|
const realExtensionRoot = path.join(os.homedir(), '.lbdb', 'extension');
|
|
const installed = findInstalledFtsExtension(realExtensionRoot);
|
|
if (installed) {
|
|
extensionRelPath = path.relative(os.homedir(), installed);
|
|
seedExtensionFile = installed;
|
|
return;
|
|
}
|
|
// No local copy — run the real installer against a hermetic probe home.
|
|
const probeHome = makeTmpDir('seed-home');
|
|
const install = spawnSync(process.execPath, getExtensionInstallChildProcessArgs('fts'), {
|
|
encoding: 'utf8',
|
|
timeout: 120_000,
|
|
env: { ...process.env, HOME: probeHome, USERPROFILE: probeHome },
|
|
});
|
|
const probeExtensionRoot = path.join(probeHome, '.lbdb', 'extension');
|
|
const probeInstalled = findInstalledFtsExtension(probeExtensionRoot);
|
|
if (install.status === 0 && probeInstalled) {
|
|
extensionRelPath = path.relative(probeHome, probeInstalled);
|
|
seedExtensionFile = probeInstalled;
|
|
return;
|
|
}
|
|
};
|
|
|
|
type ExtensionState = 'valid' | 'broken' | 'missing';
|
|
|
|
/** Create a hermetic fake home whose `.lbdb` holds the requested extension state. */
|
|
const makeHome = (state: ExtensionState): { home: string; extensionFile: string } => {
|
|
const home = makeTmpDir(`home-${state}`);
|
|
const extensionFile = path.join(home, extensionRelPath);
|
|
fs.mkdirSync(path.dirname(extensionFile), { recursive: true });
|
|
if (state === 'valid' && seedExtensionFile) fs.copyFileSync(seedExtensionFile, extensionFile);
|
|
if (state === 'broken') fs.writeFileSync(extensionFile, 'not a shared library');
|
|
return { home, extensionFile };
|
|
};
|
|
|
|
/** Fresh git-initialised throwaway repo with a uniquely named symbol to search for. */
|
|
const makeFixtureRepo = (label: string): string => {
|
|
const repo = path.join(makeTmpDir(`repo-${label}`), `fts-e2e-${label}`);
|
|
fs.mkdirSync(path.join(repo, 'src'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(repo, 'src', 'greeter.ts'),
|
|
'export function greetE2eSymbol(name: string): string {\n' +
|
|
' return `Hello, ${name}`;\n' +
|
|
'}\n' +
|
|
"greetE2eSymbol('world');\n",
|
|
);
|
|
const gitEnv = {
|
|
...process.env,
|
|
GIT_AUTHOR_NAME: 'test',
|
|
GIT_AUTHOR_EMAIL: 'test@test',
|
|
GIT_COMMITTER_NAME: 'test',
|
|
GIT_COMMITTER_EMAIL: 'test@test',
|
|
};
|
|
spawnSync('git', ['init'], { cwd: repo, stdio: 'pipe' });
|
|
spawnSync('git', ['add', '-A'], { cwd: repo, stdio: 'pipe' });
|
|
spawnSync('git', ['commit', '-m', 'initial'], { cwd: repo, stdio: 'pipe', env: gitEnv });
|
|
return repo;
|
|
};
|
|
|
|
interface CliResult {
|
|
status: number | null;
|
|
/** stdout + stderr combined — warn lines and progress renderer interleave streams. */
|
|
output: string;
|
|
}
|
|
|
|
const runCli = (
|
|
args: string[],
|
|
cwd: string,
|
|
home: string,
|
|
policy: 'load-only' | 'auto',
|
|
timeoutMs = 180_000,
|
|
): CliResult => {
|
|
const result = spawnSync(process.execPath, [...CLI_SPAWN_PREFIX, ...args], {
|
|
cwd,
|
|
encoding: 'utf8',
|
|
timeout: timeoutMs,
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
env: {
|
|
...process.env,
|
|
HOME: home,
|
|
USERPROFILE: home,
|
|
GITNEXUS_HOME: path.join(home, '.gitnexus'),
|
|
GITNEXUS_LANG: 'en',
|
|
GITNEXUS_LBUG_EXTENSION_INSTALL: policy,
|
|
GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS: '60000',
|
|
// Skip analyzeCommand's ensureHeap re-exec, which would drop the tsx loader.
|
|
NODE_OPTIONS: `${process.env.NODE_OPTIONS || ''} --max-old-space-size=8192`.trim(),
|
|
},
|
|
});
|
|
return { status: result.status, output: `${result.stdout ?? ''}\n${result.stderr ?? ''}` };
|
|
};
|
|
|
|
beforeAll(() => {
|
|
resolveSeedExtension();
|
|
if (!seedExtensionFile && REQUIRE_FTS) {
|
|
throw new Error(
|
|
'GITNEXUS_REQUIRE_FTS=1 but no FTS extension could be located or installed for the E2E suite.',
|
|
);
|
|
}
|
|
}, 180_000);
|
|
|
|
afterAll(() => {
|
|
for (const dir of tmpDirs) cleanupTempDirSync(dir);
|
|
});
|
|
|
|
// Skip everything (visibly) when no valid extension exists and the machine is
|
|
// offline — mirrors the dynamic-skip convention in test/helpers/fts-availability.ts.
|
|
beforeEach((ctx) => {
|
|
if (!seedExtensionFile) ctx.skip();
|
|
});
|
|
|
|
describe('happy path — extension pre-installed, fully offline (load-only)', () => {
|
|
let home: string;
|
|
let repo: string;
|
|
|
|
beforeAll(() => {
|
|
// The file-level beforeEach skip fires only per-test; this hook runs first,
|
|
// so guard makeHome() (which needs extensionRelPath) when there is no seed.
|
|
if (!seedExtensionFile) return;
|
|
({ home } = makeHome('valid'));
|
|
repo = makeFixtureRepo('happy');
|
|
});
|
|
|
|
it('analyze builds the index with FTS and emits no degradation warning', () => {
|
|
const result = runCli(['analyze'], repo, home, 'load-only');
|
|
expect(result.status).toBe(0);
|
|
expect(result.output).toContain('indexed successfully');
|
|
expect(result.output).not.toContain('FTS extension unavailable');
|
|
expect(result.output).not.toContain('search is disabled');
|
|
}, 180_000);
|
|
|
|
it('query finds the symbol via BM25 with no degradation warning', () => {
|
|
const result = runCli(['query', 'greetE2eSymbol'], repo, home, 'load-only');
|
|
expect(result.status).toBe(0);
|
|
expect(result.output).toContain('greetE2eSymbol');
|
|
expect(result.output).not.toContain('keyword search degraded');
|
|
}, 60_000);
|
|
|
|
it('doctor reports a live-probed available FTS and a resolved LadybugDB version', () => {
|
|
const result = runCli(['doctor'], repo, home, 'load-only');
|
|
expect(result.status).toBe(0);
|
|
expect(result.output).toContain('Full-text search: available');
|
|
// #2374: version used to print as "unknown" on every platform.
|
|
expect(result.output).toMatch(/LadybugDB:\s*\d+\.\d+\.\d+/);
|
|
}, 60_000);
|
|
|
|
it('analyze --repair-fts rebuilds the search indexes offline', () => {
|
|
const result = runCli(['analyze', '--repair-fts'], repo, home, 'load-only');
|
|
expect(result.status).toBe(0);
|
|
expect(result.output).toContain('FTS indexes repaired successfully');
|
|
}, 180_000);
|
|
});
|
|
|
|
describe('packaged vendor survives a broken or missing home copy', () => {
|
|
let home: string;
|
|
let repo: string;
|
|
|
|
beforeAll(() => {
|
|
// See the happy-path note: skip setup when no seed extension is available
|
|
// so the per-test beforeEach skip is reached instead of throwing here.
|
|
if (!seedExtensionFile) return;
|
|
({ home } = makeHome('broken'));
|
|
repo = makeFixtureRepo('broken');
|
|
});
|
|
|
|
it('analyze stays FTS-available when ~/.lbdb is broken', () => {
|
|
const result = runCli(['analyze'], repo, home, 'load-only');
|
|
expect(result.status).toBe(0);
|
|
expect(result.output).toContain('indexed successfully');
|
|
expect(result.output).not.toContain('FTS extension unavailable');
|
|
expect(result.output).not.toContain('search is disabled');
|
|
}, 180_000);
|
|
|
|
it('analyze --repair-fts succeeds from the packaged artifact', () => {
|
|
const result = runCli(['analyze', '--repair-fts'], repo, home, 'load-only');
|
|
expect(result.status).toBe(0);
|
|
expect(result.output).toContain('FTS indexes repaired successfully');
|
|
}, 180_000);
|
|
|
|
it('query finds the symbol with no HOME-copy degradation warning', () => {
|
|
const result = runCli(['query', 'greetE2eSymbol'], repo, home, 'load-only');
|
|
expect(result.status).toBe(0);
|
|
expect(result.output).toContain('greetE2eSymbol');
|
|
expect(result.output).not.toContain('keyword search degraded');
|
|
expect(result.output).not.toContain('FTS extension failed to load');
|
|
}, 60_000);
|
|
|
|
it('doctor reports a live-probed available FTS despite a broken HOME copy', () => {
|
|
const result = runCli(['doctor'], repo, home, 'load-only');
|
|
expect(result.status).toBe(0);
|
|
expect(result.output).toContain('Full-text search: available');
|
|
}, 60_000);
|
|
|
|
it('analyze stays FTS-available when ~/.lbdb is missing entirely', () => {
|
|
const missing = makeHome('missing');
|
|
const missingRepo = makeFixtureRepo('missing');
|
|
const result = runCli(['analyze'], missingRepo, missing.home, 'load-only');
|
|
expect(result.status).toBe(0);
|
|
expect(result.output).toContain('indexed successfully');
|
|
expect(result.output).not.toContain('FTS extension unavailable');
|
|
expect(result.output).not.toContain('has not been installed');
|
|
}, 180_000);
|
|
});
|
|
|
|
describe('regression — the home copy disappears between analyze runs (#2841)', () => {
|
|
it('the incremental run completes without a Binder exception or a full-DB escalation', (ctx) => {
|
|
const { home, extensionFile } = makeHome('valid');
|
|
const repo = makeFixtureRepo('vanishing-extension');
|
|
|
|
// 1. First analyze with the extension in place: the index ends up carrying
|
|
// an FTS index on every searchable table.
|
|
const first = runCli(['analyze'], repo, home, 'load-only');
|
|
expect(first.status).toBe(0);
|
|
// This case needs run 1 to actually BUILD the indexes — without them there
|
|
// is nothing for the gate to trip on and the assertions below would be
|
|
// vacuous. When the seeded extension cannot load on this host (the same
|
|
// environment gap the other cases in this file hit), skip VISIBLY rather
|
|
// than report a red that says nothing about the fix.
|
|
if (first.output.includes('FTS extension unavailable')) {
|
|
if (REQUIRE_FTS) {
|
|
throw new Error(
|
|
'GITNEXUS_REQUIRE_FTS=1 but the seeded FTS extension did not load — cannot verify the #2841 regression.',
|
|
);
|
|
}
|
|
ctx.skip();
|
|
}
|
|
|
|
// 2. The extension becomes unloadable — the reporter moved
|
|
// ~/.lbdb/extension away; a HOME change or a wiped cache does the same.
|
|
fs.rmSync(extensionFile);
|
|
|
|
// 3. A content change makes the next run incremental, so it must rewrite
|
|
// rows of tables that still carry the indexes from step 1.
|
|
fs.appendFileSync(path.join(repo, 'src', 'greeter.ts'), '\n// #2841 incremental touch\n');
|
|
const gitEnv = {
|
|
...process.env,
|
|
GIT_AUTHOR_NAME: 'test',
|
|
GIT_AUTHOR_EMAIL: 'test@test',
|
|
GIT_COMMITTER_NAME: 'test',
|
|
GIT_COMMITTER_EMAIL: 'test@test',
|
|
};
|
|
spawnSync('git', ['add', '-A'], { cwd: repo, stdio: 'pipe' });
|
|
spawnSync('git', ['commit', '-m', '#2841 touch'], { cwd: repo, stdio: 'pipe', env: gitEnv });
|
|
|
|
const second = runCli(['analyze'], repo, home, 'load-only');
|
|
// Pre-fix: exit 1 with "Binder exception: Trying to delete from an index on
|
|
// table File but its extension is not loaded" and no mention of FTS at all.
|
|
// Packaged vendor still loads after HOME vanishes, so incremental stays
|
|
// incremental (no Binder, no full-DB escalation).
|
|
expect(second.status).toBe(0);
|
|
expect(second.output).not.toContain('its extension is not loaded');
|
|
expect(second.output).not.toContain('full DB write');
|
|
expect(second.output).not.toContain('forcing full rebuild');
|
|
expect(second.output).toMatch(/Incremental:|indexed successfully/);
|
|
}, 400_000);
|
|
});
|
|
|
|
describe('auto policy — packaged vendor does not need a HOME reinstall', () => {
|
|
it('analyze --repair-fts with auto succeeds from the packaged artifact when HOME is broken', () => {
|
|
const { home } = makeHome('broken');
|
|
const repo = makeFixtureRepo('heal');
|
|
|
|
const first = runCli(['analyze'], repo, home, 'load-only');
|
|
expect(first.status).toBe(0);
|
|
expect(first.output).not.toContain('FTS extension unavailable');
|
|
|
|
const repair = runCli(['analyze', '--repair-fts'], repo, home, 'auto');
|
|
expect(repair.status).toBe(0);
|
|
expect(repair.output).toContain('FTS indexes repaired successfully');
|
|
|
|
const query = runCli(['query', 'greetE2eSymbol'], repo, home, 'load-only');
|
|
expect(query.status).toBe(0);
|
|
expect(query.output).toContain('greetE2eSymbol');
|
|
expect(query.output).not.toContain('keyword search degraded');
|
|
}, 600_000);
|
|
|
|
it('a fresh machine with no HOME copy still gets full FTS under load-only', () => {
|
|
const { home } = makeHome('missing');
|
|
const repo = makeFixtureRepo('fresh');
|
|
const result = runCli(['analyze'], repo, home, 'load-only');
|
|
expect(result.status).toBe(0);
|
|
expect(result.output).toContain('indexed successfully');
|
|
expect(result.output).not.toContain('FTS extension unavailable');
|
|
}, 600_000);
|
|
});
|