1use super::{ValidationError, ValidationResult};
6use crate::actuator::v1::{
7 ActuatorCommand, ActuatorCommandType, ActuatorMount, ActuatorSpec, ActuatorStateUpdate,
8 ActuatorStatus, ActuatorType, BarrierLimits, BarrierState, GripperLimits, GripperState,
9 LinearLimits, LinearState, LockState, RotaryLimits, RotaryState, ValveLimits, ValveState,
10 WinchLimits, WinchState,
11};
12
13pub fn validate_linear_limits(limits: &LinearLimits) -> ValidationResult<()> {
20 if limits.position_min_m > limits.position_max_m {
21 return Err(ValidationError::ConstraintViolation(
22 "position_min_m must be <= position_max_m".to_string(),
23 ));
24 }
25
26 if limits.velocity_max_mps < 0.0 {
27 return Err(ValidationError::InvalidValue(
28 "velocity_max_mps must be non-negative".to_string(),
29 ));
30 }
31
32 if limits.force_max_n < 0.0 {
33 return Err(ValidationError::InvalidValue(
34 "force_max_n must be non-negative".to_string(),
35 ));
36 }
37
38 Ok(())
39}
40
41pub fn validate_rotary_limits(limits: &RotaryLimits) -> ValidationResult<()> {
48 if !limits.continuous_rotation && limits.angle_min_deg > limits.angle_max_deg {
50 return Err(ValidationError::ConstraintViolation(
51 "angle_min_deg must be <= angle_max_deg for non-continuous rotation".to_string(),
52 ));
53 }
54
55 if limits.velocity_max_dps < 0.0 {
56 return Err(ValidationError::InvalidValue(
57 "velocity_max_dps must be non-negative".to_string(),
58 ));
59 }
60
61 if limits.torque_max_nm < 0.0 {
62 return Err(ValidationError::InvalidValue(
63 "torque_max_nm must be non-negative".to_string(),
64 ));
65 }
66
67 Ok(())
68}
69
70pub fn validate_gripper_limits(limits: &GripperLimits) -> ValidationResult<()> {
78 if limits.aperture_min_m < 0.0 {
79 return Err(ValidationError::InvalidValue(
80 "aperture_min_m must be non-negative".to_string(),
81 ));
82 }
83
84 if limits.aperture_min_m > limits.aperture_max_m {
85 return Err(ValidationError::ConstraintViolation(
86 "aperture_min_m must be <= aperture_max_m".to_string(),
87 ));
88 }
89
90 if limits.grip_force_max_n < 0.0 {
91 return Err(ValidationError::InvalidValue(
92 "grip_force_max_n must be non-negative".to_string(),
93 ));
94 }
95
96 if limits.payload_max_kg < 0.0 {
97 return Err(ValidationError::InvalidValue(
98 "payload_max_kg must be non-negative".to_string(),
99 ));
100 }
101
102 Ok(())
103}
104
105pub fn validate_valve_limits(limits: &ValveLimits) -> ValidationResult<()> {
112 if limits.position_min < 0.0 || limits.position_min > 1.0 {
113 return Err(ValidationError::InvalidValue(format!(
114 "position_min {} must be in range [0.0, 1.0]",
115 limits.position_min
116 )));
117 }
118
119 if limits.position_max < 0.0 || limits.position_max > 1.0 {
120 return Err(ValidationError::InvalidValue(format!(
121 "position_max {} must be in range [0.0, 1.0]",
122 limits.position_max
123 )));
124 }
125
126 if limits.position_min > limits.position_max {
127 return Err(ValidationError::ConstraintViolation(
128 "position_min must be <= position_max".to_string(),
129 ));
130 }
131
132 if limits.full_travel_time_s <= 0.0 {
133 return Err(ValidationError::InvalidValue(
134 "full_travel_time_s must be positive".to_string(),
135 ));
136 }
137
138 Ok(())
139}
140
141pub fn validate_barrier_limits(limits: &BarrierLimits) -> ValidationResult<()> {
148 if limits.position_min < 0.0 || limits.position_min > 1.0 {
149 return Err(ValidationError::InvalidValue(format!(
150 "position_min {} must be in range [0.0, 1.0]",
151 limits.position_min
152 )));
153 }
154
155 if limits.position_max < 0.0 || limits.position_max > 1.0 {
156 return Err(ValidationError::InvalidValue(format!(
157 "position_max {} must be in range [0.0, 1.0]",
158 limits.position_max
159 )));
160 }
161
162 if limits.full_cycle_time_s <= 0.0 {
163 return Err(ValidationError::InvalidValue(
164 "full_cycle_time_s must be positive".to_string(),
165 ));
166 }
167
168 if limits.clear_width_m < 0.0 {
169 return Err(ValidationError::InvalidValue(
170 "clear_width_m must be non-negative".to_string(),
171 ));
172 }
173
174 if limits.clear_height_m < 0.0 {
175 return Err(ValidationError::InvalidValue(
176 "clear_height_m must be non-negative".to_string(),
177 ));
178 }
179
180 Ok(())
181}
182
183pub fn validate_winch_limits(limits: &WinchLimits) -> ValidationResult<()> {
190 if limits.cable_min_m < 0.0 {
191 return Err(ValidationError::InvalidValue(
192 "cable_min_m must be non-negative".to_string(),
193 ));
194 }
195
196 if limits.cable_min_m > limits.cable_max_m {
197 return Err(ValidationError::ConstraintViolation(
198 "cable_min_m must be <= cable_max_m".to_string(),
199 ));
200 }
201
202 if limits.line_speed_max_mps < 0.0 {
203 return Err(ValidationError::InvalidValue(
204 "line_speed_max_mps must be non-negative".to_string(),
205 ));
206 }
207
208 if limits.tension_max_n < 0.0 {
209 return Err(ValidationError::InvalidValue(
210 "tension_max_n must be non-negative".to_string(),
211 ));
212 }
213
214 if limits.payload_max_kg < 0.0 {
215 return Err(ValidationError::InvalidValue(
216 "payload_max_kg must be non-negative".to_string(),
217 ));
218 }
219
220 Ok(())
221}
222
223pub fn validate_linear_state(
225 state: &LinearState,
226 limits: Option<&LinearLimits>,
227) -> ValidationResult<()> {
228 if let Some(limits) = limits {
229 if state.position_m < limits.position_min_m || state.position_m > limits.position_max_m {
230 return Err(ValidationError::ConstraintViolation(format!(
231 "position_m {} must be within limits [{}, {}]",
232 state.position_m, limits.position_min_m, limits.position_max_m
233 )));
234 }
235 }
236 Ok(())
237}
238
239pub fn validate_rotary_state(
241 state: &RotaryState,
242 limits: Option<&RotaryLimits>,
243) -> ValidationResult<()> {
244 if let Some(limits) = limits {
245 if !limits.continuous_rotation
246 && (state.angle_deg < limits.angle_min_deg || state.angle_deg > limits.angle_max_deg)
247 {
248 return Err(ValidationError::ConstraintViolation(format!(
249 "angle_deg {} must be within limits [{}, {}]",
250 state.angle_deg, limits.angle_min_deg, limits.angle_max_deg
251 )));
252 }
253 }
254 Ok(())
255}
256
257pub fn validate_gripper_state(
259 state: &GripperState,
260 limits: Option<&GripperLimits>,
261) -> ValidationResult<()> {
262 if state.aperture_m < 0.0 {
263 return Err(ValidationError::InvalidValue(
264 "aperture_m must be non-negative".to_string(),
265 ));
266 }
267
268 if let Some(limits) = limits {
269 if state.aperture_m < limits.aperture_min_m || state.aperture_m > limits.aperture_max_m {
270 return Err(ValidationError::ConstraintViolation(format!(
271 "aperture_m {} must be within limits [{}, {}]",
272 state.aperture_m, limits.aperture_min_m, limits.aperture_max_m
273 )));
274 }
275 }
276 Ok(())
277}
278
279pub fn validate_valve_state(state: &ValveState) -> ValidationResult<()> {
281 if state.position < 0.0 || state.position > 1.0 {
282 return Err(ValidationError::InvalidValue(format!(
283 "position {} must be in range [0.0, 1.0]",
284 state.position
285 )));
286 }
287 Ok(())
288}
289
290pub fn validate_barrier_state(state: &BarrierState) -> ValidationResult<()> {
292 if state.position < 0.0 || state.position > 1.0 {
293 return Err(ValidationError::InvalidValue(format!(
294 "position {} must be in range [0.0, 1.0]",
295 state.position
296 )));
297 }
298
299 if state.is_closed && state.position > 0.01 {
301 return Err(ValidationError::ConstraintViolation(
302 "is_closed should not be true when position > 0".to_string(),
303 ));
304 }
305
306 if state.is_open && state.position < 0.99 {
307 return Err(ValidationError::ConstraintViolation(
308 "is_open should not be true when position < 1".to_string(),
309 ));
310 }
311
312 Ok(())
313}
314
315pub fn validate_winch_state(
317 state: &WinchState,
318 limits: Option<&WinchLimits>,
319) -> ValidationResult<()> {
320 if state.cable_out_m < 0.0 {
321 return Err(ValidationError::InvalidValue(
322 "cable_out_m must be non-negative".to_string(),
323 ));
324 }
325
326 if let Some(limits) = limits {
327 if state.cable_out_m > limits.cable_max_m {
328 return Err(ValidationError::ConstraintViolation(format!(
329 "cable_out_m {} exceeds max {}",
330 state.cable_out_m, limits.cable_max_m
331 )));
332 }
333 }
334 Ok(())
335}
336
337pub fn validate_lock_state(_state: &LockState) -> ValidationResult<()> {
339 Ok(())
342}
343
344pub fn validate_actuator_spec(spec: &ActuatorSpec) -> ValidationResult<()> {
354 if spec.actuator_id.is_empty() {
356 return Err(ValidationError::MissingField("actuator_id".to_string()));
357 }
358
359 if spec.name.is_empty() {
360 return Err(ValidationError::MissingField("name".to_string()));
361 }
362
363 if spec.actuator_type == ActuatorType::Unspecified as i32 {
365 return Err(ValidationError::InvalidValue(
366 "actuator_type must be specified".to_string(),
367 ));
368 }
369
370 if spec.mount == ActuatorMount::Unspecified as i32 {
372 return Err(ValidationError::InvalidValue(
373 "mount must be specified".to_string(),
374 ));
375 }
376
377 if let Some(ref limits) = spec.limits {
379 use crate::actuator::v1::actuator_spec::Limits;
380 match limits {
381 Limits::LinearLimits(l) => validate_linear_limits(l)?,
382 Limits::RotaryLimits(l) => validate_rotary_limits(l)?,
383 Limits::GripperLimits(l) => validate_gripper_limits(l)?,
384 Limits::ValveLimits(l) => validate_valve_limits(l)?,
385 Limits::BarrierLimits(l) => validate_barrier_limits(l)?,
386 Limits::WinchLimits(l) => validate_winch_limits(l)?,
387 }
388 }
389
390 if let Some(ref state) = spec.state {
392 use crate::actuator::v1::actuator_spec::State;
393 match state {
394 State::LinearState(s) => {
395 let limits = spec.limits.as_ref().and_then(|l| {
396 if let crate::actuator::v1::actuator_spec::Limits::LinearLimits(ll) = l {
397 Some(ll)
398 } else {
399 None
400 }
401 });
402 validate_linear_state(s, limits)?;
403 }
404 State::RotaryState(s) => {
405 let limits = spec.limits.as_ref().and_then(|l| {
406 if let crate::actuator::v1::actuator_spec::Limits::RotaryLimits(rl) = l {
407 Some(rl)
408 } else {
409 None
410 }
411 });
412 validate_rotary_state(s, limits)?;
413 }
414 State::GripperState(s) => {
415 let limits = spec.limits.as_ref().and_then(|l| {
416 if let crate::actuator::v1::actuator_spec::Limits::GripperLimits(gl) = l {
417 Some(gl)
418 } else {
419 None
420 }
421 });
422 validate_gripper_state(s, limits)?;
423 }
424 State::ValveState(s) => validate_valve_state(s)?,
425 State::BarrierState(s) => validate_barrier_state(s)?,
426 State::WinchState(s) => {
427 let limits = spec.limits.as_ref().and_then(|l| {
428 if let crate::actuator::v1::actuator_spec::Limits::WinchLimits(wl) = l {
429 Some(wl)
430 } else {
431 None
432 }
433 });
434 validate_winch_state(s, limits)?;
435 }
436 State::LockState(s) => validate_lock_state(s)?,
437 }
438 }
439
440 Ok(())
441}
442
443pub fn validate_actuator_state_update(update: &ActuatorStateUpdate) -> ValidationResult<()> {
451 if update.platform_id.is_empty() {
452 return Err(ValidationError::MissingField("platform_id".to_string()));
453 }
454
455 let actuator = update
457 .actuator
458 .as_ref()
459 .ok_or_else(|| ValidationError::MissingField("actuator".to_string()))?;
460 validate_actuator_spec(actuator)?;
461
462 if update.status == ActuatorStatus::Unspecified as i32 {
464 return Err(ValidationError::InvalidValue(
465 "status must be specified".to_string(),
466 ));
467 }
468
469 if update.timestamp.is_none() {
471 return Err(ValidationError::MissingField("timestamp".to_string()));
472 }
473
474 Ok(())
475}
476
477pub fn validate_actuator_command(cmd: &ActuatorCommand) -> ValidationResult<()> {
486 if cmd.command_id.is_empty() {
487 return Err(ValidationError::MissingField("command_id".to_string()));
488 }
489
490 if cmd.platform_id.is_empty() {
491 return Err(ValidationError::MissingField("platform_id".to_string()));
492 }
493
494 if cmd.actuator_id.is_empty() {
495 return Err(ValidationError::MissingField("actuator_id".to_string()));
496 }
497
498 if cmd.command_type == ActuatorCommandType::ActuatorCommandUnspecified as i32 {
499 return Err(ValidationError::InvalidValue(
500 "command_type must be specified".to_string(),
501 ));
502 }
503
504 if cmd.issued_at.is_none() {
505 return Err(ValidationError::MissingField("issued_at".to_string()));
506 }
507
508 if let (Some(issued), Some(expires)) = (&cmd.issued_at, &cmd.expires_at) {
510 if expires.seconds < issued.seconds
511 || (expires.seconds == issued.seconds && expires.nanos < issued.nanos)
512 {
513 return Err(ValidationError::ConstraintViolation(
514 "expires_at must be after issued_at".to_string(),
515 ));
516 }
517 }
518
519 Ok(())
520}
521
522#[cfg(test)]
523mod tests {
524 use super::*;
525 use crate::actuator::v1::{actuator_spec::Limits, actuator_spec::State, ActuatorDrive};
526 use crate::common::v1::Timestamp;
527
528 fn valid_barrier_actuator() -> ActuatorSpec {
529 ActuatorSpec {
530 actuator_id: "gate-main".to_string(),
531 name: "Main Entry Gate".to_string(),
532 actuator_type: ActuatorType::Barrier as i32,
533 mount: ActuatorMount::Fixed as i32,
534 drive: ActuatorDrive::Electric as i32,
535 limits: Some(Limits::BarrierLimits(BarrierLimits {
536 position_min: 0.0,
537 position_max: 1.0,
538 full_cycle_time_s: 8.0,
539 clear_width_m: 4.0,
540 clear_height_m: 2.5,
541 })),
542 state: Some(State::BarrierState(BarrierState {
543 position: 0.0,
544 is_closed: true,
545 is_open: false,
546 obstruction_detected: false,
547 })),
548 updated_at: None,
549 metadata_json: String::new(),
550 }
551 }
552
553 fn valid_linear_actuator() -> ActuatorSpec {
554 ActuatorSpec {
555 actuator_id: "lift-main".to_string(),
556 name: "Cargo Lift".to_string(),
557 actuator_type: ActuatorType::Linear as i32,
558 mount: ActuatorMount::Fixed as i32,
559 drive: ActuatorDrive::Hydraulic as i32,
560 limits: Some(Limits::LinearLimits(LinearLimits {
561 position_min_m: 0.0,
562 position_max_m: 3.0,
563 velocity_max_mps: 0.5,
564 acceleration_max: 0.2,
565 force_max_n: 50000.0,
566 })),
567 state: Some(State::LinearState(LinearState {
568 position_m: 1.5,
569 velocity_mps: 0.0,
570 force_n: 0.0,
571 })),
572 updated_at: None,
573 metadata_json: String::new(),
574 }
575 }
576
577 fn valid_winch_actuator() -> ActuatorSpec {
578 ActuatorSpec {
579 actuator_id: "crane-hoist".to_string(),
580 name: "Container Crane Hoist".to_string(),
581 actuator_type: ActuatorType::Winch as i32,
582 mount: ActuatorMount::Fixed as i32,
583 drive: ActuatorDrive::Electric as i32,
584 limits: Some(Limits::WinchLimits(WinchLimits {
585 cable_min_m: 0.0,
586 cable_max_m: 50.0,
587 line_speed_max_mps: 2.0,
588 tension_max_n: 500000.0,
589 payload_max_kg: 40000.0,
590 })),
591 state: Some(State::WinchState(WinchState {
592 cable_out_m: 25.0,
593 line_speed_mps: 0.0,
594 tension_n: 150000.0,
595 payload_kg: 15000.0,
596 })),
597 updated_at: None,
598 metadata_json: String::new(),
599 }
600 }
601
602 #[test]
603 fn test_valid_barrier_actuator() {
604 let actuator = valid_barrier_actuator();
605 assert!(validate_actuator_spec(&actuator).is_ok());
606 }
607
608 #[test]
609 fn test_valid_linear_actuator() {
610 let actuator = valid_linear_actuator();
611 assert!(validate_actuator_spec(&actuator).is_ok());
612 }
613
614 #[test]
615 fn test_valid_winch_actuator() {
616 let actuator = valid_winch_actuator();
617 assert!(validate_actuator_spec(&actuator).is_ok());
618 }
619
620 #[test]
621 fn test_missing_actuator_id() {
622 let mut actuator = valid_barrier_actuator();
623 actuator.actuator_id = String::new();
624 let err = validate_actuator_spec(&actuator).unwrap_err();
625 assert!(matches!(err, ValidationError::MissingField(f) if f == "actuator_id"));
626 }
627
628 #[test]
629 fn test_missing_name() {
630 let mut actuator = valid_barrier_actuator();
631 actuator.name = String::new();
632 let err = validate_actuator_spec(&actuator).unwrap_err();
633 assert!(matches!(err, ValidationError::MissingField(f) if f == "name"));
634 }
635
636 #[test]
637 fn test_unspecified_actuator_type() {
638 let mut actuator = valid_barrier_actuator();
639 actuator.actuator_type = ActuatorType::Unspecified as i32;
640 let err = validate_actuator_spec(&actuator).unwrap_err();
641 assert!(matches!(err, ValidationError::InvalidValue(_)));
642 }
643
644 #[test]
645 fn test_unspecified_mount() {
646 let mut actuator = valid_barrier_actuator();
647 actuator.mount = ActuatorMount::Unspecified as i32;
648 let err = validate_actuator_spec(&actuator).unwrap_err();
649 assert!(matches!(err, ValidationError::InvalidValue(_)));
650 }
651
652 #[test]
653 fn test_invalid_barrier_position() {
654 let mut actuator = valid_barrier_actuator();
655 actuator.state = Some(State::BarrierState(BarrierState {
656 position: 1.5, is_closed: false,
658 is_open: false,
659 obstruction_detected: false,
660 }));
661 let err = validate_actuator_spec(&actuator).unwrap_err();
662 assert!(matches!(err, ValidationError::InvalidValue(_)));
663 }
664
665 #[test]
666 fn test_barrier_state_consistency() {
667 let mut actuator = valid_barrier_actuator();
668 actuator.state = Some(State::BarrierState(BarrierState {
669 position: 0.5, is_closed: true, is_open: false,
672 obstruction_detected: false,
673 }));
674 let err = validate_actuator_spec(&actuator).unwrap_err();
675 assert!(matches!(err, ValidationError::ConstraintViolation(_)));
676 }
677
678 #[test]
679 fn test_linear_state_outside_limits() {
680 let mut actuator = valid_linear_actuator();
681 actuator.state = Some(State::LinearState(LinearState {
682 position_m: 5.0, velocity_mps: 0.0,
684 force_n: 0.0,
685 }));
686 let err = validate_actuator_spec(&actuator).unwrap_err();
687 assert!(matches!(err, ValidationError::ConstraintViolation(_)));
688 }
689
690 #[test]
691 fn test_invalid_linear_limits() {
692 let mut actuator = valid_linear_actuator();
693 actuator.limits = Some(Limits::LinearLimits(LinearLimits {
694 position_min_m: 5.0, position_max_m: 3.0,
696 velocity_max_mps: 0.5,
697 acceleration_max: 0.2,
698 force_max_n: 50000.0,
699 }));
700 let err = validate_actuator_spec(&actuator).unwrap_err();
701 assert!(matches!(err, ValidationError::ConstraintViolation(_)));
702 }
703
704 #[test]
705 fn test_winch_cable_exceeds_max() {
706 let mut actuator = valid_winch_actuator();
707 actuator.state = Some(State::WinchState(WinchState {
708 cable_out_m: 100.0, line_speed_mps: 0.0,
710 tension_n: 0.0,
711 payload_kg: 0.0,
712 }));
713 let err = validate_actuator_spec(&actuator).unwrap_err();
714 assert!(matches!(err, ValidationError::ConstraintViolation(_)));
715 }
716
717 #[test]
718 fn test_valid_actuator_state_update() {
719 let update = ActuatorStateUpdate {
720 platform_id: "PORT-GATE-1".to_string(),
721 actuator: Some(valid_barrier_actuator()),
722 status: ActuatorStatus::Operational as i32,
723 timestamp: Some(Timestamp {
724 seconds: 1702000000,
725 nanos: 0,
726 }),
727 };
728 assert!(validate_actuator_state_update(&update).is_ok());
729 }
730
731 #[test]
732 fn test_actuator_update_missing_platform_id() {
733 let update = ActuatorStateUpdate {
734 platform_id: String::new(),
735 actuator: Some(valid_barrier_actuator()),
736 status: ActuatorStatus::Operational as i32,
737 timestamp: Some(Timestamp {
738 seconds: 1702000000,
739 nanos: 0,
740 }),
741 };
742 let err = validate_actuator_state_update(&update).unwrap_err();
743 assert!(matches!(err, ValidationError::MissingField(f) if f == "platform_id"));
744 }
745
746 #[test]
747 fn test_actuator_update_unspecified_status() {
748 let update = ActuatorStateUpdate {
749 platform_id: "PORT-GATE-1".to_string(),
750 actuator: Some(valid_barrier_actuator()),
751 status: ActuatorStatus::Unspecified as i32,
752 timestamp: Some(Timestamp {
753 seconds: 1702000000,
754 nanos: 0,
755 }),
756 };
757 let err = validate_actuator_state_update(&update).unwrap_err();
758 assert!(matches!(err, ValidationError::InvalidValue(_)));
759 }
760
761 #[test]
762 fn test_valid_actuator_command() {
763 let cmd = ActuatorCommand {
764 command_id: "CMD-001".to_string(),
765 platform_id: "PORT-GATE-1".to_string(),
766 actuator_id: "gate-main".to_string(),
767 command_type: ActuatorCommandType::ActuatorCommandDisengage as i32,
768 target_position: 1.0,
769 target_velocity: 0.0,
770 priority: 1,
771 issued_by: "operator-1".to_string(),
772 issued_at: Some(Timestamp {
773 seconds: 1702000000,
774 nanos: 0,
775 }),
776 expires_at: None,
777 };
778 assert!(validate_actuator_command(&cmd).is_ok());
779 }
780
781 #[test]
782 fn test_command_missing_command_id() {
783 let cmd = ActuatorCommand {
784 command_id: String::new(),
785 platform_id: "PORT-GATE-1".to_string(),
786 actuator_id: "gate-main".to_string(),
787 command_type: ActuatorCommandType::ActuatorCommandDisengage as i32,
788 target_position: 1.0,
789 target_velocity: 0.0,
790 priority: 1,
791 issued_by: "operator-1".to_string(),
792 issued_at: Some(Timestamp {
793 seconds: 1702000000,
794 nanos: 0,
795 }),
796 expires_at: None,
797 };
798 let err = validate_actuator_command(&cmd).unwrap_err();
799 assert!(matches!(err, ValidationError::MissingField(f) if f == "command_id"));
800 }
801
802 #[test]
803 fn test_command_expires_before_issued() {
804 let cmd = ActuatorCommand {
805 command_id: "CMD-001".to_string(),
806 platform_id: "PORT-GATE-1".to_string(),
807 actuator_id: "gate-main".to_string(),
808 command_type: ActuatorCommandType::ActuatorCommandDisengage as i32,
809 target_position: 1.0,
810 target_velocity: 0.0,
811 priority: 1,
812 issued_by: "operator-1".to_string(),
813 issued_at: Some(Timestamp {
814 seconds: 1702000000,
815 nanos: 0,
816 }),
817 expires_at: Some(Timestamp {
818 seconds: 1701000000, nanos: 0,
820 }),
821 };
822 let err = validate_actuator_command(&cmd).unwrap_err();
823 assert!(matches!(err, ValidationError::ConstraintViolation(_)));
824 }
825}