Back to Funclet
SOURCE / PINNED RELEASE

Made of little things.

Funclet

Release
0b497cfc2f66…
Author-recorded commit
1e531b24f490…
License
LICENSE
Author’s source reference
nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-6b7a37cac1d

Archive hash verified: 71d97a89ed8c40c2…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

tests/funclet.test.mjs
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { runInNewContext } from 'node:vm';
import test from 'node:test';
import ts from 'typescript';

async function loadModule(name, dependencies = {}) {
  const source = await readFile(new URL(`../src/${name}.ts`, import.meta.url), 'utf8');
  const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText;
  const module = { exports: {} };
  runInNewContext(output, { module, exports: module.exports, URL, Error, require: name => {
    if (!(name in dependencies)) throw new Error(`Unexpected dependency ${name}`);
    return dependencies[name];
  } });
  return module.exports;
}
const domain = await loadModule('domain');
const author = 'a'.repeat(64);
const address = `31237:${author}:radio:one`;
const stationEvent = { id: 'f'.repeat(64), kind: 31237, pubkey: author, created_at: 123,
  tags: [['d','radio:one'],['name','Radio One'],['c','Jazz','genre']],
  content: JSON.stringify({ description:'All day jazz', streams:[{url:'https://radio.example/live.mp3',format:'mp3',primary:true}] }) };
const normal = v => JSON.parse(JSON.stringify(v));

test('parses Wavefunc stations, stream tags and legacy JSON without trusting HTML', () => {
  const station = domain.parseStation(stationEvent);
  assert.equal(station.address, address);
  assert.equal(station.description,'All day jazz');
  assert.equal(domain.playableStreams(station).length,1);
  const tagged = domain.parseStation({...stationEvent, tags:[...stationEvent.tags,['name','<img onerror=bad>'], ['stream','https://radio.example/ogg','audio/ogg','{"bitrate":192}','primary']]});
  assert.equal(tagged.streams[0].bitrate,192);
  assert.equal(tagged.streams.length,1);
  const codecOnly=domain.parseStation({...stationEvent,tags:[...stationEvent.tags,['stream','https://radio.example/live','','{"codec":"audio/mpeg"}','primary']]});
  assert.equal(domain.playableStreams(codecOnly).length,1);
  assert.equal(domain.parseStation({...stationEvent,kind:1}),null);
  assert.equal(domain.parseStation({...stationEvent,tags:[]}),null);
  assert.equal(domain.parseStation({...stationEvent,content:'malformed'}),null);
});

test('rejects kind collisions instead of inventing untitled radio stations', () => {
  // Public kind-31237 disco-vault records are albums, not Wavefunc stations.
  const album={...stationEvent,tags:[['d','disco-vault:785'],['title','The Sounds of Machines Our Parents Used'],['type','music'],['country','UK']],content:''};
  assert.equal(domain.parseStation(album),null);
  assert.equal(domain.parseStation({...stationEvent,tags:[['d','unnamed']]}),null);
  assert.equal(domain.parseStation({...stationEvent,content:'{}'}),null);
});

test('keeps successful search results when an unrelated relay rejects NIP-50', async () => {
  const services=await loadModule('services', {'./domain.js':domain,'@napplet/sdk':{outbox:{query:async()=>({events:[{event:stationEvent}],incomplete:true,error:'OUTBOX_READ: Primal does not support search'})}}});
  const response=await services.searchStations('jazz');
  assert.equal(response.stations[0].name,'Radio One');
  assert.equal(response.incomplete,true);
});

test('retains station candidates before applying the display limit to a mixed relay response', async () => {
  const calls=[];
  const albums=Array.from({length:48},(_,i)=>({...stationEvent,id:`album-${i}`,created_at:200+i,tags:[['d',`disco-vault:${i}`],['title',`Album ${i}`]],content:''}));
  const services=await loadModule('services', {'./domain.js':domain,'@napplet/sdk':{outbox:{query:async(filters,options)=>{
    calls.push({filters,options});
    // The production host applies options.limit to the union of relay results.
    return {events:[...albums,stationEvent].slice(0,options.limit??filters[0].limit).map(event=>({event})),incomplete:false};
  }}}});
  const response=await services.searchStations('');
  assert.equal(response.stations.length,1);
  assert.equal(response.stations[0].name,'Radio One');
  assert.equal(calls[0].filters[0].limit,48);
  assert.equal(calls[0].options.limit,200);
  assert.deepEqual(normal(calls[0].options.relays),[domain.WAVEFUNC_RELAY]);
});

test('retains partial favourite lists and stations with retry status', async () => {
  const list={...stationEvent,kind:30078,tags:[['d','favs'],['l',domain.FAVOURITES_LABEL],['a',address]],content:'{"name":"Favourites"}'};
  const services=await loadModule('services', {'./domain.js':domain,'@napplet/sdk':{outbox:{query:async filters=>({events:[{event:filters[0].kinds[0]===30078?list:stationEvent}],error:'One relay is offline'})}}});
  const lists=await services.readFavourites(author);
  assert.equal(lists.lists.length,1);assert.equal(lists.incomplete,true);
  const stations=await services.readFavouriteStations([address]);
  assert.equal(stations.stations.length,1);assert.equal(stations.incomplete,true);
});

test('does not offer insecure streams, HLS, playlists or credential URLs for playback', () => {
  const station = domain.parseStation(stationEvent);
  for (const [url,format] of [['http://example.org/a.mp3','mp3'],['https://u:p@example.org/a.mp3','mp3'],['https://example.org/radio.m3u8','mp3'],['https://example.org/live','application/vnd.apple.mpegurl'],['javascript:alert(1)','mp3'],['https://example.org/live','flac'],['https://example.org/live','']]) {
    assert.equal(domain.playableStreams({...station, streams:[{url,format}]}).length,0);
  }
});

test('HTTP links remain external, preserve ports, reject credentials and prefer the primary stream', () => {
  const station=domain.parseStation(stationEvent);
  const streams=[
    {url:'http://example.org:8000/secondary',format:'mp3'},
    {url:'HTTP://example.org:8000/primary',format:'audio/mpeg',primary:true},
    {url:'https://example.org/live.mp3',format:'mp3'},
    {url:'http://u:p@example.org/stream',format:'mp3'},
    {url:'javascript:alert(1)',format:'mp3'},
    {url:'//example.org/stream',format:'mp3'},
  ];
  assert.deepEqual(normal(domain.httpStreams({...station,streams})).map(s=>s.url),['http://example.org:8000/primary','http://example.org:8000/secondary']);
  assert.deepEqual(normal(domain.playableStreams({...station,streams})).map(s=>s.url),['https://example.org/live.mp3']);
  assert.equal(domain.wavefuncStationUrl(station),`https://wavefunc.live/station/${stationEvent.id}`);
  assert.equal(domain.wavefuncStationUrl({...station,id:'../../other'}),'');
});

test('reads only Wavefunc favourites, deduplicates valid addresses and keeps colon identifiers', () => {
  const event={...stationEvent, kind:30078, tags:[['d','favs'],['l',domain.FAVOURITES_LABEL],['a',address],['a',address],['a','31237:invalid:foo']],content:'{"name":"Evening radio"}'};
  const list=domain.parseFavouriteList(event);
  assert.equal(list.name,'Evening radio');
  assert.deepEqual(normal(list.addresses),[address]);
  assert.equal(domain.stationReference(address).identifier,'radio:one');
  assert.equal(domain.parseFavouriteList({...event,tags:[['d','favs']]}),null);
});

test('selects replaceable events by latest timestamp and lowest ID on ties', () => {
  const items=[{id:'z',createdAt:1,key:'one'},{id:'b',createdAt:2,key:'one'},{id:'a',createdAt:2,key:'one'}];
  assert.equal(domain.latestBy(items,x=>x.key)[0].id,'a');
});

test('accepts both MCP response shapes and rejects server errors/malformed payloads', () => {
  const value={result:{title:'On air',artist:'Artist',source:'ICY'}};
  assert.deepEqual(normal(domain.toolPayload({structuredContent:value})),value.result);
  assert.deepEqual(normal(domain.toolPayload({content:[{type:'text',text:JSON.stringify(value)}]})),value.result);
  assert.throws(()=>domain.toolPayload({isError:true,content:[]}));
  assert.throws(()=>domain.toolPayload({content:[{type:'text',text:'oops'}]}));
  assert.throws(()=>domain.toolPayload({structuredContent:{error:'No source'}}));
  assert.equal(domain.parseNowPlaying({title:'No metadata available',source:'UNKNOWN'}),null);
  assert.equal(domain.parseNowPlaying({song:'Raw title',enriched:{title:'Guess',confidence:'low'}}).title,'Raw title');
});

test('resolves favourite addresses in exact author/identifier batches within host limits', async () => {
  const calls=[];
  const services=await loadModule('services', {'./domain.js':domain,'@napplet/sdk':{outbox:{query: async (filters,options)=>{calls.push({filters,options});return {events:[],incomplete:calls.length===2};}}}});
  const addresses=Array.from({length:9},(_,i)=>`31237:${author}:station-${i}`);
  const response=await services.readFavouriteStations(addresses);
  assert.equal(calls.length,3);
  assert.equal(calls[0].filters.length,4);
  assert.equal(calls[2].filters[0]['#d'][0],'station-8');
  assert.equal(response.incomplete,true);
});

test('a failed favourite batch does not discard stations from another batch', async () => {
  let calls=0;
  const services=await loadModule('services', {'./domain.js':domain,'@napplet/sdk':{outbox:{query:async()=>++calls===1?{events:[],error:'relay unavailable'}:{events:[{event:stationEvent}]}}}});
  const response=await services.readFavouriteStations([...Array.from({length:4},(_,i)=>`31237:${author}:missing-${i}`),address]);
  assert.equal(response.stations.length,1);assert.equal(response.incomplete,true);
});

test('completed missing favourite lookups never substitute a different station', async () => {
  const services=await loadModule('services', {'./domain.js':domain,'@napplet/sdk':{outbox:{query:async()=>({events:[{event:stationEvent}],incomplete:false})}}});
  const response=await services.readFavouriteStations([`31237:${author}:missing`]);
  assert.equal(response.stations.length,0);assert.equal(response.incomplete,false);
});

test('partial and failed station queries are not silently converted to empty success', async () => {
  const services=await loadModule('services', {'./domain.js':domain,'@napplet/sdk':{outbox:{query:async()=>({events:[],incomplete:true})}}});
  assert.equal((await services.searchStations('jazz')).incomplete,true);
  const failed=await loadModule('services', {'./domain.js':domain,'@napplet/sdk':{outbox:{query:async()=>({events:[],error:'offline'})}}});
  await assert.rejects(failed.searchStations('jazz'),/offline/);
});

test('player uses canonical shell IDs, advertised controls and actual playback state', async () => {
  const commands=[]; let state,capabilities; let closed=0;
  const media={createSession:async()=>({sessionId:'canonical',owner:'shell'}),onState:(id,cb)=>{assert.equal(id,'canonical');state=cb;return {close(){closed++;}};},onCapabilities:(id,cb)=>{capabilities=cb;return {close(){closed++;}};},sendCommand:(...args)=>commands.push(args),destroySession:id=>commands.push(['destroy',id])};
  const {RadioPlayer}=await loadModule('player',{'@napplet/sdk':{media}});
  const player=new RadioPlayer(()=>{});
  const station=domain.parseStation(stationEvent);
  await player.tune(station,station.streams[0]);
  assert.equal(commands.length,0);
  state({status:'stopped',volume:1});
  capabilities(['play','pause','volume']);
  assert.deepEqual(commands,[['canonical','volume',.75],['canonical','play']],'initial host state must not discard the requested volume');
  assert.equal(player.status,'stopped');
  state({status:'playing',volume:.5});
  assert.equal(player.status,'playing');
  player.command('seek',50);
  assert.equal(commands.length,2);
  player.command('pause');
  assert.deepEqual(commands.at(-1),['canonical','pause',undefined]);
  player.destroy();
  assert.equal(closed,2);
});

test('late session creation is destroyed after a station switch or teardown', async () => {
  const pending=[]; const destroyed=[];
  const media={createSession:()=>new Promise(resolve=>pending.push(resolve)),destroySession:id=>destroyed.push(id),onState:()=>({close(){}}),onCapabilities:()=>({close(){}})};
  const {RadioPlayer}=await loadModule('player',{'@napplet/sdk':{media}});
  const player=new RadioPlayer(()=>{}); const station=domain.parseStation(stationEvent);
  const first=player.tune(station,station.streams[0]);
  const second=player.tune({...station,name:'Second'},station.streams[0]);
  pending[1]({sessionId:'new',owner:'shell'}); await second;
  pending[0]({sessionId:'old',owner:'shell'}); await first;
  assert.equal(player.sessionId,'new'); assert.deepEqual(destroyed,['old']);
  player.destroy(); assert.deepEqual(destroyed,['old','new']);
});

test('volume storage validates restores, ignores late reads, and keeps writes ordered', async () => {
  const { VolumePreference } = await loadModule('volume-preference');
  for (const [raw, expected] of [[null,null],['',null],['broken',null],['2',null],['-1',null],['0',0],['0.35',.35],['1',1]]) {
    assert.equal(await new VolumePreference({getItem:async()=>raw}).load(),expected);
  }
  assert.equal(await new VolumePreference(undefined).load(),null);
  const unavailable=new VolumePreference({getItem:async()=>{throw Error('denied');},setItem:async()=>{throw Error('denied');}});
  assert.equal(await unavailable.load(),null);unavailable.save(.4);
  let finishRead, finishWrite;
  const writes=[];
  const preference=new VolumePreference({getItem:()=>new Promise(resolve=>{finishRead=resolve;}),setItem:async(key,value)=>{
    writes.push([key,value]);if(writes.length===1)await new Promise(resolve=>{finishWrite=resolve;});
  }});
  const loading=preference.load();preference.save(.4);preference.save(.5);preference.save(.6);
  finishRead('.9');assert.equal(await loading,null,'a late read cannot undo user input');
  assert.deepEqual(normal(writes),[['receiver-volume','0.4']]);
  finishWrite();await new Promise(resolve=>setImmediate(resolve));
  assert.deepEqual(normal(writes),[['receiver-volume','0.4'],['receiver-volume','0.6']]);
});

test('saved station addresses are validated before an OUTBOX lookup', async () => {
  const { StationPreference }=await loadModule('volume-preference');
  for(const raw of ['broken','https://example.org','31237:bad:id',`31237:${author}:`]) {
    assert.equal(await new StationPreference({getItem:async()=>raw}).load(),null);
  }
  assert.equal(await new StationPreference({getItem:async()=>address}).load(),address);
});