VulpesOS/tests/messages.test.mjs
OverSu 1311bed5e9 Release Preview 20 shared desktop, Android and Sargo updates
Restore Gaia settings and media workflows, extend native network adapters, and align the APK payload. Generate French/English changelogs and website entries from the simplified JSON source.

Validation: shared JS and Tundra unit tests; desktop workflows and persistence; signed APK payload parity; Sargo RAM and installed startup with boot readback. Android runtime qualification is recorded separately.
2026-10-13 00:19:45 +02:00

109 lines
6.6 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {Messages} from '../host/Messages.sys.mjs';
const app={manifest:{permissions:{sms:{}}}};
function store(initial=[]) {
let records=structuredClone(initial);
return {run:async (_store,op,args)=>{if(op==='get')return {value:structuredClone(records)};records=structuredClone(args[0]);}};
}
test('concurrent sends keep both records and numeric stable thread IDs',async()=>{
const service=new Messages(store(),async()=>({path:'/ril_0/message1'}));
const send=body=>service.request(app,{operation:'send',args:['+33100000000',body]});
const [a,b]=await Promise.all([send('one'),send('two')]);
assert.ok(Number.isSafeInteger(a.id));assert.ok(Number.isSafeInteger(a.threadId));
assert.notEqual(a.id,b.id);assert.equal(a.threadId,b.threadId);
const rows=await service.request(app,{operation:'getMessages',args:[{threadId:a.threadId}]});
assert.equal(rows.length,2);
});
test('legacy modem-path IDs migrate once without losing message bodies',async()=>{
const service=new Messages(store([{id:'/ril_0/message1',threadId:'+33100000000',type:'sms',receiver:'+33100000000',body:'old',timestamp:1}]));
const a=await service.request(app,{operation:'getMessages'});
const b=await service.request(app,{operation:'getMessages'});
assert.deepEqual(a,b);assert.equal(a[0].body,'old');assert.ok(Number.isSafeInteger(a[0].threadId));
});
test('radio rejection never creates a sent message',async()=>{
const service=new Messages(store(),async()=>{throw Error('RADIO_NOT_REGISTERED');});
await assert.rejects(service.request(app,{operation:'send',args:['+33100000000','test']}),/RADIO_NOT_REGISTERED/);
const failed=await service.request(app,{operation:'getMessages'});
assert.equal(failed.length,1);assert.equal(failed[0].delivery,'error');
assert.equal(failed[0].body,'test');
assert.equal((await service.request(app,{operation:'getThreads'})).length,1);
});
test('SMS filters isolate unread messages, recipients and date ranges',async()=>{
const service=new Messages(store([
{id:1,threadId:10,sender:'111',timestamp:1000,read:false,delivery:'received'},
{id:2,threadId:10,sender:'111',timestamp:2000,read:true,delivery:'received'},
{id:3,threadId:20,receiver:'222',timestamp:3000,read:true,delivery:'sent'},
]));
const list=async(filter,reverse=false)=>(await service.request(app,{operation:'getMessages',args:[filter,reverse]})).map(m=>m.id);
assert.deepEqual(await list({threadId:10,read:false}),[1]);
assert.deepEqual(await list({numbers:['222']}),[3]);
assert.deepEqual(await list({numbers:['333']}),[]);
assert.deepEqual(await list({startDate:new Date(2000),endDate:new Date(3000)},true),[3,2]);
assert.deepEqual(await list({delivery:'received'}),[1,2]);
await service.request(app,{operation:'markMessageRead',args:[1,true]});
assert.deepEqual(await list({read:false}),[]);
assert.equal((await service.request(app,{operation:'getThreads'}))[1].unreadCount,0);
assert.deepEqual(await service.request(app,{operation:'delete',args:[[1,2,999]]}),[true,true,false]);
assert.deepEqual((await service.request(app,{operation:'getThreads'})).map(t=>t.id),[20]);
});
test('incoming retries persist once and share the outgoing conversation',async()=>{
const database=store();
const service=new Messages(database,async()=>({path:'/ril_0/message1'}));
const sent=await service.request(app,{operation:'send',args:['111','out']});
const incoming={receipt:'receipt-1',sender:'111',body:'in',timestamp:10000};
const [a,b]=await Promise.all([service.receive(incoming),service.receive(incoming)]);
assert.equal(a.id,b.id);assert.equal(a.threadId,sent.threadId);
assert.equal(a.read,false);assert.equal(a.delivery,'received');
const restarted=new Messages(database);
assert.equal((await restarted.receive(incoming)).id,a.id);
assert.equal((await restarted.request(app,{operation:'getMessages'})).length,2);
});
test('failed inbox persistence is retryable without poisoning the queue',async()=>{
let fail=true;const database=store(),run=database.run;
database.run=async(...args)=>{if(args[1]==='put' && fail){fail=false;throw Error('DISK_FULL');}return run(...args);};
const service=new Messages(database);
const incoming={receipt:'receipt-2',sender:'222',body:'test',timestamp:1000};
await assert.rejects(service.receive(incoming),/DISK_FULL/);
await service.receive(incoming);
assert.equal((await service.request(app,{operation:'getMessages'})).length,1);
});
test('failed send remains in conversation and emits sending then failed, never sent',async()=>{
const events=[];
const service=new Messages(store(),async()=>{throw Error('RADIO_NOT_REGISTERED');},(type,m)=>events.push([type,m.delivery]));
await assert.rejects(service.request(app,{operation:'send',args:['111','keep me']}));
assert.deepEqual(events,[['sending','sending'],['failed','error']]);
});
test('queued modem messages remain pending until a terminal state arrives',async()=>{
const events=[];let state='pending';const database=store();
const service=new Messages(database,async()=>({path:'/ril_0/message_123',state:'pending'}),
(type,m)=>events.push([type,m.delivery]),async()=>({state}));
const message=await service.request(app,{operation:'send',args:['111','queued']});
assert.equal(message.delivery,'sending');
await service.refreshOutgoing();assert.deepEqual(events,[['sending','sending']]);
state='failed';await service.refreshOutgoing();
assert.deepEqual(events,[['sending','sending'],['failed','error']]);
await service.refreshOutgoing();assert.equal(events.length,2);
assert.equal((await service.request(app,{operation:'getMessages'}))[0].delivery,'error');
});
test('pending transmission status survives a host restart',async()=>{
const database=store();
const first=new Messages(database,async()=>({path:'/ril_0/message_123',state:'pending'}));
await first.request(app,{operation:'send',args:['111','queued']});
const events=[];
const restarted=new Messages(database,null,(type)=>events.push(type),async()=>({state:'sent'}));
await restarted.refreshOutgoing();
assert.deepEqual(events,['sent']);
assert.equal((await restarted.request(app,{operation:'getMessages'}))[0].delivery,'sent');
});
test('unsupported MMS never invokes the SMS modem or records a sent message',async()=>{
let submissions=0;
const service=new Messages(store(),async()=>{submissions++;return {};});
for(const operation of ['sendMMS','retrieveMMS']) {
await assert.rejects(service.request(app,{operation,args:[{}]}),/NotSupportedError/);
}
assert.equal(submissions,0);
assert.deepEqual(await service.request(app,{operation:'getMessages'}),[]);
});