import test from 'node:test'; import assert from 'node:assert/strict'; import { makeNodes, connectNodes, ConstellationModel } from '../src/constellation-data.js'; const frame = (time, frequency = 1500) => ({time, rms: 0.2, entropy: 0.4, spread: 900, flatness: 0.03, peaks: [{frequency, amplitude: 0.2}]}); const settings = {trail:12, sensitivity:1}; test('constellation uses measured peak data and excludes expired or silent frames', () => { const nodes = makeNodes([frame(-20), {...frame(0), rms:0}, frame(1)], 2, settings); assert.equal(nodes.length, 1); assert.equal(nodes[0].frequency, 1500); assert.equal(nodes[0].amplitude, 0.2); assert.equal(nodes[0].time, 1); }); test('node positions are stable as they age and respond to changed frequency', () => { const node = makeNodes([frame(1)], 2, settings)[0]; assert.deepEqual(node.position, makeNodes([frame(1)], 3, settings)[0].position); assert.notDeepEqual(node.position, makeNodes([frame(1, 4000)], 2, settings)[0].position); }); test('dense tracks have a bounded point count and finite positions', () => { const frames = Array.from({length:3000}, (_, i) => ({...frame(i / 100), peaks:Array.from({length:12}, (_, j) => ({frequency:200+j*400, amplitude:0.1}))})); const nodes = makeNodes(frames, 30, {...settings, trail:30}); assert.ok(nodes.length <= 2400); assert.ok(nodes.length > 100); for (const node of nodes) assert.ok(node.position.every(Number.isFinite)); }); test('network connections are local, bounded and never self-connect', () => { const nodes = [0, 1, 2, 50].map(x => ({position:[x,0,0]})); const links = connectNodes(nodes); assert.ok(links.length > 0); for (const [a,b] of links) { assert.notEqual(a,b); assert.ok(Math.abs(nodes[a].position[0]-nodes[b].position[0]) < 3); } assert.ok(links.length <= nodes.length * 3); }); test('rolling history retains the identity, position and parents of surviving points', () => { const model = new ConstellationModel(); const history = Array.from({length:60}, (_,i) => frame(i/30,1500+i*2)); const first = model.update(history,2,{...settings,trail:3}); const survivor = first.nodes.find(n=>n.time>1); const saved = {id:survivor.id,position:[...survivor.position],parents:[...survivor.parents]}; history.splice(0,20); history.push(frame(2.1,1630)); const next = model.update(history,2.1,{...settings,trail:3}); const same = next.nodes.find(n=>n.id===saved.id); assert.deepEqual(same.position,saved.position); assert.deepEqual(same.parents,saved.parents); }); test('frequency tracks survive peak loudness reordering and create continuous links', () => { const model = new ConstellationModel(), history=[]; history.push({...frame(0),peaks:[{frequency:1000,amplitude:.3},{frequency:2000,amplitude:.2}]}); const first = model.update(history,0,settings); const tone = first.nodes.find(n=>n.frequency===1000); history.push({...frame(.05),peaks:[{frequency:2000,amplitude:.4},{frequency:1005,amplitude:.1}]}); const next = model.update(history,.05,settings); const continuation = next.nodes.find(n=>n.frequency===1005); assert.equal(continuation.trackId,tone.trackId); assert.ok(continuation.parents.includes(tone.id)); }); test('unchanged audio does not rebuild geometry or connections', () => { const model = new ConstellationModel(), history=[frame(1)]; const first=model.update(history,1,settings); const next=model.update(history,1.01,settings); assert.equal(next.revision,first.revision); assert.equal(next.nodes,first.nodes); assert.equal(next.links,first.links); }); test('a new source resets tracks and never links back to the previous song', () => { const model=new ConstellationModel(); model.update([frame(1)],1,settings); const next=model.update([frame(2)],2,settings); assert.equal(next.nodes.length,1); assert.deepEqual(next.nodes[0].parents,[]); }); test('new samples are smoothly connected through sudden amplitude changes', () => { const model=new ConstellationModel(), history=[{...frame(0),peaks:[{frequency:1500,amplitude:.01}]}]; const start=model.update(history,0,settings).nodes[0]; history.push({...frame(.05),peaks:[{frequency:1500,amplitude:.8}]}); const next=model.update(history,.05,settings).nodes.at(-1); const raw=makeNodes([history[1]],.05,settings)[0]; const distance = (a,b) => Math.hypot(...a.position.map((v,i)=>v-b.position[i])); assert.ok(distance(start,next) { const model=new ConstellationModel(), history=[frame(0,700)]; const first=model.update(history,0,settings).nodes[0]; history.push(frame(.1,6000)); const next=model.update(history,.1,settings); assert.ok(next.nodes.at(-1).parents.includes(first.id)); assert.ok(next.links.some(([, , trail])=>trail)); }); test('sound paths break across silence and all points and links expire', () => { const model=new ConstellationModel(), history=[frame(0,700),frame(1,6000)]; const result=model.update(history,1,settings); assert.equal(result.nodes.at(-1).trailParent,null); const expired=model.update(history,14,settings); assert.equal(expired.nodes.length,0); assert.equal(expired.links.length,0); });