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:
rUv
2026-03-03 13:27:03 -05:00
committed by GitHub
parent 977da0f28e
commit d4fb7d30d3
34 changed files with 2975 additions and 87 deletions
+196 -3
View File
@@ -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);
});
});
});