test(hands): expand Slideshow tests (4→34) and fix Clip invalid action test
Slideshow: add navigation edge cases, autoplay/pause/resume, spotlight/ laser/highlight defaults, content block deserialization, Hand trait dispatch, and add_slide helper tests. Clip: fix test_execute_invalid_action to expect Err (execute returns HandError for unknown variants). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -640,3 +640,390 @@ impl Hand for ClipHand {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
// === Config & Defaults ===
|
||||
|
||||
#[test]
|
||||
fn test_hand_config() {
|
||||
let hand = ClipHand::new();
|
||||
assert_eq!(hand.config().id, "clip");
|
||||
assert_eq!(hand.config().name, "视频剪辑");
|
||||
assert!(!hand.config().needs_approval);
|
||||
assert!(hand.config().enabled);
|
||||
assert!(hand.config().tags.contains(&"video".to_string()));
|
||||
assert!(hand.config().input_schema.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_impl() {
|
||||
let hand = ClipHand::default();
|
||||
assert_eq!(hand.config().id, "clip");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_needs_approval() {
|
||||
let hand = ClipHand::new();
|
||||
assert!(!hand.needs_approval());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_dependencies() {
|
||||
let hand = ClipHand::new();
|
||||
let deps = hand.check_dependencies().unwrap();
|
||||
// May or may not find ffmpeg depending on test environment
|
||||
// Just verify it doesn't panic
|
||||
let _ = deps;
|
||||
}
|
||||
|
||||
// === VideoFormat ===
|
||||
|
||||
#[test]
|
||||
fn test_video_format_default() {
|
||||
assert!(matches!(VideoFormat::default(), VideoFormat::Mp4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_video_format_deserialize() {
|
||||
let fmt: VideoFormat = serde_json::from_value(json!("mp4")).unwrap();
|
||||
assert!(matches!(fmt, VideoFormat::Mp4));
|
||||
|
||||
let fmt: VideoFormat = serde_json::from_value(json!("webm")).unwrap();
|
||||
assert!(matches!(fmt, VideoFormat::Webm));
|
||||
|
||||
let fmt: VideoFormat = serde_json::from_value(json!("gif")).unwrap();
|
||||
assert!(matches!(fmt, VideoFormat::Gif));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_video_format_serialize() {
|
||||
assert_eq!(serde_json::to_value(&VideoFormat::Mp4).unwrap(), "mp4");
|
||||
assert_eq!(serde_json::to_value(&VideoFormat::Webm).unwrap(), "webm");
|
||||
}
|
||||
|
||||
// === Resolution ===
|
||||
|
||||
#[test]
|
||||
fn test_resolution_default() {
|
||||
assert!(matches!(Resolution::default(), Resolution::Original));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolution_presets() {
|
||||
let r: Resolution = serde_json::from_value(json!("p720")).unwrap();
|
||||
assert!(matches!(r, Resolution::P720));
|
||||
|
||||
let r: Resolution = serde_json::from_value(json!("p1080")).unwrap();
|
||||
assert!(matches!(r, Resolution::P1080));
|
||||
|
||||
let r: Resolution = serde_json::from_value(json!("p4k")).unwrap();
|
||||
assert!(matches!(r, Resolution::P4k));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolution_custom() {
|
||||
let r: Resolution = serde_json::from_value(json!({"custom": {"width": 800, "height": 600}})).unwrap();
|
||||
match r {
|
||||
Resolution::Custom { width, height } => {
|
||||
assert_eq!(width, 800);
|
||||
assert_eq!(height, 600);
|
||||
}
|
||||
_ => panic!("Expected Custom"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolution_serialize() {
|
||||
assert_eq!(serde_json::to_value(&Resolution::P720).unwrap(), "p720");
|
||||
assert_eq!(serde_json::to_value(&Resolution::Original).unwrap(), "original");
|
||||
}
|
||||
|
||||
// === TrimConfig ===
|
||||
|
||||
#[test]
|
||||
fn test_trim_config_deserialize() {
|
||||
let config: TrimConfig = serde_json::from_value(json!({
|
||||
"inputPath": "/input.mp4",
|
||||
"outputPath": "/output.mp4",
|
||||
"startTime": 5.0,
|
||||
"duration": 10.0
|
||||
})).unwrap();
|
||||
assert_eq!(config.input_path, "/input.mp4");
|
||||
assert_eq!(config.output_path, "/output.mp4");
|
||||
assert_eq!(config.start_time, Some(5.0));
|
||||
assert_eq!(config.duration, Some(10.0));
|
||||
assert!(config.end_time.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trim_config_minimal() {
|
||||
let config: TrimConfig = serde_json::from_value(json!({
|
||||
"inputPath": "/in.mp4",
|
||||
"outputPath": "/out.mp4"
|
||||
})).unwrap();
|
||||
assert!(config.start_time.is_none());
|
||||
assert!(config.end_time.is_none());
|
||||
assert!(config.duration.is_none());
|
||||
}
|
||||
|
||||
// === ConvertConfig ===
|
||||
|
||||
#[test]
|
||||
fn test_convert_config_deserialize() {
|
||||
let config: ConvertConfig = serde_json::from_value(json!({
|
||||
"inputPath": "/input.avi",
|
||||
"outputPath": "/output.mp4",
|
||||
"format": "mp4",
|
||||
"resolution": "p1080",
|
||||
"videoBitrate": "4M",
|
||||
"audioBitrate": "192k"
|
||||
})).unwrap();
|
||||
assert_eq!(config.input_path, "/input.avi");
|
||||
assert!(matches!(config.format, VideoFormat::Mp4));
|
||||
assert!(matches!(config.resolution, Resolution::P1080));
|
||||
assert_eq!(config.video_bitrate, Some("4M".to_string()));
|
||||
assert_eq!(config.audio_bitrate, Some("192k".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convert_config_defaults() {
|
||||
let config: ConvertConfig = serde_json::from_value(json!({
|
||||
"inputPath": "/in.mp4",
|
||||
"outputPath": "/out.mp4"
|
||||
})).unwrap();
|
||||
assert!(matches!(config.format, VideoFormat::Mp4));
|
||||
assert!(matches!(config.resolution, Resolution::Original));
|
||||
assert!(config.video_bitrate.is_none());
|
||||
assert!(config.audio_bitrate.is_none());
|
||||
}
|
||||
|
||||
// === ThumbnailConfig ===
|
||||
|
||||
#[test]
|
||||
fn test_thumbnail_config_deserialize() {
|
||||
let config: ThumbnailConfig = serde_json::from_value(json!({
|
||||
"inputPath": "/video.mp4",
|
||||
"outputPath": "/thumb.jpg",
|
||||
"time": 5.0,
|
||||
"width": 320,
|
||||
"height": 240
|
||||
})).unwrap();
|
||||
assert_eq!(config.input_path, "/video.mp4");
|
||||
assert_eq!(config.time, 5.0);
|
||||
assert_eq!(config.width, Some(320));
|
||||
assert_eq!(config.height, Some(240));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_thumbnail_config_defaults() {
|
||||
let config: ThumbnailConfig = serde_json::from_value(json!({
|
||||
"inputPath": "/v.mp4",
|
||||
"outputPath": "/t.jpg"
|
||||
})).unwrap();
|
||||
assert_eq!(config.time, 0.0);
|
||||
assert!(config.width.is_none());
|
||||
assert!(config.height.is_none());
|
||||
}
|
||||
|
||||
// === ConcatConfig ===
|
||||
|
||||
#[test]
|
||||
fn test_concat_config_deserialize() {
|
||||
let config: ConcatConfig = serde_json::from_value(json!({
|
||||
"inputPaths": ["/a.mp4", "/b.mp4"],
|
||||
"outputPath": "/merged.mp4"
|
||||
})).unwrap();
|
||||
assert_eq!(config.input_paths.len(), 2);
|
||||
assert_eq!(config.output_path, "/merged.mp4");
|
||||
}
|
||||
|
||||
// === VideoInfo ===
|
||||
|
||||
#[test]
|
||||
fn test_video_info_deserialize() {
|
||||
let info: VideoInfo = serde_json::from_value(json!({
|
||||
"path": "/test.mp4",
|
||||
"durationSecs": 120.5,
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"fps": 30.0,
|
||||
"format": "mp4",
|
||||
"videoCodec": "h264",
|
||||
"audioCodec": "aac",
|
||||
"bitrateKbps": 5000,
|
||||
"fileSizeBytes": 75_000_000
|
||||
})).unwrap();
|
||||
assert_eq!(info.path, "/test.mp4");
|
||||
assert_eq!(info.duration_secs, 120.5);
|
||||
assert_eq!(info.width, 1920);
|
||||
assert_eq!(info.fps, 30.0);
|
||||
assert_eq!(info.video_codec, "h264");
|
||||
assert_eq!(info.audio_codec, Some("aac".to_string()));
|
||||
assert_eq!(info.bitrate_kbps, Some(5000));
|
||||
assert_eq!(info.file_size_bytes, 75_000_000);
|
||||
}
|
||||
|
||||
// === ClipAction Deserialization ===
|
||||
|
||||
#[test]
|
||||
fn test_action_trim() {
|
||||
let action: ClipAction = serde_json::from_value(json!({
|
||||
"action": "trim",
|
||||
"config": {
|
||||
"inputPath": "/in.mp4",
|
||||
"outputPath": "/out.mp4",
|
||||
"startTime": 1.0,
|
||||
"endTime": 5.0
|
||||
}
|
||||
})).unwrap();
|
||||
match action {
|
||||
ClipAction::Trim { config } => {
|
||||
assert_eq!(config.input_path, "/in.mp4");
|
||||
assert_eq!(config.start_time, Some(1.0));
|
||||
}
|
||||
_ => panic!("Expected Trim"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_action_convert() {
|
||||
let action: ClipAction = serde_json::from_value(json!({
|
||||
"action": "convert",
|
||||
"config": {
|
||||
"inputPath": "/in.avi",
|
||||
"outputPath": "/out.mp4"
|
||||
}
|
||||
})).unwrap();
|
||||
assert!(matches!(action, ClipAction::Convert { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_action_resize() {
|
||||
let action: ClipAction = serde_json::from_value(json!({
|
||||
"action": "resize",
|
||||
"input_path": "/in.mp4",
|
||||
"output_path": "/out.mp4",
|
||||
"resolution": "p720"
|
||||
})).unwrap();
|
||||
match action {
|
||||
ClipAction::Resize { input_path, resolution, .. } => {
|
||||
assert_eq!(input_path, "/in.mp4");
|
||||
assert!(matches!(resolution, Resolution::P720));
|
||||
}
|
||||
_ => panic!("Expected Resize"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_action_thumbnail() {
|
||||
let action: ClipAction = serde_json::from_value(json!({
|
||||
"action": "thumbnail",
|
||||
"config": {
|
||||
"inputPath": "/in.mp4",
|
||||
"outputPath": "/thumb.jpg"
|
||||
}
|
||||
})).unwrap();
|
||||
assert!(matches!(action, ClipAction::Thumbnail { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_action_concat() {
|
||||
let action: ClipAction = serde_json::from_value(json!({
|
||||
"action": "concat",
|
||||
"config": {
|
||||
"inputPaths": ["/a.mp4", "/b.mp4"],
|
||||
"outputPath": "/out.mp4"
|
||||
}
|
||||
})).unwrap();
|
||||
assert!(matches!(action, ClipAction::Concat { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_action_info() {
|
||||
let action: ClipAction = serde_json::from_value(json!({
|
||||
"action": "info",
|
||||
"path": "/video.mp4"
|
||||
})).unwrap();
|
||||
match action {
|
||||
ClipAction::Info { path } => assert_eq!(path, "/video.mp4"),
|
||||
_ => panic!("Expected Info"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_action_check_ffmpeg() {
|
||||
let action: ClipAction = serde_json::from_value(json!({"action": "check_ffmpeg"})).unwrap();
|
||||
assert!(matches!(action, ClipAction::CheckFfmpeg));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_action_invalid() {
|
||||
let result = serde_json::from_value::<ClipAction>(json!({"action": "nonexistent"}));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// === Hand execute dispatch ===
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_execute_check_ffmpeg() {
|
||||
let hand = ClipHand::new();
|
||||
let ctx = HandContext::default();
|
||||
let result = hand.execute(&ctx, json!({"action": "check_ffmpeg"})).await.unwrap();
|
||||
// Just verify it doesn't crash and returns a valid result
|
||||
assert!(result.output.is_object());
|
||||
// "available" field should exist
|
||||
assert!(result.output["available"].is_boolean());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_execute_invalid_action() {
|
||||
let hand = ClipHand::new();
|
||||
let ctx = HandContext::default();
|
||||
let result = hand.execute(&ctx, json!({"action": "bogus"})).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// === Status ===
|
||||
|
||||
#[test]
|
||||
fn test_status() {
|
||||
let hand = ClipHand::new();
|
||||
let status = hand.status();
|
||||
// Either Idle (ffmpeg found) or Failed (not found) — just verify it doesn't panic
|
||||
assert!(matches!(status, crate::HandStatus::Idle | crate::HandStatus::Failed));
|
||||
}
|
||||
|
||||
// === Roundtrip ===
|
||||
|
||||
#[test]
|
||||
fn test_trim_action_roundtrip() {
|
||||
let json = json!({
|
||||
"action": "trim",
|
||||
"config": {
|
||||
"inputPath": "/in.mp4",
|
||||
"outputPath": "/out.mp4",
|
||||
"startTime": 2.0,
|
||||
"duration": 5.0
|
||||
}
|
||||
});
|
||||
let action: ClipAction = serde_json::from_value(json).unwrap();
|
||||
let serialized = serde_json::to_value(&action).unwrap();
|
||||
assert_eq!(serialized["action"], "trim");
|
||||
assert_eq!(serialized["config"]["inputPath"], "/in.mp4");
|
||||
assert_eq!(serialized["config"]["startTime"], 2.0);
|
||||
assert_eq!(serialized["config"]["duration"], 5.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_info_action_roundtrip() {
|
||||
let json = json!({"action": "info", "path": "/video.mp4"});
|
||||
let action: ClipAction = serde_json::from_value(json).unwrap();
|
||||
let serialized = serde_json::to_value(&action).unwrap();
|
||||
assert_eq!(serialized["action"], "info");
|
||||
assert_eq!(serialized["path"], "/video.mp4");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user