task_packet.rs 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. use serde::{Deserialize, Serialize};
  2. use std::fmt::{Display, Formatter};
  3. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  4. pub struct TaskPacket {
  5. pub objective: String,
  6. pub scope: String,
  7. pub repo: String,
  8. pub branch_policy: String,
  9. pub acceptance_tests: Vec<String>,
  10. pub commit_policy: String,
  11. pub reporting_contract: String,
  12. pub escalation_policy: String,
  13. }
  14. #[derive(Debug, Clone, PartialEq, Eq)]
  15. pub struct TaskPacketValidationError {
  16. errors: Vec<String>,
  17. }
  18. impl TaskPacketValidationError {
  19. #[must_use]
  20. pub fn new(errors: Vec<String>) -> Self {
  21. Self { errors }
  22. }
  23. #[must_use]
  24. pub fn errors(&self) -> &[String] {
  25. &self.errors
  26. }
  27. }
  28. impl Display for TaskPacketValidationError {
  29. fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
  30. write!(f, "{}", self.errors.join("; "))
  31. }
  32. }
  33. impl std::error::Error for TaskPacketValidationError {}
  34. #[derive(Debug, Clone, PartialEq, Eq)]
  35. pub struct ValidatedPacket(TaskPacket);
  36. impl ValidatedPacket {
  37. #[must_use]
  38. pub fn packet(&self) -> &TaskPacket {
  39. &self.0
  40. }
  41. #[must_use]
  42. pub fn into_inner(self) -> TaskPacket {
  43. self.0
  44. }
  45. }
  46. pub fn validate_packet(packet: TaskPacket) -> Result<ValidatedPacket, TaskPacketValidationError> {
  47. let mut errors = Vec::new();
  48. validate_required("objective", &packet.objective, &mut errors);
  49. validate_required("scope", &packet.scope, &mut errors);
  50. validate_required("repo", &packet.repo, &mut errors);
  51. validate_required("branch_policy", &packet.branch_policy, &mut errors);
  52. validate_required("commit_policy", &packet.commit_policy, &mut errors);
  53. validate_required(
  54. "reporting_contract",
  55. &packet.reporting_contract,
  56. &mut errors,
  57. );
  58. validate_required(
  59. "escalation_policy",
  60. &packet.escalation_policy,
  61. &mut errors,
  62. );
  63. for (index, test) in packet.acceptance_tests.iter().enumerate() {
  64. if test.trim().is_empty() {
  65. errors.push(format!(
  66. "acceptance_tests contains an empty value at index {index}"
  67. ));
  68. }
  69. }
  70. if errors.is_empty() {
  71. Ok(ValidatedPacket(packet))
  72. } else {
  73. Err(TaskPacketValidationError::new(errors))
  74. }
  75. }
  76. fn validate_required(field: &str, value: &str, errors: &mut Vec<String>) {
  77. if value.trim().is_empty() {
  78. errors.push(format!("{field} must not be empty"));
  79. }
  80. }
  81. #[cfg(test)]
  82. mod tests {
  83. use super::*;
  84. fn sample_packet() -> TaskPacket {
  85. TaskPacket {
  86. objective: "Implement typed task packet format".to_string(),
  87. scope: "runtime/task system".to_string(),
  88. repo: "claw-code-parity".to_string(),
  89. branch_policy: "origin/main only".to_string(),
  90. acceptance_tests: vec![
  91. "cargo build --workspace".to_string(),
  92. "cargo test --workspace".to_string(),
  93. ],
  94. commit_policy: "single verified commit".to_string(),
  95. reporting_contract: "print build result, test result, commit sha".to_string(),
  96. escalation_policy: "stop only on destructive ambiguity".to_string(),
  97. }
  98. }
  99. #[test]
  100. fn valid_packet_passes_validation() {
  101. let packet = sample_packet();
  102. let validated = validate_packet(packet.clone()).expect("packet should validate");
  103. assert_eq!(validated.packet(), &packet);
  104. assert_eq!(validated.into_inner(), packet);
  105. }
  106. #[test]
  107. fn invalid_packet_accumulates_errors() {
  108. let packet = TaskPacket {
  109. objective: " ".to_string(),
  110. scope: String::new(),
  111. repo: String::new(),
  112. branch_policy: "\t".to_string(),
  113. acceptance_tests: vec!["ok".to_string(), " ".to_string()],
  114. commit_policy: String::new(),
  115. reporting_contract: String::new(),
  116. escalation_policy: String::new(),
  117. };
  118. let error = validate_packet(packet).expect_err("packet should be rejected");
  119. assert!(error.errors().len() >= 7);
  120. assert!(error
  121. .errors()
  122. .contains(&"objective must not be empty".to_string()));
  123. assert!(error
  124. .errors()
  125. .contains(&"scope must not be empty".to_string()));
  126. assert!(error
  127. .errors()
  128. .contains(&"repo must not be empty".to_string()));
  129. assert!(error.errors().contains(
  130. &"acceptance_tests contains an empty value at index 1".to_string()
  131. ));
  132. }
  133. #[test]
  134. fn serialization_roundtrip_preserves_packet() {
  135. let packet = sample_packet();
  136. let serialized = serde_json::to_string(&packet).expect("packet should serialize");
  137. let deserialized: TaskPacket =
  138. serde_json::from_str(&serialized).expect("packet should deserialize");
  139. assert_eq!(deserialized, packet);
  140. }
  141. }