Files
LWLT-AIBOT/control-plane/test/input-attachment.test.ts
2026-08-30 16:01:42 +08:00

110 lines
4.6 KiB
TypeScript

import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
import { sha256Bytes } from '../src/crypto.js';
import {
InputAttachmentError,
decodeInlineInputAttachment,
downloadAgentBusInputAttachment,
isPrivateOrReservedIp,
parseAgentBusInputAttachment,
validateAgentBusAttachmentUrl
} from '../src/input-attachment.js';
function codeOf(callback: () => unknown): string {
try {
callback();
return '';
} catch (error) {
return error instanceof InputAttachmentError ? error.code : String(error);
}
}
test('manual roster attachment base64 is bounded and hash verified', () => {
const content = Buffer.from('PK\u0003\u0004synthetic-workbook');
const decoded = decodeInlineInputAttachment({
name: 'synthetic.xlsx',
content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
size: content.byteLength,
sha256: sha256Bytes(content),
content_base64: content.toString('base64')
}, 1_000);
assert.equal(decoded.fileName, 'synthetic.xlsx');
assert.deepEqual(decoded.content, content);
assert.equal(decoded.source, 'manual');
});
test('manual roster attachment rejects unsupported names, invalid base64 and mismatched hash', () => {
assert.equal(codeOf(() => decodeInlineInputAttachment({
name: 'synthetic.csv', content_base64: 'YQ=='
}, 1_000)), 'roster_file_type_unsupported');
assert.equal(codeOf(() => decodeInlineInputAttachment({
name: 'synthetic.xls', content_base64: '%%%'
}, 1_000)), 'roster_file_base64_invalid');
assert.equal(codeOf(() => decodeInlineInputAttachment({
name: 'synthetic.xls', sha256: 'a'.repeat(64), content_base64: 'YQ=='
}, 1_000)), 'roster_file_sha256_mismatch');
});
test('AgentBus attachment URLs require public HTTPS endpoints', () => {
for (const value of [
'http://files.example.test/list.xlsx',
'https://localhost/list.xlsx',
'https://127.0.0.1/list.xlsx',
'https://10.0.0.8/list.xlsx',
'https://[::1]/list.xlsx',
'https://user:secret@files.example.test/list.xlsx'
]) {
assert.equal(codeOf(() => validateAgentBusAttachmentUrl(value)), 'roster_attachment_url_unsafe');
}
assert.equal(validateAgentBusAttachmentUrl('https://files.example.com/list.xlsx').protocol, 'https:');
});
test('private and documentation address ranges are rejected', () => {
for (const address of ['0.0.0.1', '10.1.2.3', '100.64.0.1', '127.0.0.1', '169.254.1.1', '172.31.0.1', '192.168.1.1', '198.51.100.1', '203.0.113.8', '::1', 'fc00::1', 'fe80::1', '2001:db8::1']) {
assert.equal(isPrivateOrReservedIp(address), true, address);
}
assert.equal(isPrivateOrReservedIp('8.8.8.8'), false);
assert.equal(isPrivateOrReservedIp('2606:4700:4700::1111'), false);
});
test('AgentBus attachment metadata is normalized without exposing URL credentials', () => {
const reference = parseAgentBusInputAttachment({
name: 'synthetic.xls',
content_type: 'application/vnd.ms-excel',
size: 128,
sha256: 'a'.repeat(64),
url: 'https://files.example.com/roster.xls'
}, 1_000);
assert.equal(reference.name, 'synthetic.xls');
assert.equal(reference.size, 128);
assert.equal(reference.sha256, 'a'.repeat(64));
});
test('AgentBus attachment diagnostics expose stages and codes without URL data', async () => {
const events: Array<{ event: string; metadata: Record<string, unknown> }> = [];
await assert.rejects(
() => downloadAgentBusInputAttachment({
name: 'synthetic.xls',
contentType: 'application/vnd.ms-excel',
size: 128,
url: 'https://127.0.0.1/private-roster.xls?token=secret'
}, 1_000, (event, metadata) => events.push({ event, metadata })),
(error: unknown) => error instanceof InputAttachmentError
&& error.code === 'roster_attachment_url_unsafe'
);
assert.deepEqual(events.map((event) => event.event), ['download_started', 'download_failed']);
assert.equal(events[1].metadata.error_code, 'roster_attachment_url_unsafe');
assert.doesNotMatch(JSON.stringify(events), /127\.0\.0\.1|private-roster|token|secret/u);
});
test('input attachment migration stores only encrypted normalized data and waiting-task index', async () => {
const sql = await readFile(new URL('../migrations/014_task_input_attachments.sql', import.meta.url), 'utf8');
assert.match(sql, /CREATE TABLE IF NOT EXISTS task_input_attachments/);
assert.match(sql, /normalized_text_ciphertext text/);
assert.match(sql, /file_name_ciphertext text NOT NULL/);
assert.doesNotMatch(sql, /\n\s*file_name text/);
assert.doesNotMatch(sql, /public_url|content bytea|content_ciphertext/);
assert.match(sql, /WHERE status = 'awaiting_attachment'/);
});