fix: harden daemon and large-index recovery paths (#1562)
* fix: harden indexing recovery and daemon liveness * test: cover daemon and recovery review gaps * test: pin that a failure marker never blocks a later successful parse (#1557 retry-discard guard) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: danusha2345 <ewidusoc498@gmail.com> Co-authored-by: Colby McHenry <me@colbymchenry.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
danusha2345
Colby McHenry
parent
d8f2eeaddf
commit
81e1f4a92f
@@ -0,0 +1,102 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { execFile, execFileSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as net from 'net';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CodeGraph } from '../src';
|
||||
import { getDaemonPidPath, getDaemonSocketPath } from '../src/mcp/daemon-paths';
|
||||
import { CodeGraphPackageVersion } from '../src/mcp/version';
|
||||
|
||||
const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');
|
||||
|
||||
function runCodegraph(args: string[], cwd: string): string {
|
||||
return execFileSync(process.execPath, [BIN, ...args], {
|
||||
cwd,
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, CODEGRAPH_NO_DAEMON: '1' },
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
}
|
||||
|
||||
function runCodegraphAsync(args: string[], cwd: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile(
|
||||
process.execPath,
|
||||
[BIN, ...args],
|
||||
{ cwd, encoding: 'utf8', env: { ...process.env, CODEGRAPH_NO_DAEMON: '1' } },
|
||||
(error, stdout, stderr) => {
|
||||
if (error) reject(new Error(`${error.message}\n${stderr}`));
|
||||
else resolve(stdout);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
describe('codegraph unlock — daemon artifact recovery (#1553)', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-unlock-'));
|
||||
const cg = CodeGraph.initSync(tempDir);
|
||||
cg.close();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('removes indexing and phantom-daemon artifacts, then permits indexing', () => {
|
||||
const graphDir = path.join(tempDir, '.codegraph');
|
||||
const pidPath = getDaemonPidPath(tempDir);
|
||||
const socketPath = getDaemonSocketPath(tempDir);
|
||||
fs.writeFileSync(path.join(graphDir, 'codegraph.lock'), 'stale\n');
|
||||
fs.writeFileSync(pidPath, JSON.stringify({
|
||||
pid: process.pid,
|
||||
version: CodeGraphPackageVersion,
|
||||
socketPath,
|
||||
startedAt: Date.now() - 60_000,
|
||||
}));
|
||||
if (process.platform !== 'win32') fs.writeFileSync(socketPath, 'stale\n');
|
||||
|
||||
const output = runCodegraph(['unlock', tempDir], tempDir);
|
||||
|
||||
expect(output).toContain('Removed stale lock artifacts');
|
||||
expect(fs.existsSync(path.join(graphDir, 'codegraph.lock'))).toBe(false);
|
||||
expect(fs.existsSync(pidPath)).toBe(false);
|
||||
if (process.platform !== 'win32') expect(fs.existsSync(socketPath)).toBe(false);
|
||||
expect(() => process.kill(process.pid, 0)).not.toThrow();
|
||||
expect(() => runCodegraph(['index', '--quiet', tempDir], tempDir)).not.toThrow();
|
||||
});
|
||||
|
||||
it('preserves artifacts when the recorded live daemon answers the socket hello', async () => {
|
||||
const pidPath = getDaemonPidPath(tempDir);
|
||||
const socketPath = getDaemonSocketPath(tempDir);
|
||||
const server = net.createServer((socket) => {
|
||||
socket.end(JSON.stringify({
|
||||
codegraph: CodeGraphPackageVersion,
|
||||
pid: process.pid,
|
||||
socketPath,
|
||||
protocol: 1,
|
||||
}) + '\n');
|
||||
});
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(socketPath, resolve);
|
||||
});
|
||||
fs.writeFileSync(pidPath, JSON.stringify({
|
||||
pid: process.pid,
|
||||
version: CodeGraphPackageVersion,
|
||||
socketPath,
|
||||
startedAt: Date.now(),
|
||||
}));
|
||||
|
||||
try {
|
||||
const output = await runCodegraphAsync(['unlock', tempDir], tempDir);
|
||||
expect(output).toContain('No stale lock files found');
|
||||
expect(fs.existsSync(pidPath)).toBe(true);
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { spawn } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as net from 'net';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
@@ -9,8 +10,11 @@ import {
|
||||
registerDaemon,
|
||||
deregisterDaemon,
|
||||
listDaemons,
|
||||
listVerifiedDaemons,
|
||||
stopDaemonAt,
|
||||
type DaemonRecord,
|
||||
} from '../src/mcp/daemon-registry';
|
||||
import { encodeLockInfo, getDaemonPidPath } from '../src/mcp/daemon-paths';
|
||||
|
||||
/** A pid that's guaranteed dead: spawn a trivial process, let it exit, reap it. */
|
||||
async function deadPid(): Promise<number> {
|
||||
@@ -100,4 +104,55 @@ describe('daemon-registry', () => {
|
||||
const live = listDaemons();
|
||||
expect(live.map((d) => d.root)).toEqual(['/proj/new', '/proj/old']);
|
||||
});
|
||||
|
||||
it('keeps a registry entry whose socket hello matches its PID and version', async () => {
|
||||
const root = fs.mkdtempSync(path.join(tmpHome, 'verified-'));
|
||||
const socketPath = process.platform === 'win32'
|
||||
? `\\\\.\\pipe\\cg-reg-${process.pid}-${Date.now()}`
|
||||
: path.join(tmpHome, 'verified.sock');
|
||||
const server = net.createServer((socket) => {
|
||||
socket.end(JSON.stringify({
|
||||
protocol: 1,
|
||||
pid: process.pid,
|
||||
codegraph: '1.5.0',
|
||||
socketPath,
|
||||
}) + '\n');
|
||||
});
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(socketPath, resolve);
|
||||
});
|
||||
try {
|
||||
registerDaemon({ root, pid: process.pid, version: '1.5.0', socketPath, startedAt: 1 });
|
||||
expect((await listVerifiedDaemons()).map((d) => d.root)).toEqual([root]);
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
});
|
||||
|
||||
it('never signals a reused live PID when no matching daemon answers (#1553)', async () => {
|
||||
const root = fs.mkdtempSync(path.join(tmpHome, 'project-'));
|
||||
const pidPath = getDaemonPidPath(root);
|
||||
fs.mkdirSync(path.dirname(pidPath), { recursive: true });
|
||||
fs.writeFileSync(pidPath, encodeLockInfo({
|
||||
pid: process.pid,
|
||||
version: '1.5.0',
|
||||
socketPath: path.join(root, '.codegraph', 'missing.sock'),
|
||||
startedAt: Date.now() - 60_000,
|
||||
}));
|
||||
|
||||
registerDaemon({
|
||||
root,
|
||||
pid: process.pid,
|
||||
version: '1.5.0',
|
||||
socketPath: path.join(root, '.codegraph', 'missing.sock'),
|
||||
startedAt: Date.now() - 60_000,
|
||||
});
|
||||
|
||||
expect(await listVerifiedDaemons()).toEqual([]);
|
||||
const result = await stopDaemonAt(root);
|
||||
expect(result).toMatchObject({ pid: process.pid, outcome: 'not-running' });
|
||||
expect(isProcessAlive(process.pid)).toBe(true);
|
||||
expect(fs.existsSync(pidPath)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,6 +120,41 @@ describe('CodeGraph Foundation', () => {
|
||||
cg.close();
|
||||
});
|
||||
|
||||
it('restores every secondary index after a crash inside bulk parse load (#1556)', () => {
|
||||
const dbPath = getDatabasePath(tempDir);
|
||||
const first = DatabaseConnection.initialize(dbPath);
|
||||
const before = (first.getDb()
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type = 'index' ORDER BY name")
|
||||
.all() as Array<{ name: string }>).map((r) => r.name);
|
||||
first.beginBulkParseLoad();
|
||||
first.close();
|
||||
|
||||
const reopened = DatabaseConnection.open(dbPath);
|
||||
const after = (reopened.getDb()
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type = 'index' ORDER BY name")
|
||||
.all() as Array<{ name: string }>).map((r) => r.name);
|
||||
reopened.close();
|
||||
|
||||
expect(after).toEqual(before);
|
||||
});
|
||||
|
||||
it('skips secondary-index DDL when the schema is already healthy', () => {
|
||||
const dbPath = getDatabasePath(tempDir);
|
||||
const connection = DatabaseConnection.initialize(dbPath);
|
||||
const db = connection.getDb();
|
||||
const originalExec = db.exec.bind(db);
|
||||
let execCalls = 0;
|
||||
db.exec = (sql: string) => {
|
||||
execCalls++;
|
||||
originalExec(sql);
|
||||
};
|
||||
|
||||
(connection as any).healBulkSecondaryIndexes();
|
||||
connection.close();
|
||||
|
||||
expect(execCalls).toBe(0);
|
||||
});
|
||||
|
||||
it('should return correct database size', () => {
|
||||
const cg = CodeGraph.initSync(tempDir);
|
||||
const stats = cg.getStats();
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import CodeGraph from '../src/index';
|
||||
import { QueryBuilder } from '../src/db/queries';
|
||||
|
||||
describe('large-corpus regression fixes', () => {
|
||||
it('collects a dense unresolved-reference chunk without spreading it onto the V8 stack (#1558)', () => {
|
||||
const row = {
|
||||
id: 1,
|
||||
from_node_id: 'source',
|
||||
reference_name: 'target',
|
||||
reference_kind: 'calls',
|
||||
line: 1,
|
||||
col: 1,
|
||||
candidates: null,
|
||||
file_path: 'dense.c',
|
||||
language: 'c',
|
||||
status: 'pending',
|
||||
name_tail: 'target',
|
||||
};
|
||||
const denseRows = new Array(200_000).fill(row);
|
||||
const db = { prepare: () => ({ all: () => denseRows }) };
|
||||
const queries = new QueryBuilder(db as any);
|
||||
expect(queries.getUnresolvedReferencesByFiles(['dense.c'])).toHaveLength(200_000);
|
||||
});
|
||||
|
||||
it('records an oversized file during a fresh index so sync does not retry it (#1557)', async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-skipped-file-'));
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, 'oversized.py'), 'value = 1\n'.repeat(120_000));
|
||||
const cg = await CodeGraph.init(dir, { silent: true });
|
||||
const indexed = await cg.indexAll();
|
||||
expect(indexed.filesSkipped).toBe(1);
|
||||
expect(cg.getFiles().find((f) => f.path === 'oversized.py')?.errors?.[0]?.code).toBe('size_exceeded');
|
||||
const synced = await cg.sync();
|
||||
expect(synced.filesAdded).toBe(0);
|
||||
cg.close();
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('records an oversized file through the single-file indexing path (#1557)', async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-single-skipped-file-'));
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, 'oversized.py'), 'value = 1\n'.repeat(120_000));
|
||||
const cg = await CodeGraph.init(dir, { silent: true });
|
||||
const indexed = await cg.indexFiles(['oversized.py']);
|
||||
expect(indexed.filesSkipped).toBe(1);
|
||||
expect(cg.getFiles().find((f) => f.path === 'oversized.py')?.errors?.[0]?.code).toBe('size_exceeded');
|
||||
const synced = await cg.sync();
|
||||
expect(synced.filesAdded).toBe(0);
|
||||
expect(synced.filesModified).toBe(0);
|
||||
cg.close();
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('JSX synthesis language boundary (#1560)', () => {
|
||||
let dir: string;
|
||||
beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-jsx-gate-')); });
|
||||
afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
|
||||
|
||||
it('does not create jsx-render edges from JSX-looking text in a C-only project', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'only.c'),
|
||||
'void Foo(void) {}\nvoid parent(void) { const char *s = "<Foo/>"; }\n'
|
||||
);
|
||||
const cg = await CodeGraph.init(dir, { silent: true });
|
||||
await cg.indexAll();
|
||||
const rows = (cg as any).db.db.prepare(
|
||||
"SELECT count(*) AS c FROM edges WHERE json_extract(metadata, '$.synthesizedBy') = 'jsx-render'"
|
||||
).get() as { c: number };
|
||||
cg.close();
|
||||
expect(rows.c).toBe(0);
|
||||
});
|
||||
|
||||
it('runs for JavaScript while excluding C parents in the same project', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'native.c'),
|
||||
'void Widget(void) {}\nvoid native_parent(void) { const char *s = "<Widget/>"; }\n'
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'ui.jsx'),
|
||||
'export function Widget() { return <span/>; }\nexport function App() { return <Widget/>; }\n'
|
||||
);
|
||||
const cg = await CodeGraph.init(dir, { silent: true });
|
||||
await cg.indexAll();
|
||||
const rows = (cg as any).db.db.prepare(`
|
||||
SELECT source.file_path AS source_file, target.name AS target_name
|
||||
FROM edges e
|
||||
JOIN nodes source ON source.id = e.source
|
||||
JOIN nodes target ON target.id = e.target
|
||||
WHERE json_extract(e.metadata, '$.synthesizedBy') = 'jsx-render'
|
||||
`).all() as Array<{ source_file: string; target_name: string }>;
|
||||
cg.close();
|
||||
expect(rows).toContainEqual({ source_file: 'ui.jsx', target_name: 'Widget' });
|
||||
expect(rows.some((row) => row.source_file === 'native.c')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('failure markers vs later real results (#1557 × #1541)', () => {
|
||||
it('a failure marker never blocks storing a later successful parse of the same bytes', async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-marker-override-'));
|
||||
try {
|
||||
const rel = 'flaky.py';
|
||||
const content = 'def real_fn():\n return 1\n\nclass RealClass:\n def m(self):\n return 2\n';
|
||||
fs.writeFileSync(path.join(dir, rel), content);
|
||||
const cg = await CodeGraph.init(dir, { silent: true });
|
||||
const { initGrammars, loadGrammarsForLanguages } = await import('../src/extraction/grammars');
|
||||
await initGrammars();
|
||||
await loadGrammarsForLanguages(['python']);
|
||||
const orch = (cg as any).orchestrator;
|
||||
const stats = fs.statSync(path.join(dir, rel));
|
||||
|
||||
// What recordParseFailure persists when a parse worker dies: a marker
|
||||
// row under the SAME content hash the retry will store with.
|
||||
await orch.storeExtractionResult(rel, content, 'python', stats, {
|
||||
nodes: [], edges: [], unresolvedReferences: [],
|
||||
errors: [{ message: 'Worker exited with code 1', filePath: rel, severity: 'error', code: 'parse_error' }],
|
||||
durationMs: 0,
|
||||
});
|
||||
expect(cg.getFile(rel)?.nodeCount).toBe(0);
|
||||
|
||||
// The retry pass succeeds with identical bytes — the marker must be
|
||||
// replaced, not treated as "no changes".
|
||||
const { extractFromSource } = await import('../src/extraction/tree-sitter');
|
||||
const real = extractFromSource(rel, content, 'python');
|
||||
expect(real.nodes.length).toBeGreaterThan(0);
|
||||
await orch.storeExtractionResult(rel, content, 'python', stats, real);
|
||||
|
||||
expect(cg.getFile(rel)?.nodeCount).toBe(real.nodes.length);
|
||||
expect(cg.getNodesInFile(rel).map((n: { name: string }) => n.name)).toContain('real_fn');
|
||||
cg.close();
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -39,6 +39,7 @@ import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CodeGraph } from '../src';
|
||||
import { getDaemonSocketPath } from '../src/mcp/daemon-paths';
|
||||
import { CodeGraphPackageVersion } from '../src/mcp/version';
|
||||
|
||||
const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');
|
||||
|
||||
@@ -336,6 +337,44 @@ describe('Shared MCP daemon (issue #411)', () => {
|
||||
expect(isAlive(livePid!)).toBe(true);
|
||||
}, 40000);
|
||||
|
||||
it('takes over after SIGKILL even when the stale PID has been reused (#1553)', async () => {
|
||||
const env = { CODEGRAPH_DAEMON_IDLE_TIMEOUT_MS: '30000' };
|
||||
const first = spawnServer(tempDir, env);
|
||||
servers.push(first);
|
||||
sendInitialize(first.child, `file://${tempDir}`, 1);
|
||||
await waitFor(() => findResponse(first.stdout, 1), 10000);
|
||||
await waitFor(() => countListeningLines(realRoot) >= 1, 10000);
|
||||
const killedPid = readLockPid(realRoot)!;
|
||||
|
||||
process.kill(killedPid, 'SIGKILL');
|
||||
expect(await waitProcessExit(killedPid, 8000)).toBe(true);
|
||||
|
||||
// Model OS PID reuse without risking another process: the stale lock now
|
||||
// names this live vitest worker, but no daemon answers the leftover socket.
|
||||
fs.writeFileSync(
|
||||
path.join(realRoot, '.codegraph', 'daemon.pid'),
|
||||
JSON.stringify({
|
||||
pid: process.pid,
|
||||
version: CodeGraphPackageVersion,
|
||||
socketPath: getDaemonSocketPath(realRoot),
|
||||
startedAt: Date.now() - 60_000,
|
||||
}),
|
||||
);
|
||||
|
||||
const second = spawnServer(tempDir, env);
|
||||
servers.push(second);
|
||||
sendInitialize(second.child, `file://${tempDir}`, 2);
|
||||
const response = await waitFor(() => findResponse(second.stdout, 2), 12000);
|
||||
expect(response.result.serverInfo.name).toBe('codegraph');
|
||||
await waitFor(() => countListeningLines(realRoot) >= 2, 10000);
|
||||
|
||||
const replacementPid = readLockPid(realRoot)!;
|
||||
expect(replacementPid).not.toBe(killedPid);
|
||||
expect(replacementPid).not.toBe(process.pid);
|
||||
expect(isAlive(replacementPid)).toBe(true);
|
||||
expect(isAlive(process.pid)).toBe(true);
|
||||
}, 50000);
|
||||
|
||||
it('proxy falls back to direct mode on a daemon version mismatch', async () => {
|
||||
const net = await import('net');
|
||||
const sockPath = getDaemonSocketPath(realRoot);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* parallelism safe.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ParseWorkerPool, resolveParsePoolSize, resolveParseTimeoutMs, type ParsePoolWorker, type ParseTask } from '../src/extraction/parse-pool';
|
||||
import { ParseWorkerPool, resolveParseBudgetMs, resolveParsePoolSize, resolveParseTimeoutMs, type ParsePoolWorker, type ParseTask } from '../src/extraction/parse-pool';
|
||||
import type { Language, ExtractionResult } from '../src/types';
|
||||
|
||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||
@@ -94,6 +94,17 @@ describe('resolveParseTimeoutMs', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveParseBudgetMs', () => {
|
||||
it('caps near-limit blob headers at a 20s soft / 60s hard window (#1555)', () => {
|
||||
expect(resolveParseBudgetMs(10_000, 940_800)).toBe(20_000);
|
||||
expect(resolveParseBudgetMs(10_000, 857_376)).toBe(20_000);
|
||||
});
|
||||
|
||||
it('does not clamp an explicit larger base timeout', () => {
|
||||
expect(resolveParseBudgetMs(45_000, 940_800)).toBe(45_000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveParsePoolSize', () => {
|
||||
it('treats explicit 0 and 1 as a single worker (the rollback path)', () => {
|
||||
expect(resolveParsePoolSize('0', 8)).toBe(1);
|
||||
|
||||
@@ -149,6 +149,28 @@ describe('Sync Module', () => {
|
||||
expect(result.filesRemoved).toBe(0);
|
||||
expect(result.filesChecked).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('persists an oversized skipped file so later syncs do not retry it (#1557)', async () => {
|
||||
const filePath = path.join(testDir, 'src', 'oversized.ts');
|
||||
fs.writeFileSync(filePath, 'const value = 1;\n'.repeat(70_000));
|
||||
|
||||
const first = await cg.sync();
|
||||
expect(first.filesAdded).toBe(1);
|
||||
expect(cg.getFiles().find((f) => f.path === 'src/oversized.ts')?.errors?.[0]?.code).toBe('size_exceeded');
|
||||
|
||||
const second = await cg.sync();
|
||||
expect(second.filesAdded).toBe(0);
|
||||
expect(second.filesModified).toBe(0);
|
||||
});
|
||||
|
||||
it('marks a successfully recovered indexing state complete (#1556)', async () => {
|
||||
(cg as any).queries.setMetadata('index_state', 'indexing');
|
||||
await cg.sync({ paths: ['src/index.ts'] });
|
||||
expect(cg.getIndexState()).toBe('indexing');
|
||||
|
||||
await cg.sync();
|
||||
expect(cg.getIndexState()).toBe('complete');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user