After the DC-005 module reorganization (41 files moved into src/ subdirs),
138 test suites failed because the refactor script's path-rewrite logic
missed three categories:
1. Files inside src/ doing 'require("./src/...")' — should be 'require("../...")'
2. Files in src/X/Y/ doing 'require("../../../src/...")' — should be 'require("../../...")'
3. Test files in __tests__/ with leftover 'require("../../../src/...")' paths
Root cause: the original refactor script ran before all files were moved,
so it computed relative paths against stale filesystem state.
Result:
- 30/30 test suites pass
- 879/879 tests pass (was: 18/30 suites, 614/687 tests)
Also fixed:
- routes/apps/restore.js: wrong responses import path
- routes/*/*.js: '../../src/utilities/X' → '../src/utilities/X' (depth 2 routes)
214 lines
7.0 KiB
JavaScript
214 lines
7.0 KiB
JavaScript
jest.mock('proper-lockfile');
|
|
jest.mock('fs', () => ({
|
|
existsSync: jest.fn().mockReturnValue(true),
|
|
mkdirSync: jest.fn(),
|
|
writeFileSync: jest.fn(),
|
|
promises: {
|
|
readFile: jest.fn().mockResolvedValue('[]'),
|
|
writeFile: jest.fn().mockResolvedValue(),
|
|
},
|
|
}));
|
|
|
|
const lockfile = require('proper-lockfile');
|
|
const fs = require('fs');
|
|
const StateManager = require('../src/managers/state-manager');
|
|
|
|
describe('StateManager', () => {
|
|
let sm;
|
|
const TEST_PATH = '/tmp/test-state.json';
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
fs.existsSync.mockReturnValue(true);
|
|
fs.promises.readFile.mockResolvedValue('[]');
|
|
fs.promises.writeFile.mockResolvedValue();
|
|
lockfile.lock.mockResolvedValue(jest.fn().mockResolvedValue());
|
|
lockfile.check.mockResolvedValue(false);
|
|
lockfile.unlock.mockResolvedValue();
|
|
|
|
sm = new StateManager(TEST_PATH);
|
|
});
|
|
|
|
describe('constructor', () => {
|
|
it('creates file with [] if it does not exist', () => {
|
|
fs.existsSync.mockReturnValue(false);
|
|
new StateManager('/tmp/new-state.json');
|
|
expect(fs.writeFileSync).toHaveBeenCalledWith('/tmp/new-state.json', '[]', 'utf8');
|
|
});
|
|
|
|
it('creates directory recursively if needed', () => {
|
|
fs.existsSync.mockReturnValue(false);
|
|
new StateManager('/tmp/deep/nested/state.json');
|
|
expect(fs.mkdirSync).toHaveBeenCalledWith(expect.any(String), { recursive: true });
|
|
});
|
|
|
|
it('does not create file if it exists', () => {
|
|
fs.existsSync.mockReturnValue(true);
|
|
fs.writeFileSync.mockClear();
|
|
new StateManager(TEST_PATH);
|
|
expect(fs.writeFileSync).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('read', () => {
|
|
it('returns parsed JSON from file', async () => {
|
|
fs.promises.readFile.mockResolvedValue(JSON.stringify([{ id: 'svc1' }]));
|
|
const data = await sm.read();
|
|
expect(data).toEqual([{ id: 'svc1' }]);
|
|
});
|
|
|
|
it('returns [] and recreates file on ENOENT', async () => {
|
|
const err = new Error('ENOENT');
|
|
err.code = 'ENOENT';
|
|
fs.promises.readFile.mockRejectedValue(err);
|
|
fs.existsSync.mockReturnValue(false);
|
|
|
|
const data = await sm.read();
|
|
expect(data).toEqual([]);
|
|
});
|
|
|
|
it('throws on invalid JSON', async () => {
|
|
fs.promises.readFile.mockResolvedValue('{bad json}');
|
|
await expect(sm.read()).rejects.toThrow('Failed to read state file');
|
|
});
|
|
});
|
|
|
|
describe('write', () => {
|
|
it('acquires lock, writes JSON, releases lock', async () => {
|
|
const releaseFn = jest.fn().mockResolvedValue();
|
|
lockfile.lock.mockResolvedValue(releaseFn);
|
|
|
|
await sm.write([{ id: 'new' }]);
|
|
|
|
expect(lockfile.lock).toHaveBeenCalledWith(TEST_PATH, expect.any(Object));
|
|
expect(fs.promises.writeFile).toHaveBeenCalledWith(
|
|
TEST_PATH,
|
|
JSON.stringify([{ id: 'new' }], null, 2),
|
|
'utf8'
|
|
);
|
|
expect(releaseFn).toHaveBeenCalled();
|
|
});
|
|
|
|
it('throws on ELOCKED', async () => {
|
|
const err = new Error('locked');
|
|
err.code = 'ELOCKED';
|
|
lockfile.lock.mockRejectedValue(err);
|
|
|
|
await expect(sm.write([])).rejects.toThrow('locked by another process');
|
|
});
|
|
|
|
it('releases lock even on write error', async () => {
|
|
const releaseFn = jest.fn().mockResolvedValue();
|
|
lockfile.lock.mockResolvedValue(releaseFn);
|
|
fs.promises.writeFile.mockRejectedValue(new Error('disk full'));
|
|
|
|
await expect(sm.write([])).rejects.toThrow();
|
|
expect(releaseFn).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('update', () => {
|
|
it('atomic read-modify-write cycle', async () => {
|
|
const releaseFn = jest.fn().mockResolvedValue();
|
|
lockfile.lock.mockResolvedValue(releaseFn);
|
|
fs.promises.readFile.mockResolvedValue(JSON.stringify([{ id: '1' }]));
|
|
|
|
const result = await sm.update(items => {
|
|
items.push({ id: '2' });
|
|
return items;
|
|
});
|
|
|
|
expect(result).toEqual([{ id: '1' }, { id: '2' }]);
|
|
expect(fs.promises.writeFile).toHaveBeenCalled();
|
|
expect(releaseFn).toHaveBeenCalled();
|
|
});
|
|
|
|
it('passes current data to updateFn', async () => {
|
|
const releaseFn = jest.fn().mockResolvedValue();
|
|
lockfile.lock.mockResolvedValue(releaseFn);
|
|
fs.promises.readFile.mockResolvedValue(JSON.stringify([{ id: 'existing' }]));
|
|
|
|
const updateFn = jest.fn(data => data);
|
|
await sm.update(updateFn);
|
|
|
|
expect(updateFn).toHaveBeenCalledWith([{ id: 'existing' }]);
|
|
});
|
|
|
|
it('throws on ELOCKED', async () => {
|
|
const err = new Error('locked');
|
|
err.code = 'ELOCKED';
|
|
lockfile.lock.mockRejectedValue(err);
|
|
|
|
await expect(sm.update(d => d)).rejects.toThrow('locked by another process');
|
|
});
|
|
});
|
|
|
|
describe('convenience methods', () => {
|
|
beforeEach(() => {
|
|
const releaseFn = jest.fn().mockResolvedValue();
|
|
lockfile.lock.mockResolvedValue(releaseFn);
|
|
});
|
|
|
|
it('addItem appends to array', async () => {
|
|
fs.promises.readFile.mockResolvedValue(JSON.stringify([{ id: '1' }]));
|
|
const result = await sm.addItem({ id: '2', name: 'New' });
|
|
expect(result).toEqual([{ id: '1' }, { id: '2', name: 'New' }]);
|
|
});
|
|
|
|
it('removeItem filters by id', async () => {
|
|
fs.promises.readFile.mockResolvedValue(JSON.stringify([{ id: '1' }, { id: '2' }]));
|
|
const result = await sm.removeItem('1');
|
|
expect(result).toEqual([{ id: '2' }]);
|
|
});
|
|
|
|
it('updateItem merges updates for matching id', async () => {
|
|
fs.promises.readFile.mockResolvedValue(JSON.stringify([{ id: '1', name: 'Old' }]));
|
|
const result = await sm.updateItem('1', { name: 'New', port: 8080 });
|
|
expect(result).toEqual([{ id: '1', name: 'New', port: 8080 }]);
|
|
});
|
|
|
|
it('findItem returns matching item or null', async () => {
|
|
fs.promises.readFile.mockResolvedValue(JSON.stringify([{ id: '1', name: 'Found' }]));
|
|
const found = await sm.findItem('1');
|
|
expect(found).toEqual({ id: '1', name: 'Found' });
|
|
|
|
const missing = await sm.findItem('999');
|
|
expect(missing).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('isLocked', () => {
|
|
it('returns lockfile.check result', async () => {
|
|
lockfile.check.mockResolvedValue(true);
|
|
expect(await sm.isLocked()).toBe(true);
|
|
|
|
lockfile.check.mockResolvedValue(false);
|
|
expect(await sm.isLocked()).toBe(false);
|
|
});
|
|
|
|
it('returns false on error', async () => {
|
|
lockfile.check.mockRejectedValue(new Error('fail'));
|
|
expect(await sm.isLocked()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('forceUnlock', () => {
|
|
it('calls lockfile.unlock', async () => {
|
|
await sm.forceUnlock();
|
|
expect(lockfile.unlock).toHaveBeenCalledWith(TEST_PATH);
|
|
});
|
|
|
|
it('ignores ENOTACQUIRED error', async () => {
|
|
const err = new Error('not locked');
|
|
err.code = 'ENOTACQUIRED';
|
|
lockfile.unlock.mockRejectedValue(err);
|
|
await expect(sm.forceUnlock()).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('throws other errors', async () => {
|
|
lockfile.unlock.mockRejectedValue(new Error('other'));
|
|
await expect(sm.forceUnlock()).rejects.toThrow('other');
|
|
});
|
|
});
|
|
});
|