FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
assetTemplates.test.ts
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
3
4import { describe, expect, test } from 'bun:test';
5import {
6 cloneTemplateIntoScenarioData,
7 createAssetLibraryFile,
8 createTemplateFromScenarioItem,
9 parseAssetTemplates,
10 prepareTemplatesForCatalog,
11} from './assetTemplates';
12import { defaultGlobalParameters } from './scenarioStore/defaults';
13import type { ScenarioData } from './scenarioStore/types';
14
15function createScenarioFixture(): ScenarioData {
16 return {
17 globalParameters: {
18 ...defaultGlobalParameters,
19 simulation_name: 'Library Fixture',
20 },
21 waveforms: [
22 {
23 id: '6001',
24 type: 'Waveform',
25 name: 'Pulse A',
26 waveformType: 'pulsed_from_file',
27 power: 100,
28 carrier_frequency: 1e9,
29 filename: '/tmp/pulse.h5',
30 },
31 ],
32 timings: [
33 {
34 id: '7001',
35 type: 'Timing',
36 name: 'Timing A',
37 frequency: 10e6,
38 freqOffset: null,
39 randomFreqOffsetStdev: null,
40 phaseOffset: null,
41 randomPhaseOffsetStdev: null,
42 noiseEntries: [{ id: '7002', alpha: 0.2, weight: 0.5 }],
43 },
44 ],
45 antennas: [
46 {
47 id: '5001',
48 type: 'Antenna',
49 name: 'Yagi A',
50 pattern: 'sinc',
51 efficiency: 0.9,
52 meshScale: 1,
53 design_frequency: 1e9,
54 alpha: 1,
55 beta: 2,
56 gamma: 3,
57 },
58 ],
59 platforms: [
60 {
61 id: '1001',
62 type: 'Platform',
63 name: 'Aircraft A',
64 motionPath: {
65 interpolation: 'static',
66 waypoints: [
67 {
68 id: '1002',
69 x: 0,
70 y: 0,
71 altitude: 1000,
72 time: 0,
73 },
74 ],
75 },
76 rotation: {
77 type: 'path',
78 interpolation: 'static',
79 waypoints: [
80 {
81 id: '1003',
82 azimuth: 0,
83 elevation: 0,
84 time: 0,
85 },
86 ],
87 },
88 components: [
89 {
90 id: '2001',
91 type: 'transmitter',
92 name: 'Tx A',
93 radarType: 'pulsed',
94 prf: 1000,
95 antennaId: '5001',
96 waveformId: '6001',
97 timingId: '7001',
98 schedule: [],
99 },
100 ],
101 pathPoints: [
102 {
103 x: 0,
104 y: 0,
105 z: 0,
106 vx: 0,
107 vy: 0,
108 vz: 0,
109 },
110 ],
111 rotationPathPoints: [{ azimuth: 0, elevation: 0 }],
112 },
113 ],
114 };
115}
116
117describe('asset templates', () => {
118 test('creates top-level asset snapshots', () => {
119 const scenario = createScenarioFixture();
120 const template = createTemplateFromScenarioItem(scenario, '5001', {
121 id: 'template-1',
122 timestamp: '2026-04-13T00:00:00.000Z',
123 });
124
125 expect(template).toMatchObject({
126 id: 'template-1',
127 kind: 'antenna',
128 name: 'Yagi A',
129 payload: {
130 id: '5001',
131 pattern: 'sinc',
132 },
133 });
134 });
135
136 test('captures platform dependencies and strips runtime path caches', () => {
137 const scenario = createScenarioFixture();
138 const template = createTemplateFromScenarioItem(scenario, '1001', {
139 id: 'template-2',
140 timestamp: '2026-04-13T00:00:00.000Z',
141 });
142
143 expect(template?.kind).toBe('platform');
144 if (template?.kind !== 'platform') {
145 throw new Error('Expected platform template.');
146 }
147
148 expect(template.dependencies.antennas).toHaveLength(1);
149 expect(template.dependencies.waveforms).toHaveLength(1);
150 expect(template.dependencies.timings).toHaveLength(1);
151 expect(template.payload).not.toHaveProperty('pathPoints');
152 expect(template.payload).not.toHaveProperty('rotationPathPoints');
153 });
154
155 test('loads platform templates with fresh IDs and remapped dependencies', () => {
156 const scenario = createScenarioFixture();
157 const template = createTemplateFromScenarioItem(scenario, '1001');
158 if (template?.kind !== 'platform') {
159 throw new Error('Expected platform template.');
160 }
161
162 const { scenarioData, result } = cloneTemplateIntoScenarioData(
163 scenario,
164 template
165 );
166 const insertedPlatform = scenarioData.platforms.at(-1);
167 const insertedComponent = insertedPlatform?.components[0];
168
169 expect(result.warnings).toEqual([]);
170 expect(insertedPlatform?.id).not.toBe('1001');
171 expect(insertedPlatform?.name).toBe('Aircraft A Copy');
172 expect(insertedComponent?.id).not.toBe('2001');
173 expect(insertedComponent?.type).toBe('transmitter');
174 if (insertedComponent?.type !== 'transmitter') {
175 throw new Error('Expected transmitter component.');
176 }
177 expect(insertedComponent.antennaId).not.toBe('5001');
178 expect(insertedComponent.waveformId).not.toBe('6001');
179 expect(insertedComponent.timingId).not.toBe('7001');
180 expect(
181 scenarioData.antennas.some(
182 (antenna) => antenna.id === insertedComponent.antennaId
183 )
184 ).toBe(true);
185 expect(
186 scenarioData.waveforms.some(
187 (waveform) => waveform.id === insertedComponent.waveformId
188 )
189 ).toBe(true);
190 expect(
191 scenarioData.timings.some(
192 (timing) => timing.id === insertedComponent.timingId
193 )
194 ).toBe(true);
195 });
196
197 test('uses template display names when loading renamed assets', () => {
198 const scenario = createScenarioFixture();
199 const template = createTemplateFromScenarioItem(scenario, '6001');
200 if (template?.kind !== 'waveform') {
201 throw new Error('Expected waveform template.');
202 }
203
204 const { scenarioData, result } = cloneTemplateIntoScenarioData(
205 scenario,
206 {
207 ...template,
208 name: 'Reusable Pulse',
209 }
210 );
211
212 expect(result.insertedName).toBe('Reusable Pulse Copy');
213 expect(scenarioData.waveforms.at(-1)?.name).toBe('Reusable Pulse Copy');
214 });
215
216 test('clears missing platform dependency references with warnings', () => {
217 const scenario = createScenarioFixture();
218 const template = createTemplateFromScenarioItem(scenario, '1001');
219 if (template?.kind !== 'platform') {
220 throw new Error('Expected platform template.');
221 }
222 const templateWithoutDependencies = {
223 ...template,
224 dependencies: {
225 waveforms: [],
226 timings: [],
227 antennas: [],
228 },
229 };
230
231 const { scenarioData, result } = cloneTemplateIntoScenarioData(
232 scenario,
233 templateWithoutDependencies
234 );
235 const insertedComponent = scenarioData.platforms.at(-1)?.components[0];
236
237 expect(result.warnings).toHaveLength(3);
238 expect(insertedComponent?.type).toBe('transmitter');
239 if (insertedComponent?.type !== 'transmitter') {
240 throw new Error('Expected transmitter component.');
241 }
242 expect(insertedComponent.antennaId).toBeNull();
243 expect(insertedComponent.waveformId).toBeNull();
244 expect(insertedComponent.timingId).toBeNull();
245 });
246
247 test('parses catalog files and assigns imported templates new catalog IDs', () => {
248 const scenario = createScenarioFixture();
249 const template = createTemplateFromScenarioItem(scenario, '6001', {
250 id: 'template-original',
251 });
252 if (!template) {
253 throw new Error('Expected waveform template.');
254 }
255
256 const parsed = parseAssetTemplates(createAssetLibraryFile([template]));
257 const prepared = prepareTemplatesForCatalog(parsed);
258
259 expect(parsed).toHaveLength(1);
260 expect(prepared).toHaveLength(1);
261 expect(prepared[0].id).not.toBe('template-original');
262 expect(prepared[0].payload.id).toBe('6001');
263 });
264});