GitNexus/gitnexus/test/unit/lbug-readonly-error.test.ts
sburdges-eng b79278705a
fix(hook): resolve canonical repo root + guard read-only FTS ensure (#1226)
* fix(hook): resolve canonical repo root + guard read-only FTS ensure (#1224)

Two bugs in the Claude Code hook + query layer integration:

1. `findGitNexusDir` (in `gitnexus/hooks/claude/gitnexus-hook.cjs` and
   `gitnexus-claude-plugin/hooks/gitnexus-hook.js`) walked upward from
   cwd looking for a non-registry `.gitnexus/`. In linked git worktrees
   created via `git worktree add`, the canonical repo's `.gitnexus/`
   never sits above the worktree path, so the walk silently fails and
   neither augmentation nor staleness notifications fire.

   Fix: keep the cwd-walk as the fast path, then fall back to
   `git rev-parse --git-common-dir` to resolve the shared `.git/`
   directory (which lives inside the canonical repo across all linked
   worktrees) and walk up from its parent. Returns null cleanly when
   `git` isn't on PATH or cwd isn't inside any working tree.

2. `ensureFTSIndex` in the LadybugDB adapter rethrew when the active
   connection is read-only (e.g. the MCP query pool, which opens DBs
   read-only by design). Defensive callers used to surface five
   "Cannot execute write operations in a read-only database" warnings
   per query.

   Fix: extract `isReadOnlyDbError` (mirroring the existing
   `isDbBusyError` discriminator) and have `ensureFTSIndex` catch the
   read-only error, cache the key, and return silently. Index creation
   is owned by `gitnexus analyze` on a writable connection — the
   ensure call is safely a no-op on the read pool. Lock / busy /
   "already exists" / schema errors continue to propagate.

Tests:
- `test/unit/hooks.test.ts`: new "Linked git worktree resolution"
  block exercises both hooks against a real linked worktree to confirm
  PostToolUse stale notifications fire, plus a negative case when the
  canonical repo has no `.gitnexus/`.
- `test/unit/lbug-readonly-error.test.ts`: new file unit-tests the
  `isReadOnlyDbError` discriminator (positive matches, case
  insensitivity, non-Error inputs, and unrelated errors that must
  still surface — lock contention, "already exists", schema misses).
- `test/integration/lbug-core-adapter.test.ts`: extends the existing
  FTS coverage with an idempotency assertion for `ensureFTSIndex` to
  pin the read-only guard's success-path contract.

Verified with `npx tsc --noEmit` and `vitest run` on the affected
files (hooks + readonly + lbug-core-adapter + bm25-search +
lbug-extension-loader + lbug-embedding-hashes — 136 tests pass).
Build: `npm run build` succeeds.

Closes #1224

* fix(local-backend): cover supported vector path

Add the supported-platform regression assertion for QUERY_VECTOR_INDEX and align the unsupported VECTOR diagnostic wording with platform policy.

Made-with: Cursor

---------

Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
2026-04-30 18:12:03 +01:00

55 lines
2.4 KiB
TypeScript

/**
* Regression Tests: read-only DB error discriminator (#1224)
*
* The MCP query pool opens LadybugDB read-only. Defensive callers of
* `ensureFTSIndex` from that pool used to spam stderr with five
* "Cannot execute write operations in a read-only database" warnings
* per query because the cache was invalidated each time. The fix:
* `ensureFTSIndex` now treats the read-only error as a no-op and
* caches the key — but to do that it relies on a precise discriminator
* that does NOT swallow lock / busy / "already exists" errors.
*
* This file unit-tests the discriminator directly so future refactors
* keep the contract.
*/
import { describe, it, expect } from 'vitest';
import { isReadOnlyDbError } from '../../src/core/lbug/lbug-adapter.js';
describe('isReadOnlyDbError', () => {
it('matches the canonical LadybugDB read-only message verbatim', () => {
const err = new Error(
'Connection exception: Cannot execute write operations in a read-only database!',
);
expect(isReadOnlyDbError(err)).toBe(true);
});
it('matches when the error is wrapped in additional prefix text', () => {
const err = new Error(
'Runtime exception: Cannot execute write operations in a read-only database',
);
expect(isReadOnlyDbError(err)).toBe(true);
});
it('is case-insensitive on the "read-only" substring', () => {
expect(isReadOnlyDbError(new Error('Read-Only Database access denied'))).toBe(true);
});
it('accepts non-Error values (string, unknown) without throwing', () => {
expect(isReadOnlyDbError('write rejected: read-only database')).toBe(true);
expect(isReadOnlyDbError({ toString: () => 'read-only database' })).toBe(true);
expect(isReadOnlyDbError(null)).toBe(false);
expect(isReadOnlyDbError(undefined)).toBe(false);
});
it('does NOT match unrelated errors that the ensure path must still surface', () => {
// Lock contention — handled separately by isDbBusyError; must not be
// silenced by the read-only filter.
expect(isReadOnlyDbError(new Error('Could not set lock on file'))).toBe(false);
// "already exists" — the happy idempotent path inside createFTSIndex.
expect(isReadOnlyDbError(new Error('Index file_fts already exists'))).toBe(false);
// Schema-level problem.
expect(isReadOnlyDbError(new Error('Table File does not exist'))).toBe(false);
// Generic transient error.
expect(isReadOnlyDbError(new Error('Connection refused'))).toBe(false);
});
});