import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { execFileSync } from 'node:child_process'; import { scanArtifactsDirectory, exportArtifacts, closeArtifacts, validateRelativeArtifactPath } from '../artifacts.js'; function fixture(t: any) { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pi-artifacts-')); t.after(() => fs.rmSync(root, { recursive: true, force: true })); return root; } test('path validation rejects traversal and portable-name violations', () => { for (const name of ['', '/x', '../x', 'a/./x', 'a//x', 'a\\x', 'a\n', 'x'.repeat(1025)]) assert(!validateRelativeArtifactPath(name).valid); assert(validateRelativeArtifactPath('docs/雪.md').valid); }); test('symlink parents, hardlinks, FIFO and normalized duplicates are omitted', t => { const root = fixture(t); const exports = path.join(root, 'artifacts'); fs.mkdirSync(exports); fs.writeFileSync(path.join(exports, 'ok'), 'bytes'); fs.writeFileSync(path.join(root, 'private'), 'private'); fs.symlinkSync(root, path.join(exports, 'parent')); fs.symlinkSync(path.join(root, 'private'), path.join(exports, 'link')); fs.linkSync(path.join(root, 'private'), path.join(exports, 'hard')); execFileSync('mkfifo', [path.join(exports, 'fifo')]); fs.writeFileSync(path.join(exports, 'é'), '1'); fs.writeFileSync(path.join(exports, 'e\u0301'), '2'); const scan = scanArtifactsDirectory(exports); t.after(() => closeArtifacts(scan.candidates)); assert.equal(scan.candidates.length, 2); assert(scan.warnings.some(w => w.code === 'artifact_not_regular' && /Repeated 4 times/.test(w.message))); assert(scan.warnings.some(w => w.code === 'artifact_duplicate_name')); fs.symlinkSync(exports, path.join(root, 'alias')); assert.equal(scanArtifactsDirectory(path.join(root, 'alias')).candidates.length, 0); }); test('held descriptor prevents a replaced parent from redirecting export', async t => { const root = fixture(t); const dir = path.join(root, 'artifacts'); fs.mkdirSync(dir); fs.mkdirSync(path.join(dir, 'nested')); fs.writeFileSync(path.join(dir, 'nested', 'ok'), 'safe'); const scan = scanArtifactsDirectory(dir); t.after(() => closeArtifacts(scan.candidates)); fs.renameSync(path.join(dir, 'nested'), path.join(root, 'moved')); fs.symlinkSync(root, path.join(dir, 'nested')); fs.writeFileSync(path.join(root, 'ok'), 'evil'); let bytes = ''; assert.equal(await exportArtifacts(scan.candidates, { sendBegin: async () => 'accept', sendChunk: async (_id, _i, b) => { bytes += Buffer.from(b, 'base64').toString(); }, sendEnd: async () => {} }), 1); assert.equal(bytes, 'safe'); }); test('empty files, skip and bounded chunks; changed files fail', async t => { const root = fixture(t); fs.writeFileSync(path.join(root, 'empty'), ''); fs.writeFileSync(path.join(root, 'data'), Buffer.alloc(70000)); const scan = scanArtifactsDirectory(root); t.after(() => closeArtifacts(scan.candidates)); const chunks: number[] = []; const ends: number[] = []; assert.equal(await exportArtifacts(scan.candidates, { sendBegin: async () => 'accept', sendChunk: async (_id, _i, b) => { chunks.push(Buffer.from(b, 'base64').length); }, sendEnd: async (_id, _size, n) => { ends.push(n); } }), 2); assert.deepEqual(chunks, [65536, 4464]); assert(ends.includes(0)); assert.equal(await exportArtifacts(scan.candidates, { sendBegin: async () => 'skip', sendChunk: async () => { throw Error(); }, sendEnd: async () => { throw Error(); } }), 0); fs.appendFileSync(path.join(root, 'data'), 'x'); await assert.rejects(exportArtifacts(scan.candidates, { sendBegin: async () => 'accept', sendChunk: async () => {}, sendEnd: async () => {} })); }); test('file count, individual size and total byte limits', t => { const root = fixture(t); for (let i = 0; i < 25; i++) fs.writeFileSync(path.join(root, `f${i}`), ''); fs.closeSync(fs.openSync(path.join(root, 'oversized'), 'w')); fs.truncateSync(path.join(root, 'oversized'), 10 * 1024 * 1024 + 1); const scan = scanArtifactsDirectory(root); t.after(() => closeArtifacts(scan.candidates)); assert.equal(scan.candidates.length, 20); assert(scan.warnings.some(w => w.code === 'artifact_oversized')); const large = fixture(t); for (let i = 0; i < 6; i++) { const p = path.join(large, `f${i}`); fs.writeFileSync(p, ''); fs.truncateSync(p, 10 * 1024 * 1024); } const largeScan = scanArtifactsDirectory(large); t.after(() => closeArtifacts(largeScan.candidates)); assert.equal(largeScan.candidates.length, 5); assert(largeScan.warnings.some(w => w.code === 'artifact_total_limit_exceeded')); }); test('socket files are omitted', async t => { const root = fixture(t); const { createServer } = await import('node:net'); const server = createServer(); await new Promise((resolve, reject) => { server.on('error', reject); server.listen(path.join(root, 'socket'), resolve); }); t.after(() => new Promise(resolve => server.close(() => resolve()))); const scan = scanArtifactsDirectory(root); t.after(() => closeArtifacts(scan.candidates)); assert.equal(scan.candidates.length, 0); assert(scan.warnings.some(w => w.code === 'artifact_not_regular')); }); test('a growing file fails without sending excess bytes', async t => { const root = fixture(t); const file = path.join(root, 'growing'); fs.writeFileSync(file, Buffer.alloc(70000)); const scan = scanArtifactsDirectory(root); t.after(() => closeArtifacts(scan.candidates)); let sent = 0; await assert.rejects(exportArtifacts(scan.candidates, { sendBegin: async () => 'accept', sendChunk: async (_id, _index, data) => { sent += Buffer.from(data, 'base64').length; fs.appendFileSync(file, Buffer.alloc(1000)); }, sendEnd: async () => { throw new Error('Changed file must not close a successful transfer'); } })); assert.equal(sent, 70000); }); test('repeated filesystem rejections are aggregated without losing counts', t => { const root = fixture(t); for (let i = 0; i < 130; i++) fs.symlinkSync('/nonexistent-dummy-target', path.join(root, `link_${i}`)); const scan = scanArtifactsDirectory(root); t.after(() => closeArtifacts(scan.candidates)); assert.equal(scan.candidates.length, 0); assert.equal(scan.warnings.length, 1); assert.equal(scan.warnings[0].code, 'artifact_not_regular'); assert.match(scan.warnings[0].message, /Repeated 130 times/); });