Skip to main content

akash_deploy_rs/
workflow.rs

1//! Deployment Workflow Engine
2//!
3//! The state machine that drives deployments. It's dumb — it just
4//! transitions between steps and calls the backend. No storage,
5//! no signing, no transport. Just logic.
6
7use crate::error::DeployError;
8use crate::state::{DeploymentState, Step};
9use crate::traits::AkashBackend;
10use crate::types::{Bid, BidId};
11
12/// Workflow configuration.
13#[derive(Debug, Clone)]
14pub struct WorkflowConfig {
15    /// Minimum balance required to proceed (uakt).
16    pub min_balance_uakt: u64,
17    /// How long to wait between bid checks (seconds).
18    pub bid_wait_seconds: u64,
19    /// Max attempts to wait for bids.
20    pub max_bid_wait_attempts: u32,
21    /// Max attempts to wait for endpoints.
22    pub max_endpoint_wait_attempts: u32,
23    /// Auto-select cheapest bid without user input.
24    pub auto_select_cheapest_bid: bool,
25    /// Trusted providers to prefer.
26    pub trusted_providers: Vec<String>,
27}
28
29impl Default for WorkflowConfig {
30    fn default() -> Self {
31        Self {
32            min_balance_uakt: 5_000_000, // 5 AKT
33            bid_wait_seconds: 12,        // ~2 blocks
34            max_bid_wait_attempts: 10,
35            max_endpoint_wait_attempts: 30,
36            auto_select_cheapest_bid: false,
37            trusted_providers: Vec::new(),
38        }
39    }
40}
41
42/// Result of advancing one step.
43#[derive(Debug)]
44pub enum StepResult {
45    /// Keep going, call advance() again.
46    Continue,
47    /// Workflow needs input from caller.
48    NeedsInput(InputRequired),
49    /// Done successfully.
50    Complete,
51    /// Failed.
52    Failed(String),
53}
54
55/// What input the workflow needs.
56#[derive(Debug)]
57pub enum InputRequired {
58    /// User must select a provider from available bids.
59    SelectProvider { bids: Vec<Bid> },
60    /// SDL content is missing.
61    ProvideSdl,
62}
63
64/// The deployment workflow engine.
65///
66/// Parameterized by the backend — you provide the implementation.
67pub struct DeploymentWorkflow<'a, B: AkashBackend> {
68    backend: &'a B,
69    signer: &'a B::Signer,
70    config: WorkflowConfig,
71}
72
73impl<'a, B: AkashBackend> DeploymentWorkflow<'a, B> {
74    /// Create a new workflow engine.
75    pub fn new(backend: &'a B, signer: &'a B::Signer, config: WorkflowConfig) -> Self {
76        Self {
77            backend,
78            signer,
79            config,
80        }
81    }
82
83    /// Advance the workflow by one step.
84    ///
85    /// Each step does ONE thing — query or broadcast — then transitions.
86    /// Call this in a loop until you get Complete, Failed, or NeedsInput.
87    pub async fn advance(&self, state: &mut DeploymentState) -> Result<StepResult, DeployError> {
88        let result = match &state.step {
89            Step::Init => self.step_init(state).await,
90            Step::CheckBalance => self.step_check_balance(state).await,
91            Step::EnsureCertificate => self.step_ensure_certificate(state).await,
92            Step::CreateDeployment => self.step_create_deployment(state).await,
93            Step::WaitForBids { waited_blocks } => {
94                self.step_wait_for_bids(state, *waited_blocks).await
95            }
96            Step::SelectProvider => self.step_select_provider(state).await,
97            Step::CreateLease => self.step_create_lease(state).await,
98            Step::SendManifest => self.step_send_manifest(state).await,
99            Step::WaitForEndpoints { attempts } => {
100                self.step_wait_for_endpoints(state, *attempts).await
101            }
102            Step::Complete => return Ok(StepResult::Complete),
103            Step::Failed { reason, .. } => return Ok(StepResult::Failed(reason.clone())),
104        };
105
106        // Always save state after a step (even on error, state might have changed)
107        self.backend.save_state(&state.session_id, state).await?;
108
109        result
110    }
111
112    /// Run until completion or until input is needed.
113    pub async fn run_to_completion(
114        &self,
115        state: &mut DeploymentState,
116    ) -> Result<StepResult, DeployError> {
117        loop {
118            match self.advance(state).await? {
119                StepResult::Continue => continue,
120                other => return Ok(other),
121            }
122        }
123    }
124
125    // ═══════════════════════════════════════════════════════════════
126    // STEP IMPLEMENTATIONS
127    // ═══════════════════════════════════════════════════════════════
128
129    async fn step_init(&self, state: &mut DeploymentState) -> Result<StepResult, DeployError> {
130        // Check we have SDL
131        if state.sdl_content.is_none() {
132            return Ok(StepResult::NeedsInput(InputRequired::ProvideSdl));
133        }
134
135        state.transition(Step::CheckBalance);
136        Ok(StepResult::Continue)
137    }
138
139    async fn step_check_balance(
140        &self,
141        state: &mut DeploymentState,
142    ) -> Result<StepResult, DeployError> {
143        let balance = self.backend.query_balance(&state.owner, "uakt").await?;
144
145        if balance < self.config.min_balance_uakt as u128 {
146            state.fail(
147                format!(
148                    "insufficient balance: {} uakt < {} uakt required",
149                    balance, self.config.min_balance_uakt
150                ),
151                false, // not recoverable by retry
152            );
153            return Ok(StepResult::Failed(format!(
154                "insufficient balance: {}",
155                balance
156            )));
157        }
158
159        state.transition(Step::EnsureCertificate);
160        Ok(StepResult::Continue)
161    }
162
163    async fn step_ensure_certificate(
164        &self,
165        state: &mut DeploymentState,
166    ) -> Result<StepResult, DeployError> {
167        // Check if cert exists on chain
168        let cert = self.backend.query_certificate(&state.owner).await?;
169
170        if let Some(cert_info) = cert {
171            // Cert exists, try to load the key
172            let key = self.backend.load_cert_key(&state.owner).await?;
173            if let Some(key_pem) = key {
174                state.cert_pem = Some(cert_info.cert_pem);
175                state.key_pem = Some(key_pem);
176                state.transition(Step::CreateDeployment);
177                return Ok(StepResult::Continue);
178            }
179            // Cert exists but we don't have the key — need to recreate
180        }
181
182        // Generate new certificate
183        let (cert_pem, key_pem, pubkey_pem) = generate_certificate(&state.owner)?;
184
185        // Broadcast cert creation
186        let tx = self
187            .backend
188            .broadcast_create_certificate(self.signer, &state.owner, &cert_pem, &pubkey_pem)
189            .await?;
190
191        if !tx.is_success() {
192            state.fail(format!("certificate tx failed: {}", tx.raw_log), true);
193            return Ok(StepResult::Failed(tx.raw_log));
194        }
195
196        state.record_tx(&tx.hash);
197
198        // Save the key for future mTLS
199        self.backend.save_cert_key(&state.owner, &key_pem).await?;
200
201        state.cert_pem = Some(cert_pem);
202        state.key_pem = Some(key_pem);
203        state.transition(Step::CreateDeployment);
204        Ok(StepResult::Continue)
205    }
206
207    async fn step_create_deployment(
208        &self,
209        state: &mut DeploymentState,
210    ) -> Result<StepResult, DeployError> {
211        let sdl = state.sdl_content.as_ref().ok_or_else(|| {
212            DeployError::InvalidState("SDL content missing at CreateDeployment".into())
213        })?;
214
215        let (tx, dseq) = self
216            .backend
217            .broadcast_create_deployment(self.signer, &state.owner, sdl, state.deposit_uakt)
218            .await?;
219
220        if !tx.is_success() {
221            state.fail(format!("create deployment tx failed: {}", tx.raw_log), true);
222            return Ok(StepResult::Failed(tx.raw_log));
223        }
224
225        state.record_tx(&tx.hash);
226        state.dseq = Some(dseq);
227        state.transition(Step::WaitForBids { waited_blocks: 0 });
228        Ok(StepResult::Continue)
229    }
230
231    async fn step_wait_for_bids(
232        &self,
233        state: &mut DeploymentState,
234        waited_blocks: u32,
235    ) -> Result<StepResult, DeployError> {
236        let dseq = state
237            .dseq
238            .ok_or_else(|| DeployError::InvalidState("dseq missing at WaitForBids".into()))?;
239
240        // Query bids
241        let bids = self.backend.query_bids(&state.owner, dseq).await?;
242
243        if !bids.is_empty() {
244            state.bids = bids;
245            state.transition(Step::SelectProvider);
246            return Ok(StepResult::Continue);
247        }
248
249        // No bids yet
250        if waited_blocks >= self.config.max_bid_wait_attempts {
251            state.fail(
252                format!(
253                    "no bids after {} attempts",
254                    self.config.max_bid_wait_attempts
255                ),
256                true,
257            );
258            return Ok(StepResult::Failed("no bids received".into()));
259        }
260
261        // Wait and try again
262        tokio::time::sleep(std::time::Duration::from_secs(self.config.bid_wait_seconds)).await;
263        state.transition(Step::WaitForBids {
264            waited_blocks: waited_blocks + 1,
265        });
266        Ok(StepResult::Continue)
267    }
268
269    async fn step_select_provider(
270        &self,
271        state: &mut DeploymentState,
272    ) -> Result<StepResult, DeployError> {
273        if state.bids.is_empty() {
274            state.fail("no bids available", false);
275            return Ok(StepResult::Failed("no bids".into()));
276        }
277
278        // If provider already selected (user provided it), proceed
279        if state.selected_provider.is_some() {
280            state.transition(Step::CreateLease);
281            return Ok(StepResult::Continue);
282        }
283
284        // Auto-select if configured
285        if self.config.auto_select_cheapest_bid {
286            // Prefer trusted providers, then cheapest
287            let selected = self.auto_select_provider(&state.bids);
288            state.selected_provider = Some(selected.provider.clone());
289            state.transition(Step::CreateLease);
290            return Ok(StepResult::Continue);
291        }
292
293        // Need user input
294        Ok(StepResult::NeedsInput(InputRequired::SelectProvider {
295            bids: state.bids.clone(),
296        }))
297    }
298
299    async fn step_create_lease(
300        &self,
301        state: &mut DeploymentState,
302    ) -> Result<StepResult, DeployError> {
303        let dseq = state
304            .dseq
305            .ok_or_else(|| DeployError::InvalidState("dseq missing at CreateLease".into()))?;
306
307        let provider = state
308            .selected_provider
309            .as_ref()
310            .ok_or_else(|| DeployError::InvalidState("provider not selected".into()))?;
311
312        // Find the bid for this provider
313        let bid = state
314            .bids
315            .iter()
316            .find(|b| &b.provider == provider)
317            .ok_or_else(|| {
318                DeployError::InvalidState(format!("no bid from provider {}", provider))
319            })?;
320
321        let bid_id = BidId::from_bid(&state.owner, dseq, state.gseq, state.oseq, bid);
322
323        let tx = self
324            .backend
325            .broadcast_create_lease(self.signer, &bid_id)
326            .await?;
327
328        if !tx.is_success() {
329            state.fail(format!("create lease tx failed: {}", tx.raw_log), true);
330            return Ok(StepResult::Failed(tx.raw_log));
331        }
332
333        state.record_tx(&tx.hash);
334        state.lease_id = Some(bid_id.into());
335        state.transition(Step::SendManifest);
336        Ok(StepResult::Continue)
337    }
338
339    async fn step_send_manifest(
340        &self,
341        state: &mut DeploymentState,
342    ) -> Result<StepResult, DeployError> {
343        let lease = state
344            .lease_id
345            .as_ref()
346            .ok_or_else(|| DeployError::InvalidState("lease_id missing at SendManifest".into()))?;
347
348        let cert = state
349            .cert_pem
350            .as_ref()
351            .ok_or_else(|| DeployError::InvalidState("cert_pem missing at SendManifest".into()))?;
352
353        let key = state
354            .key_pem
355            .as_ref()
356            .ok_or_else(|| DeployError::InvalidState("key_pem missing at SendManifest".into()))?;
357
358        let sdl = state.sdl_content.as_ref().ok_or_else(|| {
359            DeployError::InvalidState("sdl_content missing at SendManifest".into())
360        })?;
361
362        // Process template if feature enabled and is_template flag set
363        #[cfg(feature = "sdl-templates")]
364        let processed_sdl = if state.is_template {
365            let template = crate::sdl::template::SdlTemplate::new(sdl)?;
366            let empty_vars = std::collections::HashMap::new();
367            let empty_defaults = std::collections::HashMap::new();
368            let variables = state.template_variables.as_ref().unwrap_or(&empty_vars);
369            let defaults = state.template_defaults.as_ref().unwrap_or(&empty_defaults);
370            template.process(variables, defaults)?
371        } else {
372            sdl.clone()
373        };
374
375        #[cfg(not(feature = "sdl-templates"))]
376        let processed_sdl = sdl.clone();
377
378        // Get provider URI
379        let provider_info = self
380            .backend
381            .query_provider_info(&lease.provider)
382            .await?
383            .ok_or_else(|| DeployError::Provider("provider not found".into()))?;
384
385        // Build manifest from SDL using the actual ManifestBuilder
386        let manifest = build_manifest(&state.owner, &processed_sdl, lease.dseq)?;
387
388        self.backend
389            .send_manifest(&provider_info.host_uri, lease, &manifest, cert, key)
390            .await?;
391
392        state.transition(Step::WaitForEndpoints { attempts: 0 });
393        Ok(StepResult::Continue)
394    }
395
396    async fn step_wait_for_endpoints(
397        &self,
398        state: &mut DeploymentState,
399        attempts: u32,
400    ) -> Result<StepResult, DeployError> {
401        let lease = state.lease_id.as_ref().ok_or_else(|| {
402            DeployError::InvalidState("lease_id missing at WaitForEndpoints".into())
403        })?;
404
405        let cert = state.cert_pem.as_ref().ok_or_else(|| {
406            DeployError::InvalidState("cert_pem missing at WaitForEndpoints".into())
407        })?;
408
409        let key = state.key_pem.as_ref().ok_or_else(|| {
410            DeployError::InvalidState("key_pem missing at WaitForEndpoints".into())
411        })?;
412
413        let provider_info = self
414            .backend
415            .query_provider_info(&lease.provider)
416            .await?
417            .ok_or_else(|| DeployError::Provider("provider not found".into()))?;
418
419        let status = self
420            .backend
421            .query_provider_status(&provider_info.host_uri, lease, cert, key)
422            .await?;
423
424        if status.ready && !status.endpoints.is_empty() {
425            state.endpoints = status.endpoints;
426            state.transition(Step::Complete);
427            return Ok(StepResult::Complete);
428        }
429
430        if attempts >= self.config.max_endpoint_wait_attempts {
431            state.fail(
432                format!("endpoints not ready after {} attempts", attempts),
433                true,
434            );
435            return Ok(StepResult::Failed("endpoints not ready".into()));
436        }
437
438        // Wait and try again
439        tokio::time::sleep(std::time::Duration::from_secs(5)).await;
440        state.transition(Step::WaitForEndpoints {
441            attempts: attempts + 1,
442        });
443        Ok(StepResult::Continue)
444    }
445
446    // ═══════════════════════════════════════════════════════════════
447    // HELPERS
448    // ═══════════════════════════════════════════════════════════════
449
450    fn auto_select_provider<'b>(&self, bids: &'b [Bid]) -> &'b Bid {
451        // First try trusted providers
452        for trusted in &self.config.trusted_providers {
453            if let Some(bid) = bids.iter().find(|b| &b.provider == trusted) {
454                return bid;
455            }
456        }
457        // Otherwise cheapest
458        bids.iter()
459            .min_by_key(|b| b.price_uakt)
460            .expect("bids should not be empty")
461    }
462
463    /// Provide user's provider selection.
464    pub fn select_provider(state: &mut DeploymentState, provider: &str) -> Result<(), DeployError> {
465        if !state.bids.iter().any(|b| b.provider == provider) {
466            return Err(DeployError::InvalidState(format!(
467                "provider {} not in available bids",
468                provider
469            )));
470        }
471        state.selected_provider = Some(provider.to_string());
472        Ok(())
473    }
474
475    /// Provide SDL content.
476    pub fn provide_sdl(state: &mut DeploymentState, sdl: &str) {
477        state.sdl_content = Some(sdl.to_string());
478    }
479}
480
481// ═══════════════════════════════════════════════════════════════════
482// CERTIFICATE GENERATION
483// ═══════════════════════════════════════════════════════════════════
484
485/// Generate a self-signed certificate for Akash mTLS.
486/// Returns (cert_pem, private_key_pem, public_key_pem).
487fn generate_certificate(owner: &str) -> Result<(Vec<u8>, Vec<u8>, Vec<u8>), DeployError> {
488    let cert = crate::auth::certificate::generate_certificate(owner)?;
489    Ok((cert.cert_pem, cert.privkey_pem, cert.pubkey_pem))
490}
491
492/// Build manifest from SDL.
493///
494/// Uses `ManifestBuilder` to parse SDL and generate the canonical JSON manifest
495/// that providers expect. The manifest hash computed from this JSON must match
496/// the on-chain deployment.version hash.
497fn build_manifest(owner: &str, sdl: &str, dseq: u64) -> Result<Vec<u8>, DeployError> {
498    if sdl.is_empty() {
499        return Err(DeployError::Manifest("empty SDL".into()));
500    }
501
502    // Use the actual ManifestBuilder to parse SDL
503    let builder = crate::manifest::manifest::ManifestBuilder::new(owner, dseq);
504    let manifest_groups = builder.build_from_sdl(sdl)?;
505
506    // Serialize to canonical JSON (deterministic, matches Go's encoding/json)
507    let canonical_json = crate::manifest::canonical::to_canonical_json(&manifest_groups)?;
508
509    Ok(canonical_json.into_bytes())
510}
511
512#[cfg(test)]
513mod tests {
514    use super::*;
515    use crate::types::*;
516    use std::sync::{Arc, Mutex};
517
518    // Common SDL fixtures for testing
519    const SIMPLE_SDL: &str = r#"
520version: "2.0"
521services:
522  web:
523    image: nginx
524    expose:
525      - port: 80
526        as: 80
527        to:
528          - global: true
529profiles:
530  compute:
531    web:
532      resources:
533        cpu:
534          units: 1
535        memory:
536          size: 512Mi
537        storage:
538          size: 1Gi
539  placement:
540    dc:
541      pricing:
542        web:
543          denom: uakt
544          amount: 1000
545deployment:
546  web:
547    dc:
548      profile: web
549      count: 1
550"#;
551
552    // Mock signer - just a placeholder
553    #[derive(Debug, Clone)]
554    struct MockSigner;
555
556    // Mock backend with configurable responses
557    struct MockBackend {
558        balance: Arc<Mutex<u128>>,
559        certificate: Arc<Mutex<Option<CertificateInfo>>>,
560        cert_key: Arc<Mutex<Option<Vec<u8>>>>,
561        bids: Arc<Mutex<Vec<Bid>>>,
562        provider_status: Arc<Mutex<Option<ProviderLeaseStatus>>>,
563        call_counts: Arc<Mutex<CallCounts>>,
564        fail_cert_tx: Arc<Mutex<bool>>,
565        fail_deployment_tx: Arc<Mutex<bool>>,
566        fail_lease_tx: Arc<Mutex<bool>>,
567    }
568
569    #[derive(Debug, Default, Clone)]
570    struct CallCounts {
571        query_balance: usize,
572        query_bids: usize,
573        broadcast_create_deployment: usize,
574        broadcast_create_lease: usize,
575        send_manifest: usize,
576        query_provider_status: usize,
577    }
578
579    impl MockBackend {
580        fn new() -> Self {
581            Self {
582                balance: Arc::new(Mutex::new(10_000_000)), // 10 AKT
583                certificate: Arc::new(Mutex::new(None)),
584                cert_key: Arc::new(Mutex::new(None)),
585                bids: Arc::new(Mutex::new(Vec::new())),
586                provider_status: Arc::new(Mutex::new(None)),
587                call_counts: Arc::new(Mutex::new(CallCounts::default())),
588                fail_cert_tx: Arc::new(Mutex::new(false)),
589                fail_deployment_tx: Arc::new(Mutex::new(false)),
590                fail_lease_tx: Arc::new(Mutex::new(false)),
591            }
592        }
593
594        fn set_balance(&self, balance: u128) {
595            *self.balance.lock().unwrap() = balance;
596        }
597
598        fn set_bids(&self, bids: Vec<Bid>) {
599            *self.bids.lock().unwrap() = bids;
600        }
601
602        fn set_provider_status(&self, status: ProviderLeaseStatus) {
603            *self.provider_status.lock().unwrap() = Some(status);
604        }
605
606        fn set_certificate(&self, cert: CertificateInfo) {
607            *self.certificate.lock().unwrap() = Some(cert);
608        }
609
610        fn set_cert_key(&self, key: Vec<u8>) {
611            *self.cert_key.lock().unwrap() = Some(key);
612        }
613
614        fn set_fail_cert_tx(&self, fail: bool) {
615            *self.fail_cert_tx.lock().unwrap() = fail;
616        }
617
618        fn set_fail_deployment_tx(&self, fail: bool) {
619            *self.fail_deployment_tx.lock().unwrap() = fail;
620        }
621
622        fn set_fail_lease_tx(&self, fail: bool) {
623            *self.fail_lease_tx.lock().unwrap() = fail;
624        }
625
626        fn get_call_counts(&self) -> CallCounts {
627            self.call_counts.lock().unwrap().clone()
628        }
629    }
630
631    impl AkashBackend for MockBackend {
632        type Signer = MockSigner;
633
634        async fn query_balance(&self, _address: &str, _denom: &str) -> Result<u128, DeployError> {
635            self.call_counts.lock().unwrap().query_balance += 1;
636            Ok(*self.balance.lock().unwrap())
637        }
638
639        async fn query_certificate(
640            &self,
641            _address: &str,
642        ) -> Result<Option<CertificateInfo>, DeployError> {
643            Ok(self.certificate.lock().unwrap().clone())
644        }
645
646        async fn query_provider_info(
647            &self,
648            _provider: &str,
649        ) -> Result<Option<ProviderInfo>, DeployError> {
650            Ok(Some(ProviderInfo {
651                address: "akash1provider".to_string(),
652                host_uri: "https://provider.akash.net".to_string(),
653                email: "test@example.com".to_string(),
654                website: "https://example.com".to_string(),
655                attributes: vec![],
656                cached_at: 0,
657            }))
658        }
659
660        async fn query_bids(&self, _owner: &str, _dseq: u64) -> Result<Vec<Bid>, DeployError> {
661            self.call_counts.lock().unwrap().query_bids += 1;
662            Ok(self.bids.lock().unwrap().clone())
663        }
664
665        async fn query_lease(
666            &self,
667            _owner: &str,
668            _dseq: u64,
669            _gseq: u32,
670            _oseq: u32,
671            _bseq: u32,
672            _provider: &str,
673        ) -> Result<LeaseInfo, DeployError> {
674            Ok(LeaseInfo {
675                state: LeaseState::Active,
676                price_uakt: 1000,
677            })
678        }
679
680        async fn query_escrow(&self, _owner: &str, _dseq: u64) -> Result<EscrowInfo, DeployError> {
681            Ok(EscrowInfo {
682                balance_uakt: 5_000_000,
683                deposited_uakt: 5_000_000,
684            })
685        }
686
687        async fn broadcast_create_certificate(
688            &self,
689            _signer: &Self::Signer,
690            _owner: &str,
691            _cert_pem: &[u8],
692            _pubkey_pem: &[u8],
693        ) -> Result<TxResult, DeployError> {
694            if *self.fail_cert_tx.lock().unwrap() {
695                Ok(TxResult {
696                    hash: "CERT_TX_FAIL".to_string(),
697                    code: 5,
698                    raw_log: "certificate creation failed".to_string(),
699                    height: 1000,
700                })
701            } else {
702                Ok(TxResult {
703                    hash: "CERT_TX".to_string(),
704                    code: 0,
705                    raw_log: "success".to_string(),
706                    height: 1000,
707                })
708            }
709        }
710
711        async fn broadcast_create_deployment(
712            &self,
713            _signer: &Self::Signer,
714            _owner: &str,
715            _sdl_content: &str,
716            _deposit_uakt: u64,
717        ) -> Result<(TxResult, u64), DeployError> {
718            self.call_counts.lock().unwrap().broadcast_create_deployment += 1;
719            if *self.fail_deployment_tx.lock().unwrap() {
720                Ok((
721                    TxResult {
722                        hash: "DEPLOY_TX_FAIL".to_string(),
723                        code: 5,
724                        raw_log: "deployment creation failed".to_string(),
725                        height: 1001,
726                    },
727                    123456,
728                ))
729            } else {
730                Ok((
731                    TxResult {
732                        hash: "DEPLOY_TX".to_string(),
733                        code: 0,
734                        raw_log: "success".to_string(),
735                        height: 1001,
736                    },
737                    123456,
738                ))
739            }
740        }
741
742        async fn broadcast_create_lease(
743            &self,
744            _signer: &Self::Signer,
745            _bid: &BidId,
746        ) -> Result<TxResult, DeployError> {
747            self.call_counts.lock().unwrap().broadcast_create_lease += 1;
748            if *self.fail_lease_tx.lock().unwrap() {
749                Ok(TxResult {
750                    hash: "LEASE_TX_FAIL".to_string(),
751                    code: 5,
752                    raw_log: "lease creation failed".to_string(),
753                    height: 1002,
754                })
755            } else {
756                Ok(TxResult {
757                    hash: "LEASE_TX".to_string(),
758                    code: 0,
759                    raw_log: "success".to_string(),
760                    height: 1002,
761                })
762            }
763        }
764
765        async fn broadcast_deposit(
766            &self,
767            _signer: &Self::Signer,
768            _owner: &str,
769            _dseq: u64,
770            _amount_uakt: u64,
771        ) -> Result<TxResult, DeployError> {
772            Ok(TxResult {
773                hash: "DEPOSIT_TX".to_string(),
774                code: 0,
775                raw_log: "success".to_string(),
776                height: 1003,
777            })
778        }
779
780        async fn broadcast_close_deployment(
781            &self,
782            _signer: &Self::Signer,
783            _owner: &str,
784            _dseq: u64,
785        ) -> Result<TxResult, DeployError> {
786            Ok(TxResult {
787                hash: "CLOSE_TX".to_string(),
788                code: 0,
789                raw_log: "success".to_string(),
790                height: 1004,
791            })
792        }
793
794        async fn send_manifest(
795            &self,
796            _provider_uri: &str,
797            _lease: &LeaseId,
798            _manifest: &[u8],
799            _cert_pem: &[u8],
800            _key_pem: &[u8],
801        ) -> Result<(), DeployError> {
802            self.call_counts.lock().unwrap().send_manifest += 1;
803            Ok(())
804        }
805
806        async fn query_provider_status(
807            &self,
808            _provider_uri: &str,
809            _lease: &LeaseId,
810            _cert_pem: &[u8],
811            _key_pem: &[u8],
812        ) -> Result<ProviderLeaseStatus, DeployError> {
813            self.call_counts.lock().unwrap().query_provider_status += 1;
814            self.provider_status
815                .lock()
816                .unwrap()
817                .clone()
818                .ok_or_else(|| DeployError::Provider("no status".into()))
819        }
820
821        async fn load_state(
822            &self,
823            _session_id: &str,
824        ) -> Result<Option<DeploymentState>, DeployError> {
825            Ok(None)
826        }
827
828        async fn save_state(
829            &self,
830            _session_id: &str,
831            _state: &DeploymentState,
832        ) -> Result<(), DeployError> {
833            Ok(())
834        }
835
836        async fn load_cert_key(&self, _owner: &str) -> Result<Option<Vec<u8>>, DeployError> {
837            Ok(None)
838        }
839
840        async fn save_cert_key(&self, _owner: &str, _key: &[u8]) -> Result<(), DeployError> {
841            Ok(())
842        }
843
844        async fn delete_cert_key(&self, _owner: &str) -> Result<(), DeployError> {
845            Ok(())
846        }
847
848        async fn load_cached_provider(
849            &self,
850            _provider: &str,
851        ) -> Result<Option<ProviderInfo>, DeployError> {
852            Ok(None)
853        }
854
855        async fn cache_provider(&self, _info: &ProviderInfo) -> Result<(), DeployError> {
856            Ok(())
857        }
858    }
859
860    #[test]
861    fn test_default_config() {
862        let config = WorkflowConfig::default();
863        assert_eq!(config.min_balance_uakt, 5_000_000);
864        assert!(!config.auto_select_cheapest_bid);
865    }
866
867    #[test]
868    fn test_step_result_variants() {
869        let _ = StepResult::Continue;
870        let _ = StepResult::Complete;
871        let _ = StepResult::Failed("oops".into());
872        let _ = StepResult::NeedsInput(InputRequired::ProvideSdl);
873    }
874
875    #[tokio::test]
876    async fn test_workflow_check_balance_sufficient() {
877        let backend = MockBackend::new();
878        let signer = MockSigner;
879        let config = WorkflowConfig::default();
880        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
881
882        let mut state = DeploymentState::new("test", "akash1owner");
883        state.step = Step::CheckBalance;
884
885        let result = workflow.advance(&mut state).await.unwrap();
886        assert!(matches!(result, StepResult::Continue));
887        assert_eq!(backend.get_call_counts().query_balance, 1);
888    }
889
890    #[tokio::test]
891    async fn test_workflow_check_balance_insufficient() {
892        let backend = MockBackend::new();
893        backend.set_balance(1_000_000);
894
895        let signer = MockSigner;
896        let config = WorkflowConfig::default();
897        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
898
899        let mut state = DeploymentState::new("test", "akash1owner");
900        state.step = Step::CheckBalance;
901
902        let result = workflow.advance(&mut state).await.unwrap();
903        assert!(matches!(result, StepResult::Failed(_)));
904    }
905
906    #[tokio::test]
907    async fn test_workflow_wait_for_bids() {
908        let backend = MockBackend::new();
909        backend.set_bids(vec![Bid {
910            provider: "akash1provider".to_string(),
911            price_uakt: 1000,
912            resources: Resources::default(),
913        }]);
914
915        let signer = MockSigner;
916        let config = WorkflowConfig::default();
917        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
918
919        let mut state = DeploymentState::new("test", "akash1owner");
920        state.step = Step::WaitForBids { waited_blocks: 0 };
921        state.dseq = Some(123456);
922
923        let result = workflow.advance(&mut state).await.unwrap();
924        assert!(matches!(result, StepResult::Continue));
925        assert_eq!(state.bids.len(), 1);
926    }
927
928    #[tokio::test]
929    async fn test_workflow_auto_select_cheapest() {
930        let backend = MockBackend::new();
931
932        let signer = MockSigner;
933        let mut config = WorkflowConfig::default();
934        config.auto_select_cheapest_bid = true;
935        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
936
937        let mut state = DeploymentState::new("test", "akash1owner");
938        state.step = Step::SelectProvider;
939        state.bids = vec![
940            Bid {
941                provider: "akash1expensive".to_string(),
942                price_uakt: 5000,
943                resources: Resources::default(),
944            },
945            Bid {
946                provider: "akash1cheap".to_string(),
947                price_uakt: 1000,
948                resources: Resources::default(),
949            },
950        ];
951
952        let result = workflow.advance(&mut state).await.unwrap();
953        assert!(matches!(result, StepResult::Continue));
954        assert_eq!(state.selected_provider, Some("akash1cheap".to_string()));
955    }
956
957    #[tokio::test]
958    async fn test_workflow_endpoints_ready() {
959        let backend = MockBackend::new();
960        backend.set_provider_status(ProviderLeaseStatus {
961            ready: true,
962            endpoints: vec![ServiceEndpoint {
963                service: "web".to_string(),
964                uri: "https://web.example.com".to_string(),
965                port: 80,
966            }],
967        });
968
969        let signer = MockSigner;
970        let config = WorkflowConfig::default();
971        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
972
973        let mut state = DeploymentState::new("test", "akash1owner");
974        state.step = Step::WaitForEndpoints { attempts: 0 };
975        state.dseq = Some(123456);
976        state.selected_provider = Some("akash1provider".to_string());
977        state.lease_id = Some(LeaseId {
978            owner: "akash1owner".to_string(),
979            dseq: 123456,
980            gseq: 1,
981            oseq: 1,
982            provider: "akash1provider".to_string(),
983        });
984        state.cert_pem = Some(vec![1, 2, 3]); // Mock cert
985        state.key_pem = Some(vec![4, 5, 6]); // Mock key
986
987        let result = workflow.advance(&mut state).await.unwrap();
988        assert!(matches!(result, StepResult::Complete));
989        assert_eq!(state.endpoints.len(), 1);
990    }
991
992    #[tokio::test]
993    async fn test_select_provider_invalid() {
994        let mut state = DeploymentState::new("test", "akash1owner");
995        state.bids = vec![Bid {
996            provider: "akash1provider1".to_string(),
997            price_uakt: 1000,
998            resources: Resources::default(),
999        }];
1000
1001        let result =
1002            DeploymentWorkflow::<MockBackend>::select_provider(&mut state, "akash1nonexistent");
1003        assert!(result.is_err());
1004    }
1005
1006    #[tokio::test]
1007    async fn test_select_provider_valid() {
1008        let mut state = DeploymentState::new("test", "akash1owner");
1009        state.bids = vec![Bid {
1010            provider: "akash1provider1".to_string(),
1011            price_uakt: 1000,
1012            resources: Resources::default(),
1013        }];
1014
1015        DeploymentWorkflow::<MockBackend>::select_provider(&mut state, "akash1provider1").unwrap();
1016        assert_eq!(state.selected_provider, Some("akash1provider1".to_string()));
1017    }
1018
1019    #[tokio::test]
1020    async fn test_provide_sdl() {
1021        let mut state = DeploymentState::new("test", "akash1owner");
1022        DeploymentWorkflow::<MockBackend>::provide_sdl(&mut state, "version: 2.0");
1023        assert_eq!(state.sdl_content, Some("version: 2.0".to_string()));
1024    }
1025
1026    #[tokio::test]
1027    async fn test_step_init_missing_sdl() {
1028        let backend = MockBackend::new();
1029        let signer = MockSigner;
1030        let config = WorkflowConfig::default();
1031        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1032
1033        let mut state = DeploymentState::new("test", "akash1owner");
1034        state.step = Step::Init;
1035
1036        let result = workflow.advance(&mut state).await.unwrap();
1037        assert!(matches!(
1038            result,
1039            StepResult::NeedsInput(InputRequired::ProvideSdl)
1040        ));
1041    }
1042
1043    #[tokio::test]
1044    async fn test_run_to_completion_needs_input() {
1045        let backend = MockBackend::new();
1046        let signer = MockSigner;
1047        let config = WorkflowConfig::default();
1048        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1049
1050        let mut state = DeploymentState::new("test", "akash1owner");
1051        state.step = Step::Init;
1052
1053        let result = workflow.run_to_completion(&mut state).await.unwrap();
1054        assert!(matches!(result, StepResult::NeedsInput(_)));
1055    }
1056
1057    #[tokio::test]
1058    async fn test_step_init_with_sdl() {
1059        let backend = MockBackend::new();
1060        let signer = MockSigner;
1061        let config = WorkflowConfig::default();
1062        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1063
1064        let mut state = DeploymentState::new("test", "akash1owner");
1065        state.step = Step::Init;
1066        state.sdl_content = Some("version: 2.0".to_string());
1067
1068        let result = workflow.advance(&mut state).await.unwrap();
1069        assert!(matches!(result, StepResult::Continue));
1070        assert!(matches!(state.step, Step::CheckBalance));
1071    }
1072
1073    #[tokio::test]
1074    async fn test_step_complete() {
1075        let backend = MockBackend::new();
1076        let signer = MockSigner;
1077        let config = WorkflowConfig::default();
1078        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1079
1080        let mut state = DeploymentState::new("test", "akash1owner");
1081        state.step = Step::Complete;
1082
1083        let result = workflow.advance(&mut state).await.unwrap();
1084        assert!(matches!(result, StepResult::Complete));
1085    }
1086
1087    #[tokio::test]
1088    async fn test_step_failed() {
1089        let backend = MockBackend::new();
1090        let signer = MockSigner;
1091        let config = WorkflowConfig::default();
1092        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1093
1094        let mut state = DeploymentState::new("test", "akash1owner");
1095        state.step = Step::Failed {
1096            reason: "test error".to_string(),
1097            recoverable: false,
1098        };
1099
1100        let result = workflow.advance(&mut state).await.unwrap();
1101        assert!(matches!(result, StepResult::Failed(_)));
1102    }
1103
1104    #[tokio::test]
1105    async fn test_step_create_deployment_missing_dseq() {
1106        let backend = MockBackend::new();
1107        let signer = MockSigner;
1108        let config = WorkflowConfig::default();
1109        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1110
1111        let mut state = DeploymentState::new("test", "akash1owner");
1112        state.step = Step::CreateDeployment;
1113        state.sdl_content = Some(SIMPLE_SDL.to_string());
1114
1115        let result = workflow.advance(&mut state).await.unwrap();
1116        assert!(matches!(result, StepResult::Continue));
1117        assert!(matches!(state.step, Step::WaitForBids { .. }));
1118        assert!(state.dseq.is_some());
1119    }
1120
1121    #[tokio::test]
1122    async fn test_step_create_lease_missing_provider() {
1123        let backend = MockBackend::new();
1124        let signer = MockSigner;
1125        let config = WorkflowConfig::default();
1126        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1127
1128        let mut state = DeploymentState::new("test", "akash1owner");
1129        state.step = Step::CreateLease;
1130        state.dseq = Some(123456);
1131        // No selected_provider
1132
1133        let result = workflow.advance(&mut state).await;
1134        assert!(result.is_err() || matches!(result, Ok(StepResult::Failed(_))));
1135    }
1136
1137    #[tokio::test]
1138    async fn test_step_send_manifest_missing_sdl() {
1139        let backend = MockBackend::new();
1140        let signer = MockSigner;
1141        let config = WorkflowConfig::default();
1142        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1143
1144        let mut state = DeploymentState::new("test", "akash1owner");
1145        state.step = Step::SendManifest;
1146        state.dseq = Some(123456);
1147        state.selected_provider = Some("akash1provider".to_string());
1148        state.cert_pem = Some(vec![1, 2, 3]);
1149        state.key_pem = Some(vec![4, 5, 6]);
1150        // No SDL content
1151
1152        let result = workflow.advance(&mut state).await;
1153        assert!(result.is_err() || matches!(result, Ok(StepResult::Failed(_))));
1154    }
1155
1156    #[tokio::test]
1157    async fn test_step_wait_for_endpoints_missing_lease_id() {
1158        let backend = MockBackend::new();
1159        let signer = MockSigner;
1160        let config = WorkflowConfig::default();
1161        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1162
1163        let mut state = DeploymentState::new("test", "akash1owner");
1164        state.step = Step::WaitForEndpoints { attempts: 0 };
1165        state.dseq = Some(123456);
1166        state.selected_provider = Some("akash1provider".to_string());
1167        state.cert_pem = Some(vec![1, 2, 3]);
1168        state.key_pem = Some(vec![4, 5, 6]);
1169        // No lease_id
1170
1171        let result = workflow.advance(&mut state).await;
1172        assert!(result.is_err());
1173    }
1174
1175    #[tokio::test]
1176    async fn test_step_wait_for_endpoints_missing_cert() {
1177        let backend = MockBackend::new();
1178        let signer = MockSigner;
1179        let config = WorkflowConfig::default();
1180        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1181
1182        let mut state = DeploymentState::new("test", "akash1owner");
1183        state.step = Step::WaitForEndpoints { attempts: 0 };
1184        state.dseq = Some(123456);
1185        state.selected_provider = Some("akash1provider".to_string());
1186        state.lease_id = Some(LeaseId {
1187            owner: "akash1owner".to_string(),
1188            dseq: 123456,
1189            gseq: 1,
1190            oseq: 1,
1191            provider: "akash1provider".to_string(),
1192        });
1193        // No cert_pem or key_pem
1194
1195        let result = workflow.advance(&mut state).await;
1196        assert!(result.is_err());
1197    }
1198
1199    #[test]
1200    fn test_build_manifest_empty() {
1201        let result = build_manifest("akash1owner", "", 123);
1202        assert!(result.is_err());
1203        assert!(result.unwrap_err().to_string().contains("empty"));
1204    }
1205
1206    #[test]
1207    fn test_build_manifest_valid_sdl() {
1208        let result = build_manifest("akash1owner", SIMPLE_SDL, 123);
1209        assert!(result.is_ok());
1210        let manifest_bytes = result.unwrap();
1211        assert!(!manifest_bytes.is_empty());
1212    }
1213
1214    #[test]
1215    fn test_generate_cert() {
1216        let result = generate_certificate("akash1owner");
1217        assert!(result.is_ok());
1218        let (cert_pem, key_pem, pubkey_pem) = result.unwrap();
1219        assert!(!cert_pem.is_empty());
1220        assert!(!key_pem.is_empty());
1221        assert!(!pubkey_pem.is_empty());
1222    }
1223
1224    #[tokio::test]
1225    async fn test_step_select_provider_with_trusted() {
1226        let backend = MockBackend::new();
1227        let signer = MockSigner;
1228        let mut config = WorkflowConfig::default();
1229        config.trusted_providers = vec!["akash1trusted".to_string()];
1230        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1231
1232        let mut state = DeploymentState::new("test", "akash1owner");
1233        state.step = Step::SelectProvider;
1234        state.bids = vec![
1235            Bid {
1236                provider: "akash1trusted".to_string(),
1237                price_uakt: 5000,
1238                resources: Resources::default(),
1239            },
1240            Bid {
1241                provider: "akash1cheap".to_string(),
1242                price_uakt: 1000,
1243                resources: Resources::default(),
1244            },
1245        ];
1246
1247        let result = workflow.advance(&mut state).await.unwrap();
1248        // Should need input since auto_select is false
1249        assert!(matches!(result, StepResult::NeedsInput(_)));
1250    }
1251
1252    #[tokio::test]
1253    async fn test_step_create_deployment_with_valid_sdl() {
1254        let backend = MockBackend::new();
1255        let signer = MockSigner;
1256        let config = WorkflowConfig::default();
1257        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1258
1259        let mut state = DeploymentState::new("test", "akash1owner");
1260        state.step = Step::CreateDeployment;
1261        state.sdl_content = Some(SIMPLE_SDL.to_string());
1262
1263        let result = workflow.advance(&mut state).await.unwrap();
1264        assert!(matches!(result, StepResult::Continue));
1265        assert!(state.dseq.is_some());
1266        assert!(matches!(state.step, Step::WaitForBids { .. }));
1267    }
1268
1269    #[tokio::test]
1270    async fn test_full_workflow_to_bids() {
1271        let backend = MockBackend::new();
1272        backend.set_bids(vec![Bid {
1273            provider: "akash1provider".to_string(),
1274            price_uakt: 1000,
1275            resources: Resources::default(),
1276        }]);
1277
1278        let signer = MockSigner;
1279        let config = WorkflowConfig::default();
1280        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1281
1282        let mut state = DeploymentState::new("test", "akash1owner");
1283        state.sdl_content = Some(SIMPLE_SDL.to_string());
1284
1285        // Run through Init -> CheckBalance -> EnsureCertificate -> CreateDeployment -> WaitForBids
1286        let mut steps = 0;
1287        loop {
1288            let result = workflow.advance(&mut state).await.unwrap();
1289            steps += 1;
1290
1291            match result {
1292                StepResult::Continue => {
1293                    if steps > 10 {
1294                        panic!("Too many steps");
1295                    }
1296                    continue;
1297                }
1298                StepResult::NeedsInput(_) => break,
1299                StepResult::Complete => break,
1300                StepResult::Failed(reason) => panic!("Failed: {}", reason),
1301            }
1302        }
1303
1304        // Should have bids now and be at SelectProvider step
1305        assert!(!state.bids.is_empty());
1306        assert!(matches!(state.step, Step::SelectProvider));
1307    }
1308
1309    #[tokio::test]
1310    async fn test_workflow_wait_for_bids_timeout() {
1311        let backend = MockBackend::new();
1312        backend.set_bids(vec![]); // No bids
1313        let signer = MockSigner;
1314        let mut config = WorkflowConfig::default();
1315        config.max_bid_wait_attempts = 2; // Short timeout
1316        config.bid_wait_seconds = 0; // No actual wait
1317        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1318
1319        let mut state = DeploymentState::new("test", "akash1owner");
1320        state.dseq = Some(123456);
1321        state.step = Step::WaitForBids { waited_blocks: 0 };
1322
1323        // First attempt - no bids, should retry
1324        let result = workflow.advance(&mut state).await.unwrap();
1325        assert!(matches!(result, StepResult::Continue));
1326        assert!(matches!(state.step, Step::WaitForBids { waited_blocks: 1 }));
1327
1328        // Second attempt - still no bids, should retry
1329        let result = workflow.advance(&mut state).await.unwrap();
1330        assert!(matches!(result, StepResult::Continue));
1331        assert!(matches!(state.step, Step::WaitForBids { waited_blocks: 2 }));
1332
1333        // Third attempt - max reached, should fail
1334        let result = workflow.advance(&mut state).await.unwrap();
1335        assert!(matches!(result, StepResult::Failed(_)));
1336    }
1337
1338    #[tokio::test]
1339    async fn test_workflow_select_provider_no_bids() {
1340        let backend = MockBackend::new();
1341        let signer = MockSigner;
1342        let config = WorkflowConfig::default();
1343        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1344
1345        let mut state = DeploymentState::new("test", "akash1owner");
1346        state.step = Step::SelectProvider;
1347        state.bids = vec![]; // Empty bids
1348
1349        let result = workflow.advance(&mut state).await.unwrap();
1350        assert!(matches!(result, StepResult::Failed(_)));
1351    }
1352
1353    #[tokio::test]
1354    async fn test_workflow_select_provider_already_selected() {
1355        let backend = MockBackend::new();
1356        let signer = MockSigner;
1357        let config = WorkflowConfig::default();
1358        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1359
1360        let mut state = DeploymentState::new("test", "akash1owner");
1361        state.step = Step::SelectProvider;
1362        state.bids = vec![Bid {
1363            provider: "akash1provider".to_string(),
1364            price_uakt: 1000,
1365            resources: Resources::default(),
1366        }];
1367        state.selected_provider = Some("akash1provider".to_string());
1368
1369        let result = workflow.advance(&mut state).await.unwrap();
1370        assert!(matches!(result, StepResult::Continue));
1371        assert!(matches!(state.step, Step::CreateLease));
1372    }
1373
1374    #[tokio::test]
1375    async fn test_workflow_select_provider_auto_select() {
1376        let backend = MockBackend::new();
1377        let signer = MockSigner;
1378        let mut config = WorkflowConfig::default();
1379        config.auto_select_cheapest_bid = true;
1380        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1381
1382        let mut state = DeploymentState::new("test", "akash1owner");
1383        state.step = Step::SelectProvider;
1384        state.bids = vec![
1385            Bid {
1386                provider: "akash1expensive".to_string(),
1387                price_uakt: 5000,
1388                resources: Resources::default(),
1389            },
1390            Bid {
1391                provider: "akash1cheap".to_string(),
1392                price_uakt: 1000,
1393                resources: Resources::default(),
1394            },
1395        ];
1396
1397        let result = workflow.advance(&mut state).await.unwrap();
1398        assert!(matches!(result, StepResult::Continue));
1399        assert_eq!(state.selected_provider, Some("akash1cheap".to_string()));
1400    }
1401
1402    #[tokio::test]
1403    async fn test_workflow_auto_select_trusted_provider() {
1404        let backend = MockBackend::new();
1405        let signer = MockSigner;
1406        let mut config = WorkflowConfig::default();
1407        config.auto_select_cheapest_bid = true;
1408        config.trusted_providers = vec!["akash1trusted".to_string()];
1409        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1410
1411        let mut state = DeploymentState::new("test", "akash1owner");
1412        state.step = Step::SelectProvider;
1413        state.bids = vec![
1414            Bid {
1415                provider: "akash1trusted".to_string(),
1416                price_uakt: 5000, // More expensive
1417                resources: Resources::default(),
1418            },
1419            Bid {
1420                provider: "akash1cheap".to_string(),
1421                price_uakt: 1000, // Cheaper
1422                resources: Resources::default(),
1423            },
1424        ];
1425
1426        let result = workflow.advance(&mut state).await.unwrap();
1427        assert!(matches!(result, StepResult::Continue));
1428        // Should select trusted provider even though it's more expensive
1429        assert_eq!(state.selected_provider, Some("akash1trusted".to_string()));
1430    }
1431
1432    #[tokio::test]
1433    async fn test_workflow_create_lease_missing_dseq() {
1434        let backend = MockBackend::new();
1435        let signer = MockSigner;
1436        let config = WorkflowConfig::default();
1437        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1438
1439        let mut state = DeploymentState::new("test", "akash1owner");
1440        state.step = Step::CreateLease;
1441        state.selected_provider = Some("akash1provider".to_string());
1442        // Missing dseq
1443
1444        let result = workflow.advance(&mut state).await;
1445        assert!(result.is_err());
1446    }
1447
1448    #[tokio::test]
1449    async fn test_workflow_create_lease_no_provider_selected() {
1450        let backend = MockBackend::new();
1451        let signer = MockSigner;
1452        let config = WorkflowConfig::default();
1453        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1454
1455        let mut state = DeploymentState::new("test", "akash1owner");
1456        state.step = Step::CreateLease;
1457        state.dseq = Some(123456);
1458        // No provider selected
1459
1460        let result = workflow.advance(&mut state).await;
1461        assert!(result.is_err());
1462    }
1463
1464    #[tokio::test]
1465    async fn test_workflow_create_lease_bid_not_found() {
1466        let backend = MockBackend::new();
1467        let signer = MockSigner;
1468        let config = WorkflowConfig::default();
1469        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1470
1471        let mut state = DeploymentState::new("test", "akash1owner");
1472        state.step = Step::CreateLease;
1473        state.dseq = Some(123456);
1474        state.selected_provider = Some("akash1nonexistent".to_string());
1475        state.bids = vec![Bid {
1476            provider: "akash1other".to_string(),
1477            price_uakt: 1000,
1478            resources: Resources::default(),
1479        }];
1480
1481        let result = workflow.advance(&mut state).await;
1482        assert!(result.is_err());
1483    }
1484
1485    #[tokio::test]
1486    async fn test_workflow_send_manifest_missing_lease_id() {
1487        let backend = MockBackend::new();
1488        let signer = MockSigner;
1489        let config = WorkflowConfig::default();
1490        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1491
1492        let mut state = DeploymentState::new("test", "akash1owner");
1493        state.step = Step::SendManifest;
1494        state.cert_pem = Some(vec![1, 2, 3]);
1495        state.key_pem = Some(vec![4, 5, 6]);
1496        state.sdl_content = Some(SIMPLE_SDL.to_string());
1497        // Missing lease_id
1498
1499        let result = workflow.advance(&mut state).await;
1500        assert!(result.is_err());
1501    }
1502
1503    #[tokio::test]
1504    async fn test_workflow_send_manifest_missing_cert() {
1505        let backend = MockBackend::new();
1506        let signer = MockSigner;
1507        let config = WorkflowConfig::default();
1508        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1509
1510        let mut state = DeploymentState::new("test", "akash1owner");
1511        state.step = Step::SendManifest;
1512        state.lease_id = Some(LeaseId {
1513            owner: "akash1owner".to_string(),
1514            dseq: 123456,
1515            gseq: 1,
1516            oseq: 1,
1517            provider: "akash1provider".to_string(),
1518        });
1519        state.sdl_content = Some(SIMPLE_SDL.to_string());
1520        // Missing cert and key
1521
1522        let result = workflow.advance(&mut state).await;
1523        assert!(result.is_err());
1524    }
1525
1526    #[tokio::test]
1527    async fn test_workflow_send_manifest_missing_key() {
1528        let backend = MockBackend::new();
1529        let signer = MockSigner;
1530        let config = WorkflowConfig::default();
1531        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1532
1533        let mut state = DeploymentState::new("test", "akash1owner");
1534        state.step = Step::SendManifest;
1535        state.lease_id = Some(LeaseId {
1536            owner: "akash1owner".to_string(),
1537            dseq: 123456,
1538            gseq: 1,
1539            oseq: 1,
1540            provider: "akash1provider".to_string(),
1541        });
1542        state.cert_pem = Some(vec![1, 2, 3]);
1543        state.sdl_content = Some(SIMPLE_SDL.to_string());
1544        // Missing key
1545
1546        let result = workflow.advance(&mut state).await;
1547        assert!(result.is_err());
1548    }
1549
1550    #[tokio::test]
1551    async fn test_workflow_send_manifest_missing_sdl() {
1552        let backend = MockBackend::new();
1553        let signer = MockSigner;
1554        let config = WorkflowConfig::default();
1555        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1556
1557        let mut state = DeploymentState::new("test", "akash1owner");
1558        state.step = Step::SendManifest;
1559        state.lease_id = Some(LeaseId {
1560            owner: "akash1owner".to_string(),
1561            dseq: 123456,
1562            gseq: 1,
1563            oseq: 1,
1564            provider: "akash1provider".to_string(),
1565        });
1566        state.cert_pem = Some(vec![1, 2, 3]);
1567        state.key_pem = Some(vec![4, 5, 6]);
1568        // Missing SDL
1569
1570        let result = workflow.advance(&mut state).await;
1571        assert!(result.is_err());
1572    }
1573
1574    #[tokio::test]
1575    async fn test_workflow_wait_for_endpoints_missing_key() {
1576        let backend = MockBackend::new();
1577        let signer = MockSigner;
1578        let config = WorkflowConfig::default();
1579        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1580
1581        let mut state = DeploymentState::new("test", "akash1owner");
1582        state.step = Step::WaitForEndpoints { attempts: 0 };
1583        state.lease_id = Some(LeaseId {
1584            owner: "akash1owner".to_string(),
1585            dseq: 123456,
1586            gseq: 1,
1587            oseq: 1,
1588            provider: "akash1provider".to_string(),
1589        });
1590        state.cert_pem = Some(vec![1, 2, 3]);
1591        // Missing key
1592
1593        let result = workflow.advance(&mut state).await;
1594        assert!(result.is_err());
1595    }
1596
1597    #[tokio::test]
1598    async fn test_workflow_wait_for_endpoints_timeout() {
1599        let backend = MockBackend::new();
1600        // Status shows not ready
1601        backend.set_provider_status(ProviderLeaseStatus {
1602            ready: false,
1603            endpoints: vec![],
1604        });
1605
1606        let signer = MockSigner;
1607        let mut config = WorkflowConfig::default();
1608        config.max_endpoint_wait_attempts = 1; // Short timeout
1609        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1610
1611        let mut state = DeploymentState::new("test", "akash1owner");
1612        state.step = Step::WaitForEndpoints { attempts: 0 };
1613        state.lease_id = Some(LeaseId {
1614            owner: "akash1owner".to_string(),
1615            dseq: 123456,
1616            gseq: 1,
1617            oseq: 1,
1618            provider: "akash1provider".to_string(),
1619        });
1620        state.cert_pem = Some(vec![1, 2, 3]);
1621        state.key_pem = Some(vec![4, 5, 6]);
1622
1623        // First attempt - not ready, should retry
1624        let result = workflow.advance(&mut state).await.unwrap();
1625        assert!(matches!(result, StepResult::Continue));
1626        assert!(matches!(state.step, Step::WaitForEndpoints { attempts: 1 }));
1627
1628        // Second attempt - max reached, should fail
1629        let result = workflow.advance(&mut state).await.unwrap();
1630        assert!(matches!(result, StepResult::Failed(_)));
1631    }
1632
1633    #[tokio::test]
1634    async fn test_workflow_wait_for_bids_missing_dseq() {
1635        let backend = MockBackend::new();
1636        let signer = MockSigner;
1637        let config = WorkflowConfig::default();
1638        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1639
1640        let mut state = DeploymentState::new("test", "akash1owner");
1641        state.step = Step::WaitForBids { waited_blocks: 0 };
1642        // Missing dseq
1643
1644        let result = workflow.advance(&mut state).await;
1645        assert!(result.is_err());
1646    }
1647
1648    #[tokio::test]
1649    async fn test_workflow_create_deployment_missing_sdl() {
1650        let backend = MockBackend::new();
1651        let signer = MockSigner;
1652        let config = WorkflowConfig::default();
1653        let workflow = DeploymentWorkflow::new(&backend, &signer, config);
1654
1655        let mut state = DeploymentState::new("test", "akash1owner");
1656        state.step = Step::CreateDeployment;
1657        // Missing SDL
1658
1659        let result = workflow.advance(&mut state).await;
1660        assert!(result.is_err());
1661    }
1662
1663    #[test]
1664    fn test_build_manifest_empty_sdl() {
1665        let result = build_manifest("akash1owner", "", 123);
1666        assert!(result.is_err());
1667        assert!(matches!(result.unwrap_err(), DeployError::Manifest(_)));
1668    }
1669}