mirror of
https://github.com/ruvnet/RuView
synced 2026-08-03 19:21:42 +00:00
fix: complete sensing server API, WebSocket connectivity, and mobile tests (#125)
The web UI had persistent 404 errors on model, recording, and training endpoints, and the sensing WebSocket never connected on Dashboard/Live Demo tabs because sensingService.start() was only called lazily on Sensing tab visit. Server (main.rs): - Add 14 fully-functional Axum handlers: model CRUD (7), recording lifecycle (4), training control (3) - Scan data/models/ and data/recordings/ at startup - Recording writes CSI frames to .jsonl via tokio background task - Model load/unload lifecycle with state tracking Web UI (app.js): - Import and start sensingService early in initializeServices() so Dashboard and Live Demo tabs connect to /ws/sensing immediately Mobile (ws.service.ts): - Fix WebSocket URL builder to use same-origin port instead of hardcoded port 3001 Mobile (jest.config.js): - Fix testPathIgnorePatterns that was ignoring the entire test directory Mobile (25 test files): - Replace all it.todo() placeholder tests with real implementations covering components, services, stores, hooks, screens, and utils ADR-043 documents all changes.
This commit is contained in:
@@ -1,5 +1,198 @@
|
||||
describe('placeholder', () => {
|
||||
it('passes', () => {
|
||||
expect(true).toBe(true);
|
||||
import { useMatStore } from '@/stores/matStore';
|
||||
import { AlertPriority, TriageStatus, ZoneStatus } from '@/types/mat';
|
||||
import type { Alert, DisasterEvent, ScanZone, Survivor } from '@/types/mat';
|
||||
|
||||
const makeEvent = (overrides: Partial<DisasterEvent> = {}): DisasterEvent => ({
|
||||
event_id: 'evt-1',
|
||||
disaster_type: 1,
|
||||
latitude: 37.77,
|
||||
longitude: -122.41,
|
||||
description: 'Earthquake in SF',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const makeZone = (overrides: Partial<ScanZone> = {}): ScanZone => ({
|
||||
id: 'zone-1',
|
||||
name: 'Zone A',
|
||||
zone_type: 'rectangle',
|
||||
status: ZoneStatus.Active,
|
||||
scan_count: 0,
|
||||
detection_count: 0,
|
||||
bounds_json: '{}',
|
||||
...overrides,
|
||||
} as ScanZone);
|
||||
|
||||
const makeSurvivor = (overrides: Partial<Survivor> = {}): Survivor => ({
|
||||
id: 'surv-1',
|
||||
zone_id: 'zone-1',
|
||||
x: 100,
|
||||
y: 150,
|
||||
depth: 2.5,
|
||||
triage_status: TriageStatus.Immediate,
|
||||
triage_color: '#FF0000',
|
||||
confidence: 0.9,
|
||||
breathing_rate: 16,
|
||||
heart_rate: 80,
|
||||
first_detected: '2024-01-01T00:00:00Z',
|
||||
last_updated: '2024-01-01T00:01:00Z',
|
||||
is_deteriorating: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const makeAlert = (overrides: Partial<Alert> = {}): Alert => ({
|
||||
id: 'alert-1',
|
||||
survivor_id: 'surv-1',
|
||||
priority: AlertPriority.Critical,
|
||||
title: 'Critical survivor',
|
||||
message: 'Breathing rate dropping',
|
||||
recommended_action: 'Immediate extraction',
|
||||
triage_status: TriageStatus.Immediate,
|
||||
location_x: 100,
|
||||
location_y: 150,
|
||||
created_at: '2024-01-01T00:01:00Z',
|
||||
priority_color: '#FF0000',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('useMatStore', () => {
|
||||
beforeEach(() => {
|
||||
useMatStore.setState({
|
||||
events: [],
|
||||
zones: [],
|
||||
survivors: [],
|
||||
alerts: [],
|
||||
selectedEventId: null,
|
||||
});
|
||||
});
|
||||
|
||||
describe('initial state', () => {
|
||||
it('has empty events array', () => {
|
||||
expect(useMatStore.getState().events).toEqual([]);
|
||||
});
|
||||
|
||||
it('has empty zones array', () => {
|
||||
expect(useMatStore.getState().zones).toEqual([]);
|
||||
});
|
||||
|
||||
it('has empty survivors array', () => {
|
||||
expect(useMatStore.getState().survivors).toEqual([]);
|
||||
});
|
||||
|
||||
it('has empty alerts array', () => {
|
||||
expect(useMatStore.getState().alerts).toEqual([]);
|
||||
});
|
||||
|
||||
it('has null selectedEventId', () => {
|
||||
expect(useMatStore.getState().selectedEventId).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('upsertEvent', () => {
|
||||
it('adds a new event', () => {
|
||||
const event = makeEvent();
|
||||
useMatStore.getState().upsertEvent(event);
|
||||
expect(useMatStore.getState().events).toEqual([event]);
|
||||
});
|
||||
|
||||
it('updates an existing event by event_id', () => {
|
||||
const event = makeEvent();
|
||||
useMatStore.getState().upsertEvent(event);
|
||||
|
||||
const updated = makeEvent({ description: 'Updated description' });
|
||||
useMatStore.getState().upsertEvent(updated);
|
||||
|
||||
const events = useMatStore.getState().events;
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0].description).toBe('Updated description');
|
||||
});
|
||||
|
||||
it('adds a second event with different event_id', () => {
|
||||
useMatStore.getState().upsertEvent(makeEvent({ event_id: 'evt-1' }));
|
||||
useMatStore.getState().upsertEvent(makeEvent({ event_id: 'evt-2' }));
|
||||
expect(useMatStore.getState().events).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addZone', () => {
|
||||
it('adds a new zone', () => {
|
||||
const zone = makeZone();
|
||||
useMatStore.getState().addZone(zone);
|
||||
expect(useMatStore.getState().zones).toEqual([zone]);
|
||||
});
|
||||
|
||||
it('updates an existing zone by id', () => {
|
||||
const zone = makeZone();
|
||||
useMatStore.getState().addZone(zone);
|
||||
|
||||
const updated = makeZone({ name: 'Zone A Updated', scan_count: 5 });
|
||||
useMatStore.getState().addZone(updated);
|
||||
|
||||
const zones = useMatStore.getState().zones;
|
||||
expect(zones).toHaveLength(1);
|
||||
expect(zones[0].name).toBe('Zone A Updated');
|
||||
expect(zones[0].scan_count).toBe(5);
|
||||
});
|
||||
|
||||
it('adds multiple distinct zones', () => {
|
||||
useMatStore.getState().addZone(makeZone({ id: 'zone-1' }));
|
||||
useMatStore.getState().addZone(makeZone({ id: 'zone-2' }));
|
||||
expect(useMatStore.getState().zones).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('upsertSurvivor', () => {
|
||||
it('adds a new survivor', () => {
|
||||
const survivor = makeSurvivor();
|
||||
useMatStore.getState().upsertSurvivor(survivor);
|
||||
expect(useMatStore.getState().survivors).toEqual([survivor]);
|
||||
});
|
||||
|
||||
it('updates an existing survivor by id', () => {
|
||||
useMatStore.getState().upsertSurvivor(makeSurvivor());
|
||||
const updated = makeSurvivor({ confidence: 0.95, is_deteriorating: true });
|
||||
useMatStore.getState().upsertSurvivor(updated);
|
||||
|
||||
const survivors = useMatStore.getState().survivors;
|
||||
expect(survivors).toHaveLength(1);
|
||||
expect(survivors[0].confidence).toBe(0.95);
|
||||
expect(survivors[0].is_deteriorating).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addAlert', () => {
|
||||
it('adds a new alert', () => {
|
||||
const alert = makeAlert();
|
||||
useMatStore.getState().addAlert(alert);
|
||||
expect(useMatStore.getState().alerts).toEqual([alert]);
|
||||
});
|
||||
|
||||
it('updates an existing alert by id', () => {
|
||||
useMatStore.getState().addAlert(makeAlert());
|
||||
const updated = makeAlert({ message: 'Updated message' });
|
||||
useMatStore.getState().addAlert(updated);
|
||||
|
||||
const alerts = useMatStore.getState().alerts;
|
||||
expect(alerts).toHaveLength(1);
|
||||
expect(alerts[0].message).toBe('Updated message');
|
||||
});
|
||||
|
||||
it('adds multiple distinct alerts', () => {
|
||||
useMatStore.getState().addAlert(makeAlert({ id: 'alert-1' }));
|
||||
useMatStore.getState().addAlert(makeAlert({ id: 'alert-2' }));
|
||||
expect(useMatStore.getState().alerts).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSelectedEvent', () => {
|
||||
it('sets the selected event id', () => {
|
||||
useMatStore.getState().setSelectedEvent('evt-1');
|
||||
expect(useMatStore.getState().selectedEventId).toBe('evt-1');
|
||||
});
|
||||
|
||||
it('clears the selection with null', () => {
|
||||
useMatStore.getState().setSelectedEvent('evt-1');
|
||||
useMatStore.getState().setSelectedEvent(null);
|
||||
expect(useMatStore.getState().selectedEventId).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,168 @@
|
||||
describe('placeholder', () => {
|
||||
it('passes', () => {
|
||||
expect(true).toBe(true);
|
||||
import { usePoseStore } from '@/stores/poseStore';
|
||||
import type { SensingFrame } from '@/types/sensing';
|
||||
|
||||
const makeFrame = (overrides: Partial<SensingFrame> = {}): SensingFrame => ({
|
||||
type: 'sensing_update',
|
||||
timestamp: Date.now(),
|
||||
source: 'simulated',
|
||||
nodes: [{ node_id: 1, rssi_dbm: -45, position: [0, 0, 0] }],
|
||||
features: {
|
||||
mean_rssi: -45,
|
||||
variance: 1.5,
|
||||
motion_band_power: 0.1,
|
||||
breathing_band_power: 0.05,
|
||||
spectral_entropy: 0.8,
|
||||
},
|
||||
classification: {
|
||||
motion_level: 'present_still',
|
||||
presence: true,
|
||||
confidence: 0.85,
|
||||
},
|
||||
signal_field: {
|
||||
grid_size: [20, 1, 20],
|
||||
values: new Array(400).fill(0.5),
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('usePoseStore', () => {
|
||||
beforeEach(() => {
|
||||
usePoseStore.getState().reset();
|
||||
});
|
||||
|
||||
describe('initial state', () => {
|
||||
it('has disconnected connectionStatus', () => {
|
||||
expect(usePoseStore.getState().connectionStatus).toBe('disconnected');
|
||||
});
|
||||
|
||||
it('has isSimulated false', () => {
|
||||
expect(usePoseStore.getState().isSimulated).toBe(false);
|
||||
});
|
||||
|
||||
it('has null lastFrame', () => {
|
||||
expect(usePoseStore.getState().lastFrame).toBeNull();
|
||||
});
|
||||
|
||||
it('has empty rssiHistory', () => {
|
||||
expect(usePoseStore.getState().rssiHistory).toEqual([]);
|
||||
});
|
||||
|
||||
it('has null features', () => {
|
||||
expect(usePoseStore.getState().features).toBeNull();
|
||||
});
|
||||
|
||||
it('has null classification', () => {
|
||||
expect(usePoseStore.getState().classification).toBeNull();
|
||||
});
|
||||
|
||||
it('has null signalField', () => {
|
||||
expect(usePoseStore.getState().signalField).toBeNull();
|
||||
});
|
||||
|
||||
it('has zero messageCount', () => {
|
||||
expect(usePoseStore.getState().messageCount).toBe(0);
|
||||
});
|
||||
|
||||
it('has null uptimeStart', () => {
|
||||
expect(usePoseStore.getState().uptimeStart).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleFrame', () => {
|
||||
it('updates features from frame', () => {
|
||||
const frame = makeFrame();
|
||||
usePoseStore.getState().handleFrame(frame);
|
||||
expect(usePoseStore.getState().features).toEqual(frame.features);
|
||||
});
|
||||
|
||||
it('updates classification from frame', () => {
|
||||
const frame = makeFrame();
|
||||
usePoseStore.getState().handleFrame(frame);
|
||||
expect(usePoseStore.getState().classification).toEqual(frame.classification);
|
||||
});
|
||||
|
||||
it('updates signalField from frame', () => {
|
||||
const frame = makeFrame();
|
||||
usePoseStore.getState().handleFrame(frame);
|
||||
expect(usePoseStore.getState().signalField).toEqual(frame.signal_field);
|
||||
});
|
||||
|
||||
it('increments messageCount', () => {
|
||||
usePoseStore.getState().handleFrame(makeFrame());
|
||||
usePoseStore.getState().handleFrame(makeFrame());
|
||||
usePoseStore.getState().handleFrame(makeFrame());
|
||||
expect(usePoseStore.getState().messageCount).toBe(3);
|
||||
});
|
||||
|
||||
it('tracks RSSI history from mean_rssi', () => {
|
||||
usePoseStore.getState().handleFrame(
|
||||
makeFrame({ features: { mean_rssi: -40, variance: 1, motion_band_power: 0.1, breathing_band_power: 0.05, spectral_entropy: 0.8 } }),
|
||||
);
|
||||
usePoseStore.getState().handleFrame(
|
||||
makeFrame({ features: { mean_rssi: -50, variance: 1, motion_band_power: 0.1, breathing_band_power: 0.05, spectral_entropy: 0.8 } }),
|
||||
);
|
||||
const history = usePoseStore.getState().rssiHistory;
|
||||
expect(history).toEqual([-40, -50]);
|
||||
});
|
||||
|
||||
it('sets uptimeStart on first frame only', () => {
|
||||
usePoseStore.getState().handleFrame(makeFrame());
|
||||
const firstUptime = usePoseStore.getState().uptimeStart;
|
||||
expect(firstUptime).not.toBeNull();
|
||||
|
||||
usePoseStore.getState().handleFrame(makeFrame());
|
||||
expect(usePoseStore.getState().uptimeStart).toBe(firstUptime);
|
||||
});
|
||||
|
||||
it('stores lastFrame', () => {
|
||||
const frame = makeFrame();
|
||||
usePoseStore.getState().handleFrame(frame);
|
||||
expect(usePoseStore.getState().lastFrame).toBe(frame);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setConnectionStatus', () => {
|
||||
it('updates connectionStatus', () => {
|
||||
usePoseStore.getState().setConnectionStatus('connected');
|
||||
expect(usePoseStore.getState().connectionStatus).toBe('connected');
|
||||
});
|
||||
|
||||
it('sets isSimulated true for simulated status', () => {
|
||||
usePoseStore.getState().setConnectionStatus('simulated');
|
||||
expect(usePoseStore.getState().isSimulated).toBe(true);
|
||||
});
|
||||
|
||||
it('sets isSimulated false for connected status', () => {
|
||||
usePoseStore.getState().setConnectionStatus('simulated');
|
||||
usePoseStore.getState().setConnectionStatus('connected');
|
||||
expect(usePoseStore.getState().isSimulated).toBe(false);
|
||||
});
|
||||
|
||||
it('sets isSimulated false for disconnected status', () => {
|
||||
usePoseStore.getState().setConnectionStatus('simulated');
|
||||
usePoseStore.getState().setConnectionStatus('disconnected');
|
||||
expect(usePoseStore.getState().isSimulated).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
it('clears everything back to initial state', () => {
|
||||
usePoseStore.getState().setConnectionStatus('connected');
|
||||
usePoseStore.getState().handleFrame(makeFrame());
|
||||
usePoseStore.getState().handleFrame(makeFrame());
|
||||
|
||||
usePoseStore.getState().reset();
|
||||
|
||||
const state = usePoseStore.getState();
|
||||
expect(state.connectionStatus).toBe('disconnected');
|
||||
expect(state.isSimulated).toBe(false);
|
||||
expect(state.lastFrame).toBeNull();
|
||||
expect(state.rssiHistory).toEqual([]);
|
||||
expect(state.features).toBeNull();
|
||||
expect(state.classification).toBeNull();
|
||||
expect(state.signalField).toBeNull();
|
||||
expect(state.messageCount).toBe(0);
|
||||
expect(state.uptimeStart).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,87 @@
|
||||
describe('placeholder', () => {
|
||||
it('passes', () => {
|
||||
expect(true).toBe(true);
|
||||
import { useSettingsStore } from '@/stores/settingsStore';
|
||||
|
||||
describe('useSettingsStore', () => {
|
||||
beforeEach(() => {
|
||||
// Reset to defaults by manually setting all values
|
||||
useSettingsStore.setState({
|
||||
serverUrl: 'http://localhost:3000',
|
||||
rssiScanEnabled: false,
|
||||
theme: 'system',
|
||||
alertSoundEnabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe('default values', () => {
|
||||
it('has default serverUrl as http://localhost:3000', () => {
|
||||
expect(useSettingsStore.getState().serverUrl).toBe('http://localhost:3000');
|
||||
});
|
||||
|
||||
it('has rssiScanEnabled false by default', () => {
|
||||
expect(useSettingsStore.getState().rssiScanEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('has theme as system by default', () => {
|
||||
expect(useSettingsStore.getState().theme).toBe('system');
|
||||
});
|
||||
|
||||
it('has alertSoundEnabled true by default', () => {
|
||||
expect(useSettingsStore.getState().alertSoundEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setServerUrl', () => {
|
||||
it('updates the server URL', () => {
|
||||
useSettingsStore.getState().setServerUrl('http://10.0.0.1:8080');
|
||||
expect(useSettingsStore.getState().serverUrl).toBe('http://10.0.0.1:8080');
|
||||
});
|
||||
|
||||
it('handles empty string', () => {
|
||||
useSettingsStore.getState().setServerUrl('');
|
||||
expect(useSettingsStore.getState().serverUrl).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setRssiScanEnabled', () => {
|
||||
it('toggles to true', () => {
|
||||
useSettingsStore.getState().setRssiScanEnabled(true);
|
||||
expect(useSettingsStore.getState().rssiScanEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('toggles back to false', () => {
|
||||
useSettingsStore.getState().setRssiScanEnabled(true);
|
||||
useSettingsStore.getState().setRssiScanEnabled(false);
|
||||
expect(useSettingsStore.getState().rssiScanEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setTheme', () => {
|
||||
it('sets theme to dark', () => {
|
||||
useSettingsStore.getState().setTheme('dark');
|
||||
expect(useSettingsStore.getState().theme).toBe('dark');
|
||||
});
|
||||
|
||||
it('sets theme to light', () => {
|
||||
useSettingsStore.getState().setTheme('light');
|
||||
expect(useSettingsStore.getState().theme).toBe('light');
|
||||
});
|
||||
|
||||
it('sets theme back to system', () => {
|
||||
useSettingsStore.getState().setTheme('dark');
|
||||
useSettingsStore.getState().setTheme('system');
|
||||
expect(useSettingsStore.getState().theme).toBe('system');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setAlertSoundEnabled', () => {
|
||||
it('disables alert sound', () => {
|
||||
useSettingsStore.getState().setAlertSoundEnabled(false);
|
||||
expect(useSettingsStore.getState().alertSoundEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('re-enables alert sound', () => {
|
||||
useSettingsStore.getState().setAlertSoundEnabled(false);
|
||||
useSettingsStore.getState().setAlertSoundEnabled(true);
|
||||
expect(useSettingsStore.getState().alertSoundEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user