fix(storage): preserve Windows deletion and checkpoint contracts

This commit is contained in:
azizur100389 2026-09-15 18:06:34 +01:00
parent 81a47c8a78
commit 676f674dc9
3 changed files with 47 additions and 2 deletions

View file

@ -690,6 +690,20 @@ export const requireDeletableStoragePath = async (entry: {
const expectedStoragePath = defaultStoragePath(repoPath);
const storageIsLocal = isRepositoryLocalStoragePath(repoPath, actualStoragePath);
// Lookup may normalize extended-length paths, but deletion keeps the legacy
// fail-closed rule for a local registry entry with mixed namespace spellings.
if (
process.platform === 'win32' &&
storageIsLocal &&
expectedStoragePath.startsWith('\\\\?\\') !== actualStoragePath.startsWith('\\\\?\\')
) {
throw new StorageDeletionError(
expectedStoragePath,
actualStoragePath,
undefined,
'repository-local storage must use the same extended-length path spelling as the repository',
);
}
const comparableStorage = comparablePath(actualStoragePath);
const comparableRepo = comparablePath(repoPath);
const comparableRoot = comparablePath(path.parse(actualStoragePath).root);

View file

@ -74,6 +74,7 @@ import {
type RegistryEntry,
} from '../../src/storage/repo-manager.js';
import { resolveRegisteredRepoEntry } from '../../src/server/api.js';
import { requireDeletableStoragePath } from '../../src/storage/storage-resolver.js';
/**
* The lookup every registry consumer performs — `resolveRegistryEntry`,
@ -164,6 +165,19 @@ describe('assertSafeStoragePath vs the `\\\\?\\` prefix (#2667)', () => {
assertSafeStoragePath({ ...base, storagePath: 'D:\\Projects\\repo\\.gitnexus' }),
).rejects.toThrow();
});
itOnWindows.each([
['D:\\Projects\\repo', '\\\\?\\D:\\Projects\\repo\\.gitnexus'],
['\\\\server\\share\\repo', '\\\\?\\UNC\\server\\share\\repo\\.gitnexus'],
['\\\\?\\UNC\\server\\share\\repo', '\\\\server\\share\\repo\\.gitnexus'],
])(
'rejects mixed namespace spellings in the shared deletion guard: %s',
async (repoPath, storagePath) => {
await expect(requireDeletableStoragePath({ path: repoPath, storagePath })).rejects.toThrow(
'same extended-length path spelling',
);
},
);
});
// The consumer surface the fix exists for: an MCP `repo` argument or an

View file

@ -1909,6 +1909,7 @@ describe('runFullAnalysis embedding-checkpoint meta write (#2790)', () => {
});
it('preserves lastCommit / fileHashes / the dirty flag, and never restates a stale count', async () => {
vi.stubEnv('GITNEXUS_ATOMIC_WINDOWS_SWAP', '0');
const STALE_COMMIT = '1111111111111111111111111111111111111111';
const STALE_HASHES = { 'src/app.ts': 'stale-hash' };
const LIVE_EMBEDDING_COUNT = 42;
@ -2000,6 +2001,7 @@ describe('runFullAnalysis embedding-checkpoint meta write (#2790)', () => {
_existingEmbeddings: unknown,
pipelineOptions: EmbeddingPipelineOptions,
): Promise<EmbeddingPipelineResult> => {
snapshots.beforeCheckpoint = await loadMeta(storagePath);
// Window 1 — fires before ANY embedding row exists.
await pipelineOptions.onCheckpointWindowStart?.({
nodesProcessed: 0,
@ -2047,6 +2049,21 @@ describe('runFullAnalysis embedding-checkpoint meta write (#2790)', () => {
expect(runEmbeddingPipeline).toHaveBeenCalledTimes(1);
// Windows writes in place and advances the dirty marker to FTS; a
// staged rebuild retains full-rebuild until publication. Neither marker
// may be changed or cleared by an embedding checkpoint.
const dirtyBeforeCheckpoint = snapshots.beforeCheckpoint?.incrementalInProgress;
expect(dirtyBeforeCheckpoint).toMatchObject({
phase: process.platform === 'win32' ? 'fts' : 'full-rebuild',
});
for (const snapshot of [
snapshots.windowStart,
snapshots.postWindow,
snapshots.secondWindow,
]) {
expect(snapshot?.incrementalInProgress).toEqual(dirtyBeforeCheckpoint);
}
// ── Window 1: the checkpoint landed… ──────────────────────────────
expect(snapshots.windowStart).toMatchObject({
embeddingCheckpoint: {
@ -2063,7 +2080,7 @@ describe('runFullAnalysis embedding-checkpoint meta write (#2790)', () => {
expect(snapshots.windowStart).toMatchObject({
lastCommit: STALE_COMMIT,
fileHashes: STALE_HASHES,
incrementalInProgress: { phase: 'full-rebuild' },
incrementalInProgress: dirtyBeforeCheckpoint,
stats: { embeddings: 7 },
});
expect(snapshots.windowStart?.lastCommit).not.toBe(currentCommit);
@ -2072,7 +2089,7 @@ describe('runFullAnalysis embedding-checkpoint meta write (#2790)', () => {
expect(snapshots.postWindow).toMatchObject({
lastCommit: STALE_COMMIT,
fileHashes: STALE_HASHES,
incrementalInProgress: { phase: 'full-rebuild' },
incrementalInProgress: dirtyBeforeCheckpoint,
stats: { embeddings: LIVE_EMBEDDING_COUNT },
});