Files
Hermes 7bc2a207f3 DC-005: Fix all 138 broken test paths after src/ refactor
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)
2026-06-13 12:16:56 -07:00

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');
});
});
});