pointbreak 0.6.0

Durable terminal code review for changes humans and coding agents collaborate on together
Documentation
use serde::{Deserialize, Serialize};

use super::kind::EventType;
use super::payload::{BodyContentType, EventPayload};
use super::type_code::type_code;
use crate::model::{
    RevisionId, TrackId, ValidationCheckId, ValidationStatus, ValidationTarget, ValidationTrigger,
};

#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ValidationCheckRecordedPayload {
    pub validation_check_id: ValidationCheckId,
    pub target: ValidationTarget,
    pub check_name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub command: Option<String>,
    pub status: ValidationStatus,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub exit_code: Option<i64>,
    pub trigger: ValidationTrigger,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source_fingerprint: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub summary: Option<String>,
    #[serde(default, skip_serializing_if = "BodyContentType::is_text_plain")]
    pub summary_content_type: BodyContentType,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub summary_artifact_path: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub summary_byte_size: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub summary_content_hash: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub started_at: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub completed_at: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub log_artifact_content_hashes: Vec<String>,
}

impl ValidationCheckRecordedPayload {
    pub fn idempotency_key(
        revision_id: &RevisionId,
        track_id: &TrackId,
        source_key: &str,
    ) -> String {
        format!(
            "{}:{}:{}:{}",
            type_code(EventType::ValidationCheckRecorded),
            revision_id.as_str(),
            track_id.as_str(),
            source_key
        )
    }
}

impl EventPayload for ValidationCheckRecordedPayload {
    fn event_type(&self) -> EventType {
        EventType::ValidationCheckRecorded
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::model::{
        RevisionId, TrackId, ValidationCheckId, ValidationStatus, ValidationTarget,
        ValidationTrigger,
    };

    fn fixture_payload() -> ValidationCheckRecordedPayload {
        ValidationCheckRecordedPayload {
            validation_check_id: ValidationCheckId::new("validation:sha256:ghi"),
            target: ValidationTarget::Revision {
                revision_id: RevisionId::new("review-unit:sha256:def"),
            },
            check_name: "cargo test".to_owned(),
            command: None,
            status: ValidationStatus::Passed,
            exit_code: Some(0),
            trigger: ValidationTrigger::Manual,
            source_fingerprint: None,
            summary: None,
            summary_content_type: Default::default(),
            summary_artifact_path: None,
            summary_byte_size: None,
            summary_content_hash: None,
            started_at: None,
            completed_at: None,
            log_artifact_content_hashes: vec![],
        }
    }

    #[test]
    fn validation_check_recorded_payload_round_trips_and_uses_expected_wire_keys() {
        let payload = fixture_payload();

        let value = serde_json::to_value(&payload).unwrap();
        assert!(value.get("validationCheckId").is_some());
        assert!(value.get("checkName").is_some());
        assert_eq!(value["status"], "passed");
        assert_eq!(value["trigger"], "manual");
        assert!(value.get("logArtifactContentHashes").is_none());
        assert!(value.get("summary").is_none());

        let back: ValidationCheckRecordedPayload = serde_json::from_value(value).unwrap();
        assert_eq!(back, payload);
    }

    #[test]
    fn validation_idempotency_key_is_revision_track_and_source_scoped() {
        let key = ValidationCheckRecordedPayload::idempotency_key(
            &RevisionId::new("review-unit:sha256:def"),
            &TrackId::new("agent:codex"),
            "validation:sha256:ghi",
        );

        assert_eq!(
            key,
            format!(
                "{}:review-unit:sha256:def:agent:codex:validation:sha256:ghi",
                type_code(EventType::ValidationCheckRecorded)
            )
        );
    }

    #[test]
    fn validation_event_payload_and_target_are_path_free() {
        let payload = fixture_payload();
        let key = ValidationCheckRecordedPayload::idempotency_key(
            &RevisionId::new("review-unit:sha256:def"),
            &TrackId::new("agent:codex"),
            payload.validation_check_id.as_str(),
        );
        let serialized = serde_json::to_string(&payload).unwrap();

        for needle in ["/Users/", "worktreeRoot", ".git", ".shore/data"] {
            assert!(
                !serialized.contains(needle),
                "payload leaked path token {needle}"
            );
            assert!(
                !key.contains(needle),
                "idempotency key leaked path token {needle}"
            );
        }
    }
}