use clap::Subcommand;
use mockforge_core::snapshots::{SnapshotComponents, SnapshotManager};
use mockforge_core::Result;
use std::path::PathBuf;
use tracing::info;
#[derive(Subcommand, Debug)]
pub enum SnapshotCommands {
Save {
name: String,
#[arg(long)]
description: Option<String>,
#[arg(long, default_value = "default")]
workspace: String,
#[arg(long, value_delimiter = ',')]
components: Option<Vec<String>>,
},
Load {
name: String,
#[arg(long, default_value = "default")]
workspace: String,
#[arg(long, value_delimiter = ',')]
components: Option<Vec<String>>,
#[arg(long)]
dry_run: bool,
},
List {
#[arg(long, default_value = "default")]
workspace: String,
},
Delete {
name: String,
#[arg(long, default_value = "default")]
workspace: String,
},
Info {
name: String,
#[arg(long, default_value = "default")]
workspace: String,
},
Validate {
name: String,
#[arg(long, default_value = "default")]
workspace: String,
},
}
fn parse_components(components: Option<Vec<String>>) -> SnapshotComponents {
if let Some(comp_list) = components {
let comp_set: std::collections::HashSet<String> =
comp_list.iter().map(|s| s.to_lowercase()).collect();
SnapshotComponents {
unified_state: comp_set.is_empty()
|| comp_set.contains("unified_state")
|| comp_set.contains("unified-state"),
vbr_state: comp_set.is_empty()
|| comp_set.contains("vbr_state")
|| comp_set.contains("vbr-state"),
recorder_state: comp_set.is_empty()
|| comp_set.contains("recorder_state")
|| comp_set.contains("recorder-state"),
workspace_config: comp_set.is_empty()
|| comp_set.contains("workspace_config")
|| comp_set.contains("workspace-config"),
protocols: Vec::new(), }
} else {
SnapshotComponents::all()
}
}
fn strip_unintegrated_components(
mut components: SnapshotComponents,
) -> (SnapshotComponents, Vec<&'static str>) {
let mut skipped = Vec::new();
if components.unified_state {
components.unified_state = false;
skipped.push("unified_state");
}
if components.vbr_state {
components.vbr_state = false;
skipped.push("vbr_state");
}
if components.recorder_state {
components.recorder_state = false;
skipped.push("recorder_state");
}
if components.workspace_config {
components.workspace_config = false;
skipped.push("workspace_config");
}
(components, skipped)
}
pub async fn handle_snapshot_command(command: SnapshotCommands) -> Result<()> {
let snapshot_dir = std::env::var("MOCKFORGE_SNAPSHOT_DIR").ok().map(PathBuf::from);
let manager = SnapshotManager::new(snapshot_dir);
match command {
SnapshotCommands::Save {
name,
description,
workspace,
components,
} => {
info!("Saving snapshot '{}' for workspace '{}'", name, workspace);
let components = parse_components(components);
let (components, skipped_components) = strip_unintegrated_components(components);
if !skipped_components.is_empty() {
println!(
"âš Snapshot state integration is partial in CLI mode; skipping unsupported components: {}",
skipped_components.join(", ")
);
}
let manifest = manager
.save_snapshot(
name.clone(),
description,
workspace.clone(),
components,
None,
None,
None,
None,
)
.await?;
println!("✓ Snapshot '{}' saved successfully", name);
println!(" Workspace: {}", workspace);
println!(" Size: {} bytes", manifest.size_bytes);
println!(" Checksum: {}", manifest.checksum);
if let Some(desc) = &manifest.description {
println!(" Description: {}", desc);
}
}
SnapshotCommands::Load {
name,
workspace,
components,
dry_run,
} => {
if dry_run {
info!("Validating snapshot '{}' for workspace '{}' (dry run)", name, workspace);
let is_valid = manager.validate_snapshot(name.clone(), workspace.clone()).await?;
if is_valid {
println!("✓ Snapshot '{}' is valid", name);
} else {
println!("✗ Snapshot '{}' failed validation", name);
return Err(mockforge_core::Error::generic("Snapshot validation failed"));
}
} else {
info!("Loading snapshot '{}' for workspace '{}'", name, workspace);
let components = components.map(|c| {
let requested = parse_components(Some(c));
let (filtered, skipped_components) = strip_unintegrated_components(requested);
if !skipped_components.is_empty() {
println!(
"âš Snapshot restore integration is partial in CLI mode; skipping unsupported components: {}",
skipped_components.join(", ")
);
}
filtered
});
let (manifest, vbr_state, recorder_state) = manager
.load_snapshot(name.clone(), workspace.clone(), components, None, None)
.await?;
println!("✓ Snapshot '{}' loaded successfully", name);
println!(" Workspace: {}", workspace);
println!(" Created: {}", manifest.created_at.format("%Y-%m-%d %H:%M:%S UTC"));
if let Some(desc) = &manifest.description {
println!(" Description: {}", desc);
}
if vbr_state.is_some() {
println!(" VBR state: loaded (restore manually if needed)");
}
if recorder_state.is_some() {
println!(" Recorder state: loaded (restore manually if needed)");
}
}
}
SnapshotCommands::List { workspace } => {
info!("Listing snapshots for workspace '{}'", workspace);
let snapshots = manager.list_snapshots(&workspace).await?;
if snapshots.is_empty() {
println!("No snapshots found for workspace '{}'", workspace);
} else {
println!("Snapshots for workspace '{}':", workspace);
println!();
for snapshot in snapshots {
println!(" {}", snapshot.name);
if let Some(desc) = &snapshot.description {
println!(" Description: {}", desc);
}
println!(
" Created: {}",
snapshot.created_at.format("%Y-%m-%d %H:%M:%S UTC")
);
println!(" Size: {} bytes", snapshot.size_bytes);
println!();
}
}
}
SnapshotCommands::Delete { name, workspace } => {
info!("Deleting snapshot '{}' for workspace '{}'", name, workspace);
manager.delete_snapshot(name.clone(), workspace.clone()).await?;
println!("✓ Snapshot '{}' deleted successfully", name);
}
SnapshotCommands::Info { name, workspace } => {
info!("Getting info for snapshot '{}' in workspace '{}'", name, workspace);
let manifest = manager.get_snapshot_info(name.clone(), workspace.clone()).await?;
println!("Snapshot: {}", manifest.name);
println!(" Workspace: {}", manifest.workspace_id);
println!(" Created: {}", manifest.created_at.format("%Y-%m-%d %H:%M:%S UTC"));
println!(" Size: {} bytes", manifest.size_bytes);
println!(" Checksum: {}", manifest.checksum);
if let Some(desc) = &manifest.description {
println!(" Description: {}", desc);
}
println!(" Components:");
println!(" Unified State: {}", manifest.components.unified_state);
println!(" VBR State: {}", manifest.components.vbr_state);
println!(" Recorder State: {}", manifest.components.recorder_state);
println!(" Workspace Config: {}", manifest.components.workspace_config);
if manifest.components.protocols.is_empty() {
println!(" Protocols: all");
} else {
println!(" Protocols: {}", manifest.components.protocols.join(", "));
}
}
SnapshotCommands::Validate { name, workspace } => {
info!("Validating snapshot '{}' for workspace '{}'", name, workspace);
let is_valid = manager.validate_snapshot(name.clone(), workspace.clone()).await?;
if is_valid {
println!("✓ Snapshot '{}' is valid", name);
} else {
println!("✗ Snapshot '{}' failed validation (checksum mismatch)", name);
return Err(mockforge_core::Error::generic("Snapshot validation failed"));
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_components_none() {
let components = parse_components(None);
assert!(components.unified_state);
assert!(components.vbr_state);
assert!(components.recorder_state);
assert!(components.workspace_config);
assert!(components.protocols.is_empty());
}
#[test]
fn test_parse_components_empty_vec() {
let components = parse_components(Some(vec![]));
assert!(components.unified_state);
assert!(components.vbr_state);
assert!(components.recorder_state);
assert!(components.workspace_config);
}
#[test]
fn test_parse_components_single_underscore() {
let components = parse_components(Some(vec!["unified_state".to_string()]));
assert!(components.unified_state);
assert!(!components.vbr_state);
assert!(!components.recorder_state);
assert!(!components.workspace_config);
}
#[test]
fn test_parse_components_single_dash() {
let components = parse_components(Some(vec!["vbr-state".to_string()]));
assert!(!components.unified_state);
assert!(components.vbr_state);
assert!(!components.recorder_state);
assert!(!components.workspace_config);
}
#[test]
fn test_parse_components_multiple() {
let components =
parse_components(Some(vec!["unified_state".to_string(), "recorder-state".to_string()]));
assert!(components.unified_state);
assert!(!components.vbr_state);
assert!(components.recorder_state);
assert!(!components.workspace_config);
}
#[test]
fn test_parse_components_case_insensitive() {
let components = parse_components(Some(vec!["UNIFIED_STATE".to_string()]));
assert!(components.unified_state);
}
#[test]
fn test_parse_components_workspace_config() {
let components = parse_components(Some(vec!["workspace_config".to_string()]));
assert!(!components.unified_state);
assert!(!components.vbr_state);
assert!(!components.recorder_state);
assert!(components.workspace_config);
}
#[test]
fn test_parse_components_workspace_config_dash() {
let components = parse_components(Some(vec!["workspace-config".to_string()]));
assert!(components.workspace_config);
}
#[test]
fn test_parse_components_all_explicit() {
let components = parse_components(Some(vec![
"unified_state".to_string(),
"vbr_state".to_string(),
"recorder_state".to_string(),
"workspace_config".to_string(),
]));
assert!(components.unified_state);
assert!(components.vbr_state);
assert!(components.recorder_state);
assert!(components.workspace_config);
}
#[test]
fn test_snapshot_commands_enum_variants() {
let _save = SnapshotCommands::Save {
name: "test".to_string(),
description: Some("test description".to_string()),
workspace: "default".to_string(),
components: None,
};
let _load = SnapshotCommands::Load {
name: "test".to_string(),
workspace: "default".to_string(),
components: None,
dry_run: false,
};
let _list = SnapshotCommands::List {
workspace: "default".to_string(),
};
let _delete = SnapshotCommands::Delete {
name: "test".to_string(),
workspace: "default".to_string(),
};
let _info = SnapshotCommands::Info {
name: "test".to_string(),
workspace: "default".to_string(),
};
let _validate = SnapshotCommands::Validate {
name: "test".to_string(),
workspace: "default".to_string(),
};
}
#[test]
fn test_snapshot_commands_debug() {
let cmd = SnapshotCommands::Save {
name: "test".to_string(),
description: None,
workspace: "default".to_string(),
components: None,
};
let debug_str = format!("{:?}", cmd);
assert!(debug_str.contains("Save"));
assert!(debug_str.contains("test"));
}
}