confluence_web/agent/tests/artifacts.test.ts
Artur Mukhamadiev 38a8ca67f7 agent: pi runtime track handoff (contract revision 1)
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.
2026-09-14 21:57:54 +03:00

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/);
});