mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
* feat(analyze): add explicit FTS opt-out * chore(autofix): apply prettier + eslint fixes via /autofix command * fix(analyze): treat flag and env FTS opt-out as one mode Avoid a same-commit rebuild when only the skipReason discriminator changes, and advertise disablement on dirty meta before leftover indexes are wiped. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
101 lines
4.4 KiB
TypeScript
101 lines
4.4 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
|
|
/**
|
|
* Regression guard for issue: "Cannot open file ... lbug.shadow - Error 2"
|
|
*
|
|
* Read-only HTTP endpoints (graph, search, grep) must open the LadybugDB with
|
|
* `{ readOnly: true }` so the engine never engages the checkpoint machinery
|
|
* (`.shadow` sidecar). Write-mode opens for read-only operations were the
|
|
* trigger for the Windows-only "Cannot open file ... lbug.shadow" failures
|
|
* observed in E2E runs.
|
|
*
|
|
* If you add another read-only endpoint and forget the option, this file
|
|
* fails — keeping the contract explicit at the static-analysis layer.
|
|
*
|
|
* Companion: api-query-readonly-wiring.test.ts (covers /api/query).
|
|
* Precedent: PR #1655 set the pattern for /api/query.
|
|
*/
|
|
describe('api read-only endpoint wiring', () => {
|
|
const readSource = () =>
|
|
fs.readFile(path.join(__dirname, '..', '..', 'src', 'server', 'api.ts'), 'utf-8');
|
|
|
|
it('/api/graph stream path opens read-only', async () => {
|
|
const source = await readSource();
|
|
expect(source).toMatch(
|
|
/streamGraphNdjson\(res, includeContent, abortController\.signal\)[\s\S]{0,200}(?:readOnly:\s*true|readOnlyFtsOptions\()/,
|
|
);
|
|
});
|
|
|
|
it('/api/graph non-stream path opens read-only', async () => {
|
|
const source = await readSource();
|
|
expect(source).toMatch(
|
|
/buildGraph\(includeContent\)[\s\S]{0,80}(?:readOnly:\s*true|readOnlyFtsOptions\()/,
|
|
);
|
|
});
|
|
|
|
it('/api/search opens read-only', async () => {
|
|
const source = await readSource();
|
|
// The /api/search handler ends its withLbugDb callback with
|
|
// `return { searchResults: enriched, ftsAvailable };` immediately before
|
|
// the closing brace + options object. Match that suffix to confirm the
|
|
// search call site, not /api/query.
|
|
expect(source).toMatch(
|
|
/searchResults: enriched, ftsAvailable[\s\S]{0,80}(?:readOnly:\s*true|readOnlyFtsOptions\()/,
|
|
);
|
|
});
|
|
|
|
it('/api/grep opens read-only', async () => {
|
|
const source = await readSource();
|
|
expect(source).toMatch(
|
|
/MATCH \(n:File\)[\s\S]{0,300}(?:readOnly:\s*true|readOnlyFtsOptions\()/,
|
|
);
|
|
});
|
|
|
|
it('readOnlyFtsOptions carries readOnly: true on both branches', async () => {
|
|
const source = await readSource();
|
|
// The read-only routes above open through this helper rather than an inline
|
|
// `{ readOnly: true }` (#3091 threads the persisted FTS mode through the same
|
|
// option object). That indirection is why those assertions accept the helper
|
|
// by name — so the read-only half of the contract is pinned here instead, or
|
|
// a skip-fts index could open write-mode and re-trip the `.shadow` failure.
|
|
const helper = source.match(/function readOnlyFtsOptions\([\s\S]*?\n\}/);
|
|
expect(helper).not.toBeNull();
|
|
expect(helper![0]).toMatch(
|
|
/skipFts\s*\?\s*\{ readOnly: true, skipFts: true \}\s*:\s*\{ readOnly: true \}/,
|
|
);
|
|
});
|
|
|
|
it('/api/embed remains write-mode (writes embeddings — must not be flipped to readOnly)', async () => {
|
|
const source = await readSource();
|
|
// Negative assertion: no `readOnly: true` between the embed job's
|
|
// `runEmbeddingPipeline` call site and its withLbugDb open. Embed writes
|
|
// back vector rows; flipping this to readOnly would silently break it.
|
|
const embedSection = source.match(/runEmbeddingPipeline[\s\S]{0,400}\}\s*\)\s*;[\s\S]{0,200}/);
|
|
if (embedSection) {
|
|
expect(embedSection[0]).not.toMatch(/readOnly:\s*true/);
|
|
}
|
|
});
|
|
|
|
it('/api/embed keeps the repository lock until cancelled work actually stops', async () => {
|
|
const source = await readSource();
|
|
const timeoutSection = source.match(
|
|
/const embedTimeout = setTimeout\([\s\S]*?\/\/ Run embedding pipeline asynchronously/,
|
|
);
|
|
expect(timeoutSection).not.toBeNull();
|
|
expect(timeoutSection?.[0]).not.toContain('releaseRepoLock(repoLockPath)');
|
|
});
|
|
|
|
it('/api/embed persists and resumes bounded pending windows', async () => {
|
|
const source = await readSource();
|
|
const embedSection = source.match(
|
|
/\/\/ Run embedding pipeline asynchronously[\s\S]*?res\.status\(202\)/,
|
|
);
|
|
expect(embedSection).not.toBeNull();
|
|
expect(embedSection?.[0]).toContain('forceReembedNodeIds');
|
|
expect(embedSection?.[0]).toContain('onCheckpointWindowStart');
|
|
expect(embedSection?.[0]).toContain('pendingNodeIds');
|
|
expect(embedSection?.[0]).toContain('saveMeta');
|
|
});
|
|
});
|