@tx5dr/contracts 2.0.0 → 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 (138) hide show
  1. package/dist/index.d.ts +4 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +3 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/schema/__tests__/assisted-queue.schema.test.js +1 -0
  6. package/dist/schema/__tests__/assisted-queue.schema.test.js.map +1 -1
  7. package/dist/schema/__tests__/meter.schema.test.d.ts +2 -0
  8. package/dist/schema/__tests__/meter.schema.test.d.ts.map +1 -0
  9. package/dist/schema/__tests__/meter.schema.test.js +27 -0
  10. package/dist/schema/__tests__/meter.schema.test.js.map +1 -0
  11. package/dist/schema/__tests__/operator-context-report.schema.test.d.ts +2 -0
  12. package/dist/schema/__tests__/operator-context-report.schema.test.d.ts.map +1 -0
  13. package/dist/schema/__tests__/operator-context-report.schema.test.js +70 -0
  14. package/dist/schema/__tests__/operator-context-report.schema.test.js.map +1 -0
  15. package/dist/schema/__tests__/plugin-market.schema.test.js +107 -3
  16. package/dist/schema/__tests__/plugin-market.schema.test.js.map +1 -1
  17. package/dist/schema/__tests__/qso.schema.test.d.ts +2 -0
  18. package/dist/schema/__tests__/qso.schema.test.d.ts.map +1 -0
  19. package/dist/schema/__tests__/qso.schema.test.js +70 -0
  20. package/dist/schema/__tests__/qso.schema.test.js.map +1 -0
  21. package/dist/schema/__tests__/radio-power-support.test.js +4 -1
  22. package/dist/schema/__tests__/radio-power-support.test.js.map +1 -1
  23. package/dist/schema/__tests__/radio.schema.test.js +52 -1
  24. package/dist/schema/__tests__/radio.schema.test.js.map +1 -1
  25. package/dist/schema/__tests__/remote-access.schema.test.js +1 -1
  26. package/dist/schema/__tests__/remote-access.schema.test.js.map +1 -1
  27. package/dist/schema/__tests__/spectrum.schema.test.d.ts +2 -0
  28. package/dist/schema/__tests__/spectrum.schema.test.d.ts.map +1 -0
  29. package/dist/schema/__tests__/spectrum.schema.test.js +107 -0
  30. package/dist/schema/__tests__/spectrum.schema.test.js.map +1 -0
  31. package/dist/schema/__tests__/strategy-transmission.schema.test.d.ts +2 -0
  32. package/dist/schema/__tests__/strategy-transmission.schema.test.d.ts.map +1 -0
  33. package/dist/schema/__tests__/strategy-transmission.schema.test.js +95 -0
  34. package/dist/schema/__tests__/strategy-transmission.schema.test.js.map +1 -0
  35. package/dist/schema/api-response.schema.d.ts +235 -0
  36. package/dist/schema/api-response.schema.d.ts.map +1 -1
  37. package/dist/schema/api-response.schema.js +9 -0
  38. package/dist/schema/api-response.schema.js.map +1 -1
  39. package/dist/schema/auth.schema.d.ts +2 -2
  40. package/dist/schema/cpu-profile.schema.d.ts +2 -2
  41. package/dist/schema/device-ui.schema.d.ts +10 -10
  42. package/dist/schema/image-radio.schema.d.ts +2597 -0
  43. package/dist/schema/image-radio.schema.d.ts.map +1 -0
  44. package/dist/schema/image-radio.schema.js +283 -0
  45. package/dist/schema/image-radio.schema.js.map +1 -0
  46. package/dist/schema/logbook.schema.d.ts +203 -15
  47. package/dist/schema/logbook.schema.d.ts.map +1 -1
  48. package/dist/schema/logbook.schema.js +1 -0
  49. package/dist/schema/logbook.schema.js.map +1 -1
  50. package/dist/schema/mode.schema.d.ts +2 -0
  51. package/dist/schema/mode.schema.d.ts.map +1 -1
  52. package/dist/schema/mode.schema.js +16 -0
  53. package/dist/schema/mode.schema.js.map +1 -1
  54. package/dist/schema/openwebrx.schema.d.ts +2 -2
  55. package/dist/schema/operator.schema.d.ts +2676 -0
  56. package/dist/schema/operator.schema.d.ts.map +1 -1
  57. package/dist/schema/operator.schema.js +4 -19
  58. package/dist/schema/operator.schema.js.map +1 -1
  59. package/dist/schema/plugin.schema.d.ts +1780 -475
  60. package/dist/schema/plugin.schema.d.ts.map +1 -1
  61. package/dist/schema/plugin.schema.js +108 -12
  62. package/dist/schema/plugin.schema.js.map +1 -1
  63. package/dist/schema/qso.schema.d.ts +239 -1
  64. package/dist/schema/qso.schema.d.ts.map +1 -1
  65. package/dist/schema/qso.schema.js +61 -0
  66. package/dist/schema/qso.schema.js.map +1 -1
  67. package/dist/schema/radio-capability.schema.d.ts +1 -1
  68. package/dist/schema/radio-capability.schema.d.ts.map +1 -1
  69. package/dist/schema/radio-capability.schema.js +1 -0
  70. package/dist/schema/radio-capability.schema.js.map +1 -1
  71. package/dist/schema/radio-power.schema.d.ts +15 -0
  72. package/dist/schema/radio-power.schema.d.ts.map +1 -1
  73. package/dist/schema/radio-profile.schema.d.ts +251 -0
  74. package/dist/schema/radio-profile.schema.d.ts.map +1 -1
  75. package/dist/schema/radio-profile.schema.js +7 -0
  76. package/dist/schema/radio-profile.schema.js.map +1 -1
  77. package/dist/schema/radio.schema.d.ts +174 -3
  78. package/dist/schema/radio.schema.d.ts.map +1 -1
  79. package/dist/schema/radio.schema.js +31 -2
  80. package/dist/schema/radio.schema.js.map +1 -1
  81. package/dist/schema/rigctld.schema.d.ts +6 -6
  82. package/dist/schema/slot-info.schema.d.ts +13 -0
  83. package/dist/schema/slot-info.schema.d.ts.map +1 -1
  84. package/dist/schema/slot-info.schema.js +2 -1
  85. package/dist/schema/slot-info.schema.js.map +1 -1
  86. package/dist/schema/spectrum-config.schema.d.ts +638 -0
  87. package/dist/schema/spectrum-config.schema.d.ts.map +1 -0
  88. package/dist/schema/spectrum-config.schema.js +129 -0
  89. package/dist/schema/spectrum-config.schema.js.map +1 -0
  90. package/dist/schema/spectrum.schema.d.ts +1313 -20
  91. package/dist/schema/spectrum.schema.d.ts.map +1 -1
  92. package/dist/schema/spectrum.schema.js +72 -0
  93. package/dist/schema/spectrum.schema.js.map +1 -1
  94. package/dist/schema/transmission.schema.d.ts +7 -0
  95. package/dist/schema/transmission.schema.d.ts.map +1 -1
  96. package/dist/schema/tx-audio-input.schema.d.ts +8 -0
  97. package/dist/schema/tx-audio-input.schema.d.ts.map +1 -0
  98. package/dist/schema/tx-audio-input.schema.js +19 -0
  99. package/dist/schema/tx-audio-input.schema.js.map +1 -0
  100. package/dist/schema/voice.schema.d.ts +1 -1
  101. package/dist/schema/voice.schema.d.ts.map +1 -1
  102. package/dist/schema/voice.schema.js +1 -1
  103. package/dist/schema/voice.schema.js.map +1 -1
  104. package/dist/schema/websocket.schema.d.ts +27480 -2525
  105. package/dist/schema/websocket.schema.d.ts.map +1 -1
  106. package/dist/schema/websocket.schema.js +298 -11
  107. package/dist/schema/websocket.schema.js.map +1 -1
  108. package/package.json +1 -1
  109. package/src/index.ts +4 -0
  110. package/src/schema/__tests__/assisted-queue.schema.test.ts +1 -0
  111. package/src/schema/__tests__/meter.schema.test.ts +28 -0
  112. package/src/schema/__tests__/operator-context-report.schema.test.ts +77 -0
  113. package/src/schema/__tests__/plugin-market.schema.test.ts +116 -2
  114. package/src/schema/__tests__/qso.schema.test.ts +91 -0
  115. package/src/schema/__tests__/radio-power-support.test.ts +4 -1
  116. package/src/schema/__tests__/radio.schema.test.ts +60 -1
  117. package/src/schema/__tests__/remote-access.schema.test.ts +1 -1
  118. package/src/schema/__tests__/spectrum.schema.test.ts +119 -0
  119. package/src/schema/__tests__/strategy-transmission.schema.test.ts +106 -0
  120. package/src/schema/api-response.schema.ts +11 -0
  121. package/src/schema/image-radio.schema.ts +342 -0
  122. package/src/schema/logbook.schema.ts +1 -0
  123. package/src/schema/mode.schema.ts +16 -0
  124. package/src/schema/operator.schema.ts +4 -19
  125. package/src/schema/plugin.schema.ts +119 -13
  126. package/src/schema/qso.schema.ts +71 -0
  127. package/src/schema/radio-capability.schema.ts +1 -0
  128. package/src/schema/radio-profile.schema.ts +7 -0
  129. package/src/schema/radio.schema.ts +35 -2
  130. package/src/schema/slot-info.schema.ts +2 -1
  131. package/src/schema/spectrum-config.schema.ts +156 -0
  132. package/src/schema/spectrum.schema.ts +89 -0
  133. package/src/schema/tx-audio-input.schema.ts +22 -0
  134. package/src/schema/voice.schema.ts +1 -1
  135. package/src/schema/websocket.schema.ts +383 -12
  136. package/test/image-radio-schema.test.ts +107 -0
  137. package/test/plugin-slot-schema.test.ts +31 -0
  138. package/CLAUDE.md +0 -41
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { DigitalModeRadioModePreferenceSchema, HamlibConfigSchema, PresetFrequencySchema, RadioInfoSchema } from '../radio.schema.js';
2
+ import { DdsFrequencyRequestSchema, DigitalModeRadioModePreferenceSchema, HamlibConfigSchema, PresetFrequencySchema, RadioInfoSchema } from '../radio.schema.js';
3
3
 
4
4
  describe('HamlibConfigSchema digital mode radio mode preference', () => {
5
5
  it('accepts legacy configs without a digital mode radio mode preference', () => {
@@ -38,6 +38,12 @@ describe('HamlibConfigSchema TCI connection', () => {
38
38
  });
39
39
  });
40
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
+
41
47
  it('accepts TCI as a reported radio connection type', () => {
42
48
  const parsed = RadioInfoSchema.parse({
43
49
  manufacturer: 'Expert Electronics',
@@ -49,6 +55,17 @@ describe('HamlibConfigSchema TCI connection', () => {
49
55
  });
50
56
  });
51
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
+ });
68
+
52
69
  describe('PresetFrequencySchema repeater DUP fields', () => {
53
70
  it('accepts existing presets without repeater DUP fields', () => {
54
71
  const parsed = PresetFrequencySchema.parse({
@@ -119,6 +136,48 @@ describe('PresetFrequencySchema repeater DUP fields', () => {
119
136
  });
120
137
  });
121
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
+
122
181
  describe('PresetFrequencySchema tone squelch fields', () => {
123
182
  it('accepts CTCSS tone presets with positive 0.1 Hz values', () => {
124
183
  const parsed = PresetFrequencySchema.parse({
@@ -12,7 +12,7 @@ describe('remote access origins', () => {
12
12
  expect(normalizeRemoteAccessOrigin('https://user:pass@radio.example.com')).toBeNull();
13
13
  });
14
14
 
15
- it('requires HTTPS for public hosts while allowing private-network HTTP', () => {
15
+ it('identifies origins with transport security without enforcing public-access policy', () => {
16
16
  expect(isSecureRemoteAccessOrigin('https://radio.example.com')).toBe(true);
17
17
  expect(isSecureRemoteAccessOrigin('http://radio.example.com')).toBe(false);
18
18
  expect(isSecureRemoteAccessOrigin('http://203.0.113.10:8076')).toBe(false);
@@ -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
+ });
@@ -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>;