- auth: session cookie Secure only over https — plain-http LAN access was
silently dropping the cookie, killing WS auth ('connection unstable')
- wire ghost: server-side spike detection on jitter baseline (3σ + 2.5×mean,
20KB/s floor, 20s throttle) — wire anomalies now flood every session with
zero hardware, pushed to clients as {type:'anomaly'} frames
- telemetry cadence 3-5s for live graphs; ambient whispers unchanged
- frontend: MATRIX view (data-rain interleaved with live utterances/anomaly/
telemetry strings), GRAPHS view (scrolling jitter/variance/dns lines +
anomaly markers + counters), BOARD/MATRIX/GRAPHS switcher
- connection banner: 'connecting' is now neutral 'tuning the veil…', only
unstable/closed warns
- db: recreated quantumancy(+_test) as UTF8 (was SQL_ASCII — crashed on
non-ASCII spirit text); README quickstart updated
- i18n: seance.views.* EN/ES
142 lines
5.2 KiB
TypeScript
142 lines
5.2 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { initialSeanceState, seanceReducer } from '../state/seance'
|
|
import type { AnomalyEntry, SeanceState, Utterance } from '../state/seance'
|
|
import type { ServerFrame } from '../lib/types'
|
|
import { anomalyDensity, anomalyString, collectDataStrings, pushRing, utteranceWord, DATA_CAP } from './MatrixView'
|
|
import { adaptiveMax, anomalyColor, pushHistory, HISTORY_CAP } from './GraphView'
|
|
|
|
const AT = 1_700_000_000_000
|
|
|
|
function frame(state: SeanceState, f: ServerFrame, at = AT): SeanceState {
|
|
return seanceReducer(state, { type: 'frame', frame: f, at })
|
|
}
|
|
|
|
function lastRow(state: SeanceState) {
|
|
return state.transcript[state.transcript.length - 1]
|
|
}
|
|
|
|
describe('seanceReducer — server anomaly frames', () => {
|
|
it('appends a server anomaly to both the anomalies list and the transcript', () => {
|
|
const state = frame(initialSeanceState, {
|
|
type: 'anomaly',
|
|
source: 'wire',
|
|
frequency: 432.8,
|
|
magnitude: -3.1,
|
|
})
|
|
expect(state.anomalies).toHaveLength(1)
|
|
expect(state.anomalies[0]).toMatchObject({
|
|
source: 'wire',
|
|
frequency: 432.8,
|
|
magnitude: -3.1,
|
|
at: AT,
|
|
})
|
|
expect(lastRow(state)).toMatchObject({
|
|
kind: 'anomaly',
|
|
source: 'wire',
|
|
frequency: 432.8,
|
|
magnitude: -3.1,
|
|
at: AT,
|
|
})
|
|
// Same entry id shared between the list and the transcript row.
|
|
const row = lastRow(state)
|
|
if (row.kind === 'anomaly') expect(row.id).toBe(state.anomalies[0].id)
|
|
// The server-side count only moves when anomaly_ack arrives.
|
|
expect(state.anomalyCount).toBe(0)
|
|
expect(initialSeanceState.anomalies).toEqual([]) // reducer is pure
|
|
})
|
|
|
|
it('caps server anomalies at 200, dropping the oldest', () => {
|
|
let state = initialSeanceState
|
|
for (let i = 1; i <= 205; i++) {
|
|
state = frame(
|
|
state,
|
|
{ type: 'anomaly', source: 'radio', frequency: 100 + i, magnitude: 5 },
|
|
AT + i,
|
|
)
|
|
}
|
|
expect(state.anomalies).toHaveLength(200)
|
|
expect(state.anomalies[0].frequency).toBe(106)
|
|
expect(state.anomalies[199].frequency).toBe(305)
|
|
})
|
|
})
|
|
|
|
describe('MatrixView helpers', () => {
|
|
it('pushRing appends and caps, dropping the oldest', () => {
|
|
let ring: number[] = []
|
|
for (let i = 1; i <= 5; i++) ring = pushRing(ring, i, 3)
|
|
expect(ring).toEqual([3, 4, 5])
|
|
})
|
|
|
|
it('anomalyString formats frequency and signed magnitude', () => {
|
|
expect(anomalyString({ frequency: 101.44, magnitude: 8.23 })).toBe('101.4MHZ +8.2DB')
|
|
expect(anomalyString({ frequency: 88, magnitude: -3.06 })).toBe('88.0MHZ -3.1DB')
|
|
})
|
|
|
|
it('utteranceWord picks a single uppercase word and rejects noise', () => {
|
|
expect(utteranceWord('the veil is thin')).toBe('VEIL') // 'IS' filtered (<3), middle of the rest
|
|
expect(utteranceWord('mirela')).toBe('MIRELA')
|
|
expect(utteranceWord('... --- ...')).toBeNull()
|
|
})
|
|
|
|
it('collectDataStrings gathers words, anomalies, telemetry and the entity name', () => {
|
|
const anomalies: AnomalyEntry[] = [
|
|
{ id: 1, source: 'wire', frequency: 55.2, magnitude: 1.2, at: AT },
|
|
]
|
|
const utterances: Utterance[] = [
|
|
{ id: 'u1', kind: 'fragment', text: 'we are below', entity: null, audioUrl: null, at: AT },
|
|
]
|
|
const ring = collectDataStrings({
|
|
anomalies,
|
|
utterances,
|
|
telemetry: { jitter_bytes_per_s: 23.68, latency_variance_ms: 1, latency_mean_ms: 5.94, dns_ms: 7 },
|
|
entityName: 'Mirela',
|
|
})
|
|
expect(ring).toContain('BELOW')
|
|
expect(ring).toContain('55.2MHZ +1.2DB')
|
|
expect(ring).toContain('JIT 23.7KB/S')
|
|
expect(ring).toContain('RTT 5.9MS')
|
|
expect(ring).toContain('MIRELA')
|
|
expect(ring.length).toBeLessThanOrEqual(DATA_CAP)
|
|
})
|
|
|
|
it('collectDataStrings degrades to an empty ring with no live data', () => {
|
|
expect(
|
|
collectDataStrings({ anomalies: [], utterances: [], telemetry: null, entityName: null }),
|
|
).toEqual([])
|
|
})
|
|
|
|
it('anomalyDensity scales 0→1 with the recent anomaly rate', () => {
|
|
const now = AT
|
|
expect(anomalyDensity([], now)).toBe(0)
|
|
const recent = Array.from({ length: 10 }, (_, i) => ({ at: now - i * 500 }))
|
|
expect(anomalyDensity(recent, now, 10_000)).toBe(1)
|
|
const stale = [{ at: now - 60_000 }]
|
|
expect(anomalyDensity(stale, now, 10_000)).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe('GraphView helpers', () => {
|
|
it('pushHistory appends and caps at HISTORY_CAP, dropping the oldest', () => {
|
|
let history = pushHistory([], { at: AT, jitter: 1, variance: 1, dns: 1 })
|
|
expect(history).toHaveLength(1)
|
|
for (let i = 1; i <= HISTORY_CAP + 5; i++) {
|
|
history = pushHistory(history, { at: AT + i, jitter: i, variance: i, dns: i })
|
|
}
|
|
expect(history).toHaveLength(HISTORY_CAP)
|
|
expect(history[history.length - 1].jitter).toBe(HISTORY_CAP + 5)
|
|
expect(history[0].jitter).toBe(6)
|
|
})
|
|
|
|
it('adaptiveMax respects the floor and adds headroom', () => {
|
|
expect(adaptiveMax([], 10)).toBeCloseTo(11.5)
|
|
expect(adaptiveMax([2, 4, 100, 7], 10)).toBeCloseTo(115)
|
|
expect(adaptiveMax([1, 2], 50)).toBeCloseTo(57.5)
|
|
})
|
|
|
|
it('anomalyColor maps sources to their token colors', () => {
|
|
expect(anomalyColor('wire')).toBe('#7cffb2')
|
|
expect(anomalyColor('evp')).toBe('#b26bff')
|
|
expect(anomalyColor('radio')).toBe('#ff9d4a')
|
|
})
|
|
})
|