Files
wehub-resource-sync 0d3cb498a3
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

182 lines
6.0 KiB
TypeScript

import fs from 'fs';
import path from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { getLogFiles } from '../../src/util/logFiles';
vi.mock('fs');
// Helper to create mock Dirent objects
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createDirent(name: string, isFile: boolean = true): any {
return {
name,
isFile: () => isFile,
isDirectory: () => !isFile,
isBlockDevice: () => false,
isCharacterDevice: () => false,
isSymbolicLink: () => false,
isFIFO: () => false,
isSocket: () => false,
parentPath: '/logs',
path: '/logs',
};
}
describe('logFiles utilities', () => {
beforeEach(() => {
vi.resetAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('getLogFiles', () => {
it('should return empty array if directory does not exist', () => {
const error = new Error('ENOENT: no such file or directory') as NodeJS.ErrnoException;
error.code = 'ENOENT';
vi.mocked(fs.readdirSync).mockImplementation(() => {
throw error;
});
const result = getLogFiles('/nonexistent/path');
expect(result).toEqual([]);
});
it('should return empty array if directory has no log files', () => {
vi.mocked(fs.readdirSync).mockReturnValue([
createDirent('readme.txt'),
createDirent('config.json'),
]);
const result = getLogFiles('/logs');
expect(result).toEqual([]);
});
it('should return log files sorted by modification time (newest first)', () => {
vi.mocked(fs.readdirSync).mockReturnValue([
createDirent('promptfoo-debug-2024-01-01.log'),
createDirent('promptfoo-error-2024-01-02.log'),
createDirent('promptfoo-debug-2024-01-03.log'),
]);
const dates = {
'promptfoo-debug-2024-01-01.log': new Date('2024-01-01'),
'promptfoo-error-2024-01-02.log': new Date('2024-01-02'),
'promptfoo-debug-2024-01-03.log': new Date('2024-01-03'),
};
vi.mocked(fs.statSync).mockImplementation((filePath: unknown) => {
const fileName = path.basename(filePath as string);
return { mtime: dates[fileName as keyof typeof dates] } as fs.Stats;
});
const result = getLogFiles('/logs');
expect(result).toHaveLength(3);
expect(result[0].name).toBe('promptfoo-debug-2024-01-03.log');
expect(result[1].name).toBe('promptfoo-error-2024-01-02.log');
expect(result[2].name).toBe('promptfoo-debug-2024-01-01.log');
});
it('should filter files by promptfoo prefix and .log suffix', () => {
vi.mocked(fs.readdirSync).mockReturnValue([
createDirent('promptfoo-debug.log'),
createDirent('other-debug.log'),
createDirent('promptfoo-error.txt'),
createDirent('promptfoo.log'),
createDirent('readme.md'),
]);
vi.mocked(fs.statSync).mockReturnValue({ mtime: new Date() } as fs.Stats);
const result = getLogFiles('/logs');
expect(result).toHaveLength(1);
expect(result[0].name).toBe('promptfoo-debug.log');
});
it('should filter out directories', () => {
vi.mocked(fs.readdirSync).mockReturnValue([
createDirent('promptfoo-debug.log', true),
createDirent('promptfoo-subdir.log', false), // This is a directory
]);
vi.mocked(fs.statSync).mockReturnValue({ mtime: new Date() } as fs.Stats);
const result = getLogFiles('/logs');
expect(result).toHaveLength(1);
expect(result[0].name).toBe('promptfoo-debug.log');
});
it('should include correct path in returned objects', () => {
vi.mocked(fs.readdirSync).mockReturnValue([createDirent('promptfoo-test.log')]);
vi.mocked(fs.statSync).mockReturnValue({ mtime: new Date() } as fs.Stats);
const result = getLogFiles('/my/log/dir');
expect(result[0].path).toBe(path.join('/my/log/dir', 'promptfoo-test.log'));
});
it('should throw error if directory cannot be read (non-ENOENT)', () => {
const error = new Error('Permission denied') as NodeJS.ErrnoException;
error.code = 'EACCES';
vi.mocked(fs.readdirSync).mockImplementation(() => {
throw error;
});
expect(() => getLogFiles('/protected/logs')).toThrow('Permission denied');
});
it('should skip files that are deleted between readdir and stat (race condition)', () => {
vi.mocked(fs.readdirSync).mockReturnValue([
createDirent('promptfoo-exists.log'),
createDirent('promptfoo-deleted.log'),
createDirent('promptfoo-also-exists.log'),
]);
vi.mocked(fs.statSync).mockImplementation((filePath: unknown) => {
if ((filePath as string).includes('deleted')) {
const error = new Error('ENOENT: no such file or directory') as NodeJS.ErrnoException;
error.code = 'ENOENT';
throw error;
}
return { mtime: new Date() } as fs.Stats;
});
const result = getLogFiles('/logs');
expect(result).toHaveLength(2);
expect(result.map((f) => f.name)).toContain('promptfoo-exists.log');
expect(result.map((f) => f.name)).toContain('promptfoo-also-exists.log');
expect(result.map((f) => f.name)).not.toContain('promptfoo-deleted.log');
});
it('should rethrow non-ENOENT errors from statSync', () => {
vi.mocked(fs.readdirSync).mockReturnValue([createDirent('promptfoo-test.log')]);
vi.mocked(fs.statSync).mockImplementation(() => {
const error = new Error('Permission denied') as NodeJS.ErrnoException;
error.code = 'EACCES';
throw error;
});
expect(() => getLogFiles('/logs')).toThrow('Permission denied');
});
it('should return files with correct mtime', () => {
const testDate = new Date('2024-06-15T10:30:00Z');
vi.mocked(fs.readdirSync).mockReturnValue([createDirent('promptfoo-test.log')]);
vi.mocked(fs.statSync).mockReturnValue({ mtime: testDate } as fs.Stats);
const result = getLogFiles('/logs');
expect(result[0].mtime).toEqual(testDate);
});
});
});