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 { PlatformComponentSchema, WaveformSchema } from '../scenarioSchema';
6import { createWaveformForType, defaultGlobalParameters } from './defaults';
9 serializeComponentInner,
10 serializeGlobalParameters,
12} from './serializers';
13import type { Antenna, PlatformComponent, Waveform } from './types';
15describe('serializeAntenna', () => {
16 test('uses an isotropic backend placeholder while an H5 antenna has no filename', () => {
17 const antenna: Antenna = {
25 design_frequency: null,
28 expect(serializeAntenna(antenna)).toEqual({
36 test('preserves H5 antenna payload once a filename is present', () => {
37 const antenna: Antenna = {
42 filename: '/tmp/pattern.h5',
45 design_frequency: null,
48 expect(serializeAntenna(antenna)).toEqual({
52 filename: '/tmp/pattern.h5',
58describe('serializeGlobalParameters', () => {
59 test('fills UTM zone and hemisphere defaults before backend sync', () => {
61 serializeGlobalParameters({
62 ...defaultGlobalParameters,
63 coordinateSystem: { frame: 'UTM' },
75describe('FMCW schema', () => {
76 const validWaveform: Extract<
78 { waveformType: 'fmcw_linear_chirp' }
80 ...createWaveformForType('fmcw_linear_chirp'),
85 test('accepts valid FMCW waveform fields', () => {
86 expect(WaveformSchema.safeParse(validWaveform).success).toBe(true);
88 WaveformSchema.safeParse({
89 ...createWaveformForType('fmcw_triangle'),
96 test('keeps generated FMCW defaults within the backend aliasing limit', () => {
97 const sweepStart = validWaveform.start_frequency_offset ?? 0;
100 (validWaveform.direction === 'down' ? -1 : 1) *
101 validWaveform.chirp_bandwidth;
102 const maxBaseband = Math.max(Math.abs(sweepStart), Math.abs(sweepEnd));
103 const effectiveRate =
104 defaultGlobalParameters.rate *
105 defaultGlobalParameters.oversample_ratio;
107 expect(effectiveRate).toBeGreaterThan(maxBaseband);
110 test('rejects invalid FMCW waveform fields', () => {
111 const invalidWaveforms = [
112 { ...validWaveform, chirp_bandwidth: 0 },
113 { ...validWaveform, chirp_duration: 0 },
116 chirp_duration: 2e-6,
119 { ...validWaveform, chirp_count: 0 },
120 { ...validWaveform, chirp_count: 1.5 },
123 start_frequency_offset: Number.POSITIVE_INFINITY,
127 for (const waveform of invalidWaveforms) {
128 expect(WaveformSchema.safeParse(waveform).success).toBe(false);
131 const invalidTriangles = [
133 ...createWaveformForType('fmcw_triangle'),
135 name: 'Bad Triangle',
139 ...createWaveformForType('fmcw_triangle'),
141 name: 'Bad Triangle Count',
145 for (const waveform of invalidTriangles) {
146 expect(WaveformSchema.safeParse(waveform).success).toBe(false);
150 test('accepts FMCW radar mode on radar components', () => {
151 const component: PlatformComponent = {
163 expect(PlatformComponentSchema.safeParse(component).success).toBe(true);
167describe('serializeWaveform', () => {
168 test('serializes FMCW waveform payload and omits unset optional fields', () => {
169 const waveform: Waveform = {
170 ...createWaveformForType('fmcw_linear_chirp'),
175 expect(serializeWaveform(waveform)).toEqual({
179 carrier_frequency: 1e9,
182 chirp_bandwidth: 4e3,
183 chirp_duration: 1e-3,
189 test('serializes FMCW optional fields when set', () => {
190 const waveform: Waveform = {
191 ...createWaveformForType('fmcw_linear_chirp'),
194 start_frequency_offset: -1000,
198 expect(serializeWaveform(waveform)).toMatchObject({
201 start_frequency_offset: -1000,
207 test('serializes FMCW triangle waveform payload', () => {
208 const waveform: Waveform = {
209 ...createWaveformForType('fmcw_triangle'),
212 start_frequency_offset: -1000,
216 expect(serializeWaveform(waveform)).toEqual({
220 carrier_frequency: 1e9,
222 chirp_bandwidth: 4e3,
223 chirp_duration: 1e-3,
224 start_frequency_offset: -1000,
231describe('serializeComponentInner', () => {
232 test('serializes empty component references as backend placeholders', () => {
233 const component: PlatformComponent = {
245 expect(serializeComponentInner(component)).toMatchObject({
252 test('serializes FMCW mode without pulsed fields', () => {
253 const component: PlatformComponent = {
266 noiseTemperature: null,
267 noDirectPaths: false,
268 noPropagationLoss: false,
272 expect(serializeComponentInner(component)).toEqual({
282 nopropagationloss: false,
287 test('preserves receiver FMCW attached dechirp mode fields', () => {
288 const component: PlatformComponent = {
298 noiseTemperature: null,
299 noDirectPaths: false,
300 noPropagationLoss: false,
302 dechirp_mode: 'physical',
303 dechirp_reference: { source: 'attached' },
305 if_filter_bandwidth: 4e5,
306 if_filter_transition_width: 1e5,
311 expect(serializeComponentInner(component)).toMatchObject({
313 dechirp_mode: 'physical',
314 dechirp_reference: { source: 'attached' },
316 if_filter_bandwidth: 4e5,
317 if_filter_transition_width: 1e5,
322 test('preserves receiver FMCW transmitter dechirp reference name', () => {
323 const component: PlatformComponent = {
333 noiseTemperature: null,
334 noDirectPaths: false,
335 noPropagationLoss: false,
337 dechirp_mode: 'ideal',
339 source: 'transmitter',
340 transmitter_name: 'Reference TX',
346 expect(serializeComponentInner(component)).toMatchObject({
348 dechirp_mode: 'ideal',
350 source: 'transmitter',
351 transmitter_name: 'Reference TX',
357 test('preserves monostatic FMCW custom dechirp reference waveform name', () => {
358 const component: PlatformComponent = {
371 noiseTemperature: null,
372 noDirectPaths: false,
373 noPropagationLoss: false,
375 dechirp_mode: 'physical',
378 waveform_name: 'Reference Waveform',
384 expect(serializeComponentInner(component)).toMatchObject({
386 dechirp_mode: 'physical',
389 waveform_name: 'Reference Waveform',
395 test('uses an isotropic backend placeholder while a file target has no filename', () => {
396 const component: PlatformComponent = {
399 name: 'Draft Target',
402 rcs_model: 'constant',
405 expect(serializeComponentInner(component)).toEqual({
407 name: 'Draft Target',
415 test('serializes file target RCS once a filename is present', () => {
416 const component: PlatformComponent = {
419 name: 'Loaded Target',
421 rcs_filename: '/tmp/target.xml',
422 rcs_model: 'constant',
425 expect(serializeComponentInner(component)).toEqual({
427 name: 'Loaded Target',
430 filename: '/tmp/target.xml',