Skip to main content

moloch_core/agent/
outcome.rs

1//! Outcome verification for agent actions.
2//!
3//! Outcome verification confirms that recorded actions actually occurred as described.
4//! It answers: "Did this action actually happen?"
5
6use std::collections::HashMap;
7
8use serde::{Deserialize, Serialize};
9
10use crate::crypto::{hash, Hash, PublicKey, SecretKey, Sig};
11use crate::error::{Error, Result};
12use crate::event::{EventId, ResourceId};
13
14use super::hitl::Severity;
15use super::principal::PrincipalId;
16
17/// Attestation that an action outcome occurred.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct OutcomeAttestation {
20    /// The action event being attested.
21    action_event_id: EventId,
22    /// What outcome occurred.
23    outcome: ActionOutcome,
24    /// Evidence supporting the outcome.
25    evidence: Vec<Evidence>,
26    /// Who is attesting to this outcome.
27    attestor: Attestor,
28    /// When the outcome was observed (Unix timestamp ms).
29    observed_at: i64,
30    /// Signature from attestor.
31    signature: Sig,
32}
33
34impl OutcomeAttestation {
35    /// Create a new outcome attestation builder.
36    pub fn builder() -> OutcomeAttestationBuilder {
37        OutcomeAttestationBuilder::new()
38    }
39
40    /// Get the action event ID.
41    pub fn action_event_id(&self) -> EventId {
42        self.action_event_id
43    }
44
45    /// Get the outcome.
46    pub fn outcome(&self) -> &ActionOutcome {
47        &self.outcome
48    }
49
50    /// Get the evidence.
51    pub fn evidence(&self) -> &[Evidence] {
52        &self.evidence
53    }
54
55    /// Get the attestor.
56    pub fn attestor(&self) -> &Attestor {
57        &self.attestor
58    }
59
60    /// Get the observation timestamp.
61    pub fn observed_at(&self) -> i64 {
62        self.observed_at
63    }
64
65    /// Get the signature.
66    pub fn signature(&self) -> &Sig {
67        &self.signature
68    }
69
70    /// Compute canonical bytes for signing/verification.
71    pub fn canonical_bytes(&self) -> Vec<u8> {
72        let mut data = Vec::new();
73        data.extend_from_slice(self.action_event_id.0.as_bytes());
74
75        let outcome_json = serde_json::to_vec(&self.outcome).unwrap_or_default();
76        data.extend_from_slice(&outcome_json);
77
78        for evidence in &self.evidence {
79            let evidence_json = serde_json::to_vec(evidence).unwrap_or_default();
80            data.extend_from_slice(&evidence_json);
81        }
82
83        let attestor_json = serde_json::to_vec(&self.attestor).unwrap_or_default();
84        data.extend_from_slice(&attestor_json);
85
86        data.extend_from_slice(&self.observed_at.to_le_bytes());
87
88        data
89    }
90
91    /// Verify the signature against an arbitrary public key.
92    ///
93    /// **Warning**: This does not check that `public_key` matches the embedded
94    /// [`Attestor`]. Prefer [`verify_against_attestor`](Self::verify_against_attestor)
95    /// which enforces signature-attestor binding.
96    pub fn verify_signature(&self, public_key: &PublicKey) -> Result<()> {
97        let message = self.canonical_bytes();
98        public_key.verify(&message, &self.signature)
99    }
100
101    /// Verify the signature against the attestor's embedded public key.
102    ///
103    /// Unlike [`verify_signature`](Self::verify_signature) which accepts an arbitrary key,
104    /// this method extracts the expected key from the [`Attestor`] and verifies against it,
105    /// ensuring the signature is cryptographically bound to the claimed attestor identity.
106    ///
107    /// Returns an error if:
108    /// - The attestor type does not carry a public key (e.g., `HumanObserver`, `CryptographicProof`)
109    /// - The signature does not verify against the attestor's key
110    pub fn verify_against_attestor(&self) -> Result<()> {
111        let public_key = self.attestor.public_key().ok_or_else(|| {
112            Error::invalid_input(
113                "attestor type does not carry a public key; \
114                 use external verification for HumanObserver/CryptographicProof",
115            )
116        })?;
117        self.verify_signature(public_key)
118    }
119
120    /// Check if evidence is sufficient for the given severity per rule 8.3.3.
121    pub fn is_evidence_sufficient(&self, severity: Severity) -> bool {
122        match severity {
123            Severity::Low => {
124                // Self-attestation is sufficient
125                true
126            }
127            Severity::Medium => {
128                // At least one piece of external evidence required
129                self.evidence.iter().any(|e| e.is_external())
130            }
131            Severity::High => {
132                // Multiple independent evidence sources
133                let external_count = self.evidence.iter().filter(|e| e.is_external()).count();
134                external_count >= 2
135            }
136            Severity::Critical => {
137                // Per Section 8.3.3: Critical severity requires independent
138                // third-party verification, not self-referential evidence.
139
140                // Cryptographic third-party attestation always suffices
141                let has_third_party = self
142                    .evidence
143                    .iter()
144                    .any(|e| matches!(e, Evidence::ThirdPartyAttestation { .. }));
145
146                // Human observer always suffices
147                let has_human = matches!(self.attestor, Attestor::HumanObserver { .. });
148
149                // Receipt alone is insufficient (could be self-referential),
150                // but receipt + additional external evidence = corroborated
151                let has_receipt = self
152                    .evidence
153                    .iter()
154                    .any(|e| matches!(e, Evidence::Receipt { .. }));
155                let external_count = self.evidence.iter().filter(|e| e.is_external()).count();
156                let has_corroborated_receipt = has_receipt && external_count >= 2;
157
158                has_third_party || has_human || has_corroborated_receipt
159            }
160        }
161    }
162
163    /// Check if this is a self-attestation.
164    pub fn is_self_attestation(&self) -> bool {
165        matches!(self.attestor, Attestor::SelfAttestation { .. })
166    }
167}
168
169/// Builder for OutcomeAttestation.
170#[derive(Debug, Default)]
171pub struct OutcomeAttestationBuilder {
172    action_event_id: Option<EventId>,
173    outcome: Option<ActionOutcome>,
174    evidence: Vec<Evidence>,
175    attestor: Option<Attestor>,
176    observed_at: Option<i64>,
177}
178
179impl OutcomeAttestationBuilder {
180    /// Create a new builder.
181    pub fn new() -> Self {
182        Self::default()
183    }
184
185    /// Set the action event ID.
186    pub fn action_event_id(mut self, id: EventId) -> Self {
187        self.action_event_id = Some(id);
188        self
189    }
190
191    /// Set the outcome.
192    pub fn outcome(mut self, outcome: ActionOutcome) -> Self {
193        self.outcome = Some(outcome);
194        self
195    }
196
197    /// Add evidence.
198    pub fn evidence(mut self, evidence: Evidence) -> Self {
199        self.evidence.push(evidence);
200        self
201    }
202
203    /// Add multiple pieces of evidence.
204    pub fn evidence_list(mut self, evidence: Vec<Evidence>) -> Self {
205        self.evidence = evidence;
206        self
207    }
208
209    /// Set the attestor.
210    pub fn attestor(mut self, attestor: Attestor) -> Self {
211        self.attestor = Some(attestor);
212        self
213    }
214
215    /// Set the observation timestamp.
216    pub fn observed_at(mut self, timestamp: i64) -> Self {
217        self.observed_at = Some(timestamp);
218        self
219    }
220
221    /// Set observation to now.
222    pub fn observed_now(mut self) -> Self {
223        self.observed_at = Some(chrono::Utc::now().timestamp_millis());
224        self
225    }
226
227    /// Sign and build the attestation.
228    pub fn sign(self, key: &SecretKey) -> Result<OutcomeAttestation> {
229        let action_event_id = self
230            .action_event_id
231            .ok_or_else(|| Error::invalid_input("action_event_id is required"))?;
232
233        let outcome = self
234            .outcome
235            .ok_or_else(|| Error::invalid_input("outcome is required"))?;
236
237        let attestor = self
238            .attestor
239            .ok_or_else(|| Error::invalid_input("attestor is required"))?;
240
241        let observed_at = self
242            .observed_at
243            .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
244
245        // Create unsigned attestation for signing
246        let mut attestation = OutcomeAttestation {
247            action_event_id,
248            outcome,
249            evidence: self.evidence,
250            attestor,
251            observed_at,
252            signature: Sig::empty(), // Placeholder
253        };
254
255        // Sign
256        let message = attestation.canonical_bytes();
257        attestation.signature = key.sign(&message);
258
259        Ok(attestation)
260    }
261}
262
263/// Detailed outcome of an action (for attestation purposes).
264#[derive(Debug, Clone, Serialize, Deserialize)]
265#[serde(tag = "status", rename_all = "snake_case")]
266pub enum ActionOutcome {
267    /// Action succeeded as expected.
268    Success {
269        /// Result data.
270        result: serde_json::Value,
271        /// Hash of the result for verification.
272        result_hash: Hash,
273    },
274    /// Action partially succeeded.
275    PartialSuccess {
276        /// What was completed.
277        completed: Vec<String>,
278        /// What failed.
279        failed: Vec<String>,
280        /// Partial result.
281        result: serde_json::Value,
282    },
283    /// Action failed.
284    Failure {
285        /// Error message.
286        error: String,
287        /// Error code if available.
288        error_code: Option<String>,
289        /// Whether the failure is recoverable.
290        recoverable: bool,
291    },
292    /// Outcome unknown or pending.
293    Pending {
294        /// Expected completion time (Unix timestamp ms).
295        expected_completion: Option<i64>,
296    },
297    /// Action was rolled back.
298    RolledBack {
299        /// Reason for rollback.
300        rollback_reason: String,
301        /// Event ID of the rollback action.
302        rollback_event_id: EventId,
303    },
304}
305
306impl ActionOutcome {
307    /// Create a success outcome.
308    pub fn success(result: serde_json::Value) -> Self {
309        let result_hash = hash(result.to_string().as_bytes());
310        Self::Success {
311            result,
312            result_hash,
313        }
314    }
315
316    /// Create a success outcome with explicit hash.
317    pub fn success_with_hash(result: serde_json::Value, result_hash: Hash) -> Self {
318        Self::Success {
319            result,
320            result_hash,
321        }
322    }
323
324    /// Create a partial success outcome.
325    pub fn partial_success(
326        completed: Vec<String>,
327        failed: Vec<String>,
328        result: serde_json::Value,
329    ) -> Self {
330        Self::PartialSuccess {
331            completed,
332            failed,
333            result,
334        }
335    }
336
337    /// Create a failure outcome.
338    pub fn failure(error: impl Into<String>, recoverable: bool) -> Self {
339        Self::Failure {
340            error: error.into(),
341            error_code: None,
342            recoverable,
343        }
344    }
345
346    /// Create a failure outcome with error code.
347    pub fn failure_with_code(
348        error: impl Into<String>,
349        error_code: impl Into<String>,
350        recoverable: bool,
351    ) -> Self {
352        Self::Failure {
353            error: error.into(),
354            error_code: Some(error_code.into()),
355            recoverable,
356        }
357    }
358
359    /// Create a pending outcome.
360    pub fn pending(expected_completion: Option<i64>) -> Self {
361        Self::Pending {
362            expected_completion,
363        }
364    }
365
366    /// Create a rolled back outcome.
367    pub fn rolled_back(reason: impl Into<String>, rollback_event_id: EventId) -> Self {
368        Self::RolledBack {
369            rollback_reason: reason.into(),
370            rollback_event_id,
371        }
372    }
373
374    /// Check if this is a successful outcome.
375    pub fn is_success(&self) -> bool {
376        matches!(self, ActionOutcome::Success { .. })
377    }
378
379    /// Check if this is a failure.
380    pub fn is_failure(&self) -> bool {
381        matches!(self, ActionOutcome::Failure { .. })
382    }
383
384    /// Check if this is pending.
385    pub fn is_pending(&self) -> bool {
386        matches!(self, ActionOutcome::Pending { .. })
387    }
388
389    /// Check if this outcome is final (not pending).
390    pub fn is_final(&self) -> bool {
391        !matches!(self, ActionOutcome::Pending { .. })
392    }
393
394    /// Check if a failed outcome is recoverable.
395    pub fn is_recoverable(&self) -> bool {
396        match self {
397            ActionOutcome::Failure { recoverable, .. } => *recoverable,
398            _ => false,
399        }
400    }
401}
402
403/// Evidence supporting an outcome attestation.
404#[derive(Debug, Clone, Serialize, Deserialize)]
405#[serde(tag = "type", rename_all = "snake_case")]
406pub enum Evidence {
407    /// Hash of data that was written.
408    DataHash {
409        /// Resource that was modified.
410        resource: ResourceId,
411        /// Hash of the data.
412        hash: Hash,
413        /// Size of the data in bytes.
414        size: u64,
415    },
416    /// External system confirmation.
417    ExternalConfirmation {
418        /// System that confirmed.
419        system: String,
420        /// Confirmation identifier.
421        confirmation_id: String,
422        /// When confirmed (Unix timestamp ms).
423        timestamp: i64,
424    },
425    /// Cryptographic receipt.
426    Receipt {
427        /// Who issued the receipt.
428        issuer: String,
429        /// Receipt data.
430        receipt: Vec<u8>,
431    },
432    /// Screenshot or visual evidence.
433    Visual {
434        /// Hash of the visual evidence.
435        hash: Hash,
436        /// Description of what the visual shows.
437        description: String,
438    },
439    /// Log entries.
440    LogEntries {
441        /// Source of the logs.
442        source: String,
443        /// Relevant log entries.
444        entries: Vec<String>,
445        /// Hash of the entries for integrity.
446        hash: Hash,
447    },
448    /// Third-party attestation.
449    ThirdPartyAttestation {
450        /// Public key of the attestor.
451        attestor: PublicKey,
452        /// Raw attestation data.
453        attestation: Vec<u8>,
454    },
455}
456
457impl Evidence {
458    /// Create data hash evidence.
459    pub fn data_hash(resource: ResourceId, hash: Hash, size: u64) -> Self {
460        Self::DataHash {
461            resource,
462            hash,
463            size,
464        }
465    }
466
467    /// Create external confirmation evidence.
468    pub fn external_confirmation(
469        system: impl Into<String>,
470        confirmation_id: impl Into<String>,
471        timestamp: i64,
472    ) -> Self {
473        Self::ExternalConfirmation {
474            system: system.into(),
475            confirmation_id: confirmation_id.into(),
476            timestamp,
477        }
478    }
479
480    /// Create receipt evidence.
481    pub fn receipt(issuer: impl Into<String>, receipt: Vec<u8>) -> Self {
482        Self::Receipt {
483            issuer: issuer.into(),
484            receipt,
485        }
486    }
487
488    /// Create visual evidence.
489    pub fn visual(hash: Hash, description: impl Into<String>) -> Self {
490        Self::Visual {
491            hash,
492            description: description.into(),
493        }
494    }
495
496    /// Create log entries evidence.
497    pub fn log_entries(source: impl Into<String>, entries: Vec<String>) -> Self {
498        let entries_json = serde_json::to_string(&entries).unwrap_or_default();
499        let hash = hash(entries_json.as_bytes());
500        Self::LogEntries {
501            source: source.into(),
502            entries,
503            hash,
504        }
505    }
506
507    /// Create third-party attestation evidence.
508    pub fn third_party_attestation(attestor: PublicKey, attestation: Vec<u8>) -> Self {
509        Self::ThirdPartyAttestation {
510            attestor,
511            attestation,
512        }
513    }
514
515    /// Check if this evidence is external (not self-generated).
516    pub fn is_external(&self) -> bool {
517        matches!(
518            self,
519            Evidence::ExternalConfirmation { .. }
520                | Evidence::Receipt { .. }
521                | Evidence::ThirdPartyAttestation { .. }
522        )
523    }
524}
525
526/// Who is attesting to the outcome.
527#[derive(Debug, Clone, Serialize, Deserialize)]
528#[serde(tag = "type", rename_all = "snake_case")]
529pub enum Attestor {
530    /// The agent that performed the action.
531    SelfAttestation {
532        /// Agent's public key.
533        agent: PublicKey,
534    },
535    /// The system that executed the action.
536    ExecutionSystem {
537        /// System identifier.
538        system_id: String,
539        /// System's public key.
540        system_key: PublicKey,
541    },
542    /// A monitoring system.
543    Monitor {
544        /// Monitor identifier.
545        monitor_id: String,
546        /// Monitor's public key.
547        monitor_key: PublicKey,
548    },
549    /// A human observer.
550    HumanObserver {
551        /// Principal ID of the human.
552        principal: PrincipalId,
553    },
554    /// Cryptographic proof (e.g., blockchain confirmation).
555    CryptographicProof {
556        /// Type of proof.
557        proof_type: String,
558    },
559}
560
561impl Attestor {
562    /// Create a self-attestation.
563    pub fn self_attestation(agent: PublicKey) -> Self {
564        Self::SelfAttestation { agent }
565    }
566
567    /// Create an execution system attestor.
568    pub fn execution_system(system_id: impl Into<String>, system_key: PublicKey) -> Self {
569        Self::ExecutionSystem {
570            system_id: system_id.into(),
571            system_key,
572        }
573    }
574
575    /// Create a monitor attestor.
576    pub fn monitor(monitor_id: impl Into<String>, monitor_key: PublicKey) -> Self {
577        Self::Monitor {
578            monitor_id: monitor_id.into(),
579            monitor_key,
580        }
581    }
582
583    /// Create a human observer attestor.
584    pub fn human_observer(principal: PrincipalId) -> Self {
585        Self::HumanObserver { principal }
586    }
587
588    /// Create a cryptographic proof attestor.
589    pub fn cryptographic_proof(proof_type: impl Into<String>) -> Self {
590        Self::CryptographicProof {
591            proof_type: proof_type.into(),
592        }
593    }
594
595    /// Get the public key of the attestor if available.
596    pub fn public_key(&self) -> Option<&PublicKey> {
597        match self {
598            Attestor::SelfAttestation { agent } => Some(agent),
599            Attestor::ExecutionSystem { system_key, .. } => Some(system_key),
600            Attestor::Monitor { monitor_key, .. } => Some(monitor_key),
601            Attestor::HumanObserver { .. } => None,
602            Attestor::CryptographicProof { .. } => None,
603        }
604    }
605}
606
607/// Unique idempotency key for actions.
608#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
609pub struct IdempotencyKey {
610    /// Agent that performed the action.
611    agent: PublicKey,
612    /// Action type.
613    action_type: String,
614    /// Unique client-provided key.
615    client_key: String,
616}
617
618impl IdempotencyKey {
619    /// Create a new idempotency key.
620    pub fn new(
621        agent: PublicKey,
622        action_type: impl Into<String>,
623        client_key: impl Into<String>,
624    ) -> Self {
625        Self {
626            agent,
627            action_type: action_type.into(),
628            client_key: client_key.into(),
629        }
630    }
631
632    /// Get the agent.
633    pub fn agent(&self) -> &PublicKey {
634        &self.agent
635    }
636
637    /// Get the action type.
638    pub fn action_type(&self) -> &str {
639        &self.action_type
640    }
641
642    /// Get the client key.
643    pub fn client_key(&self) -> &str {
644        &self.client_key
645    }
646
647    /// Compute a hash of this key for storage.
648    pub fn hash(&self) -> Hash {
649        let mut data = Vec::new();
650        data.extend_from_slice(&self.agent.as_bytes());
651        data.extend_from_slice(self.action_type.as_bytes());
652        data.extend_from_slice(self.client_key.as_bytes());
653        hash(&data)
654    }
655}
656
657impl std::fmt::Display for IdempotencyKey {
658    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
659        write!(
660            f,
661            "{}:{}:{}",
662            hex::encode(self.agent.as_bytes()),
663            self.action_type,
664            self.client_key
665        )
666    }
667}
668
669/// Record for ensuring action idempotency.
670#[derive(Debug, Clone, Serialize, Deserialize)]
671pub struct IdempotencyRecord {
672    /// Unique idempotency key.
673    key: IdempotencyKey,
674    /// The original action event.
675    original_event_id: EventId,
676    /// Original outcome.
677    outcome: ActionOutcome,
678    /// When this record was created (Unix timestamp ms).
679    created_at: i64,
680    /// When this record expires (Unix timestamp ms).
681    expires_at: i64,
682}
683
684impl IdempotencyRecord {
685    /// Create a new idempotency record.
686    pub fn new(
687        key: IdempotencyKey,
688        original_event_id: EventId,
689        outcome: ActionOutcome,
690        ttl_ms: i64,
691    ) -> Self {
692        let now = chrono::Utc::now().timestamp_millis();
693        Self {
694            key,
695            original_event_id,
696            outcome,
697            created_at: now,
698            expires_at: now + ttl_ms,
699        }
700    }
701
702    /// Get the key.
703    pub fn key(&self) -> &IdempotencyKey {
704        &self.key
705    }
706
707    /// Get the original event ID.
708    pub fn original_event_id(&self) -> EventId {
709        self.original_event_id
710    }
711
712    /// Get the outcome.
713    pub fn outcome(&self) -> &ActionOutcome {
714        &self.outcome
715    }
716
717    /// Get the creation timestamp.
718    pub fn created_at(&self) -> i64 {
719        self.created_at
720    }
721
722    /// Get the expiration timestamp.
723    pub fn expires_at(&self) -> i64 {
724        self.expires_at
725    }
726
727    /// Check if this record is expired.
728    pub fn is_expired(&self) -> bool {
729        chrono::Utc::now().timestamp_millis() > self.expires_at
730    }
731
732    /// Check if this record is still valid.
733    pub fn is_valid(&self) -> bool {
734        !self.is_expired()
735    }
736}
737
738/// Outcome dispute record per rule 8.3.5.
739#[derive(Debug, Clone, Serialize, Deserialize)]
740pub struct OutcomeDispute {
741    /// The attestation being disputed.
742    disputed_attestation_event_id: EventId,
743    /// Who is disputing.
744    disputant: Attestor,
745    /// Reason for the dispute.
746    reason: String,
747    /// Counter-evidence.
748    counter_evidence: Vec<Evidence>,
749    /// When the dispute was filed (Unix timestamp ms).
750    filed_at: i64,
751    /// Current status of the dispute.
752    status: DisputeStatus,
753}
754
755impl OutcomeDispute {
756    /// Create a new dispute.
757    pub fn new(
758        disputed_attestation_event_id: EventId,
759        disputant: Attestor,
760        reason: impl Into<String>,
761    ) -> Self {
762        Self {
763            disputed_attestation_event_id,
764            disputant,
765            reason: reason.into(),
766            counter_evidence: Vec::new(),
767            filed_at: chrono::Utc::now().timestamp_millis(),
768            status: DisputeStatus::Pending,
769        }
770    }
771
772    /// Add counter-evidence.
773    pub fn with_evidence(mut self, evidence: Evidence) -> Self {
774        self.counter_evidence.push(evidence);
775        self
776    }
777
778    /// Get the disputed attestation event ID.
779    pub fn disputed_attestation_event_id(&self) -> EventId {
780        self.disputed_attestation_event_id
781    }
782
783    /// Get the disputant.
784    pub fn disputant(&self) -> &Attestor {
785        &self.disputant
786    }
787
788    /// Get the reason.
789    pub fn reason(&self) -> &str {
790        &self.reason
791    }
792
793    /// Get the counter-evidence.
794    pub fn counter_evidence(&self) -> &[Evidence] {
795        &self.counter_evidence
796    }
797
798    /// Get when filed.
799    pub fn filed_at(&self) -> i64 {
800        self.filed_at
801    }
802
803    /// Get the status.
804    pub fn status(&self) -> &DisputeStatus {
805        &self.status
806    }
807
808    /// Update the status.
809    pub fn set_status(&mut self, status: DisputeStatus) {
810        self.status = status;
811    }
812}
813
814/// Status of an outcome dispute.
815#[derive(Debug, Clone, Serialize, Deserialize)]
816#[serde(tag = "status", rename_all = "snake_case")]
817pub enum DisputeStatus {
818    /// Dispute is pending review.
819    Pending,
820    /// Under human review.
821    UnderReview {
822        /// Who is reviewing.
823        reviewer: PrincipalId,
824        /// When review started.
825        started_at: i64,
826    },
827    /// Dispute resolved in favor of original attestation.
828    RejectedOriginalStands {
829        /// Resolution reason.
830        reason: String,
831        /// Resolution event ID.
832        resolution_event_id: EventId,
833    },
834    /// Dispute upheld - original attestation invalidated.
835    UpheldOriginalInvalidated {
836        /// Resolution reason.
837        reason: String,
838        /// Corrected outcome if any.
839        corrected_outcome: Option<Box<ActionOutcome>>,
840        /// Resolution event ID.
841        resolution_event_id: EventId,
842    },
843}
844
845impl DisputeStatus {
846    /// Check if the dispute is still pending.
847    pub fn is_pending(&self) -> bool {
848        matches!(
849            self,
850            DisputeStatus::Pending | DisputeStatus::UnderReview { .. }
851        )
852    }
853
854    /// Check if the dispute is resolved.
855    pub fn is_resolved(&self) -> bool {
856        !self.is_pending()
857    }
858}
859
860/// In-memory idempotency store with automatic expiration.
861///
862/// Provides insert, lookup, and cleanup for `IdempotencyRecord` values,
863/// keyed by the hash of the associated `IdempotencyKey`. Expired records
864/// are treated as invisible on lookup and can be removed via `cleanup()`.
865#[derive(Debug, Default)]
866pub struct IdempotencyStore {
867    records: HashMap<Hash, IdempotencyRecord>,
868}
869
870impl IdempotencyStore {
871    /// Create an empty store.
872    pub fn new() -> Self {
873        Self::default()
874    }
875
876    /// Insert a record. Overwrites any existing record with the same key hash.
877    pub fn insert(&mut self, record: IdempotencyRecord) {
878        let key_hash = record.key().hash();
879        self.records.insert(key_hash, record);
880    }
881
882    /// Look up a record by its idempotency key.
883    ///
884    /// Returns `None` if the key is unknown **or** the record has expired.
885    pub fn lookup(&self, key: &IdempotencyKey) -> Option<&IdempotencyRecord> {
886        self.records.get(&key.hash()).filter(|r| !r.is_expired())
887    }
888
889    /// Remove all expired records, returning the number removed.
890    pub fn cleanup(&mut self) -> usize {
891        let before = self.records.len();
892        self.records.retain(|_, r| !r.is_expired());
893        before - self.records.len()
894    }
895
896    /// Number of records (including expired ones not yet cleaned up).
897    pub fn len(&self) -> usize {
898        self.records.len()
899    }
900
901    /// Whether the store is empty.
902    pub fn is_empty(&self) -> bool {
903        self.records.is_empty()
904    }
905}
906
907#[cfg(test)]
908mod tests {
909    use super::*;
910    use crate::event::ResourceKind;
911
912    fn test_event_id() -> EventId {
913        EventId(hash(b"test-event"))
914    }
915
916    fn test_key() -> SecretKey {
917        SecretKey::generate()
918    }
919
920    fn test_resource_id() -> ResourceId {
921        ResourceId::new(ResourceKind::File, "/tmp/test.txt")
922    }
923
924    // === ActionOutcome Tests ===
925
926    #[test]
927    fn outcome_success() {
928        let outcome = ActionOutcome::success(serde_json::json!({"result": "ok"}));
929        assert!(outcome.is_success());
930        assert!(!outcome.is_failure());
931        assert!(outcome.is_final());
932    }
933
934    #[test]
935    fn outcome_failure() {
936        let outcome = ActionOutcome::failure("Something went wrong", true);
937        assert!(outcome.is_failure());
938        assert!(outcome.is_recoverable());
939
940        let non_recoverable = ActionOutcome::failure("Fatal error", false);
941        assert!(!non_recoverable.is_recoverable());
942    }
943
944    #[test]
945    fn outcome_partial_success() {
946        let outcome = ActionOutcome::partial_success(
947            vec!["step1".to_string(), "step2".to_string()],
948            vec!["step3".to_string()],
949            serde_json::json!({}),
950        );
951        assert!(!outcome.is_success());
952        assert!(!outcome.is_failure());
953    }
954
955    #[test]
956    fn outcome_pending() {
957        let outcome = ActionOutcome::pending(Some(chrono::Utc::now().timestamp_millis() + 60000));
958        assert!(outcome.is_pending());
959        assert!(!outcome.is_final());
960    }
961
962    #[test]
963    fn outcome_rolled_back() {
964        let outcome = ActionOutcome::rolled_back("User cancelled", test_event_id());
965        assert!(outcome.is_final());
966    }
967
968    // === Evidence Tests ===
969
970    #[test]
971    fn evidence_data_hash() {
972        let evidence = Evidence::data_hash(test_resource_id(), hash(b"data"), 1024);
973        assert!(!evidence.is_external());
974    }
975
976    #[test]
977    fn evidence_external_confirmation() {
978        let evidence = Evidence::external_confirmation("github", "pr-123", 1000);
979        assert!(evidence.is_external());
980    }
981
982    #[test]
983    fn evidence_receipt() {
984        let evidence = Evidence::receipt("blockchain", vec![1, 2, 3, 4]);
985        assert!(evidence.is_external());
986    }
987
988    #[test]
989    fn evidence_visual() {
990        let evidence = Evidence::visual(hash(b"screenshot"), "Shows successful deployment");
991        assert!(!evidence.is_external());
992    }
993
994    #[test]
995    fn evidence_log_entries() {
996        let evidence = Evidence::log_entries("server.log", vec!["INFO: Started".to_string()]);
997        assert!(!evidence.is_external());
998    }
999
1000    #[test]
1001    fn evidence_third_party() {
1002        let key = test_key();
1003        let evidence = Evidence::third_party_attestation(key.public_key(), vec![1, 2, 3]);
1004        assert!(evidence.is_external());
1005    }
1006
1007    // === Attestor Tests ===
1008
1009    #[test]
1010    fn attestor_self() {
1011        let key = test_key();
1012        let attestor = Attestor::self_attestation(key.public_key());
1013        assert!(attestor.public_key().is_some());
1014    }
1015
1016    #[test]
1017    fn attestor_execution_system() {
1018        let key = test_key();
1019        let attestor = Attestor::execution_system("docker-runtime", key.public_key());
1020        assert!(attestor.public_key().is_some());
1021    }
1022
1023    #[test]
1024    fn attestor_human() {
1025        let principal = PrincipalId::user("user@example.com").unwrap();
1026        let attestor = Attestor::human_observer(principal);
1027        assert!(attestor.public_key().is_none());
1028    }
1029
1030    // === OutcomeAttestation Tests ===
1031
1032    #[test]
1033    fn attestation_build_and_sign() {
1034        let key = test_key();
1035        let attestation = OutcomeAttestation::builder()
1036            .action_event_id(test_event_id())
1037            .outcome(ActionOutcome::success(serde_json::json!({})))
1038            .attestor(Attestor::self_attestation(key.public_key()))
1039            .observed_now()
1040            .sign(&key)
1041            .unwrap();
1042
1043        assert!(attestation.verify_signature(&key.public_key()).is_ok());
1044    }
1045
1046    #[test]
1047    fn attestation_requires_action_event_id() {
1048        let key = test_key();
1049        let result = OutcomeAttestation::builder()
1050            .outcome(ActionOutcome::success(serde_json::json!({})))
1051            .attestor(Attestor::self_attestation(key.public_key()))
1052            .sign(&key);
1053        assert!(result.is_err());
1054    }
1055
1056    #[test]
1057    fn attestation_requires_outcome() {
1058        let key = test_key();
1059        let result = OutcomeAttestation::builder()
1060            .action_event_id(test_event_id())
1061            .attestor(Attestor::self_attestation(key.public_key()))
1062            .sign(&key);
1063        assert!(result.is_err());
1064    }
1065
1066    #[test]
1067    fn attestation_requires_attestor() {
1068        let key = test_key();
1069        let result = OutcomeAttestation::builder()
1070            .action_event_id(test_event_id())
1071            .outcome(ActionOutcome::success(serde_json::json!({})))
1072            .sign(&key);
1073        assert!(result.is_err());
1074    }
1075
1076    // === Evidence Sufficiency Tests ===
1077
1078    #[test]
1079    fn evidence_sufficiency_low() {
1080        let key = test_key();
1081        let attestation = OutcomeAttestation::builder()
1082            .action_event_id(test_event_id())
1083            .outcome(ActionOutcome::success(serde_json::json!({})))
1084            .attestor(Attestor::self_attestation(key.public_key()))
1085            .sign(&key)
1086            .unwrap();
1087
1088        // Low severity: self-attestation is sufficient
1089        assert!(attestation.is_evidence_sufficient(Severity::Low));
1090    }
1091
1092    #[test]
1093    fn evidence_sufficiency_medium() {
1094        let key = test_key();
1095
1096        // Without external evidence
1097        let attestation = OutcomeAttestation::builder()
1098            .action_event_id(test_event_id())
1099            .outcome(ActionOutcome::success(serde_json::json!({})))
1100            .attestor(Attestor::self_attestation(key.public_key()))
1101            .sign(&key)
1102            .unwrap();
1103        assert!(!attestation.is_evidence_sufficient(Severity::Medium));
1104
1105        // With external evidence
1106        let attestation = OutcomeAttestation::builder()
1107            .action_event_id(test_event_id())
1108            .outcome(ActionOutcome::success(serde_json::json!({})))
1109            .attestor(Attestor::self_attestation(key.public_key()))
1110            .evidence(Evidence::external_confirmation("ci", "build-123", 1000))
1111            .sign(&key)
1112            .unwrap();
1113        assert!(attestation.is_evidence_sufficient(Severity::Medium));
1114    }
1115
1116    #[test]
1117    fn evidence_sufficiency_high() {
1118        let key = test_key();
1119
1120        // With only one external evidence
1121        let attestation = OutcomeAttestation::builder()
1122            .action_event_id(test_event_id())
1123            .outcome(ActionOutcome::success(serde_json::json!({})))
1124            .attestor(Attestor::self_attestation(key.public_key()))
1125            .evidence(Evidence::external_confirmation("ci", "build-123", 1000))
1126            .sign(&key)
1127            .unwrap();
1128        assert!(!attestation.is_evidence_sufficient(Severity::High));
1129
1130        // With two external evidence sources
1131        let attestation = OutcomeAttestation::builder()
1132            .action_event_id(test_event_id())
1133            .outcome(ActionOutcome::success(serde_json::json!({})))
1134            .attestor(Attestor::self_attestation(key.public_key()))
1135            .evidence(Evidence::external_confirmation("ci", "build-123", 1000))
1136            .evidence(Evidence::receipt("notary", vec![1, 2, 3]))
1137            .sign(&key)
1138            .unwrap();
1139        assert!(attestation.is_evidence_sufficient(Severity::High));
1140    }
1141
1142    #[test]
1143    fn evidence_sufficiency_critical() {
1144        let key = test_key();
1145
1146        // Without cryptographic proof or human verification
1147        let attestation = OutcomeAttestation::builder()
1148            .action_event_id(test_event_id())
1149            .outcome(ActionOutcome::success(serde_json::json!({})))
1150            .attestor(Attestor::self_attestation(key.public_key()))
1151            .evidence(Evidence::external_confirmation("ci", "build-123", 1000))
1152            .sign(&key)
1153            .unwrap();
1154        assert!(!attestation.is_evidence_sufficient(Severity::Critical));
1155
1156        // With third-party attestation
1157        let attestation = OutcomeAttestation::builder()
1158            .action_event_id(test_event_id())
1159            .outcome(ActionOutcome::success(serde_json::json!({})))
1160            .attestor(Attestor::self_attestation(key.public_key()))
1161            .evidence(Evidence::third_party_attestation(
1162                key.public_key(),
1163                vec![1, 2, 3],
1164            ))
1165            .sign(&key)
1166            .unwrap();
1167        assert!(attestation.is_evidence_sufficient(Severity::Critical));
1168
1169        // With human observer
1170        let principal = PrincipalId::user("admin@example.com").unwrap();
1171        let attestation = OutcomeAttestation::builder()
1172            .action_event_id(test_event_id())
1173            .outcome(ActionOutcome::success(serde_json::json!({})))
1174            .attestor(Attestor::human_observer(principal))
1175            .sign(&key)
1176            .unwrap();
1177        assert!(attestation.is_evidence_sufficient(Severity::Critical));
1178    }
1179
1180    // === verify_against_attestor Tests (Phase 1, Finding 1.1) ===
1181
1182    #[test]
1183    fn verify_against_attestor_rejects_key_mismatch() {
1184        // OutcomeAttestation signed by real_key but claiming attestor with fake_key
1185        // must fail verify_against_attestor()
1186        let real_key = test_key();
1187        let fake_key = test_key();
1188
1189        let attestation = OutcomeAttestation::builder()
1190            .action_event_id(test_event_id())
1191            .outcome(ActionOutcome::success(serde_json::json!({})))
1192            .attestor(Attestor::self_attestation(fake_key.public_key())) // Claims fake
1193            .observed_now()
1194            .sign(&real_key) // Signed by real
1195            .unwrap();
1196
1197        // Raw verify with real_key passes — this is the vulnerability
1198        assert!(attestation.verify_signature(&real_key.public_key()).is_ok());
1199
1200        // verify_against_attestor must reject: attestor says fake_key, sig is real_key
1201        assert!(attestation.verify_against_attestor().is_err());
1202    }
1203
1204    #[test]
1205    fn verify_against_attestor_accepts_matching_key() {
1206        let key = test_key();
1207
1208        let attestation = OutcomeAttestation::builder()
1209            .action_event_id(test_event_id())
1210            .outcome(ActionOutcome::success(serde_json::json!({})))
1211            .attestor(Attestor::self_attestation(key.public_key()))
1212            .observed_now()
1213            .sign(&key)
1214            .unwrap();
1215
1216        assert!(attestation.verify_against_attestor().is_ok());
1217    }
1218
1219    #[test]
1220    fn verify_against_attestor_for_human_observer_returns_error() {
1221        // HumanObserver has no public key, so attestor-based verify
1222        // must return an appropriate error (not silently pass)
1223        let key = test_key();
1224        let principal = PrincipalId::user("admin@example.com").unwrap();
1225
1226        let attestation = OutcomeAttestation::builder()
1227            .action_event_id(test_event_id())
1228            .outcome(ActionOutcome::success(serde_json::json!({})))
1229            .attestor(Attestor::human_observer(principal))
1230            .observed_now()
1231            .sign(&key)
1232            .unwrap();
1233
1234        let result = attestation.verify_against_attestor();
1235        assert!(result.is_err());
1236    }
1237
1238    #[test]
1239    fn verify_against_attestor_for_execution_system() {
1240        let system_key = test_key();
1241
1242        let attestation = OutcomeAttestation::builder()
1243            .action_event_id(test_event_id())
1244            .outcome(ActionOutcome::success(serde_json::json!({})))
1245            .attestor(Attestor::execution_system(
1246                "docker",
1247                system_key.public_key(),
1248            ))
1249            .observed_now()
1250            .sign(&system_key)
1251            .unwrap();
1252
1253        assert!(attestation.verify_against_attestor().is_ok());
1254    }
1255
1256    #[test]
1257    fn verify_against_attestor_for_monitor() {
1258        let monitor_key = test_key();
1259
1260        let attestation = OutcomeAttestation::builder()
1261            .action_event_id(test_event_id())
1262            .outcome(ActionOutcome::success(serde_json::json!({})))
1263            .attestor(Attestor::monitor("prometheus", monitor_key.public_key()))
1264            .observed_now()
1265            .sign(&monitor_key)
1266            .unwrap();
1267
1268        assert!(attestation.verify_against_attestor().is_ok());
1269    }
1270
1271    #[test]
1272    fn verify_against_attestor_for_cryptographic_proof_returns_error() {
1273        // CryptographicProof has no public key, similar to HumanObserver
1274        let key = test_key();
1275
1276        let attestation = OutcomeAttestation::builder()
1277            .action_event_id(test_event_id())
1278            .outcome(ActionOutcome::success(serde_json::json!({})))
1279            .attestor(Attestor::cryptographic_proof("blockchain-anchor"))
1280            .observed_now()
1281            .sign(&key)
1282            .unwrap();
1283
1284        assert!(attestation.verify_against_attestor().is_err());
1285    }
1286
1287    // === IdempotencyKey Tests ===
1288
1289    #[test]
1290    fn idempotency_key_hash() {
1291        let key = test_key();
1292        let idem_key1 = IdempotencyKey::new(key.public_key(), "file_write", "request-123");
1293        let idem_key2 = IdempotencyKey::new(key.public_key(), "file_write", "request-123");
1294        assert_eq!(idem_key1.hash(), idem_key2.hash());
1295
1296        let idem_key3 = IdempotencyKey::new(key.public_key(), "file_write", "request-456");
1297        assert_ne!(idem_key1.hash(), idem_key3.hash());
1298    }
1299
1300    #[test]
1301    fn idempotency_key_display() {
1302        let key = test_key();
1303        let idem_key = IdempotencyKey::new(key.public_key(), "file_write", "request-123");
1304        let display = format!("{}", idem_key);
1305        assert!(display.contains("file_write"));
1306        assert!(display.contains("request-123"));
1307    }
1308
1309    // === IdempotencyRecord Tests ===
1310
1311    #[test]
1312    fn idempotency_record_valid() {
1313        let key = test_key();
1314        let idem_key = IdempotencyKey::new(key.public_key(), "file_write", "request-123");
1315        let record = IdempotencyRecord::new(
1316            idem_key,
1317            test_event_id(),
1318            ActionOutcome::success(serde_json::json!({})),
1319            60000, // 1 minute TTL
1320        );
1321
1322        assert!(record.is_valid());
1323        assert!(!record.is_expired());
1324    }
1325
1326    #[test]
1327    fn idempotency_record_expired() {
1328        let key = test_key();
1329        let idem_key = IdempotencyKey::new(key.public_key(), "file_write", "request-123");
1330        let record = IdempotencyRecord::new(
1331            idem_key,
1332            test_event_id(),
1333            ActionOutcome::success(serde_json::json!({})),
1334            -1, // Already expired
1335        );
1336
1337        assert!(!record.is_valid());
1338        assert!(record.is_expired());
1339    }
1340
1341    // === IdempotencyStore Tests ===
1342
1343    #[test]
1344    fn idempotency_store_insert_and_lookup() {
1345        let mut store = IdempotencyStore::new();
1346        let key = test_key();
1347        let idem_key = IdempotencyKey::new(key.public_key(), "write", "req-1");
1348        let record = IdempotencyRecord::new(
1349            idem_key.clone(),
1350            test_event_id(),
1351            ActionOutcome::success(serde_json::json!({})),
1352            60000,
1353        );
1354
1355        let expected_event_id = record.original_event_id();
1356        store.insert(record);
1357        let found = store.lookup(&idem_key);
1358        assert!(found.is_some());
1359        assert_eq!(found.unwrap().original_event_id(), expected_event_id);
1360    }
1361
1362    #[test]
1363    fn idempotency_store_returns_none_for_unknown() {
1364        let store = IdempotencyStore::new();
1365        let key = test_key();
1366        let idem_key = IdempotencyKey::new(key.public_key(), "write", "unknown");
1367        assert!(store.lookup(&idem_key).is_none());
1368    }
1369
1370    #[test]
1371    fn idempotency_store_expired_records_not_returned() {
1372        let mut store = IdempotencyStore::new();
1373        let key = test_key();
1374        let idem_key = IdempotencyKey::new(key.public_key(), "write", "req-1");
1375        let record = IdempotencyRecord::new(
1376            idem_key.clone(),
1377            test_event_id(),
1378            ActionOutcome::success(serde_json::json!({})),
1379            -1, // Already expired
1380        );
1381
1382        store.insert(record);
1383        assert!(store.lookup(&idem_key).is_none()); // Expired = invisible
1384    }
1385
1386    #[test]
1387    fn idempotency_store_cleanup_removes_expired() {
1388        let mut store = IdempotencyStore::new();
1389        let key = test_key();
1390
1391        // Insert 3 expired records
1392        for i in 0..3 {
1393            let idem_key = IdempotencyKey::new(key.public_key(), "write", format!("expired-{}", i));
1394            store.insert(IdempotencyRecord::new(
1395                idem_key,
1396                test_event_id(),
1397                ActionOutcome::success(serde_json::json!({})),
1398                -1, // Already expired
1399            ));
1400        }
1401
1402        // Insert 2 valid records
1403        for i in 0..2 {
1404            let idem_key = IdempotencyKey::new(key.public_key(), "write", format!("valid-{}", i));
1405            store.insert(IdempotencyRecord::new(
1406                idem_key,
1407                test_event_id(),
1408                ActionOutcome::success(serde_json::json!({})),
1409                60000, // 1 minute TTL
1410            ));
1411        }
1412
1413        assert_eq!(store.len(), 5);
1414        let removed = store.cleanup();
1415        assert_eq!(removed, 3);
1416        assert_eq!(store.len(), 2);
1417    }
1418
1419    #[test]
1420    fn idempotency_store_overwrite_existing() {
1421        let mut store = IdempotencyStore::new();
1422        let key = test_key();
1423        let idem_key = IdempotencyKey::new(key.public_key(), "write", "req-1");
1424
1425        let record1 = IdempotencyRecord::new(
1426            idem_key.clone(),
1427            test_event_id(),
1428            ActionOutcome::success(serde_json::json!({"version": 1})),
1429            60000,
1430        );
1431        store.insert(record1);
1432
1433        let event2 = EventId(hash(b"second-event"));
1434        let record2 = IdempotencyRecord::new(
1435            idem_key.clone(),
1436            event2,
1437            ActionOutcome::success(serde_json::json!({"version": 2})),
1438            60000,
1439        );
1440        store.insert(record2);
1441
1442        assert_eq!(store.len(), 1);
1443        let found = store.lookup(&idem_key).unwrap();
1444        assert_eq!(found.original_event_id(), event2);
1445    }
1446
1447    #[test]
1448    fn idempotency_store_is_empty() {
1449        let store = IdempotencyStore::new();
1450        assert!(store.is_empty());
1451        assert_eq!(store.len(), 0);
1452    }
1453
1454    // === OutcomeDispute Tests ===
1455
1456    #[test]
1457    fn dispute_creation() {
1458        let key = test_key();
1459        let dispute = OutcomeDispute::new(
1460            test_event_id(),
1461            Attestor::self_attestation(key.public_key()),
1462            "Outcome was not as described",
1463        );
1464
1465        assert!(dispute.status().is_pending());
1466        assert!(!dispute.status().is_resolved());
1467    }
1468
1469    #[test]
1470    fn dispute_with_evidence() {
1471        let key = test_key();
1472        let dispute = OutcomeDispute::new(
1473            test_event_id(),
1474            Attestor::self_attestation(key.public_key()),
1475            "Incorrect outcome",
1476        )
1477        .with_evidence(Evidence::log_entries(
1478            "server.log",
1479            vec!["ERROR: Failed".to_string()],
1480        ));
1481
1482        assert_eq!(dispute.counter_evidence().len(), 1);
1483    }
1484
1485    #[test]
1486    fn dispute_status_transitions() {
1487        let principal = PrincipalId::user("reviewer@example.com").unwrap();
1488
1489        let pending = DisputeStatus::Pending;
1490        assert!(pending.is_pending());
1491
1492        let under_review = DisputeStatus::UnderReview {
1493            reviewer: principal.clone(),
1494            started_at: chrono::Utc::now().timestamp_millis(),
1495        };
1496        assert!(under_review.is_pending());
1497
1498        let rejected = DisputeStatus::RejectedOriginalStands {
1499            reason: "Evidence insufficient".to_string(),
1500            resolution_event_id: test_event_id(),
1501        };
1502        assert!(rejected.is_resolved());
1503
1504        let upheld = DisputeStatus::UpheldOriginalInvalidated {
1505            reason: "Clear evidence of error".to_string(),
1506            corrected_outcome: Some(Box::new(ActionOutcome::failure("Actual failure", false))),
1507            resolution_event_id: test_event_id(),
1508        };
1509        assert!(upheld.is_resolved());
1510    }
1511
1512    // === Evidence Classification Tests (Finding 4.1) ===
1513
1514    #[test]
1515    fn evidence_receipt_alone_insufficient_for_critical() {
1516        let key = test_key();
1517        let attestation = OutcomeAttestation::builder()
1518            .action_event_id(test_event_id())
1519            .outcome(ActionOutcome::success(serde_json::json!({})))
1520            .attestor(Attestor::self_attestation(key.public_key()))
1521            .evidence(Evidence::receipt("self-system", vec![1, 2, 3]))
1522            .sign(&key)
1523            .unwrap();
1524
1525        assert!(!attestation.is_evidence_sufficient(Severity::Critical));
1526    }
1527
1528    #[test]
1529    fn evidence_third_party_attestation_satisfies_critical() {
1530        let key = test_key();
1531        let third_party_key = SecretKey::generate();
1532        let attestation = OutcomeAttestation::builder()
1533            .action_event_id(test_event_id())
1534            .outcome(ActionOutcome::success(serde_json::json!({})))
1535            .attestor(Attestor::self_attestation(key.public_key()))
1536            .evidence(Evidence::third_party_attestation(
1537                third_party_key.public_key(),
1538                vec![1, 2, 3],
1539            ))
1540            .sign(&key)
1541            .unwrap();
1542
1543        assert!(attestation.is_evidence_sufficient(Severity::Critical));
1544    }
1545
1546    #[test]
1547    fn evidence_human_observer_satisfies_critical() {
1548        let key = test_key();
1549        let principal = PrincipalId::user("admin@example.com").unwrap();
1550        let attestation = OutcomeAttestation::builder()
1551            .action_event_id(test_event_id())
1552            .outcome(ActionOutcome::success(serde_json::json!({})))
1553            .attestor(Attestor::human_observer(principal))
1554            .sign(&key)
1555            .unwrap();
1556
1557        assert!(attestation.is_evidence_sufficient(Severity::Critical));
1558    }
1559
1560    #[test]
1561    fn evidence_receipt_plus_external_confirmation_satisfies_critical() {
1562        let key = test_key();
1563        let attestation = OutcomeAttestation::builder()
1564            .action_event_id(test_event_id())
1565            .outcome(ActionOutcome::success(serde_json::json!({})))
1566            .attestor(Attestor::self_attestation(key.public_key()))
1567            .evidence(Evidence::receipt("notary-service", vec![1, 2, 3]))
1568            .evidence(Evidence::external_confirmation(
1569                "monitoring",
1570                "check-456",
1571                chrono::Utc::now().timestamp_millis(),
1572            ))
1573            .sign(&key)
1574            .unwrap();
1575
1576        assert!(attestation.is_evidence_sufficient(Severity::Critical));
1577    }
1578
1579    #[test]
1580    fn evidence_external_confirmation_alone_insufficient_for_critical() {
1581        let key = test_key();
1582        let attestation = OutcomeAttestation::builder()
1583            .action_event_id(test_event_id())
1584            .outcome(ActionOutcome::success(serde_json::json!({})))
1585            .attestor(Attestor::self_attestation(key.public_key()))
1586            .evidence(Evidence::external_confirmation(
1587                "monitoring",
1588                "check-789",
1589                chrono::Utc::now().timestamp_millis(),
1590            ))
1591            .sign(&key)
1592            .unwrap();
1593
1594        // Single external confirmation without third-party attestation
1595        // or receipt corroboration is insufficient for Critical
1596        assert!(!attestation.is_evidence_sufficient(Severity::Critical));
1597    }
1598
1599    #[test]
1600    fn evidence_no_evidence_insufficient_for_critical() {
1601        let key = test_key();
1602        let attestation = OutcomeAttestation::builder()
1603            .action_event_id(test_event_id())
1604            .outcome(ActionOutcome::success(serde_json::json!({})))
1605            .attestor(Attestor::self_attestation(key.public_key()))
1606            .sign(&key)
1607            .unwrap();
1608
1609        assert!(!attestation.is_evidence_sufficient(Severity::Critical));
1610    }
1611
1612    #[test]
1613    fn evidence_low_severity_always_sufficient() {
1614        let key = test_key();
1615        let attestation = OutcomeAttestation::builder()
1616            .action_event_id(test_event_id())
1617            .outcome(ActionOutcome::success(serde_json::json!({})))
1618            .attestor(Attestor::self_attestation(key.public_key()))
1619            .sign(&key)
1620            .unwrap();
1621
1622        // Self-attestation with no evidence is sufficient for Low
1623        assert!(attestation.is_evidence_sufficient(Severity::Low));
1624    }
1625
1626    #[test]
1627    fn evidence_medium_requires_external() {
1628        let key = test_key();
1629
1630        // No evidence -> insufficient for Medium
1631        let attestation = OutcomeAttestation::builder()
1632            .action_event_id(test_event_id())
1633            .outcome(ActionOutcome::success(serde_json::json!({})))
1634            .attestor(Attestor::self_attestation(key.public_key()))
1635            .sign(&key)
1636            .unwrap();
1637        assert!(!attestation.is_evidence_sufficient(Severity::Medium));
1638
1639        // With external evidence -> sufficient for Medium
1640        let attestation2 = OutcomeAttestation::builder()
1641            .action_event_id(test_event_id())
1642            .outcome(ActionOutcome::success(serde_json::json!({})))
1643            .attestor(Attestor::self_attestation(key.public_key()))
1644            .evidence(Evidence::external_confirmation(
1645                "ci",
1646                "run-123",
1647                chrono::Utc::now().timestamp_millis(),
1648            ))
1649            .sign(&key)
1650            .unwrap();
1651        assert!(attestation2.is_evidence_sufficient(Severity::Medium));
1652    }
1653}