@tx5dr/contracts 1.7.12 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/dist/index.d.ts +7 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +6 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/schema/__tests__/android-operator-audio.schema.test.d.ts +2 -0
  6. package/dist/schema/__tests__/android-operator-audio.schema.test.d.ts.map +1 -0
  7. package/dist/schema/__tests__/android-operator-audio.schema.test.js +27 -0
  8. package/dist/schema/__tests__/android-operator-audio.schema.test.js.map +1 -0
  9. package/dist/schema/__tests__/assisted-queue.schema.test.d.ts +2 -0
  10. package/dist/schema/__tests__/assisted-queue.schema.test.d.ts.map +1 -0
  11. package/dist/schema/__tests__/assisted-queue.schema.test.js +114 -0
  12. package/dist/schema/__tests__/assisted-queue.schema.test.js.map +1 -0
  13. package/dist/schema/__tests__/audio-sidecar.schema.test.d.ts +2 -0
  14. package/dist/schema/__tests__/audio-sidecar.schema.test.d.ts.map +1 -0
  15. package/dist/schema/__tests__/audio-sidecar.schema.test.js +42 -0
  16. package/dist/schema/__tests__/audio-sidecar.schema.test.js.map +1 -0
  17. package/dist/schema/__tests__/audio.schema.test.js +92 -1
  18. package/dist/schema/__tests__/audio.schema.test.js.map +1 -1
  19. package/dist/schema/__tests__/diagnostics.schema.test.d.ts +2 -0
  20. package/dist/schema/__tests__/diagnostics.schema.test.d.ts.map +1 -0
  21. package/dist/schema/__tests__/diagnostics.schema.test.js +31 -0
  22. package/dist/schema/__tests__/diagnostics.schema.test.js.map +1 -0
  23. package/dist/schema/__tests__/logbook-health.schema.test.d.ts +2 -0
  24. package/dist/schema/__tests__/logbook-health.schema.test.d.ts.map +1 -0
  25. package/dist/schema/__tests__/logbook-health.schema.test.js +104 -0
  26. package/dist/schema/__tests__/logbook-health.schema.test.js.map +1 -0
  27. package/dist/schema/__tests__/meter.schema.test.d.ts +2 -0
  28. package/dist/schema/__tests__/meter.schema.test.d.ts.map +1 -0
  29. package/dist/schema/__tests__/meter.schema.test.js +27 -0
  30. package/dist/schema/__tests__/meter.schema.test.js.map +1 -0
  31. package/dist/schema/__tests__/observability.schema.test.d.ts +2 -0
  32. package/dist/schema/__tests__/observability.schema.test.d.ts.map +1 -0
  33. package/dist/schema/__tests__/observability.schema.test.js +12 -0
  34. package/dist/schema/__tests__/observability.schema.test.js.map +1 -0
  35. package/dist/schema/__tests__/operator-context-report.schema.test.d.ts +2 -0
  36. package/dist/schema/__tests__/operator-context-report.schema.test.d.ts.map +1 -0
  37. package/dist/schema/__tests__/operator-context-report.schema.test.js +70 -0
  38. package/dist/schema/__tests__/operator-context-report.schema.test.js.map +1 -0
  39. package/dist/schema/__tests__/operator.schema.test.js +9 -3
  40. package/dist/schema/__tests__/operator.schema.test.js.map +1 -1
  41. package/dist/schema/__tests__/plugin-market.schema.test.js +149 -3
  42. package/dist/schema/__tests__/plugin-market.schema.test.js.map +1 -1
  43. package/dist/schema/__tests__/qso.schema.test.d.ts +2 -0
  44. package/dist/schema/__tests__/qso.schema.test.d.ts.map +1 -0
  45. package/dist/schema/__tests__/qso.schema.test.js +70 -0
  46. package/dist/schema/__tests__/qso.schema.test.js.map +1 -0
  47. package/dist/schema/__tests__/radio-power-support.test.js +15 -0
  48. package/dist/schema/__tests__/radio-power-support.test.js.map +1 -1
  49. package/dist/schema/__tests__/radio-profile.schema.test.d.ts +2 -0
  50. package/dist/schema/__tests__/radio-profile.schema.test.d.ts.map +1 -0
  51. package/dist/schema/__tests__/radio-profile.schema.test.js +36 -0
  52. package/dist/schema/__tests__/radio-profile.schema.test.js.map +1 -0
  53. package/dist/schema/__tests__/radio.schema.test.js +92 -1
  54. package/dist/schema/__tests__/radio.schema.test.js.map +1 -1
  55. package/dist/schema/__tests__/realtime-settings.schema.test.js +2 -1
  56. package/dist/schema/__tests__/realtime-settings.schema.test.js.map +1 -1
  57. package/dist/schema/__tests__/remote-access.schema.test.d.ts +2 -0
  58. package/dist/schema/__tests__/remote-access.schema.test.d.ts.map +1 -0
  59. package/dist/schema/__tests__/remote-access.schema.test.js +18 -0
  60. package/dist/schema/__tests__/remote-access.schema.test.js.map +1 -0
  61. package/dist/schema/__tests__/spectrum.schema.test.d.ts +2 -0
  62. package/dist/schema/__tests__/spectrum.schema.test.d.ts.map +1 -0
  63. package/dist/schema/__tests__/spectrum.schema.test.js +107 -0
  64. package/dist/schema/__tests__/spectrum.schema.test.js.map +1 -0
  65. package/dist/schema/__tests__/strategy-transmission.schema.test.d.ts +2 -0
  66. package/dist/schema/__tests__/strategy-transmission.schema.test.d.ts.map +1 -0
  67. package/dist/schema/__tests__/strategy-transmission.schema.test.js +95 -0
  68. package/dist/schema/__tests__/strategy-transmission.schema.test.js.map +1 -0
  69. package/dist/schema/android-operator-audio.schema.d.ts +174 -0
  70. package/dist/schema/android-operator-audio.schema.d.ts.map +1 -0
  71. package/dist/schema/android-operator-audio.schema.js +38 -0
  72. package/dist/schema/android-operator-audio.schema.js.map +1 -0
  73. package/dist/schema/api-response.schema.d.ts +503 -44
  74. package/dist/schema/api-response.schema.d.ts.map +1 -1
  75. package/dist/schema/api-response.schema.js +9 -0
  76. package/dist/schema/api-response.schema.js.map +1 -1
  77. package/dist/schema/audio-sidecar.schema.d.ts +75 -0
  78. package/dist/schema/audio-sidecar.schema.d.ts.map +1 -1
  79. package/dist/schema/audio-sidecar.schema.js +32 -0
  80. package/dist/schema/audio-sidecar.schema.js.map +1 -1
  81. package/dist/schema/audio.schema.d.ts +4749 -3
  82. package/dist/schema/audio.schema.d.ts.map +1 -1
  83. package/dist/schema/audio.schema.js +101 -2
  84. package/dist/schema/audio.schema.js.map +1 -1
  85. package/dist/schema/auth.schema.d.ts +173 -14
  86. package/dist/schema/auth.schema.d.ts.map +1 -1
  87. package/dist/schema/auth.schema.js +65 -1
  88. package/dist/schema/auth.schema.js.map +1 -1
  89. package/dist/schema/cpu-profile.schema.d.ts +2 -2
  90. package/dist/schema/cw-decoder.schema.d.ts +74 -74
  91. package/dist/schema/cw-keyer.schema.d.ts +20 -14
  92. package/dist/schema/cw-keyer.schema.d.ts.map +1 -1
  93. package/dist/schema/cw-keyer.schema.js +3 -0
  94. package/dist/schema/cw-keyer.schema.js.map +1 -1
  95. package/dist/schema/desktop-https.schema.d.ts +6 -6
  96. package/dist/schema/device-ui.schema.d.ts +16 -16
  97. package/dist/schema/diagnostics.schema.d.ts +163 -0
  98. package/dist/schema/diagnostics.schema.d.ts.map +1 -0
  99. package/dist/schema/diagnostics.schema.js +46 -0
  100. package/dist/schema/diagnostics.schema.js.map +1 -0
  101. package/dist/schema/ft8.schema.d.ts +12 -2
  102. package/dist/schema/ft8.schema.d.ts.map +1 -1
  103. package/dist/schema/ft8.schema.js +8 -0
  104. package/dist/schema/ft8.schema.js.map +1 -1
  105. package/dist/schema/image-radio.schema.d.ts +2597 -0
  106. package/dist/schema/image-radio.schema.d.ts.map +1 -0
  107. package/dist/schema/image-radio.schema.js +283 -0
  108. package/dist/schema/image-radio.schema.js.map +1 -0
  109. package/dist/schema/logbook.schema.d.ts +2128 -18
  110. package/dist/schema/logbook.schema.d.ts.map +1 -1
  111. package/dist/schema/logbook.schema.js +159 -1
  112. package/dist/schema/logbook.schema.js.map +1 -1
  113. package/dist/schema/mode.schema.d.ts +4 -6
  114. package/dist/schema/mode.schema.d.ts.map +1 -1
  115. package/dist/schema/mode.schema.js +18 -6
  116. package/dist/schema/mode.schema.js.map +1 -1
  117. package/dist/schema/observability.schema.d.ts +66 -0
  118. package/dist/schema/observability.schema.d.ts.map +1 -0
  119. package/dist/schema/observability.schema.js +20 -0
  120. package/dist/schema/observability.schema.js.map +1 -0
  121. package/dist/schema/openwebrx.schema.d.ts +2 -2
  122. package/dist/schema/operator.schema.d.ts +2682 -6
  123. package/dist/schema/operator.schema.d.ts.map +1 -1
  124. package/dist/schema/operator.schema.js +10 -20
  125. package/dist/schema/operator.schema.js.map +1 -1
  126. package/dist/schema/plugin.schema.d.ts +1977 -492
  127. package/dist/schema/plugin.schema.d.ts.map +1 -1
  128. package/dist/schema/plugin.schema.js +165 -14
  129. package/dist/schema/plugin.schema.js.map +1 -1
  130. package/dist/schema/process-monitor.schema.d.ts +132 -132
  131. package/dist/schema/pskreporter.schema.d.ts +1 -3
  132. package/dist/schema/pskreporter.schema.d.ts.map +1 -1
  133. package/dist/schema/pskreporter.schema.js +1 -3
  134. package/dist/schema/pskreporter.schema.js.map +1 -1
  135. package/dist/schema/qso.schema.d.ts +246 -1
  136. package/dist/schema/qso.schema.d.ts.map +1 -1
  137. package/dist/schema/qso.schema.js +68 -0
  138. package/dist/schema/qso.schema.js.map +1 -1
  139. package/dist/schema/radio-capability.schema.d.ts +31 -40
  140. package/dist/schema/radio-capability.schema.d.ts.map +1 -1
  141. package/dist/schema/radio-capability.schema.js +8 -15
  142. package/dist/schema/radio-capability.schema.js.map +1 -1
  143. package/dist/schema/radio-power-support.d.ts.map +1 -1
  144. package/dist/schema/radio-power-support.js +3 -0
  145. package/dist/schema/radio-power-support.js.map +1 -1
  146. package/dist/schema/radio-power.schema.d.ts +102 -13
  147. package/dist/schema/radio-power.schema.d.ts.map +1 -1
  148. package/dist/schema/radio-power.schema.js +15 -8
  149. package/dist/schema/radio-power.schema.js.map +1 -1
  150. package/dist/schema/radio-profile.schema.d.ts +1491 -97
  151. package/dist/schema/radio-profile.schema.d.ts.map +1 -1
  152. package/dist/schema/radio-profile.schema.js +7 -0
  153. package/dist/schema/radio-profile.schema.js.map +1 -1
  154. package/dist/schema/radio.schema.d.ts +442 -48
  155. package/dist/schema/radio.schema.d.ts.map +1 -1
  156. package/dist/schema/radio.schema.js +75 -7
  157. package/dist/schema/radio.schema.js.map +1 -1
  158. package/dist/schema/realtime.schema.d.ts +60 -52
  159. package/dist/schema/realtime.schema.d.ts.map +1 -1
  160. package/dist/schema/realtime.schema.js +9 -1
  161. package/dist/schema/realtime.schema.js.map +1 -1
  162. package/dist/schema/rigctld.schema.d.ts +6 -6
  163. package/dist/schema/slot-info.schema.d.ts +28 -9
  164. package/dist/schema/slot-info.schema.d.ts.map +1 -1
  165. package/dist/schema/slot-info.schema.js +17 -10
  166. package/dist/schema/slot-info.schema.js.map +1 -1
  167. package/dist/schema/spectrum-config.schema.d.ts +638 -0
  168. package/dist/schema/spectrum-config.schema.d.ts.map +1 -0
  169. package/dist/schema/spectrum-config.schema.js +129 -0
  170. package/dist/schema/spectrum-config.schema.js.map +1 -0
  171. package/dist/schema/spectrum.schema.d.ts +1323 -30
  172. package/dist/schema/spectrum.schema.d.ts.map +1 -1
  173. package/dist/schema/spectrum.schema.js +72 -0
  174. package/dist/schema/spectrum.schema.js.map +1 -1
  175. package/dist/schema/station-info.schema.d.ts +5 -0
  176. package/dist/schema/station-info.schema.d.ts.map +1 -1
  177. package/dist/schema/station-info.schema.js +5 -0
  178. package/dist/schema/station-info.schema.js.map +1 -1
  179. package/dist/schema/system.schema.d.ts +56 -6
  180. package/dist/schema/system.schema.d.ts.map +1 -1
  181. package/dist/schema/system.schema.js +22 -0
  182. package/dist/schema/system.schema.js.map +1 -1
  183. package/dist/schema/transmission.schema.d.ts +8 -7
  184. package/dist/schema/transmission.schema.d.ts.map +1 -1
  185. package/dist/schema/transmission.schema.js +2 -7
  186. package/dist/schema/transmission.schema.js.map +1 -1
  187. package/dist/schema/tx-audio-input.schema.d.ts +8 -0
  188. package/dist/schema/tx-audio-input.schema.d.ts.map +1 -0
  189. package/dist/schema/tx-audio-input.schema.js +19 -0
  190. package/dist/schema/tx-audio-input.schema.js.map +1 -0
  191. package/dist/schema/voice-keyer.schema.d.ts +16 -16
  192. package/dist/schema/voice.schema.d.ts +1 -1
  193. package/dist/schema/voice.schema.d.ts.map +1 -1
  194. package/dist/schema/voice.schema.js +1 -1
  195. package/dist/schema/voice.schema.js.map +1 -1
  196. package/dist/schema/websocket.schema.d.ts +35811 -7829
  197. package/dist/schema/websocket.schema.d.ts.map +1 -1
  198. package/dist/schema/websocket.schema.js +453 -9
  199. package/dist/schema/websocket.schema.js.map +1 -1
  200. package/package.json +6 -1
  201. package/src/index.ts +7 -0
  202. package/src/schema/__tests__/android-operator-audio.schema.test.ts +31 -0
  203. package/src/schema/__tests__/assisted-queue.schema.test.ts +122 -0
  204. package/src/schema/__tests__/audio-sidecar.schema.test.ts +48 -0
  205. package/src/schema/__tests__/audio.schema.test.ts +102 -0
  206. package/src/schema/__tests__/diagnostics.schema.test.ts +32 -0
  207. package/src/schema/__tests__/logbook-health.schema.test.ts +117 -0
  208. package/src/schema/__tests__/meter.schema.test.ts +28 -0
  209. package/src/schema/__tests__/observability.schema.test.ts +15 -0
  210. package/src/schema/__tests__/operator-context-report.schema.test.ts +77 -0
  211. package/src/schema/__tests__/operator.schema.test.ts +11 -3
  212. package/src/schema/__tests__/plugin-market.schema.test.ts +165 -2
  213. package/src/schema/__tests__/qso.schema.test.ts +91 -0
  214. package/src/schema/__tests__/radio-power-support.test.ts +17 -0
  215. package/src/schema/__tests__/radio-profile.schema.test.ts +44 -0
  216. package/src/schema/__tests__/radio.schema.test.ts +108 -1
  217. package/src/schema/__tests__/realtime-settings.schema.test.ts +2 -1
  218. package/src/schema/__tests__/remote-access.schema.test.ts +22 -0
  219. package/src/schema/__tests__/spectrum.schema.test.ts +119 -0
  220. package/src/schema/__tests__/strategy-transmission.schema.test.ts +106 -0
  221. package/src/schema/android-operator-audio.schema.ts +49 -0
  222. package/src/schema/api-response.schema.ts +11 -0
  223. package/src/schema/audio-sidecar.schema.ts +35 -0
  224. package/src/schema/audio.schema.ts +115 -2
  225. package/src/schema/auth.schema.ts +79 -1
  226. package/src/schema/cw-keyer.schema.ts +5 -0
  227. package/src/schema/diagnostics.schema.ts +55 -0
  228. package/src/schema/ft8.schema.ts +8 -0
  229. package/src/schema/image-radio.schema.ts +342 -0
  230. package/src/schema/logbook.schema.ts +205 -1
  231. package/src/schema/mode.schema.ts +18 -6
  232. package/src/schema/observability.schema.ts +26 -0
  233. package/src/schema/operator.schema.ts +11 -20
  234. package/src/schema/plugin.schema.ts +186 -15
  235. package/src/schema/pskreporter.schema.ts +1 -3
  236. package/src/schema/qso.schema.ts +78 -0
  237. package/src/schema/radio-capability.schema.ts +8 -15
  238. package/src/schema/radio-power-support.ts +3 -0
  239. package/src/schema/radio-power.schema.ts +15 -8
  240. package/src/schema/radio-profile.schema.ts +7 -0
  241. package/src/schema/radio.schema.ts +90 -7
  242. package/src/schema/realtime.schema.ts +9 -1
  243. package/src/schema/slot-info.schema.ts +18 -11
  244. package/src/schema/spectrum-config.schema.ts +156 -0
  245. package/src/schema/spectrum.schema.ts +89 -0
  246. package/src/schema/station-info.schema.ts +5 -0
  247. package/src/schema/system.schema.ts +29 -0
  248. package/src/schema/transmission.schema.ts +2 -7
  249. package/src/schema/tx-audio-input.schema.ts +22 -0
  250. package/src/schema/voice.schema.ts +1 -1
  251. package/src/schema/websocket.schema.ts +601 -13
  252. package/test/image-radio-schema.test.ts +107 -0
  253. package/test/plugin-slot-schema.test.ts +31 -0
  254. package/test/radio-capability-schema.test.ts +44 -0
  255. package/CLAUDE.md +0 -41
@@ -1,5 +1,70 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { PresetFrequencySchema } from '../radio.schema.js';
2
+ import { DdsFrequencyRequestSchema, DigitalModeRadioModePreferenceSchema, HamlibConfigSchema, PresetFrequencySchema, RadioInfoSchema } from '../radio.schema.js';
3
+
4
+ describe('HamlibConfigSchema digital mode radio mode preference', () => {
5
+ it('accepts legacy configs without a digital mode radio mode preference', () => {
6
+ const parsed = HamlibConfigSchema.parse({ type: 'none' });
7
+
8
+ expect(parsed.digitalModeRadioMode).toBeUndefined();
9
+ });
10
+
11
+ it('accepts supported FT8/FT4 radio mode preferences', () => {
12
+ expect(DigitalModeRadioModePreferenceSchema.parse('none')).toBe('none');
13
+ expect(DigitalModeRadioModePreferenceSchema.parse('usb')).toBe('usb');
14
+ expect(DigitalModeRadioModePreferenceSchema.parse('usb-data')).toBe('usb-data');
15
+ expect(HamlibConfigSchema.parse({ type: 'serial', digitalModeRadioMode: 'usb-data' }).digitalModeRadioMode).toBe('usb-data');
16
+ });
17
+
18
+ it('rejects invalid FT8/FT4 radio mode preferences', () => {
19
+ expect(() => HamlibConfigSchema.parse({ type: 'none', digitalModeRadioMode: 'lsb-data' })).toThrow();
20
+ });
21
+ });
22
+
23
+ describe('HamlibConfigSchema TCI connection', () => {
24
+ it('accepts TCI configs and applies SunSDR defaults', () => {
25
+ const parsed = HamlibConfigSchema.parse({
26
+ type: 'tci',
27
+ tci: {},
28
+ });
29
+
30
+ expect(parsed.tci).toMatchObject({
31
+ host: '127.0.0.1',
32
+ port: 40001,
33
+ receiver: 0,
34
+ trx: 0,
35
+ vfo: 0,
36
+ audioEnabled: true,
37
+ audioSampleRate: 12000,
38
+ });
39
+ });
40
+
41
+ it('accepts only credential-free WebSocket URLs for TCI', () => {
42
+ expect(() => HamlibConfigSchema.parse({ type: 'tci', tci: { url: 'wss://radio.example/tci' } })).not.toThrow();
43
+ expect(() => HamlibConfigSchema.parse({ type: 'tci', tci: { url: 'https://radio.example/tci' } })).toThrow();
44
+ expect(() => HamlibConfigSchema.parse({ type: 'tci', tci: { url: 'ws://user:secret@radio.example/tci' } })).toThrow();
45
+ });
46
+
47
+ it('accepts TCI as a reported radio connection type', () => {
48
+ const parsed = RadioInfoSchema.parse({
49
+ manufacturer: 'Expert Electronics',
50
+ model: 'SunSDR',
51
+ connectionType: 'tci',
52
+ });
53
+
54
+ expect(parsed.connectionType).toBe('tci');
55
+ });
56
+ });
57
+
58
+ describe('DdsFrequencyRequestSchema', () => {
59
+ it('accepts non-negative center frequency and receiver values', () => {
60
+ expect(DdsFrequencyRequestSchema.parse({ frequency: 14_075_000, receiver: 0 })).toEqual({ frequency: 14_075_000, receiver: 0 });
61
+ });
62
+
63
+ it('rejects invalid center frequency and receiver values', () => {
64
+ expect(() => DdsFrequencyRequestSchema.parse({ frequency: -1 })).toThrow();
65
+ expect(() => DdsFrequencyRequestSchema.parse({ frequency: 14_075_000, receiver: 0.5 })).toThrow();
66
+ });
67
+ });
3
68
 
4
69
  describe('PresetFrequencySchema repeater DUP fields', () => {
5
70
  it('accepts existing presets without repeater DUP fields', () => {
@@ -71,6 +136,48 @@ describe('PresetFrequencySchema repeater DUP fields', () => {
71
136
  });
72
137
  });
73
138
 
139
+ describe('PresetFrequencySchema dial frequency precision', () => {
140
+ it('accepts integer-Hz presets represented by up to six MHz decimal places', () => {
141
+ const parsed = PresetFrequencySchema.parse({
142
+ band: '4MHz',
143
+ mode: 'FAX',
144
+ radioMode: 'USB',
145
+ frequency: 4_233_100,
146
+ });
147
+
148
+ expect(parsed.frequency).toBe(4_233_100);
149
+ });
150
+
151
+ it('accepts marine FAX assigned-frequency and emission metadata', () => {
152
+ const parsed = PresetFrequencySchema.parse({
153
+ band: '4MHz',
154
+ mode: 'FAX',
155
+ radioMode: 'USB',
156
+ frequency: 4_197_850,
157
+ assignedFrequency: 4_199_750,
158
+ audioCenterHz: 1_900,
159
+ faxEmission: 'F3C',
160
+ imagePurpose: 'weatherfax',
161
+ });
162
+
163
+ expect(parsed.assignedFrequency).toBe(4_199_750);
164
+ expect(parsed.faxEmission).toBe('F3C');
165
+ });
166
+
167
+ it('rejects fractional-Hz and out-of-range preset frequencies', () => {
168
+ expect(() => PresetFrequencySchema.parse({
169
+ band: '20m',
170
+ mode: 'SSTV',
171
+ frequency: 14_230_000.5,
172
+ })).toThrow();
173
+ expect(() => PresetFrequencySchema.parse({
174
+ band: 'custom',
175
+ mode: 'VOICE',
176
+ frequency: 1_000_000_001,
177
+ })).toThrow();
178
+ });
179
+ });
180
+
74
181
  describe('PresetFrequencySchema tone squelch fields', () => {
75
182
  it('accepts CTCSS tone presets with positive 0.1 Hz values', () => {
76
183
  const parsed = PresetFrequencySchema.parse({
@@ -9,9 +9,10 @@ import {
9
9
  } from '../realtime.schema.js';
10
10
 
11
11
  describe('Realtime transport schemas', () => {
12
- it('accepts only rtc-data-audio and ws-compat transports', () => {
12
+ it('accepts realtime network transports plus the Android native diagnostics transport', () => {
13
13
  expect(RealtimeTransportKindSchema.parse('rtc-data-audio')).toBe('rtc-data-audio');
14
14
  expect(RealtimeTransportKindSchema.parse('ws-compat')).toBe('ws-compat');
15
+ expect(RealtimeTransportKindSchema.parse('android-native')).toBe('android-native');
15
16
  const retiredTransport = 'live' + 'kit';
16
17
  expect(() => RealtimeTransportKindSchema.parse(retiredTransport)).toThrow();
17
18
  expect(() => RealtimeSessionRequestSchema.parse({
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ isSecureRemoteAccessOrigin,
4
+ normalizeRemoteAccessOrigin,
5
+ } from '../auth.schema.js';
6
+
7
+ describe('remote access origins', () => {
8
+ it('normalizes exact browser origins and rejects paths or credentials', () => {
9
+ expect(normalizeRemoteAccessOrigin('https://radio.example.com/')).toBe('https://radio.example.com');
10
+ expect(normalizeRemoteAccessOrigin('http://10.8.0.2:8076')).toBe('http://10.8.0.2:8076');
11
+ expect(normalizeRemoteAccessOrigin('https://radio.example.com/tx5dr')).toBeNull();
12
+ expect(normalizeRemoteAccessOrigin('https://user:pass@radio.example.com')).toBeNull();
13
+ });
14
+
15
+ it('identifies origins with transport security without enforcing public-access policy', () => {
16
+ expect(isSecureRemoteAccessOrigin('https://radio.example.com')).toBe(true);
17
+ expect(isSecureRemoteAccessOrigin('http://radio.example.com')).toBe(false);
18
+ expect(isSecureRemoteAccessOrigin('http://203.0.113.10:8076')).toBe(false);
19
+ expect(isSecureRemoteAccessOrigin('http://192.168.1.20:8076')).toBe(true);
20
+ expect(isSecureRemoteAccessOrigin('http://100.64.0.2:8076')).toBe(true);
21
+ });
22
+ });
@@ -0,0 +1,119 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { SpectrumFrameSchema, SpectrumLevelDescriptorSchema, SpectrumSessionInteractionStateSchema } from '../spectrum.schema.js';
3
+ import {
4
+ SpectrumSettingsResponseSchema,
5
+ SpectrumSettingsUpdateRequestSchema,
6
+ TciSpectrumSettingsSchema,
7
+ getSpectrumPresetDefinition,
8
+ } from '../spectrum-config.schema.js';
9
+
10
+ describe('spectrum level descriptor schema', () => {
11
+ it('accepts dBFS, calibrated dB and raw Level descriptors', () => {
12
+ for (const level of [
13
+ { domain: 'dbfs', unit: 'dBFS', reference: 'full-scale', calibrated: true, min: -120, max: 0 },
14
+ { domain: 'calibrated-db', unit: 'dB', reference: 'device', calibrated: true, min: -130, max: 10 },
15
+ { domain: 'raw', unit: 'Level', reference: 'none', calibrated: false, min: 0, max: 255 },
16
+ ]) {
17
+ expect(SpectrumLevelDescriptorSchema.safeParse(level).success).toBe(true);
18
+ }
19
+ });
20
+
21
+ it('rejects invalid or non-increasing level ranges', () => {
22
+ expect(SpectrumLevelDescriptorSchema.safeParse({
23
+ domain: 'raw',
24
+ unit: 'dBFS',
25
+ reference: 'none',
26
+ calibrated: false,
27
+ min: 255,
28
+ max: 0,
29
+ }).success).toBe(false);
30
+ });
31
+
32
+ it('keeps level optional for old spectrum frames', () => {
33
+ const result = SpectrumFrameSchema.safeParse({
34
+ timestamp: 1,
35
+ kind: 'radio-sdr',
36
+ frequencyRange: { min: 7_000_000, max: 7_100_000 },
37
+ binaryData: {
38
+ data: 'AA==',
39
+ format: { type: 'int16', length: 1 },
40
+ },
41
+ meta: {
42
+ sourceBinCount: 1,
43
+ displayBinCount: 1,
44
+ },
45
+ });
46
+ expect(result.success).toBe(true);
47
+ });
48
+
49
+ it('defaults legacy interaction state to radio-center and accepts split TX targets', () => {
50
+ const interaction = SpectrumSessionInteractionStateSchema.parse({
51
+ showTxMarkers: false,
52
+ showRxMarkers: false,
53
+ canDragTx: false,
54
+ canRightClickSetFrequency: false,
55
+ canDoubleClickSetFrequency: false,
56
+ canDragFrequency: false,
57
+ frequencyGestureTarget: null,
58
+ frequencyStepHz: null,
59
+ presetMarkers: [],
60
+ frequencyOverlays: [{
61
+ id: 'split',
62
+ label: 'TX',
63
+ lineFrequency: 14_052_500,
64
+ rangeStartFrequency: 14_052_500,
65
+ rangeEndFrequency: 14_052_500,
66
+ variant: 'tx',
67
+ draggable: true,
68
+ frequencyTarget: 'split-frequency',
69
+ }],
70
+ canDragVoiceOverlay: false,
71
+ showVoiceOverlay: false,
72
+ canLocalViewportZoom: false,
73
+ canLocalViewportPan: false,
74
+ supportsManualRange: true,
75
+ supportsAutoRange: false,
76
+ defaultRangeMode: 'manual',
77
+ });
78
+ expect(interaction.viewMode).toBe('radio-center');
79
+ expect(interaction.viewport.enabled).toBe(false);
80
+ expect(interaction.frequencyOverlays[0]?.frequencyTarget).toBe('split-frequency');
81
+ });
82
+ });
83
+
84
+ describe('spectrum analysis settings schemas', () => {
85
+ it('validates preset updates and derived render settings', () => {
86
+ expect(SpectrumSettingsUpdateRequestSchema.parse({ preset: 'fine' })).toEqual({ preset: 'fine' });
87
+ const balanced = getSpectrumPresetDefinition('balanced');
88
+ expect(SpectrumSettingsResponseSchema.parse({
89
+ success: true,
90
+ currentSettings: { ...balanced, revision: 3 },
91
+ presets: [getSpectrumPresetDefinition('responsive'), balanced, getSpectrumPresetDefinition('fine')],
92
+ }).currentSettings.displayBinCount).toBe(4097);
93
+ });
94
+
95
+ it('rejects arbitrary analysis parameters', () => {
96
+ expect(() => SpectrumSettingsUpdateRequestSchema.parse({ preset: 'custom', fftSize: 4096 })).toThrow();
97
+ });
98
+
99
+ it('accepts a validated custom analysis draft', () => {
100
+ expect(SpectrumSettingsUpdateRequestSchema.parse({
101
+ preset: 'custom',
102
+ settings: {
103
+ analysisIntervalMs: 275,
104
+ fftSize: 4096,
105
+ targetSampleRate: 8000,
106
+ windowFunction: 'hann',
107
+ haloReduce: true,
108
+ },
109
+ }).preset).toBe('custom');
110
+ });
111
+
112
+ it('accepts high-performance TCI analysis settings within the supported bounds', () => {
113
+ expect(TciSpectrumSettingsSchema.parse({
114
+ fftSize: 65536,
115
+ displayBinCount: 16384,
116
+ analysisIntervalMs: 20,
117
+ })).toEqual({ fftSize: 65536, displayBinCount: 16384, analysisIntervalMs: 20 });
118
+ });
119
+ });
@@ -0,0 +1,106 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ OperatorStatusSchema,
4
+ StrategyStreamSnapshotSchema,
5
+ StrategyTransmissionSchema,
6
+ WSMessageSchema,
7
+ } from '../../index.js';
8
+
9
+ describe('strategy transmission contracts', () => {
10
+ it('carries the current multi-stream transmission set in operator status', () => {
11
+ const currentTransmissions = [
12
+ { streamId: 'stream-1', text: 'JA1AAA BG5DRB OL32', audioFrequencyHz: 1200 },
13
+ { streamId: 'stream-2', text: 'JA2BBB BG5DRB R PM95', audioFrequencyHz: 1320 },
14
+ { streamId: 'stream-3', text: 'JA3CCC BG5DRB RR73', audioFrequencyHz: 1440 },
15
+ ];
16
+
17
+ expect(StrategyTransmissionSchema.parse(currentTransmissions[0])).toEqual(currentTransmissions[0]);
18
+ expect(OperatorStatusSchema.parse({
19
+ id: 'operator-1',
20
+ isActive: true,
21
+ isTransmitting: true,
22
+ hasTransmitIntent: true,
23
+ currentTransmissions,
24
+ currentSlot: 'parallel',
25
+ context: {
26
+ myCall: 'BG5DRB',
27
+ myGrid: 'OL32',
28
+ targetCalls: ['JA1AAA', 'JA2BBB', 'JA3CCC'],
29
+ targetCall: 'JA1AAA',
30
+ },
31
+ strategy: {
32
+ name: 'ww-digi',
33
+ state: 'parallel',
34
+ availableSlots: ['TX6'],
35
+ },
36
+ })).toMatchObject({
37
+ currentTransmissions,
38
+ context: { targetCalls: ['JA1AAA', 'JA2BBB', 'JA3CCC'] },
39
+ });
40
+ });
41
+
42
+ it('rejects empty text and invalid audio frequencies', () => {
43
+ expect(StrategyTransmissionSchema.safeParse({
44
+ streamId: 'stream-1',
45
+ text: '',
46
+ audioFrequencyHz: 1200,
47
+ }).success).toBe(false);
48
+ expect(StrategyTransmissionSchema.safeParse({
49
+ streamId: 'stream-1',
50
+ text: 'CQ WW BG5DRB OL32',
51
+ audioFrequencyHz: 5001,
52
+ }).success).toBe(false);
53
+ });
54
+
55
+ it('accepts strategy-defined stream states and a lifecycle-scoped switch command', () => {
56
+ expect(StrategyStreamSnapshotSchema.parse({
57
+ streamId: 'stream-7',
58
+ currentState: 'awaiting-confirmation',
59
+ audioFrequencyHz: 1320,
60
+ qsoLifecycleEpoch: 4,
61
+ stateOptions: [{
62
+ id: 'send-final',
63
+ label: 'stateSendFinal',
64
+ transmitText: 'JA1AAA BG5DRB RR73',
65
+ }],
66
+ }).stateOptions?.[0]?.id).toBe('send-final');
67
+
68
+ expect(WSMessageSchema.parse({
69
+ type: 'setOperatorStreamState',
70
+ timestamp: new Date().toISOString(),
71
+ data: {
72
+ operatorId: 'operator-1',
73
+ streamId: 'stream-7',
74
+ stateId: 'send-final',
75
+ expectedLifecycleEpoch: 4,
76
+ },
77
+ }).type).toBe('setOperatorStreamState');
78
+ });
79
+
80
+ it('accepts plugin-declared actions and lifecycle-scoped action invocations', () => {
81
+ const stream = StrategyStreamSnapshotSchema.parse({
82
+ streamId: 'stream-1',
83
+ currentState: 'recovering',
84
+ audioFrequencyHz: 1500,
85
+ qsoLifecycleEpoch: 7,
86
+ completion: { state: 'committed', recordId: 'qso-1' },
87
+ attentions: [{ id: 'needs-action', tone: 'warning', title: 'attentionTitle', actionIds: ['reply'] }],
88
+ actions: [{
89
+ id: 'reply', label: 'replyLabel', icon: 'paper-plane', tone: 'primary', presentation: 'primary',
90
+ input: { kind: 'audio-frequency', value: 1500, min: 100, max: 5000, step: 10, spectrumPick: true },
91
+ }],
92
+ });
93
+ expect(stream.actions?.[0]?.id).toBe('reply');
94
+
95
+ expect(WSMessageSchema.parse({
96
+ type: 'invokeOperatorStrategyAction',
97
+ timestamp: new Date().toISOString(),
98
+ data: {
99
+ operatorId: 'operator-1',
100
+ target: { kind: 'stream', streamId: 'stream-1', lifecycleEpoch: 7 },
101
+ actionId: 'reply',
102
+ payload: { value: 1600 },
103
+ },
104
+ }).type).toBe('invokeOperatorStrategyAction');
105
+ });
106
+ });
@@ -0,0 +1,49 @@
1
+ import { z } from 'zod';
2
+
3
+ export const AndroidOperatorAudioDeviceSchema = z.object({
4
+ id: z.string(),
5
+ name: z.string(),
6
+ kind: z.string(),
7
+ socketPath: z.string(),
8
+ sampleRate: z.number().int().positive(),
9
+ connected: z.boolean().optional(),
10
+ });
11
+ export type AndroidOperatorAudioDevice = z.infer<typeof AndroidOperatorAudioDeviceSchema>;
12
+
13
+ export const AndroidOperatorAudioCaptureStateSchema = z.enum(['idle', 'preparing', 'capturing', 'error']);
14
+ export type AndroidOperatorAudioCaptureState = z.infer<typeof AndroidOperatorAudioCaptureStateSchema>;
15
+
16
+ export const AndroidOperatorAudioMonitorStateSchema = z.enum(['idle', 'starting', 'playing', 'paused-for-ptt', 'error']);
17
+ export type AndroidOperatorAudioMonitorState = z.infer<typeof AndroidOperatorAudioMonitorStateSchema>;
18
+
19
+ export const AndroidOperatorAudioStatusSchema = z.object({
20
+ available: z.boolean(),
21
+ captureState: AndroidOperatorAudioCaptureStateSchema,
22
+ monitorState: AndroidOperatorAudioMonitorStateSchema,
23
+ participantIdentity: z.string().nullable(),
24
+ inputLevel: z.number().min(0).max(1),
25
+ inputPeak: z.number().min(0).max(1),
26
+ rawInputLevel: z.number().min(0).max(1).optional(),
27
+ rawInputPeak: z.number().min(0).max(1).optional(),
28
+ inputSilenced: z.boolean(),
29
+ micGainDb: z.number(),
30
+ micGainMinDb: z.number(),
31
+ micGainMaxDb: z.number(),
32
+ monitorGainDb: z.number(),
33
+ monitorGainMinDb: z.number(),
34
+ monitorGainMaxDb: z.number(),
35
+ micDevice: AndroidOperatorAudioDeviceSchema.nullable(),
36
+ speakerDevice: AndroidOperatorAudioDeviceSchema.nullable(),
37
+ lastError: z.string().nullable(),
38
+ });
39
+ export type AndroidOperatorAudioStatus = z.infer<typeof AndroidOperatorAudioStatusSchema>;
40
+
41
+ export const AndroidOperatorAudioGainUpdateSchema = z.object({
42
+ micGainDb: z.number(),
43
+ });
44
+ export type AndroidOperatorAudioGainUpdate = z.infer<typeof AndroidOperatorAudioGainUpdateSchema>;
45
+
46
+ export const AndroidOperatorAudioMonitorGainUpdateSchema = z.object({
47
+ monitorGainDb: z.number(),
48
+ });
49
+ export type AndroidOperatorAudioMonitorGainUpdate = z.infer<typeof AndroidOperatorAudioMonitorGainUpdateSchema>;
@@ -132,6 +132,7 @@ export const LastFrequencyResponseSchema = z.object({
132
132
  band: z.string(),
133
133
  description: z.string().optional(),
134
134
  }).nullable().optional(),
135
+ lastImageFrequency: PresetFrequencySchema.nullable().optional(),
135
136
  });
136
137
 
137
138
  /**
@@ -150,6 +151,15 @@ export const SetFrequencyResponseSchema = z.object({
150
151
  ctcssToneTenthsHz: z.number().optional(),
151
152
  dcsCode: z.number().optional(),
152
153
  radioConnected: z.boolean().optional(),
154
+ confirmation: z.enum(['confirmed', 'pending', 'mismatch', 'offline']).optional(),
155
+ observedFrequency: z.number().optional(),
156
+ operationId: z.string().optional(),
157
+ });
158
+
159
+ export const SetDdsFrequencyResponseSchema = z.object({
160
+ success: z.literal(true),
161
+ frequency: z.number(),
162
+ receiver: z.number().int().nonnegative(),
153
163
  });
154
164
 
155
165
  // ========== 时隙包相关响应 ==========
@@ -203,6 +213,7 @@ export type DisconnectRadioResponse = z.infer<typeof DisconnectRadioResponseSche
203
213
 
204
214
  export type LastFrequencyResponse = z.infer<typeof LastFrequencyResponseSchema>;
205
215
  export type SetFrequencyResponse = z.infer<typeof SetFrequencyResponseSchema>;
216
+ export type SetDdsFrequencyResponse = z.infer<typeof SetDdsFrequencyResponseSchema>;
206
217
 
207
218
  export type SlotPacksResponse = z.infer<typeof SlotPacksResponseSchema>;
208
219
  export type SlotPackResponse = z.infer<typeof SlotPackResponseSchema>;
@@ -29,6 +29,29 @@ export const AudioSidecarErrorSchema = z.object({
29
29
 
30
30
  export type AudioSidecarError = z.infer<typeof AudioSidecarErrorSchema>;
31
31
 
32
+ export const AudioSidecarPhaseSchema = z.enum([
33
+ 'startStream',
34
+ 'startOutput',
35
+ 'runtime',
36
+ ]);
37
+
38
+ export const AudioSidecarClassificationSchema = z.enum([
39
+ 'startup-failure',
40
+ 'runtime-loss',
41
+ 'sample-rate-fallback',
42
+ 'sample-rate-fallback-failed',
43
+ 'device-unavailable',
44
+ 'disabled',
45
+ ]);
46
+
47
+ export const AudioSidecarFallbackSchema = z.object({
48
+ active: z.boolean(),
49
+ fromSampleRate: z.number().int().positive().nullable(),
50
+ toSampleRate: z.number().int().positive().nullable(),
51
+ persisted: z.boolean().optional(),
52
+ reason: z.string().optional(),
53
+ });
54
+
32
55
  /**
33
56
  * AudioSidecar 状态变化载荷。
34
57
  * 前端以此渲染 RadioControl 中的音频副状态 spinner + Popover。
@@ -48,6 +71,18 @@ export const AudioSidecarStatusPayloadSchema = z.object({
48
71
  lastError: AudioSidecarErrorSchema.nullable(),
49
72
  /** 当前目标设备名(来自 AudioConfig.inputDeviceName) */
50
73
  deviceName: z.string().nullable(),
74
+ /** 当前失败/重试阶段 */
75
+ phase: AudioSidecarPhaseSchema.optional(),
76
+ /** 机器可读的失败分类,用于 UI 展示和诊断 */
77
+ classification: AudioSidecarClassificationSchema.optional(),
78
+ /** 具体受影响的输入/输出设备名 */
79
+ affectedDeviceName: z.string().nullable().optional(),
80
+ /** 失败时使用的采样率 */
81
+ sampleRate: z.number().int().positive().nullable().optional(),
82
+ /** 自动降级状态 */
83
+ fallback: AudioSidecarFallbackSchema.optional(),
84
+ /** 当前重试原因摘要 */
85
+ retryReason: z.string().optional(),
51
86
  });
52
87
 
53
88
  export type AudioSidecarStatusPayload = z.infer<typeof AudioSidecarStatusPayloadSchema>;