Skip to main content

aivcs_core/
deploy_runner.rs

1//! Deploy-by-digest reference runner.
2//!
3//! Runs an agent by `AgentSpec` digest through `RunLedger` and emits a minimal,
4//! deterministic event sequence for replay/golden validation.
5
6use chrono::{DateTime, Utc};
7use oxidized_state::{
8    ContentDigest, RunEvent, RunId, RunLedger, RunMetadata, RunSummary, StorageError,
9};
10use std::time::Instant;
11
12use crate::domain::{AivcsError, Result};
13
14/// Output of a deploy-by-digest run.
15#[derive(Debug, Clone)]
16pub struct DeployRunOutput {
17    pub run_id: RunId,
18    pub emitted_events: usize,
19}
20
21/// Reference runner for deploy-by-digest execution.
22pub struct DeployByDigestRunner;
23
24impl DeployByDigestRunner {
25    /// Run deploy-by-digest using current UTC time for event timestamps.
26    pub async fn run(
27        ledger: &dyn RunLedger,
28        spec_digest: &ContentDigest,
29        agent_name: &str,
30    ) -> Result<DeployRunOutput> {
31        let now = Utc::now();
32        Self::run_at(ledger, spec_digest, agent_name, now).await
33    }
34
35    /// Run deploy-by-digest at a fixed timestamp (used for deterministic tests).
36    pub async fn run_at(
37        ledger: &dyn RunLedger,
38        spec_digest: &ContentDigest,
39        agent_name: &str,
40        timestamp: DateTime<Utc>,
41    ) -> Result<DeployRunOutput> {
42        let started = Instant::now();
43        let metadata = RunMetadata {
44            git_sha: None,
45            agent_name: agent_name.to_string(),
46            tags: serde_json::json!({
47                "mode": "deploy_by_digest",
48            }),
49        };
50
51        let run_id = ledger
52            .create_run(spec_digest, metadata)
53            .await
54            .map_err(storage_err)?;
55
56        let events = vec![
57            RunEvent {
58                seq: 1,
59                kind: "deploy_started".to_string(),
60                payload: serde_json::json!({
61                    "spec_digest": spec_digest.as_str(),
62                }),
63                timestamp,
64            },
65            RunEvent {
66                seq: 2,
67                kind: "agent_executed".to_string(),
68                payload: serde_json::json!({
69                    "agent_name": agent_name,
70                    "spec_digest": spec_digest.as_str(),
71                }),
72                timestamp,
73            },
74            RunEvent {
75                seq: 3,
76                kind: "deploy_completed".to_string(),
77                payload: serde_json::json!({
78                    "success": true,
79                }),
80                timestamp,
81            },
82        ];
83
84        for event in events {
85            ledger
86                .append_event(&run_id, event)
87                .await
88                .map_err(storage_err)?;
89        }
90
91        let summary = RunSummary {
92            total_events: 3,
93            // This reference runner emits lifecycle events only; it does not materialize
94            // a final state blob, so no final-state digest is available.
95            final_state_digest: None,
96            duration_ms: started.elapsed().as_millis() as u64,
97            success: true,
98        };
99        ledger
100            .complete_run(&run_id, summary)
101            .await
102            .map_err(storage_err)?;
103
104        Ok(DeployRunOutput {
105            run_id,
106            emitted_events: 3,
107        })
108    }
109}
110
111fn storage_err(e: StorageError) -> AivcsError {
112    AivcsError::StorageError(e.to_string())
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use oxidized_state::fakes::MemoryRunLedger;
119    use oxidized_state::RunStatus;
120
121    #[tokio::test]
122    async fn deploy_run_records_spec_digest_and_completes() {
123        let ledger = MemoryRunLedger::new();
124        let digest = ContentDigest::from_bytes(b"agent-spec-v1");
125
126        let output = DeployByDigestRunner::run(&ledger, &digest, "agent-alpha")
127            .await
128            .expect("deploy run");
129
130        assert_eq!(output.emitted_events, 3);
131        let run = ledger.get_run(&output.run_id).await.expect("get run");
132        assert_eq!(run.spec_digest, digest);
133        assert_eq!(run.status, RunStatus::Completed);
134        let summary = run.summary.expect("summary");
135        assert_eq!(summary.total_events, 3);
136        assert!(summary.final_state_digest.is_none());
137    }
138}