Files
qtalker---/frontend/src/components/views.test.ts
Indiana c372427ced feat: matrix/graph data views, wire-generated anomalies, http cookie fix
- 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
2026-07-21 00:02:41 +00:00

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')
})
})