enwiro 0.3.57

Simplify your workflow with dedicated project environments for each workspace in your window manager
use std::io::Write;

use anyhow::Context;
use enwiro_sdk::rpc::{EnvMarkParams, EnwiroRpcClient};

use crate::CommandContext;

#[derive(clap::Args)]
#[command(about = "Set the status of the current environment")]
pub struct MarkArgs {
    #[arg(value_enum)]
    pub status: MarkStatus,
}

#[derive(clap::ValueEnum, Clone)]
pub enum MarkStatus {
    Ready,
    Active,
    Waiting,
    Done,
    Evergreen,
}

impl MarkStatus {
    fn as_str(&self) -> &'static str {
        match self {
            MarkStatus::Ready => "ready",
            MarkStatus::Active => "active",
            MarkStatus::Waiting => "waiting",
            MarkStatus::Done => "done",
            MarkStatus::Evergreen => "evergreen",
        }
    }
}

pub fn mark<W: Write>(context: &mut CommandContext<W>, args: MarkArgs) -> anyhow::Result<()> {
    let env_name = context.resolve_environment_name(&None)?;
    let status_label = args.status.as_str();

    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .context("could not start async runtime")?;

    rt.block_on(async {
        let client = enwiro_sdk::rpc::connect()
            .await
            .context("could not connect to enwiro-daemon")?;
        EnwiroRpcClient::env_mark(
            &client,
            EnvMarkParams {
                env_name: env_name.clone(),
                status: status_label.to_string(),
                source: enwiro_sdk::rpc::MarkSource::User,
            },
        )
        .await
        .map_err(|e| anyhow::anyhow!("daemon error: {e}"))?;
        Ok::<(), anyhow::Error>(())
    })?;

    writeln!(context.writer, "Marked '{}' as {}", env_name, status_label)
        .context("Could not write to output")?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Cursor;
    use std::path::Path;

    use enwiro_daemon::meta::{CookedPhase, EventType, Status, load_env_meta};

    use crate::test_utils::test_utilities::{
        AdapterLog, FakeContext, NotificationLog, context_object,
    };
    use rstest::rstest;

    fn run_mark_direct(
        workspaces_dir: &Path,
        env_name: &str,
        status: MarkStatus,
    ) -> (anyhow::Result<()>, String) {
        use enwiro_daemon::meta::{
            EventLogEntry, EventType as ET, StatusSource, load_env_meta, now_utc, save_env_meta,
        };

        let env_dir = workspaces_dir.join(env_name);
        let new_status = match &status {
            MarkStatus::Ready => Status::Cooked {
                phase: None,
                detail: None,
            },
            MarkStatus::Active => Status::Cooked {
                phase: Some(CookedPhase::Active),
                detail: None,
            },
            MarkStatus::Waiting => Status::Cooked {
                phase: Some(CookedPhase::Waiting),
                detail: None,
            },
            MarkStatus::Done => Status::Done { outcome: None },
            MarkStatus::Evergreen => Status::Evergreen,
        };

        let status_label = status.as_str();
        let now = now_utc();
        let mut meta = load_env_meta(&env_dir);
        meta.status = Some(new_status);
        meta.event_log.push(EventLogEntry {
            event_type: ET::StatusChange,
            detail: status_label.to_string(),
            set_by: Some(StatusSource::User),
            started: now,
            ended: Some(now),
        });

        let result = save_env_meta(&env_dir, &meta).map_err(anyhow::Error::from);

        let mut out: Cursor<Vec<u8>> = Cursor::new(vec![]);
        if result.is_ok() {
            let _ = writeln!(out, "Marked '{}' as {}", env_name, status_label);
        }
        let output = String::from_utf8(out.into_inner()).unwrap();
        (result, output)
    }

    #[rstest]
    fn mark_ready_sets_status(
        context_object: (tempfile::TempDir, FakeContext, AdapterLog, NotificationLog),
    ) {
        let (_temp_dir, mut context, _, _) = context_object;
        context.create_mock_environment("my-env");
        let workspaces = Path::new(&context.config.workspaces_directory);

        let (result, output) = run_mark_direct(workspaces, "my-env", MarkStatus::Ready);
        assert!(result.is_ok(), "mark ready must succeed: {:?}", result);
        assert!(output.contains("ready"));

        let meta = load_env_meta(&workspaces.join("my-env"));
        assert!(matches!(
            meta.status,
            Some(Status::Cooked { phase: None, .. })
        ));
    }

    #[rstest]
    fn mark_active_sets_status(
        context_object: (tempfile::TempDir, FakeContext, AdapterLog, NotificationLog),
    ) {
        let (_temp_dir, mut context, _, _) = context_object;
        context.create_mock_environment("my-env");
        let workspaces = Path::new(&context.config.workspaces_directory);

        let (result, _) = run_mark_direct(workspaces, "my-env", MarkStatus::Active);
        assert!(result.is_ok());

        let meta = load_env_meta(&workspaces.join("my-env"));
        assert!(matches!(
            meta.status,
            Some(Status::Cooked {
                phase: Some(CookedPhase::Active),
                ..
            })
        ));
    }

    #[rstest]
    fn mark_waiting_sets_status(
        context_object: (tempfile::TempDir, FakeContext, AdapterLog, NotificationLog),
    ) {
        let (_temp_dir, mut context, _, _) = context_object;
        context.create_mock_environment("my-env");
        let workspaces = Path::new(&context.config.workspaces_directory);

        let (result, _) = run_mark_direct(workspaces, "my-env", MarkStatus::Waiting);
        assert!(result.is_ok());

        let meta = load_env_meta(&workspaces.join("my-env"));
        assert!(matches!(
            meta.status,
            Some(Status::Cooked {
                phase: Some(CookedPhase::Waiting),
                ..
            })
        ));
    }

    #[rstest]
    fn mark_done_sets_status(
        context_object: (tempfile::TempDir, FakeContext, AdapterLog, NotificationLog),
    ) {
        let (_temp_dir, mut context, _, _) = context_object;
        context.create_mock_environment("my-env");
        let workspaces = Path::new(&context.config.workspaces_directory);

        let (result, _) = run_mark_direct(workspaces, "my-env", MarkStatus::Done);
        assert!(result.is_ok());

        let meta = load_env_meta(&workspaces.join("my-env"));
        assert!(matches!(meta.status, Some(Status::Done { .. })));
    }

    #[rstest]
    fn mark_evergreen_sets_status(
        context_object: (tempfile::TempDir, FakeContext, AdapterLog, NotificationLog),
    ) {
        let (_temp_dir, mut context, _, _) = context_object;
        context.create_mock_environment("my-env");
        let workspaces = Path::new(&context.config.workspaces_directory);

        let (result, _) = run_mark_direct(workspaces, "my-env", MarkStatus::Evergreen);
        assert!(result.is_ok());

        let meta = load_env_meta(&workspaces.join("my-env"));
        assert!(matches!(meta.status, Some(Status::Evergreen)));
    }

    #[rstest]
    fn mark_appends_event_log_entry(
        context_object: (tempfile::TempDir, FakeContext, AdapterLog, NotificationLog),
    ) {
        let (_temp_dir, mut context, _, _) = context_object;
        context.create_mock_environment("my-env");
        let workspaces = Path::new(&context.config.workspaces_directory);

        let (result, _) = run_mark_direct(workspaces, "my-env", MarkStatus::Active);
        assert!(result.is_ok());

        let meta = load_env_meta(&workspaces.join("my-env"));
        assert_eq!(meta.event_log.len(), 1);
        assert_eq!(meta.event_log[0].event_type, EventType::StatusChange);
        assert_eq!(meta.event_log[0].detail, "active");
        assert!(meta.event_log[0].ended.is_some());
    }

    #[rstest]
    fn mark_preserves_existing_metadata(
        context_object: (tempfile::TempDir, FakeContext, AdapterLog, NotificationLog),
    ) {
        let (_temp_dir, mut context, _, _) = context_object;
        context.create_mock_environment("my-env");
        let workspaces = Path::new(&context.config.workspaces_directory);
        let env_dir = workspaces.join("my-env");

        enwiro_daemon::meta::record_cook_metadata_per_env(
            &env_dir,
            "github",
            "owner/repo#1",
            Some("Test"),
            "my-env",
        );

        let (result, _) = run_mark_direct(workspaces, "my-env", MarkStatus::Done);
        assert!(result.is_ok());

        let meta = load_env_meta(&env_dir);
        assert_eq!(meta.cookbook.as_deref(), Some("github"));
        assert_eq!(meta.description.as_deref(), Some("Test"));
        assert!(matches!(meta.status, Some(Status::Done { .. })));
    }
}