153 lines
5.1 KiB
TypeScript
153 lines
5.1 KiB
TypeScript
/**
|
|
* Tests for atomic package update (overwrite install).
|
|
*
|
|
* Covers:
|
|
* - cleanupResiduals: pre-existing .tmp/ and .bak/ are removed before install
|
|
* - atomicReplace: normal success path (new → .bak → rename)
|
|
* - atomicReplace: validation failure → .tmp/ is removed, existing package preserved
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import {
|
|
cleanupResiduals,
|
|
atomicReplace,
|
|
type AtomicReplaceOptions,
|
|
} from '../../features/repertoire/atomic-update.js';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// cleanupResiduals
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('cleanupResiduals', () => {
|
|
let tempDir: string;
|
|
|
|
beforeEach(() => {
|
|
tempDir = mkdtempSync(join(tmpdir(), 'takt-atomic-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('should remove pre-existing .tmp directory', () => {
|
|
// Given: a .tmp directory remains from a previous failed install
|
|
const packageDir = join(tempDir, 'takt-fullstack');
|
|
const tmpDir = join(tempDir, 'takt-fullstack.tmp');
|
|
mkdirSync(packageDir, { recursive: true });
|
|
mkdirSync(tmpDir, { recursive: true });
|
|
writeFileSync(join(tmpDir, 'stale.yaml'), 'stale');
|
|
|
|
// When: cleanup is performed
|
|
cleanupResiduals(packageDir);
|
|
|
|
// Then: .tmp directory is removed
|
|
expect(existsSync(tmpDir)).toBe(false);
|
|
});
|
|
|
|
it('should remove pre-existing .bak directory', () => {
|
|
// Given: a .bak directory remains from a previous failed install
|
|
const packageDir = join(tempDir, 'takt-fullstack');
|
|
const bakDir = join(tempDir, 'takt-fullstack.bak');
|
|
mkdirSync(packageDir, { recursive: true });
|
|
mkdirSync(bakDir, { recursive: true });
|
|
writeFileSync(join(bakDir, 'old.yaml'), 'old');
|
|
|
|
// When: cleanup is performed
|
|
cleanupResiduals(packageDir);
|
|
|
|
// Then: .bak directory is removed
|
|
expect(existsSync(bakDir)).toBe(false);
|
|
});
|
|
|
|
it('should succeed even when neither .tmp nor .bak exist', () => {
|
|
// Given: no residual directories
|
|
const packageDir = join(tempDir, 'takt-fullstack');
|
|
mkdirSync(packageDir, { recursive: true });
|
|
|
|
// When: cleanup is performed
|
|
// Then: no error thrown
|
|
expect(() => cleanupResiduals(packageDir)).not.toThrow();
|
|
});
|
|
|
|
it('should remove both .tmp and .bak when both exist', () => {
|
|
// Given: both residuals exist
|
|
const packageDir = join(tempDir, 'takt-fullstack');
|
|
const tmpDirPath = join(tempDir, 'takt-fullstack.tmp');
|
|
const bakDir = join(tempDir, 'takt-fullstack.bak');
|
|
mkdirSync(packageDir, { recursive: true });
|
|
mkdirSync(tmpDirPath, { recursive: true });
|
|
mkdirSync(bakDir, { recursive: true });
|
|
|
|
// When: cleanup is performed
|
|
cleanupResiduals(packageDir);
|
|
|
|
// Then: both are removed
|
|
expect(existsSync(tmpDirPath)).toBe(false);
|
|
expect(existsSync(bakDir)).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// atomicReplace
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('atomicReplace', () => {
|
|
let tempDir: string;
|
|
|
|
beforeEach(() => {
|
|
tempDir = mkdtempSync(join(tmpdir(), 'takt-atomic-replace-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('should replace existing package and delete .bak on success', async () => {
|
|
// Given: an existing package directory
|
|
const packageDir = join(tempDir, 'takt-fullstack');
|
|
mkdirSync(packageDir, { recursive: true });
|
|
writeFileSync(join(packageDir, 'old.yaml'), 'old content');
|
|
|
|
const options: AtomicReplaceOptions = {
|
|
packageDir,
|
|
install: async () => {
|
|
// Simulate successful install into packageDir
|
|
writeFileSync(join(packageDir, 'new.yaml'), 'new content');
|
|
},
|
|
};
|
|
|
|
// When: atomicReplace is executed
|
|
await atomicReplace(options);
|
|
|
|
// Then: new content is in place, .bak is cleaned up
|
|
expect(existsSync(join(packageDir, 'new.yaml'))).toBe(true);
|
|
expect(existsSync(join(tempDir, 'takt-fullstack.bak'))).toBe(false);
|
|
});
|
|
|
|
it('should preserve existing package when install throws (validation failure)', async () => {
|
|
// Given: an existing package with content
|
|
const packageDir = join(tempDir, 'takt-fullstack');
|
|
mkdirSync(packageDir, { recursive: true });
|
|
writeFileSync(join(packageDir, 'existing.yaml'), 'existing');
|
|
|
|
const options: AtomicReplaceOptions = {
|
|
packageDir,
|
|
install: async () => {
|
|
// Simulate validation failure
|
|
throw new Error('Validation failed: empty package');
|
|
},
|
|
};
|
|
|
|
// When: atomicReplace is executed with a failing install
|
|
await expect(atomicReplace(options)).rejects.toThrow('Validation failed');
|
|
|
|
// Then: existing package is preserved
|
|
expect(existsSync(join(packageDir, 'existing.yaml'))).toBe(true);
|
|
// .tmp directory should be cleaned up
|
|
expect(existsSync(join(tempDir, 'takt-fullstack.tmp'))).toBe(false);
|
|
});
|
|
});
|