Add unit tests for fs utilities and ValueCache

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt Isenhower 2026-02-15 15:40:54 -08:00
parent b0083d08cc
commit 6e4b57f6a7
2 changed files with 167 additions and 0 deletions

View File

@ -0,0 +1,118 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('fs/promises', () => ({
default: {
readFile: vi.fn(),
writeFile: vi.fn(),
mkdir: vi.fn(),
},
}));
const fs = (await import('fs/promises')).default;
const { default: ValueCache } = await import('./ValueCache.mjs');
describe('ValueCache', () => {
let cache;
beforeEach(() => {
cache = new ValueCache('test-key');
vi.clearAllMocks();
});
describe('path', () => {
it('returns storage/cache/{key}.json', () => {
expect(cache.path).toBe('storage/cache/test-key.json');
});
it('uses the key from constructor', () => {
const other = new ValueCache('other');
expect(other.path).toBe('storage/cache/other.json');
});
});
describe('get', () => {
it('returns parsed JSON when valid and not expired', async () => {
const item = { data: 'hello', expires: Date.now() + 60000, cachedAt: new Date().toISOString() };
fs.readFile.mockResolvedValue(JSON.stringify(item));
const result = await cache.get();
expect(result).toEqual(item);
});
it('returns null when expired', async () => {
const item = { data: 'hello', expires: Date.now() - 60000, cachedAt: new Date().toISOString() };
fs.readFile.mockResolvedValue(JSON.stringify(item));
expect(await cache.get()).toBeNull();
});
it('returns item when no expiry is set', async () => {
const item = { data: 'hello', expires: null, cachedAt: new Date().toISOString() };
fs.readFile.mockResolvedValue(JSON.stringify(item));
const result = await cache.get();
expect(result).toEqual(item);
});
it('returns null when file is missing', async () => {
fs.readFile.mockRejectedValue(new Error('ENOENT'));
expect(await cache.get()).toBeNull();
});
});
describe('getData', () => {
it('returns .data field from cached item', async () => {
const item = { data: { foo: 'bar' }, expires: Date.now() + 60000, cachedAt: new Date().toISOString() };
fs.readFile.mockResolvedValue(JSON.stringify(item));
expect(await cache.getData()).toEqual({ foo: 'bar' });
});
it('returns null when no cache', async () => {
fs.readFile.mockRejectedValue(new Error('ENOENT'));
expect(await cache.getData()).toBeNull();
});
});
describe('getCachedAt', () => {
it('returns Date from cachedAt field', async () => {
const cachedAt = '2024-06-15T10:00:00.000Z';
const item = { data: 'x', cachedAt };
fs.readFile.mockResolvedValue(JSON.stringify(item));
const result = await cache.getCachedAt();
expect(result).toEqual(new Date(cachedAt));
});
it('returns null when no data', async () => {
fs.readFile.mockRejectedValue(new Error('ENOENT'));
expect(await cache.getCachedAt()).toBeNull();
});
});
describe('setData', () => {
it('writes JSON with data, expires, and cachedAt', async () => {
fs.mkdir.mockResolvedValue(undefined);
fs.writeFile.mockResolvedValue(undefined);
await cache.setData({ key: 'value' }, Date.now() + 60000);
expect(fs.mkdir).toHaveBeenCalledWith('storage/cache', { recursive: true });
expect(fs.writeFile).toHaveBeenCalledWith(
'storage/cache/test-key.json',
expect.stringContaining('"key": "value"'),
);
const written = JSON.parse(fs.writeFile.mock.calls[0][1]);
expect(written.data).toEqual({ key: 'value' });
expect(written.expires).toBeDefined();
expect(written.cachedAt).toBeDefined();
});
it('passes null expires when not specified', async () => {
fs.mkdir.mockResolvedValue(undefined);
fs.writeFile.mockResolvedValue(undefined);
await cache.setData('test');
const written = JSON.parse(fs.writeFile.mock.calls[0][1]);
expect(written.expires).toBeNull();
});
});
});

49
app/common/fs.test.mjs Normal file
View File

@ -0,0 +1,49 @@
import { describe, it, expect, vi } from 'vitest';
import { mkdirp, exists, olderThan } from './fs.mjs';
vi.mock('fs/promises', () => ({
default: {
mkdir: vi.fn(),
access: vi.fn(),
stat: vi.fn(),
},
}));
const fs = (await import('fs/promises')).default;
describe('mkdirp', () => {
it('calls fs.mkdir with recursive: true', async () => {
fs.mkdir.mockResolvedValue(undefined);
await mkdirp('/some/path');
expect(fs.mkdir).toHaveBeenCalledWith('/some/path', { recursive: true });
});
});
describe('exists', () => {
it('returns true when fs.access succeeds', async () => {
fs.access.mockResolvedValue(undefined);
expect(await exists('/some/file')).toBe(true);
});
it('returns false when fs.access throws', async () => {
fs.access.mockRejectedValue(new Error('ENOENT'));
expect(await exists('/missing/file')).toBe(false);
});
});
describe('olderThan', () => {
it('returns true when mtime is before cutoff', async () => {
fs.stat.mockResolvedValue({ mtime: new Date('2024-01-01') });
expect(await olderThan('/file', new Date('2024-06-01'))).toBe(true);
});
it('returns false when mtime is after cutoff', async () => {
fs.stat.mockResolvedValue({ mtime: new Date('2024-06-01') });
expect(await olderThan('/file', new Date('2024-01-01'))).toBe(false);
});
it('returns true when file is missing', async () => {
fs.stat.mockRejectedValue(new Error('ENOENT'));
expect(await olderThan('/missing', new Date())).toBe(true);
});
});