Pinned pi SDK 0.85.1 bridge, Python supervisor, artifact exporter, scripted backend peer, image checks and boundary checks under agent/**. Review findings F1-F3 are recorded in docs/implementation/PI_AGENT_REVIEW.md.
92 lines
6.3 KiB
TypeScript
92 lines
6.3 KiB
TypeScript
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<void>((resolve, reject) => { server.on('error', reject); server.listen(path.join(root, 'socket'), resolve); });
|
|
t.after(() => new Promise<void>(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/);
|
|
});
|