2026-10-14 17:25:52 -07:00
import test from 'node:test' ;
import { createHash , randomUUID } from 'node:crypto' ;
import assert from 'node:assert/strict' ;
import { createAgentUploadQueue , agentAttachments , parseAgentAttachment } from '../src/lib/agent-attachments.ts' ;
import { AgentRoomClient } from '../src/lib/agent-room.ts' ;
import { createAgentOptimisticOutbox } from '../src/lib/agent-optimistic-outbox.ts' ;
import { agentTranscriptMessages } from '../src/lib/agent-room-presentation.ts' ;
2026-10-20 14:14:09 -07:00
import { createAgentRoomState , applyAgentEvent , mergeAgentMessages } from '../src/lib/agent-room.ts' ;
2026-10-14 17:25:52 -07:00
const file = { id : 'att_12345678-1234-4234-8234-123456789abc' , kind : 'text' , filename : 'notes.txt' , mimeType : 'text/plain' , sizeBytes : 4 , sha256 : createHash ( 'sha256' ). update ( 'data' ). digest ( 'hex' ), workspacePath : '/data/workspace/uploads/att_12345678-1234-4234-8234-123456789abc/notes.txt' };
const cap = { version : 1 , maxCount : 8 , maxTotalBytes : 32 * 1024 * 1024 , maxImageBytes : 6 * 1024 * 1024 , maxFileBytes : 8 * 1024 * 1024 , imageMimeTypes : [ 'image/png' , 'image/jpeg' , 'image/gif' , 'image/webp' ]};
const tick = ()=> new Promise ( r => setTimeout ( r , 0 ));
const waitFor = async check =>{ const until = Date . now () + 5000 ; while ( ! check ()){ assert . ok ( Date . now () < until , 'async condition timed out' ); await tick ();}};
2026-10-20 14:14:09 -07:00
test ( 'assistant images retain stable live/history identity, omit paths, and never enable input capability' , async ()=>{
const image = {... file , kind : 'image' , mimeType : 'image/png' , filename : 'image.png' , url : 'https://never-fetch.invalid/image' };
const safe = {... image }; delete safe . workspacePath ; delete safe . url ;
const message = { id : 'outbound:42' , role : 'assistant' , content : '' , createdAt : new Date (). toISOString (), metadata : { kind : 'message' , status : 'complete' , entryId : 42 , agentAttachments : [ image ]}};
const page = { type : 'messages' , conversationId : '1' , messages : [ message ], cursor : '42' , beforeCursor : '42' , hasMore : false };
const live = applyAgentEvent ( createAgentRoomState (), page );
assert . deepEqual ( agentAttachments ( live . messages [ 0 ]. metadata , 'assistant' ),[ safe ]);
const history = mergeAgentMessages ( live . messages ,[ structuredClone ( message )]);
assert . equal ( history . length , 1 ); assert . equal ( history [ 0 ]. id , 'outbound:42' );
assert . deepEqual ( agentAttachments ( history [ 0 ]. metadata , 'assistant' ),[ safe ]);
assert . equal ( agentTranscriptMessages ( history ). length , 1 , 'image-only assistant bubble remains visible' );
for ( const invalid of [{... image , kind : 'file' },{... image , mimeType : 'image/svg+xml' },{... image , sizeBytes : 6 * 1024 * 1024 + 1 },{... image , filename : '/private/source.png' }])
assert . deepEqual ( agentAttachments ({ agentAttachments : [ invalid ]}, 'assistant' ),[]);
assert . deepEqual ( agentAttachments ( message . metadata , 'tool' ),[]);
assert . deepEqual ( agentAttachments ({ agentAttachments : [ image , image ]}, 'assistant' ),[]);
const descriptor = { capabilities : { assistantImages : { version : 1 }}};
const queue = createAgentUploadQueue ({ capability : ()=> descriptor . capabilities . attachments , changed : ()=>{}, upload : async ()=>{ throw new Error ( 'Must not upload' );}});
assert . throws (()=> queue . add ([ new File ([ 'data' ], 'notes.txt' )]), /not available/ ); queue . dispose ();
});
test ( 'downloads bound concurrent bytes, validate hashes/types, cancel queued work, and reject changed credentials' , async ()=>{
const complete = []; let calls = 0 , token = 'one' ;
const client = new AgentRoomClient ({ token : ()=> token , fetch : async ()=>{ calls ++ ; return new Promise ( resolve => complete . push ( resolve ));}});
const a = client . download ( file ), b = client . download ( file ), cancel = new AbortController ();
const canceled = client . download ( file , cancel . signal ); const canceledAssertion = assert . rejects ( canceled ); cancel . abort ();
await canceledAssertion ; assert . equal ( calls , 2 );
complete . shift ()( new Response ( 'data' )); await a ;
const c = client . download ( file ); assert . equal ( calls , 3 );
complete . shift ()( new Response ( 'evil' )); await assert . rejects ( b , /checksum/ );
token = 'two' ; complete . shift ()( new Response ( 'data' )); await assert . rejects ( c , /Authentication changed/ );
const huge = new AgentRoomClient ({ fetch : async ()=> new Response ( 'data-exceeds-receipt' )});
await assert . rejects ( huge . download ( file ), /size limit/ );
const wrongType = new AgentRoomClient ({ fetch : async ()=> new Response ( 'data' ,{ headers : { 'content-type' : 'text/html' }})});
await assert . rejects ( wrongType . download ({... file , kind : 'image' , mimeType : 'image/png' }), /type did not match/ );
});
2026-10-14 17:25:52 -07:00
test ( 'composer gates unsupported servers, count/type/size and batches before upload; preserves retry ID and remove races' , async ()=>{
let capability , attempts = [], next = 0 , changed = 0 , finish ;
const queue = createAgentUploadQueue ({ capability : ()=> capability , newId : ()=> `upload- ${ ++ next } ` , changed : ()=> changed ++ , upload : async ( _file , id , signal )=>{ attempts . push ({ id , signal }); if ( attempts . length === 1 ) throw new Error ( 'Quota full' ); return new Promise ( resolve => finish = resolve );}});
const input = new File ([ 'data' ], 'notes.txt' ,{ type : 'text/plain' });
assert . throws (()=> queue . add ([ input ]), /not available/ ); capability = cap ;
assert . throws (()=> queue . add ( Array ( 9 ). fill ( input )), /8 files/ );
assert . throws (()=> queue . add ([ new File ([ new Uint8Array ( 6 * 1024 * 1024 + 1 )], 'big.png' ,{ type : 'image/png' })]), /6 MiB/ );
assert . throws (()=> queue . add ( Array ( 5 ). fill ( new File ([ new Uint8Array ( 7 * 1024 * 1024 )], 'large.bin' ))), /32 MiB/ );
queue . add ([ input ]); assert . throws (()=> queue . ready (), /Wait/ ); await waitFor (()=> queue . snapshot ()[ 0 ]. status === 'failed' );
assert . equal ( queue . snapshot ()[ 0 ]. status , 'failed' ); assert . match ( queue . snapshot ()[ 0 ]. error , /Quota/ );
queue . retry ( 'upload-1' ); await waitFor (()=> attempts . length === 2 ); assert . deepEqual ( attempts . map ( a => a . id ),[ 'upload-1' , 'upload-1' ]);
queue . remove ( 'upload-1' ); assert . equal ( attempts . at ( - 1 ). signal . aborted , true ); finish ( file ); await tick (); assert . deepEqual ( queue . ready (),[]);
assert . ok ( changed > 0 ); queue . dispose ();
});
test ( 'composer serializes transfers, keeps ready descriptors, and clears without orphaning pending state' , async ()=>{
const done = []; let count = 0 ;
const queue = createAgentUploadQueue ({ capability : ()=> cap , changed : ()=>{}, newId : ()=> `id- ${ ++ count } ` , upload : async ()=> new Promise ( r => done . push ( r ))});
queue . add ([ new File ([ 'data' ], 'notes.txt' ), new File ([ 'data' ], 'notes.txt' )]); await waitFor (()=> done . length === 1 ); assert . deepEqual ( queue . snapshot (). map ( r => r . status ),[ 'uploading' , 'waiting' ]);
done [ 0 ]( file ); await waitFor (()=> done . length === 2 ); assert . deepEqual ( queue . snapshot (). map ( r => r . status ),[ 'ready' , 'uploading' ]); done [ 1 ]({... file , id : file . id . replace ( '12345678' , '12345679' )}); await waitFor (()=> queue . snapshot (). every ( row => row . status === 'ready' )); assert . equal ( queue . ready (). length , 2 ); queue . clear (); assert . deepEqual ( queue . ready (),[]); queue . dispose ();
});
test ( 'transport uses auth headers/FormData, ID-only send and checked receipt, authenticated download with no token URLs' , async ()=>{
const calls = []; let acknowledge = true ;
const client = new AgentRoomClient ({ baseUrl : '/api' , token : ()=> 'browser-token' , fetch : async ( url , init )=>{
calls . push ({ url , init }); assert . ok ( ! url . includes ( 'token' )); assert . equal ( init . headers . get ( 'Authorization' ), 'Bearer browser-token' );
if ( url . endsWith ( '/attachments' )){ assert . ok ( init . body instanceof FormData ); assert . equal ( init . headers . get ( 'Content-Type' ), null ); assert . equal ( init . body . get ( 'uploadId' ), 'uuid' ); return Response . json ({ attachment : file },{ status : 201 });}
if ( url . endsWith ( '/content' )) return new Response ( 'data' ,{ headers : { 'content-type' : 'text/plain' }});
const body = JSON . parse ( init . body ); assert . deepEqual ( body ,{ content : '' , requestId : 'send-id' , attachments : [{ id : file . id }]});
return Response . json ({ id : 1 , status : 'queued' , messageId : 'inbound:sybil:send-id' ,...( acknowledge ? { attachmentIds : [ file . id ]} : {})});
}});
assert . deepEqual ( await client . upload ( new File ([ 'data' ], 'notes.txt' ), 'uuid' ), file );
assert . equal (( await client . submit ( '' , 'send-id' ,[ file ])). status , 'queued' ); acknowledge = false ; await assert . rejects ( client . submit ( '' , 'send-id' ,[ file ]), /invalid message receipt/ );
assert . equal ( await ( await client . download ( file )). text (), 'data' ); assert . equal ( calls . at ( - 1 ). init . cache , 'no-store' ); assert . equal ( calls . at ( - 1 ). init . redirect , 'error' );
});
test ( 'file-only outbox survives reload and retry without sending paths, confirms matching attachment echoes and retains tool filtering' , async ()=>{
let saved , call , fail = true ;
const options = { storage : { read : ()=> saved , write : rows => saved = structuredClone ( rows )}, changed : ()=>{}, newId : ()=> 'request' , submit : async ( content , requestId , attachments )=>{ call = { content , requestId , attachments }; if ( fail ) throw new Error ( 'lost response' ); return { status : 'queued' , messageId : 'inbound:sybil:request' };}};
let outbox = createAgentOptimisticOutbox ( options ); await assert . rejects ( outbox . send ( '' ,[ file ])); assert . deepEqual ( saved [ 0 ]. attachments ,[ file ]);
outbox = createAgentOptimisticOutbox ( options ); assert . equal ( outbox . snapshot (). length , 1 ); fail = false ; await outbox . retry ( 'inbound:sybil:request' ); assert . equal ( call . content , '' ); assert . deepEqual ( call . attachments ,[ file ]);
const row = outbox . display ([])[ 0 ]; assert . equal ( row . content , 'Attached: notes.txt.' ); assert . deepEqual ( agentAttachments ( row . metadata ),[ file ]);
outbox . reconcile ({ messages : [{... row , metadata : { kind : 'message' , status : 'complete' }}]}); assert . equal ( outbox . snapshot ()[ 0 ]. confirmed , false );
const mismatch = outbox . display ([{... row , metadata : { kind : 'message' , status : 'complete' }}])[ 0 ]; assert . deepEqual ( agentAttachments ( mismatch . metadata ),[ file ]); assert . equal ( mismatch . metadata . status , 'queued' );
outbox . reconcile ({ messages : [{... row , metadata : {... row . metadata , status : 'complete' }}]}); assert . equal ( outbox . snapshot ()[ 0 ]. confirmed , true );
const raw = [ row ,{... row , id : 'tool' , role : 'tool' , metadata : { kind : 'tool_call' }}]; assert . equal ( agentTranscriptMessages ( raw ). length , 1 ); assert . equal ( raw . length , 2 );
assert . throws (()=> parseAgentAttachment ({... file , workspacePath : 'https://bad' }));
});
test ( 'attachment drafts preserve completed receipts and unresolved IDs/hash across disposal/reload, verified reselection resumes without a new ID' , async ()=>{
let saved = [], attempts = [], complete ;
const storage = { read : ()=> saved , write : rows => saved = structuredClone ( rows )};
const options = { capability : ()=> cap , storage , changed : ()=>{}, upload : async ( _file , id , signal )=>{ attempts . push ({ id , signal }); return new Promise ( r => complete = r );}};
let queue = createAgentUploadQueue ( options ); queue . add ([ new File ([ 'data' ], 'notes.txt' ,{ type : 'text/plain' })]);
await waitFor (()=> Boolean ( complete )); const original = attempts [ 0 ]. id ; assert . equal ( saved [ 0 ]. sha256 , file . sha256 );
queue . dispose (); assert . equal ( attempts [ 0 ]. signal . aborted , true );
queue = createAgentUploadQueue ( options ); assert . equal ( queue . snapshot ()[ 0 ]. status , 'reselect' ); assert . equal ( queue . snapshot ()[ 0 ]. uploadId , original ); assert . throws (()=> queue . ready (), /Wait/ );
await assert . rejects ( queue . reselect ( original , new File ([ 'evil' ], 'notes.txt' ,{ type : 'text/plain' })), /different contents/ ); assert . equal ( attempts . length , 1 );
await queue . reselect ( original , new File ([ 'data' ], 'notes.txt' ,{ type : 'text/plain' })); await waitFor (()=> attempts . length === 2 ); assert . equal ( attempts [ 1 ]. id , original );
complete ( file ); await tick (); assert . equal ( queue . ready ()[ 0 ]. id , file . id ); queue . dispose ();
const restored = createAgentUploadQueue ( options ); assert . deepEqual ( restored . ready (),[ file ]); assert . equal ( attempts . length , 2 , 'completed upload is never repeated' ); restored . clear (); assert . deepEqual ( saved ,[]);
});
test ( 'mismatched complete echoes cannot hide local attachment errors or the retry action' , async ()=>{
const box = createAgentOptimisticOutbox ({ storage : { read : ()=> null , write : ()=>{}}, changed : ()=>{}, newId : ()=> 'mismatch' , submit : async ()=>{ throw new Error ( 'quota failure' );}});
await assert . rejects ( box . send ( 'caption' ,[ file ])); const initial = box . display ([])[ 0 ];
for ( const attachments of [ undefined ,[{... file , id : file . id . replace ( '12345678' , '12345679' )}]]){
const echo = {... initial , metadata : { kind : 'message' , status : 'complete' ,...( attachments ? { agentAttachments : attachments } : {})}};
box . reconcile ({ messages : [ echo ]}); const shown = box . display ([ echo ])[ 0 ]; assert . equal ( shown . metadata . status , 'failed' ); assert . equal ( shown . metadata . error , 'quota failure' ); assert . deepEqual ( shown . metadata . agentAttachments ,[ file ]); assert . equal ( box . snapshot ()[ 0 ]. confirmed , false );
}
});
test ( 'storage refusal prevents uploads and outbox handoff; no attachment draft is cleared before persistence' , async ()=>{
let uploads = 0 ; const queue = createAgentUploadQueue ({ capability : ()=> cap , storage : { read : ()=> null , write : ()=>{ throw new Error ( 'storage full' );}}, changed : ()=>{}, upload : async ()=>{ uploads ++ ; return file ;}});
assert . throws (()=> queue . add ([ new File ([ 'data' ], 'notes.txt' )]), /storage full/ ); await tick (); assert . equal ( uploads , 0 );
let cleared = false ; const box = createAgentOptimisticOutbox ({ storage : { read : ()=> null , write : ()=>{ throw new Error ( 'storage full' );}}, changed : ()=>{}, submit : async ()=>({ status : 'queued' })});
await assert . rejects ( box . send ( 'draft' ,[ file ],()=>{ cleared = true ;})); assert . equal ( cleared , false );
});
test ( 'a late canceled queue completion cannot overwrite a restored draft' , async ()=>{
let saved = []; const pending = [];
const options = { capability : ()=> cap , changed : ()=>{}, storage : { read : ()=> saved , write : rows => saved = structuredClone ( rows )}, upload : async ()=> new Promise ( resolve => pending . push ( resolve ))};
const old = createAgentUploadQueue ( options ); old . add ([ new File ([ 'data' ], 'notes.txt' ,{ type : 'text/plain' })]); await waitFor (()=> pending . length === 1 );
const oldId = old . snapshot ()[ 0 ]. uploadId ; old . dispose ();
const restored = createAgentUploadQueue ( options ); restored . remove ( oldId ); restored . add ([ new File ([ 'data' ], 'notes.txt' ,{ type : 'text/plain' })]); await waitFor (()=> pending . length === 2 );
pending [ 1 ]( file ); await tick (); const current = structuredClone ( saved ); assert . equal ( current [ 0 ]. status , 'ready' ); assert . notEqual ( current [ 0 ]. uploadId , oldId );
pending [ 0 ]( file ); await tick (); assert . deepEqual ( saved , current ); assert . deepEqual ( restored . ready (),[ file ]); restored . dispose ();
});