mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
dea076fd1e
* fix(mcp,extraction): a daemon whose install changed exits; grammar-load failures are never stored (#2335, #2336) A daemon that outlived an upgrade loaded grammars lazily from an install that no longer existed, and stored every file its watcher re-indexed as a zero-node row under the file's new content hash: no hash-based sync ever revisited those rows, and `codegraph status` called the index up to date. - The daemon checks its own package.json every 30 s (CODEGRAPH_DAEMON_INSTALL_CHECK_MS, 0 turns it off) and exits once the file is gone or carries another version, so the next session starts a daemon from the current install. - A result whose grammar failed to load (`parser_error`) is never stored: the file keeps its previous data and the next sync or index retries it, and a row an older engine stored that way is re-indexed even though its hash matches. The tag-based CFML path reports a missing grammar with the same code. - A launcher whose own install was deleted no longer crashes on the spawn error of the daemon it tried to start. - `codegraph status` and `status --json` (filesNeedingReindex, filesWithParseErrors) report indexed files whose symbols are missing although their content is current (#2336), and `files --json` carries each file's recorded errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(mcp): a session whose own install changed never claims the writer lock (#2335) A daemon now exits once its install is upgraded or removed, and nobody holds the writer slot for a successor at that point. Its sessions run the same replaced code and serve themselves in-process the moment it goes; one that found the slot free claimed it as its writer even though its engine could not load (a real npm upgrade: "Cannot find module './sqlite-adapter'"), and so kept a daemon from the current install from starting. Such a session now falls back read-only and says to restart it. Also adds the CHANGELOG entries for the install check and grammar-load failures (#2335) and for the status report (#2336). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
3.1 KiB
TypeScript
84 lines
3.1 KiB
TypeScript
/**
|
|
* A daemon whose install was upgraded or deleted underneath it exits (#2335).
|
|
*
|
|
* The daemon loads grammars (and the native kernel, and worker scripts) lazily
|
|
* from the directory it was started from. An upgrade replaces or deletes that
|
|
* directory while the daemon keeps running, so every later load fails; the
|
|
* 1.6.1 daemon in #2335 went on re-indexing files as empty for as long as it
|
|
* lived. It now checks its own package.json periodically and makes way for a
|
|
* daemon started from the current install. The check is pure and the Daemon's
|
|
* reaction is tested with it injected; neither starts a real daemon.
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { Daemon, installChangedReason } from '../src/mcp/daemon';
|
|
|
|
describe('installChangedReason', () => {
|
|
let dir: string;
|
|
let pkg: string;
|
|
|
|
beforeEach(() => {
|
|
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-install-check-'));
|
|
pkg = path.join(dir, 'package.json');
|
|
fs.writeFileSync(pkg, JSON.stringify({ name: '@colbymchenry/codegraph', version: '1.6.2' }));
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('is null while the install still carries the running version', () => {
|
|
expect(installChangedReason(pkg, '1.6.2')).toBeNull();
|
|
});
|
|
|
|
it('reports an install upgraded in place', () => {
|
|
fs.writeFileSync(pkg, JSON.stringify({ version: '1.6.3' }));
|
|
expect(installChangedReason(pkg, '1.6.2')).toMatch(/replaced by v1\.6\.3/);
|
|
});
|
|
|
|
it('reports an install that was deleted, file or whole directory', () => {
|
|
fs.rmSync(pkg);
|
|
expect(installChangedReason(pkg, '1.6.2')).toMatch(/removed/);
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
expect(installChangedReason(pkg, '1.6.2')).toMatch(/removed/);
|
|
});
|
|
|
|
it('waits for a package.json caught mid-write instead of exiting on it', () => {
|
|
fs.writeFileSync(pkg, '{"name": "@colbymchenry/codeg');
|
|
expect(installChangedReason(pkg, '1.6.2')).toBeNull();
|
|
fs.writeFileSync(pkg, '');
|
|
expect(installChangedReason(pkg, '1.6.2')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('Daemon.checkInstall', () => {
|
|
// A Daemon is safe to construct (nothing binds until start()); stop() is
|
|
// replaced so a positive check can't tear down the test process.
|
|
const makeDaemon = () => {
|
|
const d = new Daemon('/tmp/codegraph-install-check-unit-test', { idleTimeoutMs: 0 }) as any;
|
|
d.stop = vi.fn(async () => {});
|
|
return d;
|
|
};
|
|
|
|
it('stops the daemon when its install changed', () => {
|
|
const d = makeDaemon();
|
|
expect(d.checkInstall(() => 'Install removed')).toBe(true);
|
|
expect(d.stop).toHaveBeenCalledWith('install changed');
|
|
});
|
|
|
|
it('keeps running while the install is unchanged', () => {
|
|
const d = makeDaemon();
|
|
expect(d.checkInstall(() => null)).toBe(false);
|
|
expect(d.stop).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does nothing once the daemon is already stopping', () => {
|
|
const d = makeDaemon();
|
|
d.stopping = true;
|
|
expect(d.checkInstall(() => 'Install removed')).toBe(false);
|
|
expect(d.stop).not.toHaveBeenCalled();
|
|
});
|
|
});
|