import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import fsp from 'node:fs/promises';
import path from 'node:path';
import os from 'node:os';
import { cleanupTempDir } from '../../helpers/test-db.js';
import {
  openBridgeDb,
  ensureBridgeSchema,
  queryBridge,
  closeBridgeDb,
  contractNodeId,
  writeBridge,
  writeBridgeUnlocked,
  bridgeMetaMatchesFile,
  openBridgeDbReadOnly,
  readBridgeMeta,
  bridgeExists,
  createContractLookupIndex,
  indexContract,
  findContractNode,
  type WriteBridgeInput,
} from '../../../src/core/group/bridge-db.js';
import { getGroupSyncLockDir, withGroupSyncLock } from '../../../src/core/group/group-lock.js';
import { BRIDGE_SCHEMA_VERSION } from '../../../src/core/group/bridge-schema.js';
import { retryRename } from '../../../src/storage/fs-atomic.js';
import type { BridgeHandle, CrossLink } from '../../../src/core/group/types.js';
import { makeContract } from './fixtures.js';

/**
 * In-process close-then-reopen of `bridge.lbug` (`writeBridge →
 * openBridgeDbReadOnly`, and the read path's open→query→close→reopen) — exactly
 * what a long-lived MCP server does on repeated `@group` impact/trace calls.
 *
 * On Linux/macOS this is now a supported, exercised pattern thanks to the
 * `closeBridgeDb` fix that skips CHECKPOINT on read-only handles (a CHECKPOINT
 * on a read-only connection left a lock artifact that failed the next open).
 *
 * On WINDOWS the direct openBridgeDbReadOnly reopen still fails (see NOTE in
 * closeBridgeDb). The read-only bridge-handle cache (getCachedBridgeReadOnly)
 * solves this for production by keeping one handle alive across calls instead
 * of reopening — see the `bridge handle cache` describe block. These tests
 * exercise the DIRECT reopen path (bypassed by the cache) and stay skipped on
 * Windows.
 */
const itLbugReopen = process.platform === 'win32' ? it.skip : it;

describe('bridge-db core', () => {
  let tmpDir: string;

  beforeEach(async () => {
    tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'bridge-test-'));
  });

  afterEach(async () => {
    await cleanupTempDir(tmpDir);
  });

  it('test_openBridgeDb_returns_handle_and_closes', async () => {
    const dbPath = path.join(tmpDir, 'test.lbug');
    const handle = await openBridgeDb(dbPath);
    expect(handle).toBeDefined();
    expect(handle._db).toBeDefined();
    expect(handle._conn).toBeDefined();
    expect(handle.groupDir).toBe(tmpDir);
    // Close should not throw
    await closeBridgeDb(handle);
  });

  it('test_ensureBridgeSchema_creates_tables_idempotent', async () => {
    const dbPath = path.join(tmpDir, 'test.lbug');
    const handle = await openBridgeDb(dbPath);
    await ensureBridgeSchema(handle);
    // Run again — should not throw
    await ensureBridgeSchema(handle);
    const rows = await queryBridge<{ cnt: number }>(
      handle,
      'MATCH (c:Contract) RETURN count(c) AS cnt',
    );
    expect(rows[0].cnt).toBe(0);
    await closeBridgeDb(handle);
  });

  it('test_queryBridge_returns_inserted_data', async () => {
    const dbPath = path.join(tmpDir, 'test.lbug');
    const handle = await openBridgeDb(dbPath);
    await ensureBridgeSchema(handle);
    await queryBridge(
      handle,
      `CREATE (c:Contract {
      id: 'abc123', contractId: 'http::GET::/api', type: 'http', role: 'provider',
      repo: 'backend', confidence: 0.9
    })`,
    );
    const rows = await queryBridge<{ repo: string; confidence: number }>(
      handle,
      'MATCH (c:Contract) RETURN c.repo AS repo, c.confidence AS confidence',
    );
    expect(rows).toHaveLength(1);
    expect(rows[0].repo).toBe('backend');
    expect(rows[0].confidence).toBe(0.9);
    await closeBridgeDb(handle);
  });

  it('test_queryBridge_parameterized', async () => {
    const dbPath = path.join(tmpDir, 'test.lbug');
    const handle = await openBridgeDb(dbPath);
    await ensureBridgeSchema(handle);
    await queryBridge(
      handle,
      `CREATE (c:Contract {
      id: 'p1', contractId: 'http::GET::/api', type: 'http', role: 'provider',
      repo: 'backend', confidence: 0.9
    })`,
    );
    const rows = await queryBridge<{ repo: string }>(
      handle,
      'MATCH (c:Contract) WHERE c.repo = $r RETURN c.repo AS repo',
      { r: 'backend' },
    );
    expect(rows).toHaveLength(1);
    expect(rows[0].repo).toBe('backend');
    await closeBridgeDb(handle);
  });

  it('test_contractNodeId_full_sha256', () => {
    const id = contractNodeId('backend', 'http::GET::/api', 'provider', 'src/routes.ts');
    expect(id).toHaveLength(64); // full SHA-256 hex
    // Same inputs → same hash
    const id2 = contractNodeId('backend', 'http::GET::/api', 'provider', 'src/routes.ts');
    expect(id).toBe(id2);
    // Different filePath → different hash
    const id3 = contractNodeId('backend', 'http::GET::/api', 'provider', 'src/other.ts');
    expect(id).not.toBe(id3);
  });
});

describe('writeBridge + read', () => {
  let tmpDir: string;

  beforeEach(async () => {
    tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'bridge-write-'));
  });

  afterEach(async () => {
    await cleanupTempDir(tmpDir);
  });

  it('test_writeBridge_creates_bridge_lbug_file', async () => {
    await writeBridge(tmpDir, {
      contracts: [makeContract()],
      crossLinks: [],
      repoSnapshots: {
        backend: { indexedAt: '2026-01-01', lastCommit: 'abc' },
      },
      missingRepos: ['missing-repo'],
    });
    const exists = await bridgeExists(tmpDir);
    expect(exists).toBe(true);
  });

  it("replaces the previous sync's metadata on a successful rebuild", async () => {
    // meta.json describes the bridge's completeness, and since #3011 that is
    // load-bearing: runGroupImpact folds `unreadableRepos ∪ missingRepos` into
    // its truncation fields, so a value from an earlier sync is a wrong answer
    // about this one.
    //
    // Scope, stated because the obvious stronger reading is wrong: this covers
    // the SUCCESSFUL path only. It cannot pin the removal-before-swap ordering,
    // because writeBridge overwrites meta.json at the end either way — the
    // assertions below hold with the removal in either position. The ordering
    // is pinned in `bridge-meta-swap-window.test.ts`, which fails the swap
    // itself and checks the previous sync's metadata cannot survive it.
    await writeBridge(tmpDir, {
      contracts: [makeContract()],
      crossLinks: [],
      repoSnapshots: {},
      missingRepos: [],
    });
    const before = await readBridgeMeta(tmpDir);

    await writeBridge(tmpDir, {
      contracts: [makeContract()],
      crossLinks: [],
      repoSnapshots: {},
      missingRepos: [],
      unreadableRepos: ['svc/users'],
    });
    const after = await readBridgeMeta(tmpDir);

    expect(before.unreadableRepos).toBeUndefined();
    expect(after.unreadableRepos).toEqual(['svc/users']);
  });

  it('reports version 0 for a bridge whose meta.json is gone', async () => {
    // `version: 0` is the "no provenance" signal `runGroupImpact` fails closed
    // on, so it is worth asserting directly rather than only through the
    // callers that consume it. No fault is injected into writeBridge here —
    // the file is removed afterwards — so this pins readBridgeMeta's contract,
    // not the write ordering (see `bridge-meta-swap-window.test.ts` for that).
    await writeBridge(tmpDir, {
      contracts: [makeContract()],
      crossLinks: [],
      repoSnapshots: {},
      missingRepos: [],
      unreadableRepos: ['svc/users'],
    });

    await fsp.rm(path.join(tmpDir, 'meta.json'), { force: true });
    const meta = await readBridgeMeta(tmpDir);

    expect(meta.version).toBe(0);
    expect(meta.unreadableRepos).toBeUndefined();
  });

  it('persists an explicitly empty unreadableRepos measurement', async () => {
    // Same distinction as the registry: `[]` means the sync accounted for every
    // repo, and dropping it collapses that into "never recorded".
    await writeBridge(tmpDir, {
      contracts: [makeContract()],
      crossLinks: [],
      repoSnapshots: {},
      missingRepos: [],
      unreadableRepos: [],
    });

    const meta = await readBridgeMeta(tmpDir);
    expect(meta.unreadableRepos).toEqual([]);
  });

  it('test_writeBridge_returns_report_with_insert_counts', async () => {
    const report = await writeBridge(tmpDir, {
      contracts: [makeContract(), makeContract({ repo: 'frontend', role: 'consumer' })],
      crossLinks: [],
      repoSnapshots: {
        backend: { indexedAt: '2026-01-01', lastCommit: 'abc' },
      },
      missingRepos: [],
    });
    expect(report.contractsInserted).toBe(2);
    expect(report.contractsFailed).toBe(0);
    expect(report.snapshotsInserted).toBe(1);
    expect(report.snapshotsFailed).toBe(0);
    expect(report.linksInserted).toBe(0);
    expect(report.linksFailed).toBe(0);
    expect(report.linksDroppedMissingNode).toBe(0);
    expect(report.sampleErrors).toHaveLength(0);
  });

  it('test_writeBridge_counts_dropped_links_with_missing_nodes', async () => {
    // Provider + cross-link that references a non-existent consumer node →
    // findContractNode returns null for `from`, link gets dropped.
    const provider = makeContract({ role: 'provider' });
    const report = await writeBridge(tmpDir, {
      contracts: [provider],
      crossLinks: [
        {
          from: {
            repo: 'ghost',
            symbolUid: '',
            symbolRef: { filePath: 'nowhere.ts', name: 'ghostFn' },
          },
          to: {
            repo: provider.repo,
            symbolUid: provider.symbolUid,
            symbolRef: provider.symbolRef,
          },
          type: 'http',
          contractId: provider.contractId,
          matchType: 'exact',
          confidence: 1.0,
        },
      ],
      repoSnapshots: {},
      missingRepos: [],
    });
    expect(report.linksInserted).toBe(0);
    expect(report.linksDroppedMissingNode).toBe(1);
    expect(report.linksFailed).toBe(0);
    expect(report.contractsInserted).toBe(1);
  });

  itLbugReopen('test_writeBridge_contracts_queryable', async () => {
    await writeBridge(tmpDir, {
      contracts: [makeContract(), makeContract({ repo: 'frontend', role: 'consumer' })],
      crossLinks: [],
      repoSnapshots: {},
      missingRepos: [],
    });
    const handle = await openBridgeDbReadOnly(tmpDir);
    expect(handle).not.toBeNull();
    const rows = await queryBridge<{ repo: string }>(
      handle!,
      'MATCH (c:Contract) RETURN c.repo AS repo',
    );
    expect(rows).toHaveLength(2);
    await closeBridgeDb(handle!);
  });

  itLbugReopen('test_openBridgeDbReadOnly_can_reopen_in_same_process', async () => {
    // Regression: closeBridgeDb used to issue CHECKPOINT on read-only handles
    // too, which left a WAL/shadow lock artifact that made the next read-only
    // open of the same file fail in-process — breaking repeated @group
    // impact/trace calls in a long-lived MCP server. closeBridgeDb now skips
    // the checkpoint for read-only handles, so open→query→close→open works.
    await writeBridge(tmpDir, {
      contracts: [makeContract()],
      crossLinks: [],
      repoSnapshots: {},
      missingRepos: [],
    });

    const first = await openBridgeDbReadOnly(tmpDir);
    expect(first).not.toBeNull();
    const r1 = await queryBridge<{ n: number }>(first!, 'MATCH (c:Contract) RETURN count(c) AS n');
    expect(r1[0].n).toBe(1);
    await closeBridgeDb(first!);

    // Second open in the SAME process must succeed (previously returned null).
    const second = await openBridgeDbReadOnly(tmpDir);
    expect(second).not.toBeNull();
    const r2 = await queryBridge<{ n: number }>(second!, 'MATCH (c:Contract) RETURN count(c) AS n');
    expect(r2[0].n).toBe(1);
    await closeBridgeDb(second!);

    // And a third, to confirm it is not a one-shot.
    const third = await openBridgeDbReadOnly(tmpDir);
    expect(third).not.toBeNull();
    await closeBridgeDb(third!);
  });

  it('test_writeBridge_meta_json_persists_missingRepos', async () => {
    await writeBridge(tmpDir, {
      contracts: [],
      crossLinks: [],
      repoSnapshots: {},
      missingRepos: ['repo-a', 'repo-b'],
    });
    const meta = await readBridgeMeta(tmpDir);
    expect(meta.missingRepos).toEqual(['repo-a', 'repo-b']);
    expect(meta.version).toBeGreaterThan(0);
    expect(meta.generatedAt).toBeTruthy();
  });

  itLbugReopen('test_writeBridge_repoSnapshots_queryable', async () => {
    await writeBridge(tmpDir, {
      contracts: [],
      crossLinks: [],
      repoSnapshots: {
        'hr/backend': { indexedAt: '2026-01-01', lastCommit: 'abc' },
      },
      missingRepos: [],
    });
    const handle = await openBridgeDbReadOnly(tmpDir);
    const rows = await queryBridge<{ id: string; indexedAt: string }>(
      handle!,
      'MATCH (s:RepoSnapshot) RETURN s.id AS id, s.indexedAt AS indexedAt',
    );
    expect(rows).toHaveLength(1);
    expect(rows[0].id).toBe('hr/backend');
    expect(rows[0].indexedAt).toBe('2026-01-01');
    await closeBridgeDb(handle!);
  });

  itLbugReopen('test_writeBridge_crossLinks_queryable', async () => {
    const provider = makeContract({ repo: 'backend', role: 'provider' });
    const consumer = makeContract({
      repo: 'frontend',
      role: 'consumer',
      symbolRef: { filePath: 'src/api.ts', name: 'fetchUsers' },
      symbolName: 'fetchUsers',
    });
    const link: CrossLink = {
      from: {
        repo: 'frontend',
        symbolUid: '',
        symbolRef: { filePath: 'src/api.ts', name: 'fetchUsers' },
      },
      to: {
        repo: 'backend',
        symbolUid: 'uid-1',
        symbolRef: { filePath: 'src/routes.ts', name: 'getUsers' },
      },
      type: 'http',
      contractId: 'http::GET::/api/users',
      matchType: 'exact',
      confidence: 1.0,
    };
    await writeBridge(tmpDir, {
      contracts: [provider, consumer],
      crossLinks: [link],
      repoSnapshots: {},
      missingRepos: [],
    });
    const handle = await openBridgeDbReadOnly(tmpDir);
    const rows = await queryBridge<{
      fromRepo: string;
      toRepo: string;
      matchType: string;
    }>(
      handle!,
      'MATCH (a:Contract)-[l:ContractLink]->(b:Contract) RETURN l.fromRepo AS fromRepo, l.toRepo AS toRepo, l.matchType AS matchType',
    );
    expect(rows).toHaveLength(1);
    expect(rows[0].fromRepo).toBe('frontend');
    expect(rows[0].toRepo).toBe('backend');
    expect(rows[0].matchType).toBe('exact');
    await closeBridgeDb(handle!);
  });

  itLbugReopen('test_writeBridge_duplicate_contracts_and_links_are_deduped', async () => {
    const provider = makeContract({
      repo: 'backend',
      role: 'provider',
      symbolUid: '',
      symbolName: 'auth.AuthService/Login',
      symbolRef: { filePath: 'src/auth.proto', name: 'Login' },
      contractId: 'grpc::auth.AuthService/Login',
      type: 'grpc',
      meta: { source: 'manifest' },
    });
    const concreteProvider = makeContract({
      ...provider,
      symbolUid: 'uid-auth-login',
      symbolName: 'Login',
      confidence: 0.85,
      meta: { source: 'analyze' },
    });
    const consumer = makeContract({
      repo: 'frontend',
      role: 'consumer',
      symbolUid: '',
      symbolName: 'auth.AuthService/Login',
      symbolRef: { filePath: 'src/client.ts', name: 'AuthServiceClient' },
      contractId: 'grpc::auth.AuthService/Login',
      type: 'grpc',
      meta: { source: 'manifest' },
    });
    const link: CrossLink = {
      from: {
        repo: 'frontend',
        symbolUid: '',
        symbolRef: { filePath: 'src/client.ts', name: 'AuthServiceClient' },
      },
      to: {
        repo: 'backend',
        symbolUid: '',
        symbolRef: { filePath: 'src/auth.proto', name: 'Login' },
      },
      type: 'grpc',
      contractId: 'grpc::auth.AuthService/Login',
      matchType: 'manifest',
      confidence: 1,
    };

    await writeBridge(tmpDir, {
      contracts: [provider, concreteProvider, consumer],
      crossLinks: [link, { ...link }],
      repoSnapshots: {},
      missingRepos: [],
    });

    const handle = await openBridgeDbReadOnly(tmpDir);
    const contracts = await queryBridge<{
      repo: string;
      symbolUid: string;
      symbolName: string;
    }>(
      handle!,
      'MATCH (c:Contract) RETURN c.repo AS repo, c.symbolUid AS symbolUid, c.symbolName AS symbolName ORDER BY c.repo',
    );
    const links = await queryBridge<{ fromRepo: string; toRepo: string }>(
      handle!,
      'MATCH (a:Contract)-[l:ContractLink]->(b:Contract) RETURN l.fromRepo AS fromRepo, l.toRepo AS toRepo',
    );

    expect(contracts).toHaveLength(2);
    expect(contracts[0]).toEqual({
      repo: 'backend',
      symbolUid: 'uid-auth-login',
      symbolName: 'Login',
    });
    expect(links).toHaveLength(1);
    await closeBridgeDb(handle!);
  });

  it('test_openBridgeDbReadOnly_returns_null_for_missing', async () => {
    const handle = await openBridgeDbReadOnly(path.join(tmpDir, 'nonexistent'));
    expect(handle).toBeNull();
  });

  it('test_bridgeExists_false_for_missing', async () => {
    expect(await bridgeExists(path.join(tmpDir, 'nonexistent'))).toBe(false);
  });

  itLbugReopen('test_writeBridge_overwrites_previous', async () => {
    await writeBridge(tmpDir, {
      contracts: [makeContract()],
      crossLinks: [],
      repoSnapshots: {},
      missingRepos: [],
    });
    await writeBridge(tmpDir, {
      contracts: [makeContract({ repo: 'new-repo' })],
      crossLinks: [],
      repoSnapshots: {},
      missingRepos: [],
    });
    const handle = await openBridgeDbReadOnly(tmpDir);
    const rows = await queryBridge<{ repo: string }>(
      handle!,
      'MATCH (c:Contract) RETURN c.repo AS repo',
    );
    expect(rows).toHaveLength(1);
    expect(rows[0].repo).toBe('new-repo');
    await closeBridgeDb(handle!);
  });

  it('test_readBridgeMeta_returns_defaults_for_missing', async () => {
    const meta = await readBridgeMeta(path.join(tmpDir, 'nonexistent'));
    expect(meta.version).toBe(0);
    expect(meta.generatedAt).toBe('');
    expect(meta.missingRepos).toEqual([]);
  });
});
/* ------------------------------------------------------------------ */
/*  The bridge swap runs inside the caller's critical section (R9)     */
/* ------------------------------------------------------------------ */

/**
 * R9, writer-writer half. The swap and the metadata write are two operations:
 * `bridge.lbug` is renamed into place first, and only then is `meta.json`
 * written with the size and mtime of the file it describes. Two writers that
 * overlap can therefore leave one writer's metadata beside the other's
 * database. What prevents it is the group sync lock, held across the whole
 * swap.
 *
 * That is why the swap comes in two halves. `writeBridgeUnlocked` assumes the
 * lock is already held and is what `syncGroup` calls from inside its
 * `withGroupSyncLock` region; `writeBridge` is the thin acquiring wrapper for
 * callers that are not already in that region — every caller in this file, and
 * every other direct caller in the suite. Routing the held-lock caller through
 * the wrapper instead would be a SECOND acquisition of a non-reentrant
 * primitive, which does not fail fast: it waits out the ten-minute ceiling
 * against a lock its own call stack holds. The first case below is the
 * regression gate for exactly that, and it goes red by timeout.
 *
 * SCOPE, so the block is not read as more than it is: this is writer-writer
 * exclusion only. The reader-side promotion of a leftover backup file runs on
 * ordinary reads, outside anyone's critical section, and `bridgeMetaMatchesFile`
 * remains the reader's defense there.
 *
 * Nothing here opens `bridge.lbug`. The in-process write-then-read reopen is
 * the documented LadybugDB Windows limitation this file skips elsewhere
 * (`itLbugReopen`), so every assertion below is made on file state and on
 * `bridgeMetaMatchesFile`, which reads `meta.json` and the database's `stat`
 * and never opens it. No case in this block is platform-skipped, and the
 * surrounding `writeBridge + read` describe is the unchanged control for the
 * single-direct-call path.
 */
describe("writeBridge — the swap runs inside the caller's critical section (R9)", () => {
  let tmpDir: string;

  beforeEach(async () => {
    tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'bridge-lock-'));
  });

  afterEach(async () => {
    await cleanupTempDir(tmpDir);
  });

  /** One contract, plus a `missingRepos` marker naming the writer that built it. */
  const payload = (writer: string): WriteBridgeInput => ({
    contracts: [makeContract({ repo: writer })],
    crossLinks: [],
    repoSnapshots: {},
    missingRepos: [writer],
  });

  /**
   * How long a contended writer is given to finish while the lock is held
   * elsewhere. An uncontended write of this payload takes ~50-110ms in this
   * suite, so the window is more than fifteen times the work — "still not
   * finished" is a statement about the lock rather than about how fast the host
   * is, and a wrapper that does not acquire finishes inside it on any host.
   */
  const CONTENDED_WINDOW_MS = 2000;

  /**
   * A plain existence check. Deliberately NOT `bridgeExists`, which is a READER
   * and promotes a leftover `bridge.lbug.bak` back into place on its way to an
   * answer — the reader-side path this block makes no claim about, and one that
   * would repair the crashed state the last case is trying to hand to the next
   * writer.
   */
  const onDisk = (name: string): Promise<boolean> =>
    fsp.access(path.join(tmpDir, name)).then(
      () => true,
      () => false,
    );

  it('a caller that already holds the group lock completes the swap without acquiring a second one', async () => {
    // The production shape: `syncGroup` holds the lock across its whole persist
    // section and calls the LOCK-FREE half from inside it. `acquireIndexLock` is
    // not reentrant, so a swap that acquired for itself would not fail fast — it
    // would wait out GROUP_SYNC_LOCK_TIMEOUT_MS (ten minutes) against a lock this
    // very call stack is holding. The short per-case timeout is the assertion:
    // this case goes red by TIMEOUT the moment the inner half starts acquiring.
    const report = await withGroupSyncLock(tmpDir, () =>
      writeBridgeUnlocked(tmpDir, payload('held-lock-caller')),
    );

    expect(report.contractsInserted).toBe(1);
    expect(await bridgeExists(tmpDir)).toBe(true);
    const meta = await readBridgeMeta(tmpDir);
    expect(meta.missingRepos).toEqual(['held-lock-caller']);
    expect(await bridgeMetaMatchesFile(tmpDir, meta)).toBe(true);
  }, 15_000);

  it('a direct write cannot enter the swap while another holder has the group lock', async () => {
    // The exclusion itself, observed as ordering rather than as a race: a holder
    // takes the group lock, a direct `writeBridge` starts underneath it, and the
    // write may not complete until the holder lets go. Nothing is mocked — the
    // holder takes the same real lock the wrapper does.
    const order: string[] = [];
    let markHeld!: () => void;
    const lockIsHeld = new Promise<void>((resolve) => {
      markHeld = resolve;
    });
    let releaseHolder!: () => void;
    const holderMayRelease = new Promise<void>((resolve) => {
      releaseHolder = resolve;
    });

    const holder = withGroupSyncLock(tmpDir, async () => {
      markHeld();
      await holderMayRelease;
      order.push('holder-released');
    });
    await lockIsHeld;

    let settled = false;
    const contender = writeBridge(tmpDir, payload('contender')).then(() => {
      order.push('write-finished');
      settled = true;
    });

    await new Promise((resolve) => setTimeout(resolve, CONTENDED_WINDOW_MS));
    // The write is still outside the swap. Without the wrapper's acquisition it
    // has long since finished, and the ordering assertion below inverts.
    expect(settled).toBe(false);
    // And it has not written anything either: the swap is what produces both files.
    expect(await onDisk('bridge.lbug')).toBe(false);
    expect(await onDisk('meta.json')).toBe(false);

    releaseHolder();
    await holder;
    await contender;

    expect(order).toEqual(['holder-released', 'write-finished']);
    expect((await readBridgeMeta(tmpDir)).missingRepos).toEqual(['contender']);
  }, 30_000);

  it('after two contended direct writes the metadata on disk vouches for the database on disk', async () => {
    // Two writers into one group at once. Serialized, the loser's swap completes
    // in full before the winner's begins, so what is left is one writer's
    // database under one writer's metadata — never a mixture, and never a stamp
    // taken from the other writer's file.
    const [first, second] = await Promise.all([
      writeBridge(tmpDir, payload('writer-a')),
      writeBridge(tmpDir, payload('writer-b')),
    ]);
    expect(first.contractsInserted).toBe(1);
    expect(second.contractsInserted).toBe(1);

    const meta = await readBridgeMeta(tmpDir);
    // Exactly one writer's measurement — not both, not neither.
    expect(meta.missingRepos).toHaveLength(1);
    expect(['writer-a', 'writer-b']).toContain(meta.missingRepos[0]);
    // ...and the stamp it carries describes the database that is actually there.
    expect(await bridgeMetaMatchesFile(tmpDir, meta)).toBe(true);
    expect(meta.provenanceUnknown).toBeUndefined();
    expect(meta.version).toBe(BRIDGE_SCHEMA_VERSION);

    // Nothing is left half-swapped: the backup was consumed and neither staging
    // directory survives its writer.
    const left = await fsp.readdir(tmpDir);
    expect(left.filter((f) => f.startsWith('bridge.lbug.bak'))).toEqual([]);
    expect(left.filter((f) => f.startsWith('bridge-tmp-'))).toEqual([]);
  }, 60_000);

  it('the wrapper releases the group lock, so the next writer is not blocked by the last one', async () => {
    await writeBridge(tmpDir, payload('first'));

    // Sequential, not concurrent. A wrapper that acquired and never released
    // would not fail here — it would hang until the ten-minute ceiling, which is
    // what the short per-case timeout turns into a red.
    await expect(withGroupSyncLock(tmpDir, async () => 'free')).resolves.toBe('free');

    const again = await writeBridge(tmpDir, payload('second'));
    expect(again.contractsInserted).toBe(1);
    const meta = await readBridgeMeta(tmpDir);
    expect(meta.missingRepos).toEqual(['second']);
    expect(await bridgeMetaMatchesFile(tmpDir, meta)).toBe(true);
  }, 30_000);

  it('a writer that died mid-swap leaves state the next write recovers from', async () => {
    await writeBridge(tmpDir, payload('before-the-crash'));

    // Reproduce what a process killed between the two renames leaves behind: the
    // live database moved aside to `bridge.lbug.bak`, no `bridge.lbug` at all,
    // the staging directory it never cleaned up, and its lock directory still on
    // disk. That last one matters on the file backend, where the directory
    // outlives the holder; on the socket backend the kernel drops the binding
    // when the holder dies and there is nothing on disk to leave. Pre-creating it
    // is harmless there and load-bearing here, which is why it is not skipped.
    await fsp.rename(path.join(tmpDir, 'bridge.lbug'), path.join(tmpDir, 'bridge.lbug.bak'));
    for (const suffix of ['.wal', '.shadow']) {
      await fsp
        .rename(
          path.join(tmpDir, `bridge.lbug${suffix}`),
          path.join(tmpDir, `bridge.lbug.bak${suffix}`),
        )
        .catch(() => {
          /* sidecar absent — nothing to move */
        });
    }
    const orphanStaging = path.join(tmpDir, 'bridge-tmp-deadwriter');
    await fsp.mkdir(orphanStaging, { recursive: true });
    await fsp.writeFile(path.join(orphanStaging, 'bridge.lbug'), 'half-written');
    await fsp.mkdir(getGroupSyncLockDir(tmpDir), { recursive: true });
    expect(await onDisk('bridge.lbug')).toBe(false);
    expect(await onDisk('bridge.lbug.bak')).toBe(true);

    const report = await writeBridge(tmpDir, payload('after-the-crash'));

    expect(report.contractsInserted).toBe(1);
    expect(await onDisk('bridge.lbug')).toBe(true);
    const meta = await readBridgeMeta(tmpDir);
    expect(meta.missingRepos).toEqual(['after-the-crash']);
    expect(await bridgeMetaMatchesFile(tmpDir, meta)).toBe(true);
    // The dead writer's backup is consumed by the recovery, not inherited by it.
    expect(await onDisk('bridge.lbug.bak')).toBe(false);
  }, 60_000);
});

/* ------------------------------------------------------------------ */
/*  getCachedBridgeReadOnly cache tests                                */
/* ------------------------------------------------------------------ */

/**
 * The RO bridge-handle cache avoids reopening bridge.lbug per @group
 * tool call, which fails on Windows (the OS handle isn't fully released
 * before the next open races in). These tests verify read→read reuse and
 * mtime/size-based invalidation on macOS/Linux. Each begins with the
 * beforeEach `writeBridge` (writable) followed by a read-only open, i.e. the
 * in-process write→read reopen that is the unfixed LadybugDB Windows
 * limitation — so every test here is `itCacheReopen` (win32-skipped). The
 * cache layer (one handle held alive) is what these exercise, not the native
 * close-then-reopen the itLbugReopen tests cover.
 */
describe('bridge handle cache', () => {
  let tmpDir: string;

  beforeEach(async () => {
    tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'bridge-cache-test-'));
    // Create a valid bridge.lbug to open
    await writeBridge(tmpDir, {
      contracts: [makeContract()],
      crossLinks: [],
      repoSnapshots: {},
      missingRepos: [],
    });
  });

  afterEach(async () => {
    // Close any cached handles so cleanupTempDir doesn't hit EBUSY
    const { closeAllCachedBridges } = await import('../../../src/core/group/bridge-db.js');
    await closeAllCachedBridges();
    await cleanupTempDir(tmpDir);
  });

  // The beforeEach calls writeBridge (writable) then the test body opens
  // read-only via getCachedBridgeReadOnly. On Windows this in-process
  // write→read reopen is the known LadybugDB limitation (same class as
  // itLbugReopen) — the OS handle isn't fully released after the writer
  // closes. The cache exercises read→read reuse, not write→read, so the
  // skip only affects the test setup, not the cache logic.
  const itCacheReopen = process.platform === 'win32' ? it.skip : it;

  itCacheReopen('same groupDir returns the same handle instance', async () => {
    const { getCachedBridgeReadOnly } = await import('../../../src/core/group/bridge-db.js');
    const first = await getCachedBridgeReadOnly(tmpDir);
    expect(first).not.toBeNull();

    const second = await getCachedBridgeReadOnly(tmpDir);
    expect(second).not.toBeNull();
    // Must be the SAME object — not a new open
    expect(second).toBe(first);
  });

  itCacheReopen('writeBridge self-invalidates the cache (no manual invalidate)', async () => {
    const { getCachedBridgeReadOnly, queryBridge, closeBridgeDb } =
      await import('../../../src/core/group/bridge-db.js');
    const first = await getCachedBridgeReadOnly(tmpDir);
    expect(first).not.toBeNull();

    // Rewrite the bridge WITHOUT calling invalidateBridgeCache here — writeBridge
    // must self-invalidate (impl: invalidateBridgeCache before its atomic rename).
    // A manual invalidate would mask that, leaving the load-bearing invariant
    // untested.
    await writeBridge(tmpDir, {
      contracts: [makeContract({ repo: 'updated-repo' })],
      crossLinks: [],
      repoSnapshots: {},
      missingRepos: [],
    });

    const second = await getCachedBridgeReadOnly(tmpDir);
    expect(second).not.toBeNull();
    // Different handle — writeBridge's own invalidate dropped the old entry.
    expect(second).not.toBe(first);

    // New handle sees the updated data.
    const rows = await queryBridge<{ repo: string }>(
      second!,
      'MATCH (c:Contract) RETURN c.repo AS repo',
    );
    expect(rows).toMatchObject([{ repo: 'updated-repo' }]);

    // Release leases as real consumers do (finally{closeBridgeDb}). `first` was
    // evicted by writeBridge (close deferred behind this lease) — releasing it
    // fires the deferred native close.
    await closeBridgeDb(first!);
    await closeBridgeDb(second!);
  });

  itCacheReopen('cache reopens when an external writer bumps mtime', async () => {
    // Exercises the stat-based mtime/size invalidation branch directly, WITHOUT
    // going through writeBridge's own invalidate. bridge.lbug is a native binary,
    // so we bump mtime with fsp.utimes (rewriting bytes would corrupt it and the
    // reopen would return null). Simulates another process having written the
    // bridge out-of-band.
    const { getCachedBridgeReadOnly, queryBridge, closeBridgeDb } =
      await import('../../../src/core/group/bridge-db.js');
    const dbPath = path.join(tmpDir, 'bridge.lbug');
    const first = await getCachedBridgeReadOnly(tmpDir);
    expect(first).not.toBeNull();

    const future = new Date(Date.now() + 5000);
    await fsp.utimes(dbPath, future, future);

    const second = await getCachedBridgeReadOnly(tmpDir);
    expect(second).not.toBeNull();
    // mtime moved → the fast path missed → a fresh handle was opened.
    expect(second).not.toBe(first);

    const rows = await queryBridge<{ repo: string }>(
      second!,
      'MATCH (c:Contract) RETURN c.repo AS repo',
    );
    expect(rows).toMatchObject([{ repo: 'backend' }]);

    await closeBridgeDb(first!);
    await closeBridgeDb(second!);
  });

  itCacheReopen('concurrent calls return the same handle instance', async () => {
    const { getCachedBridgeReadOnly } = await import('../../../src/core/group/bridge-db.js');

    // Fire N concurrent cache-miss calls — the TOCTOU guard should make
    // only ONE actual openBridgeDbReadOnly call; all the rest await it.
    const N = 10;
    const results = await Promise.all(
      Array.from({ length: N }, () => getCachedBridgeReadOnly(tmpDir)),
    );

    // All returned the same handle instance (proves no double-open)
    const first = results[0]!;
    for (const h of results) {
      expect(h).toBe(first);
    }

    // Verify the handle works — query returns expected data
    const { queryBridge } = await import('../../../src/core/group/bridge-db.js');
    const rows = await queryBridge<{ repo: string }>(
      first,
      'MATCH (c:Contract) RETURN c.repo AS repo',
    );
    expect(rows).toHaveLength(1);
    expect(rows[0].repo).toBe('backend');
  });
});

/* ------------------------------------------------------------------ */
/*  Concurrency: per-handle query serialization + refcount lease       */
/* ------------------------------------------------------------------ */

/**
 * The cached RO handle is shared across concurrent @group callers in a
 * long-lived MCP serve process. Two correctness properties must hold:
 *   1. No two queries run on one connection at once (LadybugDB Connection is
 *      not concurrency-safe — conn-lock.ts). queryBridge serializes per handle.
 *   2. The native handle is never closed while a reader holds a lease, and is
 *      closed exactly once on the last release (refcount).
 */
describe('bridge handle lock (withHandleLock)', () => {
  it('serializes — never two operations overlap on one lock', async () => {
    const { withHandleLock } = await import('../../../src/core/group/bridge-db.js');
    const lock = { lockTail: Promise.resolve() };
    let active = 0;
    const observedMax: number[] = [];
    const section = async () => {
      active++;
      observedMax.push(active);
      await new Promise((r) => setTimeout(r, 1));
      active--;
    };
    await Promise.all(Array.from({ length: 8 }, () => withHandleLock(lock, section)));
    // If two sections ever overlapped, active would reach 2.
    expect(Math.max(...observedMax)).toBe(1);
  });

  it('releases the lock when an operation throws (chain not wedged)', async () => {
    const { withHandleLock } = await import('../../../src/core/group/bridge-db.js');
    const lock = { lockTail: Promise.resolve() };
    await expect(
      withHandleLock(lock, async () => {
        throw new Error('boom');
      }),
    ).rejects.toThrow('boom');
    // A subsequent op still runs — the failed op released its tail.
    const result = await withHandleLock(lock, async () => 'ok');
    expect(result).toBe('ok');
  });
});

describe('bridge handle cache — refcount lease', () => {
  let tmpDir: string;

  beforeEach(async () => {
    tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'bridge-refcount-test-'));
    await writeBridge(tmpDir, {
      contracts: [makeContract()],
      crossLinks: [],
      repoSnapshots: {},
      missingRepos: [],
    });
  });

  afterEach(async () => {
    const { closeAllCachedBridges } = await import('../../../src/core/group/bridge-db.js');
    await closeAllCachedBridges();
    await cleanupTempDir(tmpDir);
  });

  // Each test below opens RO right after the beforeEach writeBridge (write→read
  // reopen) — the unfixed Windows limitation — so all are win32-skipped.
  const itCacheReopen = process.platform === 'win32' ? it.skip : it;

  // Spy on the native close of a handle without `any` (strict-typing rule):
  // _conn is typed `unknown`, so cast to the minimal structural shape we use.
  const spyConnClose = (handle: BridgeHandle) =>
    vi.spyOn(handle._conn as { close: () => Promise<unknown> }, 'close');

  itCacheReopen('invalidate defers the native close until the last lease releases', async () => {
    const { getCachedBridgeReadOnly, invalidateBridgeCache, closeBridgeDb } =
      await import('../../../src/core/group/bridge-db.js');
    // Two leases on the same cached handle (refs === 2).
    const a = await getCachedBridgeReadOnly(tmpDir);
    const b = await getCachedBridgeReadOnly(tmpDir);
    expect(a).not.toBeNull();
    expect(b).toBe(a);

    const closeSpy = spyConnClose(a!);

    // group_sync-style invalidate while readers hold leases → close deferred.
    await invalidateBridgeCache(tmpDir);
    expect(closeSpy).not.toHaveBeenCalled();

    // First release: refs 2 → 1, still not closed.
    await closeBridgeDb(a!);
    expect(closeSpy).not.toHaveBeenCalled();

    // Last release: refs 1 → 0, native close fires exactly once.
    await closeBridgeDb(b!);
    expect(closeSpy).toHaveBeenCalledTimes(1);
  });

  itCacheReopen('refs count every awaiter under inFlightOpens (not just one)', async () => {
    const { getCachedBridgeReadOnly, invalidateBridgeCache, closeBridgeDb } =
      await import('../../../src/core/group/bridge-db.js');
    // N concurrent cache-miss calls coalesce to one open but each takes a lease.
    const N = 5;
    const handles = await Promise.all(
      Array.from({ length: N }, () => getCachedBridgeReadOnly(tmpDir)),
    );
    const first = handles[0]!;
    expect(handles).toMatchObject(Array.from({ length: N }, () => first));

    const closeSpy = spyConnClose(first);
    await invalidateBridgeCache(tmpDir);

    // Release N-1 leases — if refs had been miscounted as 1, the close would
    // have fired on the first release. It must not.
    for (let i = 0; i < N - 1; i++) await closeBridgeDb(first);
    expect(closeSpy).not.toHaveBeenCalled();

    // The Nth release drops refs to 0 → close once.
    await closeBridgeDb(first);
    expect(closeSpy).toHaveBeenCalledTimes(1);
  });

  itCacheReopen('mtime-evict also defers close while a lease is held', async () => {
    const { getCachedBridgeReadOnly, closeBridgeDb } =
      await import('../../../src/core/group/bridge-db.js');
    const dbPath = path.join(tmpDir, 'bridge.lbug');
    const stale = await getCachedBridgeReadOnly(tmpDir);
    expect(stale).not.toBeNull();
    const closeSpy = spyConnClose(stale!);

    // External writer bumps mtime; the next get evicts the stale entry. The
    // lease on `stale` is still held, so its close must defer (this is the
    // OTHER live close-under-lease path, alongside invalidate).
    const future = new Date(Date.now() + 5000);
    await fsp.utimes(dbPath, future, future);
    const fresh = await getCachedBridgeReadOnly(tmpDir);
    expect(fresh).not.toBe(stale);
    expect(closeSpy).not.toHaveBeenCalled();

    // Releasing the stale lease fires its deferred close exactly once.
    await closeBridgeDb(stale!);
    expect(closeSpy).toHaveBeenCalledTimes(1);

    await closeBridgeDb(fresh!);
  });

  // Exercises the win32-only bounded-drain branch by mocking process.platform on
  // a non-Windows runner (the real win32 path is proven by the cross-process
  // integration test; this proves the branch LOGIC — that invalidate blocks
  // until the reader releases on Windows rather than racing the rename).
  // Skipped on real win32 (its in-process setup is the unsupported reopen).
  itCacheReopen('on win32, invalidate waits for the in-flight reader to drain', async () => {
    const { getCachedBridgeReadOnly, invalidateBridgeCache, closeBridgeDb } =
      await import('../../../src/core/group/bridge-db.js');
    const handle = await getCachedBridgeReadOnly(tmpDir);
    expect(handle).not.toBeNull();

    const realPlatform = process.platform;
    Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
    try {
      let invalidateResolved = false;
      const invalidate = invalidateBridgeCache(tmpDir).then(() => {
        invalidateResolved = true;
      });

      // With a lease held, the win32 drain must NOT resolve yet (POSIX would
      // return immediately here — that's the platform difference under test).
      await new Promise((r) => setTimeout(r, 20));
      expect(invalidateResolved).toBe(false);

      // Releasing the lease drains refs→0, closes the handle, and unblocks the
      // waiting invalidate well within the bounded timeout.
      await closeBridgeDb(handle!);
      await invalidate;
      expect(invalidateResolved).toBe(true);
    } finally {
      Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
    }
  });
});

describe('retryRename', () => {
  afterEach(() => {
    vi.restoreAllMocks();
  });

  it('retries on EBUSY and eventually succeeds', async () => {
    // Spy on fs.promises.rename and make the first two attempts fail with
    // EBUSY, then succeed on the third. Verifies that Windows-style
    // transient rename failures don't immediately bubble up.
    const attempts: Array<[string, string]> = [];
    let calls = 0;
    const spy = vi.spyOn(fsp, 'rename').mockImplementation(async (src, dst) => {
      attempts.push([String(src), String(dst)]);
      calls++;
      if (calls < 3) {
        const err = new Error('resource busy or locked') as NodeJS.ErrnoException;
        err.code = 'EBUSY';
        throw err;
      }
      // Third attempt: pretend the rename worked.
      return undefined;
    });

    await retryRename('/src/a', '/dst/b', 3);

    expect(spy).toHaveBeenCalledTimes(3);
    expect(attempts.every(([s, d]) => s === '/src/a' && d === '/dst/b')).toBe(true);
  });

  it('rethrows non-retryable errors immediately', async () => {
    // A non-retryable code (e.g. ENOENT) should NOT be swallowed into a
    // retry loop — that would mask real bugs and waste time.
    let calls = 0;
    vi.spyOn(fsp, 'rename').mockImplementation(async () => {
      calls++;
      const err = new Error('no such file') as NodeJS.ErrnoException;
      err.code = 'ENOENT';
      throw err;
    });

    await expect(retryRename('/src/a', '/dst/b', 5)).rejects.toMatchObject({
      code: 'ENOENT',
    });
    expect(calls).toBe(1);
  });

  it('gives up after the configured number of attempts', async () => {
    let calls = 0;
    vi.spyOn(fsp, 'rename').mockImplementation(async () => {
      calls++;
      const err = new Error('locked') as NodeJS.ErrnoException;
      err.code = 'EPERM';
      throw err;
    });

    await expect(retryRename('/src/a', '/dst/b', 3)).rejects.toMatchObject({
      code: 'EPERM',
    });
    expect(calls).toBe(3);
  });

  it('retries on EACCES as well', async () => {
    let calls = 0;
    vi.spyOn(fsp, 'rename').mockImplementation(async () => {
      calls++;
      if (calls < 2) {
        const err = new Error('permission denied') as NodeJS.ErrnoException;
        err.code = 'EACCES';
        throw err;
      }
      return undefined;
    });

    await retryRename('/src/a', '/dst/b', 3);
    expect(calls).toBe(2);
  });
});

describe('findContractNode', () => {
  // Pure-function tests for the lookup index + three-tier resolver that
  // were previously an inner closure of `writeBridge` and therefore
  // untestable in isolation. Every test here builds its own index and
  // never touches the DB.

  it('returns null on empty index', () => {
    const index = createContractLookupIndex();
    expect(findContractNode(index, 'backend', 'provider', 'uid-1', 'src/a.ts', 'foo')).toBeNull();
  });

  it('tier 1: returns contract matched by symbolUid', () => {
    const index = createContractLookupIndex();
    const c = makeContract({
      symbolUid: 'uid-42',
      repo: 'backend',
      role: 'provider',
    });
    indexContract(index, c, 'node-A');
    expect(findContractNode(index, 'backend', 'provider', 'uid-42', 'anywhere.ts', 'anyName')).toBe(
      'node-A',
    );
  });

  it('tier 1 is repo-scoped: same uid in a different repo does not match', () => {
    const index = createContractLookupIndex();
    const c = makeContract({ symbolUid: 'uid-42', repo: 'backend' });
    indexContract(index, c, 'node-A');
    expect(
      findContractNode(index, 'frontend', 'provider', 'uid-42', 'src/routes.ts', 'getUsers'),
    ).toBeNull();
  });

  it('tier 1 is role-scoped: provider uid match does not resolve consumer query', () => {
    const index = createContractLookupIndex();
    const c = makeContract({
      symbolUid: 'uid-42',
      role: 'provider',
      repo: 'backend',
    });
    indexContract(index, c, 'node-A');
    expect(
      findContractNode(index, 'backend', 'consumer', 'uid-42', 'src/routes.ts', 'getUsers'),
    ).toBeNull();
  });

  it('tier 2: falls through to filePath + symbolName when symbolUid is empty', () => {
    const index = createContractLookupIndex();
    const c = makeContract({
      symbolUid: '',
      symbolRef: { filePath: 'src/ctrl.ts', name: 'handler' },
      symbolName: 'handler',
    });
    indexContract(index, c, 'node-B');
    expect(findContractNode(index, 'backend', 'provider', '', 'src/ctrl.ts', 'handler')).toBe(
      'node-B',
    );
  });

  it('tier 2: falls through when the given symbolUid does not match anything', () => {
    const index = createContractLookupIndex();
    const c = makeContract({
      symbolUid: 'uid-real',
      symbolRef: { filePath: 'src/ctrl.ts', name: 'handler' },
    });
    indexContract(index, c, 'node-B');
    // Wrong uid; but filePath+name still resolves.
    expect(
      findContractNode(index, 'backend', 'provider', 'uid-wrong', 'src/ctrl.ts', 'handler'),
    ).toBe('node-B');
  });

  it('tier 3: resolves by filePath alone when exactly one contract lives there', () => {
    const index = createContractLookupIndex();
    const c = makeContract({
      symbolUid: '',
      symbolRef: { filePath: 'src/solo.ts', name: 'actualName' },
    });
    indexContract(index, c, 'node-C');
    // filePath+name miss (name is wrong), but tier 3 picks the sole entry.
    expect(findContractNode(index, 'backend', 'provider', '', 'src/solo.ts', 'wrongName')).toBe(
      'node-C',
    );
  });

  it('tier 3: does NOT resolve when multiple contracts live in the same file', () => {
    const index = createContractLookupIndex();
    const a = makeContract({
      symbolUid: '',
      symbolRef: { filePath: 'src/multi.ts', name: 'handlerA' },
    });
    const b = makeContract({
      symbolUid: '',
      symbolRef: { filePath: 'src/multi.ts', name: 'handlerB' },
      contractId: 'http::GET::/api/b',
    });
    indexContract(index, a, 'node-MA');
    indexContract(index, b, 'node-MB');
    // Wrong symbolName → no tier 2 match. Two contracts in the same file
    // → tier 3 must refuse to guess.
    expect(
      findContractNode(index, 'backend', 'provider', '', 'src/multi.ts', 'unknown'),
    ).toBeNull();
  });

  it('prefers tier 1 over tier 2 when both could resolve', () => {
    const index = createContractLookupIndex();
    const tier1Contract = makeContract({
      symbolUid: 'uid-1',
      symbolRef: { filePath: 'src/a.ts', name: 'first' },
    });
    const tier2Contract = makeContract({
      symbolUid: '',
      symbolRef: { filePath: 'src/a.ts', name: 'first' },
      contractId: 'http::POST::/api/x',
    });
    indexContract(index, tier1Contract, 'tier1-id');
    indexContract(index, tier2Contract, 'tier2-id');
    expect(findContractNode(index, 'backend', 'provider', 'uid-1', 'src/a.ts', 'first')).toBe(
      'tier1-id',
    );
  });
});

// The B2 cross-process rename-clash probe moved to
// test/integration/group/bridge-cache-reopen.test.ts, where a cross-process
// seed lets it run on win32 (the in-process write→read reopen no longer gates
// it). It empirically answers whether an open cached RO handle blocks an
// external atomic rename of bridge.lbug on Windows.
