mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
fix(storage): preserve Windows deletion and checkpoint contracts
This commit is contained in:
parent
81a47c8a78
commit
676f674dc9
3 changed files with 47 additions and 2 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue