Skip to content

Commit 87a6dff

Browse files
fix: reject directory code output paths (#140)
1 parent 25ed285 commit 87a6dff

2 files changed

Lines changed: 36 additions & 0 deletions

File tree

src/commands/test.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1550,6 +1550,31 @@ describe('runCodeGet', () => {
15501550
).rejects.toMatchObject({ code: 'VALIDATION_ERROR', exitCode: 5 });
15511551
});
15521552

1553+
it('--out rejects an existing directory path with VALIDATION_ERROR (exit 5) before any network I/O', async () => {
1554+
const { credentialsPath } = makeCreds();
1555+
const dir = mkdtempSync(join(tmpdir(), 'cli-test-code-out-dir-'));
1556+
let fetchCalls = 0;
1557+
const fetchImpl = (() => {
1558+
fetchCalls += 1;
1559+
return Promise.resolve(new Response('{}'));
1560+
}) as typeof globalThis.fetch;
1561+
1562+
await expect(
1563+
runCodeGet(
1564+
{
1565+
profile: 'default',
1566+
output: 'text',
1567+
debug: false,
1568+
testId: 'test_fe',
1569+
out: dir,
1570+
},
1571+
{ credentialsPath, fetchImpl },
1572+
),
1573+
).rejects.toMatchObject({ code: 'VALIDATION_ERROR', exitCode: 5 });
1574+
expect(fetchCalls).toBe(0);
1575+
expect(readdirSync(dir)).toEqual([]);
1576+
});
1577+
15531578
// Regression: a parent dir that doesn't exist used to surface as exit 1
15541579
// (TRANSPORT_ERROR) — `createWriteStream` opens lazily and ENOENT fires
15551580
// mid-write. Synchronous parent stat keeps every `--out` user-input

src/commands/test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7953,6 +7953,17 @@ function openOutputFile(rawPath: string): FileSink {
79537953
if (!parentStat.isDirectory()) {
79547954
throw localValidationError('out', `parent path is not a directory: ${parent}`);
79557955
}
7956+
let targetStat;
7957+
try {
7958+
targetStat = statSync(resolved);
7959+
} catch (err) {
7960+
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
7961+
throw localValidationError('out', `cannot stat output path: ${resolved}`);
7962+
}
7963+
}
7964+
if (targetStat?.isDirectory()) {
7965+
throw localValidationError('out', `must point to a file, not a directory: ${resolved}`);
7966+
}
79567967
const tmpPath = join(parent, `.${basename(resolved)}.tmp-${randomUUID()}`);
79577968
const stream = createWriteStream(tmpPath, { encoding: 'utf8' });
79587969
const sink: FileSink = { stream, path: resolved, tmpPath, error: null };

0 commit comments

Comments
 (0)