fix(storage): address review feedback on the shared store (#3374)

- Hold the slot index lock for the whole server embedding job, not
  just the graph copy.
- --no-share re-registers and deletes the old slot under that slot's
  index lock, so a running shared analyze cannot re-register it.
- clean --gc previews without --force, like every other destructive arm.
- status reports a pinned branch index as private.
- Slot names prefix Windows device names that carry an extension.
- A slot or store named ..<name> is a legal direct child.
- Shared-store suites run in the serialized lbug-db vitest project and
  clear an inherited GITNEXUS_SHARED_STORE.
- Doc and test accuracy fixes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-09-24 17:05:09 +00:00
parent 28902962dd
commit d0894ae4b7
22 changed files with 199 additions and 46 deletions

View file

@ -298,9 +298,10 @@ function resolveEntryStoragePath(entry) {
return path.resolve(path.join(entry.path, GITNEXUS_DIR));
}
// A single path segment: `..repo-<hash>` is a legal slot name, `..` is not.
function isDirectChild(parent, child) {
const rel = path.relative(parent, child);
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel) && !rel.includes(path.sep);
return rel !== '' && rel !== '..' && !path.isAbsolute(rel) && !rel.includes(path.sep);
}
// Mirror gitnexus/src/storage/shared-store.ts resolveGraphPath (#3352): a

View file

@ -298,9 +298,10 @@ function resolveEntryStoragePath(entry) {
return path.resolve(path.join(entry.path, GITNEXUS_DIR));
}
// A single path segment: `..repo-<hash>` is a legal slot name, `..` is not.
function isDirectChild(parent, child) {
const rel = path.relative(parent, child);
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel) && !rel.includes(path.sep);
return rel !== '' && rel !== '..' && !path.isAbsolute(rel) && !rel.includes(path.sep);
}
// Mirror gitnexus/src/storage/shared-store.ts resolveGraphPath (#3352): a

View file

@ -298,9 +298,10 @@ function resolveEntryStoragePath(entry) {
return path.resolve(path.join(entry.path, GITNEXUS_DIR));
}
// A single path segment: `..repo-<hash>` is a legal slot name, `..` is not.
function isDirectChild(parent, child) {
const rel = path.relative(parent, child);
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel) && !rel.includes(path.sep);
return rel !== '' && rel !== '..' && !path.isAbsolute(rel) && !rel.includes(path.sep);
}
// Mirror gitnexus/src/storage/shared-store.ts resolveGraphPath (#3352): a

View file

@ -166,7 +166,7 @@ const reportReclaim = (result: ReclaimResult | null): void => {
};
/** `clean --gc`: collect every shared store under GITNEXUS_HOME (#3352). */
const collectSharedStores = async (): Promise<void> => {
const collectSharedStores = async (force: boolean): Promise<void> => {
const storesDir = path.join(getGlobalDir(), STORES_DIR);
const names = await fs.readdir(storesDir).catch(() => [] as string[]);
if (names.length === 0) {
@ -175,9 +175,10 @@ const collectSharedStores = async (): Promise<void> => {
}
for (const name of names) {
const root = path.join(storesDir, name);
const result = await reclaimSharedStore(root, { gc: true });
// Without --force this is a preview: same selection, nothing deleted.
const result = await reclaimSharedStore(root, { gc: true, dryRun: !force });
console.log(
t('clean.gc.store', {
t(force ? 'clean.gc.store' : 'clean.gc.preview', {
path: root,
members: result.droppedMembers.length,
graphs: result.removed.length,
@ -186,6 +187,7 @@ const collectSharedStores = async (): Promise<void> => {
if (result.kept.length > 0) console.log(t('clean.shared.kept', { count: result.kept.length }));
if (result.storeRemoved) console.log(t('clean.shared.storeRemoved', { path: root }));
}
if (!force) console.log(`\n${t('common.runForceConfirm')}`);
};
export const cleanCommand = async (options?: {
@ -198,7 +200,7 @@ export const cleanCommand = async (options?: {
localIndex?: boolean;
}) => {
if (options?.gc) {
await collectSharedStores();
await collectSharedStores(options.force === true);
return;
}

View file

@ -71,6 +71,8 @@ export const en = {
'clean.gc.none': 'No shared stores to collect.',
'clean.gc.store':
'Shared store {{path}}: dropped {{members}} checkout(s), removed {{graphs}} commit graph(s).',
'clean.gc.preview':
'Shared store {{path}}: would drop {{members}} checkout(s) and remove {{graphs}} commit graph(s).',
'clean.localIndex.none': 'No leftover local index in this checkout.',
'clean.localIndex.preview':
'This will delete the leftover local index at {{path}} ({{size}}). The shared index is not affected.',

View file

@ -68,6 +68,7 @@ export const zhCN = {
'clean.shared.storeRemoved': '共享存储:已删除 {{path}}(没有剩余检出)。',
'clean.gc.none': '没有可回收的共享存储。',
'clean.gc.store': '共享存储 {{path}}:移除了 {{members}} 个检出,删除了 {{graphs}} 个提交图。',
'clean.gc.preview': '共享存储 {{path}}:将移除 {{members}} 个检出并删除 {{graphs}} 个提交图。',
'clean.localIndex.none': '此检出中没有残留的本地索引。',
'clean.localIndex.preview': '将删除 {{path}} 处残留的本地索引({{size}})。共享索引不受影响。',
'clean.localIndex.deleted': '已删除 {{path}} 处残留的本地索引({{size}})。',

View file

@ -358,7 +358,12 @@ export const statusCommand = async (options: StatusOptions = {}) => {
const sharedStore = storeRoot
? {
key: path.basename(storeRoot),
graph: describeSharedGraph(resolveGraphPath(repo.storagePath), repo.storagePath),
// A pinned branch index (`branches/<slug>/lbug`) is always private;
// only the flat slot can point at a shared commit graph.
graph:
activeMeta === repo.meta
? describeSharedGraph(resolveGraphPath(repo.storagePath), repo.storagePath)
: ('private' as const),
commit: activeMeta.lastCommit,
}
: null;

View file

@ -1325,8 +1325,11 @@ export async function runFullAnalysis(
// Leaving a store (`--no-share`, or sharing turned off): the up-to-date
// path does not re-register, so point the registry at the new storage
// before the old slot goes away.
await registerLeftStore(repoPath, writeTarget.storagePath);
if (slotToLeave) await leaveSharedStore(repoPath, slotToLeave, log);
if (slotToLeave) {
await leaveSharedStore(repoPath, slotToLeave, writeTarget.storagePath, log);
} else {
await registerLeftStore(repoPath, writeTarget.storagePath);
}
}
return result;
} finally {

View file

@ -19,7 +19,7 @@ import { createHash, randomUUID } from 'crypto';
import { constants as fsConstants } from 'fs';
import fs from 'fs/promises';
import path from 'path';
import { acquireIndexLock } from '../storage/index-lock.js';
import { acquireIndexLock, requireExclusiveIndexLock } from '../storage/index-lock.js';
import { commitDistanceToHead, getRemoteUrl, isWorkingTreeDirty } from '../storage/git.js';
import {
canonicalizePath,
@ -448,16 +448,25 @@ export const optedInSlotToLeave = async (repoPath: string): Promise<string | und
};
/**
* After a successful `--no-share` run re-registered the checkout at
* `<repo>/.gitnexus`: delete its old store slot and reclaim what only that
* slot referenced.
* After a successful `--no-share` run: re-register the checkout at its new
* storage and delete its old store slot, both under the old slot's index lock
* so an analyze still running on that slot cannot re-register it afterwards
* or write into a deleted directory. Then reclaim what only that slot used.
*/
export const leaveSharedStore = async (
repoPath: string,
previousSlot: string,
newStoragePath: string,
log: Log,
): Promise<void> => {
await fs.rm(previousSlot, { recursive: true, force: true });
const lock = await acquireIndexLock(previousSlot);
try {
requireExclusiveIndexLock(lock, `Cannot acquire the index lock at ${previousSlot}.`);
await registerLeftStore(repoPath, newStoragePath);
await fs.rm(previousSlot, { recursive: true, force: true });
} finally {
lock.release();
}
await removeSharedStorePointer(repoPath);
await reclaimAfterSlotRemoval(previousSlot);
log(`Shared store: left ${previousSlot}.`);

View file

@ -8,7 +8,11 @@
* CORS is restricted to localhost, private/LAN networks, and the deployed site.
*/
import { acquireIndexLock, requireExclusiveIndexLock } from '../storage/index-lock.js';
import {
acquireIndexLock,
requireExclusiveIndexLock,
type IndexLockHandle,
} from '../storage/index-lock.js';
import { ensurePrivateSharedGraph } from '../core/shared-store-analyze.js';
import { resolveGraphPath } from '../storage/shared-store.js';
import { reclaimAfterSlotRemoval } from '../storage/shared-store-lifecycle.js';
@ -2130,22 +2134,20 @@ export const createServer = async (port: number, host: string = '127.0.0.1') =>
// Set inside withLbugDb, read after it closes (#2790).
let partialRunError: string | undefined;
let partialRunDetail: AnalyzeJobPartialOutcome | undefined;
// The in-memory repo lock only serializes this server; a CLI analyze
// in another process guards the slot with the index lock, so hold it
// for the whole embedding write, released in the finally below.
let slotLock: IndexLockHandle | undefined;
try {
slotLock = await acquireIndexLock(storagePath);
requireExclusiveIndexLock(
slotLock,
`Cannot acquire the index lock at ${storagePath}; refusing an unlocked embedding run.`,
);
// Writes go to the slot's own graph; a shared-store checkout
// reading an immutable commit graph (#3352) takes a private copy.
// The in-memory repo lock only serializes this server; a CLI
// analyze in another process guards the slot with the index lock.
const slotLock = await acquireIndexLock(storagePath);
try {
requireExclusiveIndexLock(
slotLock,
`Cannot acquire the index lock at ${storagePath}; refusing to copy the shared graph.`,
);
if (!(await ensurePrivateSharedGraph(storagePath, () => {}))) {
throw new Error('The shared graph this repository reads is gone. Re-run analyze.');
}
} finally {
slotLock.release();
if (!(await ensurePrivateSharedGraph(storagePath, () => {}))) {
throw new Error('The shared graph this repository reads is gone. Re-run analyze.');
}
const lbugPath = path.join(storagePath, LBUG_DIRECTORY);
const ftsSession = await loadFtsSession(storagePath);
@ -2380,6 +2382,7 @@ export const createServer = async (port: number, host: string = '127.0.0.1') =>
});
}
} finally {
slotLock?.release();
clearTimeout(embedTimeout);
releaseRepoLock(repoLockPath);
}

View file

@ -214,9 +214,11 @@ const GITNEXUS_EXCLUDE_ENTRY = `${GITNEXUS_DIR}/`;
* across branches (#2106 KTD7). When `branch` is provided, both `lbugPath`
* and `metaPath` are scoped under `branches/<slug>/`. For the flat call
* (no `branch`), `storagePath` and `lbugPath` remain byte-identical to the
* pre-multi-branch behavior (#2106); `metaPath`'s FILENAME changed from
* `meta.json` to `gitnexus.json` (PR #2363) — `saveMeta` keeps a `meta.json`
* mirror in sync for consumers that still read the legacy name.
* pre-multi-branch behavior (#2106), except that a shared-store checkout slot
* (#3352) returns the commit graph its metadata records (`resolveGraphPath`).
* `metaPath`'s FILENAME changed from `meta.json` to `gitnexus.json`
* (PR #2363) — `saveMeta` keeps a `meta.json` mirror in sync for consumers
* that still read the legacy name.
*
* Each branch slot has its own metadata file:
* - Primary/flat: <repo>/.gitnexus/gitnexus.json

View file

@ -95,7 +95,7 @@ const orphanMembers = async (slots: string[]): Promise<Set<string>> => {
*/
export const reclaimSharedStoreLocked = async (
storeRoot: string,
opts: { gc?: boolean } = {},
opts: { gc?: boolean; dryRun?: boolean } = {},
): Promise<ReclaimResult> => {
const result: ReclaimResult = { removed: [], kept: [], droppedMembers: [], storeRemoved: false };
const checkoutsDir = path.join(storeRoot, 'checkouts');
@ -105,7 +105,7 @@ export const reclaimSharedStoreLocked = async (
if (opts.gc) {
const orphans = await orphanMembers(slots);
for (const slot of orphans) {
await fs.rm(slot, { recursive: true, force: true });
if (!opts.dryRun) await fs.rm(slot, { recursive: true, force: true });
result.droppedMembers.push(slot);
}
slots = slots.filter((slot) => !orphans.has(slot));
@ -118,6 +118,10 @@ export const reclaimSharedStoreLocked = async (
for (const name of await listDir(commitsDir)) {
const dir = path.join(commitsDir, name);
if (referenced.has(dir)) continue;
if (opts.dryRun) {
if (!name.startsWith('.')) result.removed.push(dir);
continue;
}
try {
await fs.rm(dir, { recursive: true, force: true });
if (!name.startsWith('.')) result.removed.push(dir);
@ -129,7 +133,7 @@ export const reclaimSharedStoreLocked = async (
}
const remaining = (await listDir(checkoutsDir)).length + (await listDir(commitsDir)).length;
if (remaining === 0) {
if (remaining === 0 && !opts.dryRun) {
// The lock directory lives inside the store; removing it while held is
// safe on POSIX and is retried on the next reclaim elsewhere.
await fs
@ -149,7 +153,7 @@ export const reclaimSharedStoreLocked = async (
*/
export const reclaimSharedStore = async (
storeRoot: string,
opts: { gc?: boolean } = {},
opts: { gc?: boolean; dryRun?: boolean } = {},
): Promise<ReclaimResult> => {
if (!existsSync(storeRoot)) {
return { removed: [], kept: [], droppedMembers: [], storeRemoved: false };

View file

@ -196,9 +196,10 @@ export const commitGraphDir = (
return path.join(layout.commitsDir, `${commit}-${featureKey}`);
};
// A single path segment: `..repo-<hash>` is a legal slot name, `..` is not.
const isDirectChild = (parent: string, child: string): boolean => {
const rel = path.relative(parent, child);
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel) && !rel.includes(path.sep);
return rel !== '' && rel !== '..' && !path.isAbsolute(rel) && !rel.includes(path.sep);
};
/**

View file

@ -3,7 +3,7 @@
*
* Kept free of imports from `storage-resolver.ts` and `shared-store.ts` so both
* can use them without importing each other (#3352). `storage-resolver.ts`
* re-exports everything here, so existing import sites are unchanged.
* re-exports the two env-var names, so existing import sites are unchanged.
*/
import { createHash } from 'node:crypto';
@ -25,7 +25,8 @@ const sanitizeSlotBasename = (value: string): string => {
end--;
}
const candidate = sanitized.slice(0, end) || 'repository';
return /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(candidate)
// Windows reserves device names with any extension too (`CON.txt`).
return /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i.test(candidate)
? `repository-${candidate}`
: candidate;
};

View file

@ -152,6 +152,17 @@ describe('shared store adoption and reporting (#3352)', () => {
expect((await statusJson(wt)).status).toBe('up-to-date');
}, 240_000);
it('status reports a pinned branch index as private even when the flat slot is shared', async () => {
const { runFullAnalysis } = await import('../../src/core/run-analyze.js');
await analyze(wt);
// The flat slot now holds `wt` and points at a shared commit graph. A
// different checked-out branch pinned with --branch gets its own index.
git(wt, 'checkout', '-q', '-b', 'pinned');
await runFullAnalysis(wt, { branch: 'pinned' }, { onProgress: () => {} });
const json = await statusJson(wt);
expect(json.sharedStore).toMatchObject({ graph: 'private' });
}, 240_000);
it('status reports a private graph for an edited worktree', async () => {
await analyze(wt);
await fs.writeFile(path.join(wt, 'a.ts'), 'export function alphaEdited() { return 1; }\n');

View file

@ -2,7 +2,7 @@ import { execFileSync } from 'child_process';
import { existsSync } from 'fs';
import fs from 'fs/promises';
import path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { featureKeyOf, publishSharedGraph } from '../../src/core/shared-store-analyze.js';
import {
getStoragePaths,
@ -18,6 +18,16 @@ import {
} from '../../src/storage/shared-store.js';
import { createTempDir } from '../helpers/test-db.js';
// These suites exercise sharing; an inherited opt-out would silently disable it.
const savedSharedStoreSwitch = process.env.GITNEXUS_SHARED_STORE;
beforeAll(() => {
delete process.env.GITNEXUS_SHARED_STORE;
});
afterAll(() => {
if (savedSharedStoreSwitch === undefined) delete process.env.GITNEXUS_SHARED_STORE;
else process.env.GITNEXUS_SHARED_STORE = savedSharedStoreSwitch;
});
/**
* #3352 — linked worktrees at one commit share one immutable commit graph in
* the store under GITNEXUS_HOME, and a second worktree's analyze reuses it

View file

@ -2,7 +2,7 @@ import { execFileSync } from 'child_process';
import { existsSync } from 'fs';
import fs from 'fs/promises';
import path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { withStoreLock } from '../../src/storage/shared-store-lifecycle.js';
import { loadMeta } from '../../src/storage/repo-manager.js';
import {
@ -12,6 +12,16 @@ import {
} from '../../src/storage/shared-store.js';
import { createTempDir } from '../helpers/test-db.js';
// These suites exercise sharing; an inherited opt-out would silently disable it.
const savedSharedStoreSwitch = process.env.GITNEXUS_SHARED_STORE;
beforeAll(() => {
delete process.env.GITNEXUS_SHARED_STORE;
});
afterAll(() => {
if (savedSharedStoreSwitch === undefined) delete process.env.GITNEXUS_SHARED_STORE;
else process.env.GITNEXUS_SHARED_STORE = savedSharedStoreSwitch;
});
/**
* #3352 U5 — linked worktrees keep one parse cache and ParsedFile store per
* shared store, and one member's prune never evicts chunks another member

View file

@ -2,7 +2,7 @@ import { execFileSync } from 'child_process';
import { existsSync } from 'fs';
import fs from 'fs/promises';
import path from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { getStoragePaths, loadMeta, saveMeta } from '../../src/storage/repo-manager.js';
import {
resolveSharedStore,
@ -15,6 +15,16 @@ import {
} from '../../src/storage/shared-store-lifecycle.js';
import { createTempDir } from '../helpers/test-db.js';
// These suites exercise sharing; an inherited opt-out would silently disable it.
const savedSharedStoreSwitch = process.env.GITNEXUS_SHARED_STORE;
beforeAll(() => {
delete process.env.GITNEXUS_SHARED_STORE;
});
afterAll(() => {
if (savedSharedStoreSwitch === undefined) delete process.env.GITNEXUS_SHARED_STORE;
else process.env.GITNEXUS_SHARED_STORE = savedSharedStoreSwitch;
});
/**
* #3352 U6 — clean removes only what no remaining member references.
*/
@ -123,6 +133,21 @@ describe('shared store clean (#3352)', () => {
expect(await commitDirs(layout)).toHaveLength(1);
}, 240_000);
it('clean --gc without --force previews and deletes nothing', async () => {
await analyze(wtA);
await fs.writeFile(path.join(wtB, 'b.ts'), 'export function beta() { return 2; }\n');
commitAll(wtB, 'b');
await analyze(wtB);
const slotB = layoutOf(wtB).checkoutSlot;
git(main, 'worktree', 'remove', '--force', wtB);
const logs = await cleanIn(main, { gc: true });
expect(existsSync(slotB)).toBe(true);
expect(await commitDirs(layoutOf(wtA))).toHaveLength(2);
expect(logs.join('\n')).toMatch(/would drop 1 checkout\(s\) and remove 1 commit graph/);
}, 240_000);
it('clean --gc drops a deleted worktree and the graph only it referenced', async () => {
await analyze(wtA);
await fs.writeFile(path.join(wtB, 'b.ts'), 'export function beta() { return 2; }\n');
@ -133,7 +158,7 @@ describe('shared store clean (#3352)', () => {
expect(await commitDirs(layout)).toHaveLength(2);
git(main, 'worktree', 'remove', '--force', wtB);
const logs = await cleanIn(main, { gc: true });
const logs = await cleanIn(main, { gc: true, force: true });
expect(existsSync(slotB)).toBe(false);
expect(await commitDirs(layout)).toHaveLength(1);

View file

@ -2,11 +2,21 @@ import { execFileSync } from 'child_process';
import { existsSync } from 'fs';
import fs from 'fs/promises';
import path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { getStoragePaths, listRegisteredRepos } from '../../src/storage/repo-manager.js';
import { resolveSharedStore, type SharedStoreLayout } from '../../src/storage/shared-store.js';
import { createTempDir } from '../helpers/test-db.js';
// These suites exercise sharing; an inherited opt-out would silently disable it.
const savedSharedStoreSwitch = process.env.GITNEXUS_SHARED_STORE;
beforeAll(() => {
delete process.env.GITNEXUS_SHARED_STORE;
});
afterAll(() => {
if (savedSharedStoreSwitch === undefined) delete process.env.GITNEXUS_SHARED_STORE;
else process.env.GITNEXUS_SHARED_STORE = savedSharedStoreSwitch;
});
/**
* #3352 U7 — an independent clone joins a shared store only by explicit
* opt-in, and only when its remote matches the member it names.

View file

@ -2,12 +2,22 @@ import { execFileSync } from 'child_process';
import { existsSync } from 'fs';
import fs from 'fs/promises';
import path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { ensurePrivateSharedGraph } from '../../src/core/shared-store-analyze.js';
import { getStoragePaths, loadMeta, saveMeta } from '../../src/storage/repo-manager.js';
import { resolveSharedStore, type SharedStoreLayout } from '../../src/storage/shared-store.js';
import { createTempDir } from '../helpers/test-db.js';
// These suites exercise sharing; an inherited opt-out would silently disable it.
const savedSharedStoreSwitch = process.env.GITNEXUS_SHARED_STORE;
beforeAll(() => {
delete process.env.GITNEXUS_SHARED_STORE;
});
afterAll(() => {
if (savedSharedStoreSwitch === undefined) delete process.env.GITNEXUS_SHARED_STORE;
else process.env.GITNEXUS_SHARED_STORE = savedSharedStoreSwitch;
});
/**
* #3352 U4 — a checkout that needs its own graph is seeded from the nearest
* commit graph and updated incrementally instead of rebuilt from scratch.
@ -274,7 +284,8 @@ describe('ensurePrivateSharedGraph', () => {
const meta = await loadMeta(slot);
delete meta?.graphPath;
await saveMeta(slot, meta as NonNullable<typeof meta>);
await fs.writeFile(path.join(slot, 'lbug'), 'private graph bytes');
expect(await ensurePrivateSharedGraph(slot, () => {})).toBe(true);
expect(existsSync(path.join(slot, 'lbug'))).toBe(false);
expect(await fs.readFile(path.join(slot, 'lbug'), 'utf-8')).toBe('private graph bytes');
});
});

View file

@ -314,3 +314,29 @@ describe('resolveStoragePath store tier', () => {
expect(resolveStoragePath(wts[0])).toBe(registered);
});
});
describe('slot naming edge cases (#3352 review)', () => {
it.each(['CON.txt', 'com1.log', 'Lpt9.tar.gz'])(
'prefixes a reserved Windows device name with an extension: %s',
(base) => {
expect(storageSlotName(path.join(path.sep, 'tmp', base))).toMatch(
new RegExp(`^repository-${base.replace('.', '\\.')}-[0-9a-f]{12}$`),
);
},
);
it('keeps an ordinary name that only starts like a device name', () => {
expect(storageSlotName(path.join(path.sep, 'tmp', 'console'))).toMatch(/^console-/);
});
it('accepts a checkout slot whose name starts with two dots', async () => {
const layout = sharedStoreLayout('..repo-0123456789ab', '/tmp/..checkout');
const graph = path.join(commitGraphDir(layout, 'abc1234', 'deadbeef'), 'lbug');
await fs.mkdir(layout.checkoutSlot, { recursive: true });
await fs.writeFile(
path.join(layout.checkoutSlot, 'gitnexus.json'),
JSON.stringify({ graphPath: graph }),
);
expect(resolveGraphPath(layout.checkoutSlot)).toBe(graph);
});
});

View file

@ -98,6 +98,14 @@ export default defineConfig({
'test/integration/lbug-orphan-sidecar-recovery.test.ts',
'test/integration/lbug-interrupted-checkpoint-recovery.test.ts',
'test/integration/lbug-readonly-init.test.ts',
// Shared sibling store (#3352): each file runs real analyses and opens
// the resulting LadybugDB graphs.
'test/integration/shared-store-analyze.test.ts',
'test/integration/shared-store-seed.test.ts',
'test/integration/shared-store-cache.test.ts',
'test/integration/shared-store-clean.test.ts',
'test/integration/shared-store-clone-optin.test.ts',
'test/integration/shared-store-adoption.test.ts',
'test/integration/analyze-wal-checkpoint-failure.test.ts',
'test/integration/lbug-non-ascii-path.test.ts',
'test/integration/lbug-conn-serialization.test.ts',
@ -202,6 +210,12 @@ export default defineConfig({
// Excluded here because it is included by `lbug-db` above; a file
// in two projects would be collected (and run) twice.
'test/unit/incremental-index-extension-dml-gate.test.ts',
'test/integration/shared-store-analyze.test.ts',
'test/integration/shared-store-seed.test.ts',
'test/integration/shared-store-cache.test.ts',
'test/integration/shared-store-clean.test.ts',
'test/integration/shared-store-clone-optin.test.ts',
'test/integration/shared-store-adoption.test.ts',
],
},
},