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';
6 cloneTemplateIntoScenarioData,
7 createAssetLibraryFile,
8 createTemplateFromScenarioItem,
10 prepareTemplatesForCatalog,
11} from './assetTemplates';
12import { defaultGlobalParameters } from './scenarioStore/defaults';
13import type { ScenarioData } from './scenarioStore/types';
15function createScenarioFixture(): ScenarioData {
18 ...defaultGlobalParameters,
19 simulation_name: 'Library Fixture',
26 waveformType: 'pulsed_from_file',
28 carrier_frequency: 1e9,
29 filename: '/tmp/pulse.h5',
39 randomFreqOffsetStdev: null,
41 randomPhaseOffsetStdev: null,
42 noiseEntries: [{ id: '7002', alpha: 0.2, weight: 0.5 }],
53 design_frequency: 1e9,
65 interpolation: 'static',
78 interpolation: 'static',
111 rotationPathPoints: [{ azimuth: 0, elevation: 0 }],
117function createFmcwScenarioFixture(): ScenarioData {
118 const scenario = createScenarioFixture();
125 name: 'FMCW Up Chirp',
126 waveformType: 'fmcw_linear_chirp',
129 carrier_frequency: 10e9,
130 chirp_bandwidth: 20e6,
131 chirp_duration: 250e-6,
132 chirp_period: 500e-6,
133 start_frequency_offset: 1e6,
139 ...scenario.platforms[0],
150 schedule: [{ start: 0, end: 0.01 }],
162 noiseTemperature: 290,
163 noDirectPaths: false,
164 noPropagationLoss: false,
165 schedule: [{ start: 0, end: 0.01 }],
170 name: 'FMCW Monostatic',
180 noiseTemperature: 290,
181 noDirectPaths: false,
182 noPropagationLoss: false,
183 schedule: [{ start: 0, end: 0.01 }],
191describe('asset templates', () => {
192 test('creates top-level asset snapshots', () => {
193 const scenario = createScenarioFixture();
194 const template = createTemplateFromScenarioItem(scenario, '5001', {
196 timestamp: '2026-04-13T00:00:00.000Z',
199 expect(template).toMatchObject({
210 test('captures platform dependencies and strips runtime path caches', () => {
211 const scenario = createScenarioFixture();
212 const template = createTemplateFromScenarioItem(scenario, '1001', {
214 timestamp: '2026-04-13T00:00:00.000Z',
217 expect(template?.kind).toBe('platform');
218 if (template?.kind !== 'platform') {
219 throw new Error('Expected platform template.');
222 expect(template.dependencies.antennas).toHaveLength(1);
223 expect(template.dependencies.waveforms).toHaveLength(1);
224 expect(template.dependencies.timings).toHaveLength(1);
225 expect(template.payload).not.toHaveProperty('pathPoints');
226 expect(template.payload).not.toHaveProperty('rotationPathPoints');
229 test('loads platform templates with fresh IDs and remapped dependencies', () => {
230 const scenario = createScenarioFixture();
231 const template = createTemplateFromScenarioItem(scenario, '1001');
232 if (template?.kind !== 'platform') {
233 throw new Error('Expected platform template.');
236 const { scenarioData, result } = cloneTemplateIntoScenarioData(
240 const insertedPlatform = scenarioData.platforms.at(-1);
241 const insertedComponent = insertedPlatform?.components[0];
243 expect(result.warnings).toEqual([]);
244 expect(insertedPlatform?.id).not.toBe('1001');
245 expect(insertedPlatform?.name).toBe('Aircraft A Copy');
246 expect(insertedComponent?.id).not.toBe('2001');
247 expect(insertedComponent?.type).toBe('transmitter');
248 if (insertedComponent?.type !== 'transmitter') {
249 throw new Error('Expected transmitter component.');
251 expect(insertedComponent.name).toBe('Tx A Copy');
252 expect(insertedComponent.antennaId).not.toBe('5001');
253 expect(insertedComponent.waveformId).not.toBe('6001');
254 expect(insertedComponent.timingId).not.toBe('7001');
256 scenarioData.antennas.some(
257 (antenna) => antenna.id === insertedComponent.antennaId
261 scenarioData.waveforms.some(
262 (waveform) => waveform.id === insertedComponent.waveformId
266 scenarioData.timings.some(
267 (timing) => timing.id === insertedComponent.timingId
272 test('uses template display names when loading renamed assets', () => {
273 const scenario = createScenarioFixture();
274 const template = createTemplateFromScenarioItem(scenario, '6001');
275 if (template?.kind !== 'waveform') {
276 throw new Error('Expected waveform template.');
279 const { scenarioData, result } = cloneTemplateIntoScenarioData(
283 name: 'Reusable Pulse',
287 expect(result.insertedName).toBe('Reusable Pulse Copy');
288 expect(scenarioData.waveforms.at(-1)?.name).toBe('Reusable Pulse Copy');
291 test('clears missing platform dependency references with warnings', () => {
292 const scenario = createScenarioFixture();
293 const template = createTemplateFromScenarioItem(scenario, '1001');
294 if (template?.kind !== 'platform') {
295 throw new Error('Expected platform template.');
297 const templateWithoutDependencies = {
306 const { scenarioData, result } = cloneTemplateIntoScenarioData(
308 templateWithoutDependencies
310 const insertedComponent = scenarioData.platforms.at(-1)?.components[0];
312 expect(result.warnings).toHaveLength(3);
313 expect(insertedComponent?.type).toBe('transmitter');
314 if (insertedComponent?.type !== 'transmitter') {
315 throw new Error('Expected transmitter component.');
317 expect(insertedComponent.antennaId).toBeNull();
318 expect(insertedComponent.waveformId).toBeNull();
319 expect(insertedComponent.timingId).toBeNull();
322 test('parses catalog files and assigns imported templates new catalog IDs', () => {
323 const scenario = createScenarioFixture();
324 const template = createTemplateFromScenarioItem(scenario, '6001', {
325 id: 'template-original',
328 throw new Error('Expected waveform template.');
331 const parsed = parseAssetTemplates(createAssetLibraryFile([template]));
332 const prepared = prepareTemplatesForCatalog(parsed);
334 expect(parsed).toHaveLength(1);
335 expect(prepared).toHaveLength(1);
336 expect(prepared[0].id).not.toBe('template-original');
337 expect(prepared[0].payload.id).toBe('6001');
340 test('round trips FMCW waveform templates', () => {
341 const scenario = createFmcwScenarioFixture();
342 const template = createTemplateFromScenarioItem(scenario, '6002', {
343 id: 'template-fmcw-waveform',
344 timestamp: '2026-04-13T00:00:00.000Z',
346 if (template?.kind !== 'waveform') {
347 throw new Error('Expected waveform template.');
350 const [parsed] = parseAssetTemplates(
351 createAssetLibraryFile([template])
353 const { scenarioData, result } = cloneTemplateIntoScenarioData(
357 const insertedWaveform = scenarioData.waveforms.at(-1);
359 expect(result.warnings).toEqual([]);
360 expect(parsed.payload).toMatchObject({
361 waveformType: 'fmcw_linear_chirp',
363 chirp_bandwidth: 20e6,
364 chirp_duration: 250e-6,
365 chirp_period: 500e-6,
366 start_frequency_offset: 1e6,
369 expect(insertedWaveform).toMatchObject({
370 name: 'FMCW Up Chirp Copy',
371 waveformType: 'fmcw_linear_chirp',
373 chirp_bandwidth: 20e6,
374 chirp_duration: 250e-6,
375 chirp_period: 500e-6,
379 test('round trips platform templates with FMCW components', () => {
380 const scenario = createFmcwScenarioFixture();
381 const template = createTemplateFromScenarioItem(scenario, '1001');
382 if (template?.kind !== 'platform') {
383 throw new Error('Expected platform template.');
386 const [parsed] = parseAssetTemplates(
387 createAssetLibraryFile([template])
389 const { scenarioData, result } = cloneTemplateIntoScenarioData(
393 const insertedPlatform = scenarioData.platforms.at(-1);
395 expect(result.warnings).toEqual([]);
397 insertedPlatform?.components.map((component) => component.type)
398 ).toEqual(['transmitter', 'receiver', 'monostatic']);
400 insertedPlatform?.components.map((component) => component.name)
401 ).toEqual(['FMCW Tx Copy', 'FMCW Rx Copy', 'FMCW Monostatic Copy']);
403 insertedPlatform?.components.map((component) =>
404 'radarType' in component ? component.radarType : null
406 ).toEqual(['fmcw', 'fmcw', 'fmcw']);
408 insertedPlatform?.components.every(
410 !('waveformId' in component) ||
411 component.waveformId !== '6002'
416 test('collects FMCW waveform dependencies for platform templates', () => {
417 const scenario = createFmcwScenarioFixture();
418 const template = createTemplateFromScenarioItem(scenario, '1001');
419 if (template?.kind !== 'platform') {
420 throw new Error('Expected platform template.');
423 expect(template.dependencies.waveforms).toHaveLength(1);
424 expect(template.dependencies.waveforms[0]).toMatchObject({
426 waveformType: 'fmcw_linear_chirp',
428 chirp_bandwidth: 20e6,