mirror of
https://github.com/teamchong/pxpipe.git
synced 2026-07-22 02:02:51 +02:00
27e60d0c17
In --git mode, untracked files (git ls-files --others) were read straight to memory with only a binary check — unlike directory and single-file modes, which also apply --include/--exclude globs and the 1 MiB MAX_FILE_BYTES cap. So `pxpipe export --git --include '*.ts'` still slurped every untracked file regardless of the filter, and an untracked multi-MB file was read whole (a resource-safety hazard). - extract the shared gate into src/export-collect.ts: readExportTextFile() applies include/exclude → size → binary, in that order, and is import-safe (src/core/export.ts is contractually fs-free, so it can't live there) - route all three collection paths (walkDir, single-file target, --git untracked) through it; untracked skips now warn like explicit targets do - tests/export-collect.test.ts covers include, exclude, oversized, the size boundary, binary, and inaccessible Verified end-to-end: `export --git --include '*.ts'` in a temp repo excludes an untracked .md, skips an oversized .ts (with a warning), and keeps the .ts.
75 lines
3.1 KiB
TypeScript
75 lines
3.1 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import * as os from 'node:os';
|
|
import { readExportTextFile, looksLikeBinary, MAX_FILE_BYTES } from '../src/export-collect.js';
|
|
|
|
// readExportTextFile is the single gate now shared by every `pxpipe export`
|
|
// collection mode (directory walk, single-file target, and --git untracked).
|
|
// Before this, the --git untracked path applied none of these checks — it
|
|
// ignored --include/--exclude and read files of any size fully into memory.
|
|
describe('readExportTextFile — shared export file gate', () => {
|
|
let tmpDir: string;
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pxpipe-collect-test-'));
|
|
});
|
|
afterEach(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
const write = (name: string, content: string | Buffer): string => {
|
|
const p = path.join(tmpDir, name);
|
|
fs.writeFileSync(p, content);
|
|
return p;
|
|
};
|
|
|
|
it('reads an included text file as utf8', () => {
|
|
const p = write('a.ts', 'export const x = 1;\n');
|
|
const r = readExportTextFile(p, 'a.ts', [], []);
|
|
expect(r.kind).toBe('ok');
|
|
if (r.kind === 'ok') expect(r.content).toBe('export const x = 1;\n');
|
|
});
|
|
|
|
it('respects --include: a .md is excluded when include is *.ts, the .ts sibling passes', () => {
|
|
const md = write('b.md', '# readme\n');
|
|
const ts = write('a.ts', 'const a = 1;\n');
|
|
expect(readExportTextFile(md, 'b.md', ['*.ts'], []).kind).toBe('excluded');
|
|
expect(readExportTextFile(ts, 'a.ts', ['*.ts'], []).kind).toBe('ok');
|
|
});
|
|
|
|
it('respects --exclude', () => {
|
|
const p = write('secret.ts', 'const s = 1;\n');
|
|
expect(readExportTextFile(p, 'secret.ts', [], ['secret.*']).kind).toBe('excluded');
|
|
});
|
|
|
|
it('skips an oversized file instead of reading it into memory', () => {
|
|
const p = write('big.ts', 'a'.repeat(MAX_FILE_BYTES + 1));
|
|
expect(readExportTextFile(p, 'big.ts', [], []).kind).toBe('oversized');
|
|
});
|
|
|
|
it('accepts a file exactly at the size limit', () => {
|
|
const p = write('edge.ts', 'a'.repeat(MAX_FILE_BYTES));
|
|
expect(readExportTextFile(p, 'edge.ts', [], []).kind).toBe('ok');
|
|
});
|
|
|
|
it('skips a binary file (null byte in first 512 bytes)', () => {
|
|
const p = write('bin.dat', Buffer.from([0x41, 0x00, 0x42]));
|
|
expect(readExportTextFile(p, 'bin.dat', [], []).kind).toBe('binary');
|
|
});
|
|
|
|
it('reports a missing/inaccessible file', () => {
|
|
expect(readExportTextFile(path.join(tmpDir, 'nope.ts'), 'nope.ts', [], []).kind).toBe('inaccessible');
|
|
});
|
|
|
|
it('applies the include/exclude filter before touching the filesystem', () => {
|
|
// A path that does not exist but is filtered out reports 'excluded', not
|
|
// 'inaccessible' — the glob gate short-circuits before any stat.
|
|
expect(readExportTextFile(path.join(tmpDir, 'ghost.md'), 'ghost.md', ['*.ts'], []).kind).toBe('excluded');
|
|
});
|
|
|
|
it('looksLikeBinary flags a null byte but passes plain text', () => {
|
|
expect(looksLikeBinary(Buffer.from('plain text'))).toBe(false);
|
|
expect(looksLikeBinary(Buffer.from([0x00]))).toBe(true);
|
|
});
|
|
});
|