1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
4import { describe, expect, test } from 'bun:test';
5import { defaultGlobalParameters } from './scenarioStore/defaults';
6import type { ScenarioData } from './scenarioStore/types';
9 deriveExpectedVita49Streams,
10 mergeVita49StreamRows,
11 toVita49BackendConfig,
12 useVita49StreamingStore,
13 type Vita49PacketTraceEvent,
15} from './vita49StreamingStore';
17const packet = (sequence: number): Vita49PacketTraceEvent => ({
24 timestamp: { integer_seconds: 1_700_000_000, fractional_picoseconds: 0 },
26 context_packet: false,
32describe('VITA49 streaming config', () => {
33 test('validates malformed endpoint and runtime bounds', () => {
35 validateVita49Config({
36 ...DEFAULT_VITA49_CONFIG,
42 packetTraceRingSize: 0,
46 'Port must be an integer from 1 to 65535.',
47 'Full-scale must be a positive finite number.',
48 'Max UDP payload must be an integer from 64 to 65507.',
49 'Queue depth must be an integer from 1 to 4294967295.',
50 'Packet trace ring size must be a positive integer.',
54 test('rejects queue depth above uint32 max', () => {
56 validateVita49Config({
57 ...DEFAULT_VITA49_CONFIG,
58 queueDepth: 4_294_967_296,
60 ).toContain('Queue depth must be an integer from 1 to 4294967295.');
62 validateVita49Config({
63 ...DEFAULT_VITA49_CONFIG,
64 queueDepth: 4_294_967_295,
66 ).not.toContain('Queue depth must be an integer from 1 to 4294967295.');
69 test('validates fixed epoch and preserves ns as a string for Rust', () => {
71 validateVita49Config({
72 ...DEFAULT_VITA49_CONFIG,
74 epochUnixNanoseconds: '4294967296000000000',
76 ).toContain('Fixed epoch must fit the VRT 32-bit UTC seconds field.');
79 toVita49BackendConfig({
80 ...DEFAULT_VITA49_CONFIG,
82 epochUnixNanoseconds: '1700000000123456789',
85 epoch_unix_nanoseconds: '1700000000123456789',
87 packet_trace_ring_size: DEFAULT_VITA49_CONFIG.packetTraceRingSize,
92describe('VITA49 expected stream derivation', () => {
93 test('covers pulsed, CW, and FMCW receiver streams', () => {
96 'globalParameters' | 'platforms' | 'waveforms'
98 globalParameters: { ...defaultGlobalParameters, rate: 2e6 },
106 carrier_frequency: 9.6e9,
115 interpolation: 'static',
117 { id: '1', x: 0, y: 0, altitude: 0, time: 0 },
134 window_length: 10e-6,
138 noiseTemperature: null,
139 noDirectPaths: false,
140 noPropagationLoss: false,
153 noiseTemperature: null,
154 noDirectPaths: false,
155 noPropagationLoss: false,
171 noiseTemperature: null,
172 noDirectPaths: false,
173 noPropagationLoss: false,
174 fmcwModeConfig: { if_sample_rate: 250000 },
182 expect(deriveExpectedVita49Streams(scenario)).toMatchObject([
188 referenceFrequency: null,
192 receiverName: 'CwRx',
195 referenceFrequency: null,
199 receiverName: 'Mono',
202 referenceFrequency: 9.6e9,
208describe('VITA49 run lifecycle', () => {
209 test('tracks draining between active streaming and completion', () => {
210 useVita49StreamingStore.setState({
215 omittedPacketTraceEvents: 0,
217 finalVita49Metadata: null,
221 useVita49StreamingStore.getState().startRun([]);
222 expect(useVita49StreamingStore.getState().runState).toBe('running');
224 useVita49StreamingStore.getState().markDraining();
225 expect(useVita49StreamingStore.getState().runState).toBe('draining');
227 useVita49StreamingStore.getState().completeRun(null);
228 expect(useVita49StreamingStore.getState().runState).toBe('completed');
232describe('VITA49 telemetry rows', () => {
233 test('maps exact VRT timestamp objects and simulation span', () => {
234 const rows = mergeVita49StreamRows([], {
236 epoch_unix_nanoseconds: '1700000000123456789',
244 reference_frequency: 10000000,
245 packets_emitted: 2930,
246 samples_emitted: 1000000,
250 late_packet_count: 0,
252 first_sample_time: 0,
255 integer_seconds: 1700000000,
256 fractional_picoseconds: 123456789000,
259 integer_seconds: 1700000010,
260 fractional_picoseconds: 123456789000,
266 expect(rows).toMatchObject([
270 samplesEmitted: 1000000,
274 integer_seconds: 1700000000,
275 fractional_picoseconds: 123456789000,
278 integer_seconds: 1700000010,
279 fractional_picoseconds: 123456789000,
285 test('does not default backend-only streams to CW', () => {
286 const rows = mergeVita49StreamRows([], {
288 epoch_unix_nanoseconds: null,
292 receiver_name: 'BackendRx',
295 reference_frequency: 10000000,
301 late_packet_count: 0,
303 first_sample_time: null,
304 end_sample_time: null,
305 first_timestamp: null,
311 expect(rows).toMatchObject([
315 backendObserved: true,
321describe('VITA49 packet trace ring', () => {
322 test('bounds packets and counts omitted events', () => {
323 useVita49StreamingStore.setState({
324 config: { ...DEFAULT_VITA49_CONFIG, packetTraceRingSize: 3 },
326 omittedPacketTraceEvents: 0,
329 useVita49StreamingStore
331 .appendPacketBatch([1, 2, 3, 4, 5].map(packet));
333 const state = useVita49StreamingStore.getState();
334 expect(state.packetTrace.map((entry) => entry.sequence)).toEqual([
337 expect(state.omittedPacketTraceEvents).toBe(2);
340 test('adds backend omitted packet count when appending polled batches', () => {
341 useVita49StreamingStore.setState({
342 config: { ...DEFAULT_VITA49_CONFIG, packetTraceRingSize: 5 },
344 omittedPacketTraceEvents: 0,
347 useVita49StreamingStore
349 .appendPacketBatch([1, 2].map(packet), 7);
351 const state = useVita49StreamingStore.getState();
352 expect(state.packetTrace.map((entry) => entry.sequence)).toEqual([
355 expect(state.omittedPacketTraceEvents).toBe(7);