auv-cli 0.0.29

AUV core command-line frontend
#![cfg(unix)]

use std::net::TcpListener;
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};

struct ChildGuard(Child);

impl Drop for ChildGuard {
  fn drop(&mut self) {
    let _ = self.0.kill();
    let _ = self.0.wait();
  }
}

#[test]
fn pair_connect_saves_the_credential_without_printing_it_and_consumes_the_token() {
  let directory = tempfile::tempdir().unwrap();
  let store = directory.path().join("pairings.json");
  let socket = directory.path().join("auv.sock");
  let discovery = directory.path().join("discovery.json");
  let profiles = directory.path().join("profiles.json");
  let port = TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port();
  let auv = env!("CARGO_BIN_EXE_auv");
  let mut daemon = ChildGuard(
    Command::new(auv)
      .args([
        "serve",
        "--listen",
        &format!("unix://{}", socket.display()),
        "--listen",
        &format!("http://127.0.0.1:{port}"),
        "--pairing-store",
        store.to_str().unwrap(),
        "--store-root",
        directory.path().join("store").to_str().unwrap(),
        "--discovery-file",
        discovery.to_str().unwrap(),
      ])
      .stdin(Stdio::null())
      .stdout(Stdio::null())
      .stderr(Stdio::inherit())
      .spawn()
      .unwrap(),
  );
  wait_for_path(&mut daemon.0, &discovery);

  let created = Command::new(auv).args(["devices", "pair", "create-token"]).env("AUV_DISCOVERY_FILE", &discovery).output().unwrap();
  assert!(created.status.success(), "{}", String::from_utf8_lossy(&created.stderr));
  let token = String::from_utf8(created.stdout).unwrap().trim().to_string();
  assert_eq!(token.len(), 32);
  assert!(token.bytes().all(|byte| byte.is_ascii_hexdigit()));
  assert!(!std::fs::read_to_string(&store).unwrap().contains(&token));

  let endpoint = format!("http://127.0.0.1:{port}");
  let connected = Command::new(auv)
    .args([
      "-vv",
      "devices",
      "pair",
      "--endpoint",
      &endpoint,
      "connect",
      "--token",
      &token,
      "--label",
      "Tablet",
      "--profile",
      "gpu",
      "--json",
    ])
    .env("AUV_CONFIG_PROFILES_FILE", &profiles)
    .output()
    .unwrap();
  assert!(connected.status.success(), "{}", String::from_utf8_lossy(&connected.stderr));
  let result: serde_json::Value = serde_json::from_slice(&connected.stdout).unwrap();
  assert_eq!(result["profile"], "gpu");
  assert_eq!(result["credentials_file"], profiles.to_string_lossy().as_ref());
  let output = format!("{}{}", String::from_utf8_lossy(&connected.stdout), String::from_utf8_lossy(&connected.stderr));
  assert!(!output.contains(&token));
  assert!(!output.contains("device_credential"));
  assert!(String::from_utf8_lossy(&connected.stderr).contains("PairingService"));

  let mut profile_document: serde_json::Value = serde_json::from_slice(&std::fs::read(&profiles).unwrap()).unwrap();
  let bearer = profile_document["profiles"]["gpu"]["device_credential"].as_str().unwrap().to_string();
  assert_eq!(bearer.len(), 64);
  assert!(bearer.bytes().all(|byte| byte.is_ascii_hexdigit()));
  assert!(!std::fs::read_to_string(&store).unwrap().contains(&bearer));

  // ROOT CAUSE:
  //
  // If a daemon reports an empty or changed Device name, a stale profile name
  // previously made a valid canonical-ID connection appear invalid.
  // The fix treats configured names as display/selection hints and verifies
  // paired identity using the canonical Device ID.
  profile_document["profiles"]["gpu"]["device_name"] = "stale-profile-name".into();
  std::fs::write(&profiles, serde_json::to_vec_pretty(&profile_document).unwrap()).unwrap();

  let listed = Command::new(auv)
    .args(["-vv", "devices", "list", "--json"])
    .env("AUV_CONFIG_PROFILES_FILE", &profiles)
    .env("AUV_DISCOVERY_FILE", directory.path().join("no-local-daemon.json"))
    .env_remove("AUV_ENDPOINT")
    .output()
    .unwrap();
  assert!(listed.status.success(), "{}", String::from_utf8_lossy(&listed.stderr));
  let devices: serde_json::Value = serde_json::from_slice(&listed.stdout).unwrap();
  assert_eq!(devices[0]["config_profile"], "gpu");
  assert_eq!(devices[0]["status"], "online", "{}", String::from_utf8_lossy(&listed.stderr));
  let expected_platform = if cfg!(target_os = "macos") {
    "DEVICE_PLATFORM_MACOS"
  } else if cfg!(target_os = "linux") {
    "DEVICE_PLATFORM_LINUX"
  } else if cfg!(target_os = "windows") {
    "DEVICE_PLATFORM_WINDOWS"
  } else {
    "DEVICE_PLATFORM_UNSPECIFIED"
  };
  assert_eq!(devices[0]["platform"], expected_platform, "online profiles must retain remote Device facts");

  profile_document["profiles"]["gpu"]["device_credential"] = "invalid-credential".into();
  std::fs::write(&profiles, serde_json::to_vec_pretty(&profile_document).unwrap()).unwrap();
  let unauthorized = Command::new(auv)
    .args(["devices", "list", "--json"])
    .env("AUV_CONFIG_PROFILES_FILE", &profiles)
    .env("AUV_DISCOVERY_FILE", directory.path().join("no-local-daemon.json"))
    .env_remove("AUV_ENDPOINT")
    .output()
    .unwrap();
  assert!(unauthorized.status.success(), "{}", String::from_utf8_lossy(&unauthorized.stderr));
  let devices: serde_json::Value = serde_json::from_slice(&unauthorized.stdout).unwrap();
  assert_eq!(devices[0]["status"], "unauthorized");

  let reused = Command::new(auv)
    .args([
      "devices",
      "pair",
      "--endpoint",
      &endpoint,
      "connect",
      "--token",
      &token,
      "--device-id",
      "device_other",
      "--label",
      "Other",
    ])
    .env("AUV_CONFIG_PROFILES_FILE", &profiles)
    .output()
    .unwrap();
  assert!(!reused.status.success());
  assert!(String::from_utf8_lossy(&reused.stderr).contains("invalid, expired, or has already been consumed"));
}

// ROOT CAUSE:
//
// If `--pairing-store` was set, every http:// listener required a bearer and
// the daemon guaranteed no owner channel, so an HTTP-only daemon could not
// issue its first pairing token.
//
// Before the fix, first pairing needed a hand-configured Unix listener (or a
// hidden Runner socket). The fix always binds a registered owner socket and
// keeps pairing on by default.
#[test]
fn http_only_daemon_issues_the_first_token_through_its_owner_socket() {
  use std::io::BufRead as _;

  let directory = tempfile::tempdir().unwrap();
  let discovery = directory.path().join("discovery.json");
  let profiles = directory.path().join("profiles.json");
  let port = TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port();
  let endpoint = format!("http://127.0.0.1:{port}");
  let auv = env!("CARGO_BIN_EXE_auv");
  let mut daemon = ChildGuard(
    Command::new(auv)
      .args([
        "serve",
        "--listen",
        &endpoint,
        "--store-root",
        directory.path().join("store").to_str().unwrap(),
        "--discovery-file",
        discovery.to_str().unwrap(),
      ])
      .stdin(Stdio::null())
      .stdout(Stdio::null())
      .stderr(Stdio::inherit())
      .spawn()
      .unwrap(),
  );
  wait_for_path(&mut daemon.0, &discovery);

  let anonymous = Command::new(auv).args(["devices", "pair", "--endpoint", &endpoint, "create-token"]).output().unwrap();
  assert!(!anonymous.status.success());
  assert!(String::from_utf8_lossy(&anonymous.stderr).contains("paired Device bearer required"));

  let created = Command::new(auv).args(["devices", "pair", "create-token"]).env("AUV_DISCOVERY_FILE", &discovery).output().unwrap();
  assert!(created.status.success(), "{}", String::from_utf8_lossy(&created.stderr));
  let token = String::from_utf8(created.stdout).unwrap().trim().to_string();
  assert!(directory.path().join("store").join("pairings.json").exists(), "pairing defaults to the store root");

  let connected = Command::new(auv)
    .args([
      "devices",
      "pair",
      "--endpoint",
      &endpoint,
      "connect",
      "--token",
      &token,
      "--label",
      "Laptop",
      "--profile",
      "laptop",
    ])
    .env("AUV_CONFIG_PROFILES_FILE", &profiles)
    .output()
    .unwrap();
  assert!(connected.status.success(), "{}", String::from_utf8_lossy(&connected.stderr));

  // An unregistered daemon must leave the default descriptor and socket to
  // the registered daemon while still serving its own private owner socket.
  let published = std::fs::read_to_string(&discovery).unwrap();
  let mut scratch = Command::new(auv)
    .args([
      "serve",
      "--no-register",
      "--store-root",
      directory.path().join("scratch-store").to_str().unwrap(),
    ])
    .env("AUV_DISCOVERY_FILE", &discovery)
    .stdin(Stdio::null())
    .stdout(Stdio::piped())
    .stderr(Stdio::inherit())
    .spawn()
    .unwrap();
  let mut ready = String::new();
  std::io::BufReader::new(scratch.stdout.take().unwrap()).read_line(&mut ready).unwrap();
  let _scratch = ChildGuard(scratch);
  let private = ready.trim().strip_prefix("auv serve: ").expect("unregistered daemon endpoint").to_string();
  assert!(private.starts_with("unix://"), "{private}");
  assert_ne!(private, format!("unix://{}", directory.path().join("auv.sock").display()));

  let private_token = Command::new(auv).args(["devices", "pair", "--endpoint", &private, "create-token"]).output().unwrap();
  assert!(private_token.status.success(), "{}", String::from_utf8_lossy(&private_token.stderr));
  assert_eq!(std::fs::read_to_string(&discovery).unwrap(), published);
}

fn wait_for_path(child: &mut Child, path: &std::path::Path) {
  let deadline = Instant::now() + Duration::from_secs(15);
  while Instant::now() < deadline {
    if path.exists() {
      return;
    }
    if let Some(status) = child.try_wait().unwrap() {
      panic!("daemon exited before readiness: {status}");
    }
    std::thread::sleep(Duration::from_millis(25));
  }
  panic!("daemon did not publish {}", path.display());
}