mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
88 lines
3.3 KiB
TypeScript
88 lines
3.3 KiB
TypeScript
/**
|
|
* Unit tests: removeCommand deletion order (PR #2363 review fix, F14)
|
|
*
|
|
* The documented contract (remove.ts header): fs.rm FIRST, then unregister.
|
|
* A partial failure leaves the registry entry in place so the user can
|
|
* retry (and `listRegisteredRepos({ validate: true })` self-heals a
|
|
* rm-succeeded/unregister-failed orphan) — the registry must never be
|
|
* unregistered while index files may still remain on disk.
|
|
*/
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import path from 'node:path';
|
|
|
|
const mockRm = vi.fn();
|
|
const mockReadRegistry = vi.fn();
|
|
const mockResolveRegistryEntry = vi.fn();
|
|
const mockAssertSafeStoragePath = vi.fn();
|
|
const mockUnregisterRepo = vi.fn();
|
|
|
|
vi.mock('fs/promises', () => ({
|
|
default: {
|
|
rm: mockRm,
|
|
},
|
|
}));
|
|
|
|
vi.mock('../../src/storage/repo-manager.js', () => ({
|
|
readRegistry: mockReadRegistry,
|
|
resolveRegistryEntry: mockResolveRegistryEntry,
|
|
assertSafeStoragePath: mockAssertSafeStoragePath,
|
|
unregisterRepo: mockUnregisterRepo,
|
|
RegistryNotFoundError: class RegistryNotFoundError extends Error {},
|
|
RegistryAmbiguousTargetError: class RegistryAmbiguousTargetError extends Error {},
|
|
UnsafeStoragePathError: class UnsafeStoragePathError extends Error {},
|
|
}));
|
|
|
|
describe('removeCommand', () => {
|
|
const repoPath = path.resolve('/repo');
|
|
const entry = {
|
|
name: 'repo',
|
|
path: repoPath,
|
|
storagePath: path.join(repoPath, '.gitnexus'),
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.restoreAllMocks();
|
|
process.exitCode = undefined;
|
|
|
|
mockReadRegistry.mockResolvedValue([entry]);
|
|
mockResolveRegistryEntry.mockReturnValue(entry);
|
|
mockAssertSafeStoragePath.mockReturnValue(undefined);
|
|
mockRm.mockResolvedValue(undefined);
|
|
mockUnregisterRepo.mockResolvedValue(undefined);
|
|
});
|
|
|
|
it('removes the whole .gitnexus/ directory recursively, then unregisters', async () => {
|
|
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
|
|
const { removeCommand } = await import('../../src/cli/remove.js');
|
|
await removeCommand('repo', { force: true });
|
|
|
|
expect(mockRm).toHaveBeenCalledWith(entry.storagePath, { recursive: true, force: true });
|
|
expect(mockUnregisterRepo).toHaveBeenCalledWith(entry.path);
|
|
// rm strictly precedes unregister (retryable partial-failure contract).
|
|
expect(mockRm.mock.invocationCallOrder[0]).toBeLessThan(
|
|
mockUnregisterRepo.mock.invocationCallOrder[0],
|
|
);
|
|
// No pre-unlink of individual metadata files — fs.rm removes both
|
|
// gitnexus.json and the legacy meta.json mirror with the directory.
|
|
expect(mockRm).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('does NOT unregister when fs.rm fails (entry stays for retry)', async () => {
|
|
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
const exitSpy = vi
|
|
.spyOn(process, 'exit')
|
|
.mockImplementation((() => undefined) as unknown as typeof process.exit);
|
|
const err = new Error('EBUSY: resource busy') as NodeJS.ErrnoException;
|
|
err.code = 'EBUSY';
|
|
mockRm.mockRejectedValue(err);
|
|
|
|
const { removeCommand } = await import('../../src/cli/remove.js');
|
|
await removeCommand('repo', { force: true });
|
|
|
|
expect(mockUnregisterRepo).not.toHaveBeenCalled();
|
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
});
|
|
});
|