auv-cli-invoke 0.0.22

Application Use Via... script-able OS automation library
use auv_media_macos::output::{MediaControlOutcome, NowPlayingOutput, SCHEMA_VERSION};
use auv_tracing::RunId;

use super::*;
use crate::{InvokeOutputOptions, InvokeResult};

fn now_playing(is_playing: bool, title: &str) -> NowPlayingOutput {
  NowPlayingOutput {
    schema_version: SCHEMA_VERSION,
    present: true,
    is_playing,
    source_bundle_id: Some("com.apple.Music".to_string()),
    title: Some(title.to_string()),
    artist: Some("The Artist".to_string()),
    album: Some("The Album".to_string()),
    duration_seconds: Some(245.5),
    elapsed_seconds: Some(61.25),
    playback_rate: Some(if is_playing { 1.0 } else { 0.0 }),
    content_item_id: Some("track-42".to_string()),
    supports_like: Some(true),
    is_liked: Some(false),
  }
}

#[test]
fn now_playing_human_output_exposes_the_current_media_state() {
  let result = now_playing(true, "Current Song");
  let output =
    InvokeCommandOutput::from_result(&result).expect("now-playing output should serialize").with_report(now_playing_report(&result));
  assert_eq!(output.result(), Some(&serde_json::to_value(&result).expect("fixture should serialize")));

  let invoke_result = InvokeResult::from_command_result(RunId::new(), &media_control_now_playing_invoke_command(), Ok(output));
  let human = invoke_result.render_to_string(InvokeOutputOptions::default()).expect("human output should render");

  assert!(human.contains("State: playing"));
  assert!(human.contains("Title: Current Song"));
  assert!(human.contains("Artist: The Artist"));
  assert!(human.contains("Album: The Album"));
  assert!(human.contains("Source: com.apple.Music"));
  assert!(human.contains("Elapsed: 61.250 s"));
  assert!(human.contains("Duration: 245.500 s"));
}

#[test]
fn now_playing_rejects_target_before_platform_access() {
  let input = crate::InvokeCommandInput {
    command_id: "mediaControl.nowPlaying".to_string(),
    target: Some(crate::ExecutionTarget::Application {
      id: "com.example.Player".to_string(),
    }),
    inputs: Default::default(),
    typed_args: None,
    dry_run: false,
    cancellation: crate::InvokeCancellation::new(),
  };
  let error = futures_executor::block_on(media_control_now_playing_invoke_command().invoke(input))
    .expect_err("target must fail before reading MediaRemote");
  assert_eq!(error.code, crate::FailureCode::InvalidTarget);
}

#[test]
fn media_commands_reject_target_before_platform_access() {
  for (command_id, command) in [
    ("mediaControl.play", media_control_play_invoke_command()),
    ("mediaControl.pause", media_control_pause_invoke_command()),
    ("mediaControl.togglePlayPause", media_control_toggle_play_pause_invoke_command()),
    ("mediaControl.next", media_control_next_invoke_command()),
    ("mediaControl.previous", media_control_previous_invoke_command()),
  ] {
    let input = crate::InvokeCommandInput {
      command_id: command_id.to_string(),
      target: Some(crate::ExecutionTarget::Application {
        id: "com.example.Player".to_string(),
      }),
      inputs: Default::default(),
      typed_args: None,
      dry_run: false,
      cancellation: crate::InvokeCancellation::new(),
    };
    let error = futures_executor::block_on(command.invoke(input)).expect_err("target must fail before MediaRemote");
    assert_eq!(error.code, crate::FailureCode::InvalidTarget);
  }
}

#[test]
fn media_control_human_output_exposes_command_verification_and_before_after_state() {
  for (command, invoke_command) in [
    ("play", media_control_play_invoke_command()),
    ("pause", media_control_pause_invoke_command()),
    ("toggle", media_control_toggle_play_pause_invoke_command()),
    ("next", media_control_next_invoke_command()),
    ("previous", media_control_previous_invoke_command()),
  ] {
    let result = MediaControlOutcome {
      command,
      before: now_playing(false, "Before Song"),
      after: now_playing(true, "After Song"),
      verified: true,
    };
    let output = media_control_output(&result).expect("media-control output should serialize");
    assert_eq!(output.result(), Some(&serde_json::to_value(&result).expect("fixture should serialize")));

    let invoke_result = InvokeResult::from_command_result(RunId::new(), &invoke_command, Ok(output));
    let human = invoke_result.render_to_string(InvokeOutputOptions::default()).expect("human output should render");

    assert!(human.contains(&format!("Command: {command}")), "{command} output was missing the command: {human}");
    assert!(human.contains("Verified: yes"), "{command} output was missing verification: {human}");
    assert!(
      human.contains("Before: paused: Before Song — The Artist (com.apple.Music)"),
      "{command} output was missing the before state: {human}"
    );
    assert!(
      human.contains("After: playing: After Song — The Artist (com.apple.Music)"),
      "{command} output was missing the after state: {human}"
    );
  }
}

#[test]
fn now_playing_report_still_has_a_meaningful_field_when_nothing_is_playing() {
  let result = NowPlayingOutput {
    schema_version: SCHEMA_VERSION,
    present: false,
    is_playing: false,
    source_bundle_id: None,
    title: None,
    artist: None,
    album: None,
    duration_seconds: None,
    elapsed_seconds: None,
    playback_rate: None,
    content_item_id: None,
    supports_like: None,
    is_liked: None,
  };

  assert_eq!(now_playing_report(&result).fields, [InvokeReportField::new("State", "nothing playing")]);
}

#[test]
fn media_help_explicitly_describes_forbidden_target() {
  let registry = crate::default_registry();
  let command = registry.resolve("mediaControl.play").unwrap();
  let help = crate::help::render_command_help(command);
  assert!(help.contains("Target forbidden"), "{help}");
  assert!(!help.contains("--target <TARGET>"), "{help}");
}