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
67 lines
2.5 KiB
TypeScript
67 lines
2.5 KiB
TypeScript
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import { JsonlFileWriter } from '../../src/util/exportToFile/writeToFile';
|
|
|
|
// Unmocked counterpart to writeToFile.test.ts: pins the real-stream assumptions the
|
|
// writer relies on — createWriteStream's default emitClose means 'close' always fires
|
|
// (close() can never hang), and the file descriptor is released before close() resolves
|
|
// so the file and its directory can be removed immediately (the Windows ENOTEMPTY race).
|
|
describe('JsonlFileWriter (real filesystem)', () => {
|
|
let dir: string;
|
|
|
|
beforeEach(() => {
|
|
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptfoo-jsonl-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('releases the file descriptor before close() resolves', async () => {
|
|
const file = path.join(dir, 'out.jsonl');
|
|
const writer = new JsonlFileWriter(file);
|
|
await writer.write({ a: 1 });
|
|
await writer.write({ b: 2 });
|
|
await writer.close();
|
|
|
|
const lines = fs.readFileSync(file, 'utf-8').trim().split('\n');
|
|
expect(lines.map((line) => JSON.parse(line))).toEqual([{ a: 1 }, { b: 2 }]);
|
|
|
|
// Non-recursive removal fails (EBUSY/ENOTEMPTY on Windows) if the fd is still held.
|
|
fs.rmSync(file);
|
|
fs.rmdirSync(dir);
|
|
fs.mkdirSync(dir);
|
|
});
|
|
|
|
it('resolves a second close() call on an already-closed stream', async () => {
|
|
const writer = new JsonlFileWriter(path.join(dir, 'out.jsonl'));
|
|
await writer.write({ a: 1 });
|
|
await writer.close();
|
|
await expect(writer.close()).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('leaves the destination untouched when nothing was written', async () => {
|
|
const file = path.join(dir, 'never.jsonl');
|
|
await expect(new JsonlFileWriter(file).close()).resolves.toBeUndefined();
|
|
expect(fs.existsSync(file)).toBe(false);
|
|
});
|
|
|
|
it('rejects close() when the stream failed mid-run', async () => {
|
|
const file = path.join(dir, 'broken.jsonl');
|
|
const writer = new JsonlFileWriter(file);
|
|
await writer.write({ a: 1 });
|
|
// Destroy with an error to simulate an fd failure between writes; the recorded
|
|
// pre-flush error must reject close() rather than be swallowed.
|
|
(writer as unknown as { writeStream: { destroy: (error: Error) => void } }).writeStream.destroy(
|
|
new Error('EBADF: bad file descriptor'),
|
|
);
|
|
|
|
await expect(writer.close()).rejects.toThrow(
|
|
`Failed to close JSONL output ${file}: EBADF: bad file descriptor`,
|
|
);
|
|
});
|
|
});
|