mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-29 14:49:36 +08:00
fix(directory): a database we cannot open read-only still counts as initialized
The schema probe failed CLOSED: a real WAL codegraph.db in a directory the process cannot write (a read-only checkout, a mount, another user's tree) made the read-only node:sqlite open throw — SQLite must create `-shm` to open a WAL database — and isInitialized() reported the project as not initialized, where main's file-exists check had been right. The probe now fails OPEN. Once the size gate and the `SQLite format 3\0` header have passed, the file is a SQLite database; only a SUCCESSFUL sqlite_master query proving the `nodes` table absent may return false. Any open/prepare error (locked, busy, cantopen, readonly, disk I/O) returns true, the pre-existing behaviour for a database we cannot inspect. No error strings are matched. The per path + mtime + size memo is unchanged, and schema-less/empty files still return false, so `codegraph init` still repairs them and never touches a real db. Test: a WAL db copied into a `.codegraph/` made read-only with chmod stays initialized — skipped on win32 and when running as root (root ignores mode bits). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
committed by
Forge
co-authored by
Claude Fable 5.1
parent
986005228b
commit
24041f38b9
@@ -80,6 +80,27 @@ describe('a schema-less codegraph.db is not an initialized project (#1895)', ()
|
||||
expect(planFrontload(child, 'how does alpha call beta').exploreRoot).toBeNull();
|
||||
});
|
||||
|
||||
// Root ignores mode bits and Windows has no chmod-based directory permissions.
|
||||
const canDenyWrite = process.platform !== 'win32' && process.getuid?.() !== 0;
|
||||
it.runIf(canDenyWrite)('fails OPEN: a WAL db in a directory we cannot open read-only is still initialized', async () => {
|
||||
// SQLite needs to create `-shm` to open a WAL database; with the directory
|
||||
// non-writable the read-only open throws. That is a database we cannot
|
||||
// inspect, not a schema-less one — it must keep resolving (#1913 review).
|
||||
await indexProject(parent);
|
||||
const src = path.join(parent, '.codegraph', 'codegraph.db');
|
||||
const other = path.join(tmp, 'other');
|
||||
fs.mkdirSync(path.join(other, '.codegraph'), { recursive: true });
|
||||
fs.copyFileSync(src, path.join(other, '.codegraph', 'codegraph.db'));
|
||||
const dir = path.join(other, '.codegraph');
|
||||
fs.chmodSync(dir, 0o555);
|
||||
try {
|
||||
expect(isInitialized(other)).toBe(true);
|
||||
expect(findNearestCodeGraphRoot(path.join(other, 'sub'))).toBe(other);
|
||||
} finally {
|
||||
fs.chmodSync(dir, 0o755);
|
||||
}
|
||||
});
|
||||
|
||||
it('a real index still resolves upward from a subdirectory', async () => {
|
||||
await indexProject(parent);
|
||||
expect(isInitialized(parent)).toBe(true);
|
||||
|
||||
+13
-5
@@ -99,7 +99,9 @@ export function getCodeGraphDir(projectRoot: string): string {
|
||||
* The probe is cheap and gated so hot callers (the prompt hook, MCP root
|
||||
* resolution on every call) pay one small read: file size, then the SQLite
|
||||
* header magic, then a read-only open with a single `sqlite_master` lookup —
|
||||
* memoized per path + mtime + size so an unchanged db is never reopened.
|
||||
* memoized per path + mtime + size so an unchanged db is never reopened. A
|
||||
* database that passes the header check but cannot be opened counts as
|
||||
* initialized (see hasNodesTable) — only a proven-absent schema says no.
|
||||
*/
|
||||
export function isInitialized(projectRoot: string): boolean {
|
||||
const codegraphDir = getCodeGraphDir(projectRoot);
|
||||
@@ -151,6 +153,14 @@ function readsAsSqlite(dbPath: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fails OPEN. Once the size gate and the header magic have passed, the file IS
|
||||
* a SQLite database; only a SUCCESSFUL `sqlite_master` query that proves the
|
||||
* `nodes` table is absent may say "not initialized". Any open/prepare error —
|
||||
* locked, busy, a WAL db in a directory we cannot create `-shm` in (read-only
|
||||
* checkout, mount, another user's tree), disk I/O — returns true: the
|
||||
* pre-existing behaviour for a database we cannot inspect.
|
||||
*/
|
||||
function hasNodesTable(dbPath: string): boolean {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
@@ -159,10 +169,8 @@ function hasNodesTable(dbPath: string): boolean {
|
||||
db = new DatabaseSync(dbPath, { readOnly: true });
|
||||
const row = db.prepare("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = 'nodes'").get();
|
||||
return row !== undefined;
|
||||
} catch (err) {
|
||||
// A db another process holds an exclusive lock on is a live db, not a
|
||||
// broken one; only an unreadable or schema-less file is "not initialized".
|
||||
return /locked|busy/i.test(String((err as Error)?.message ?? ''));
|
||||
} catch {
|
||||
return true;
|
||||
} finally {
|
||||
// Never hold the handle: Windows file locking would block the owner.
|
||||
try { db?.close(); } catch { /* already closed */ }
|
||||
|
||||
Reference in New Issue
Block a user