a3s-box-runtime 3.2.3

MicroVM runtime engine — VM lifecycle, OCI images, attestation, networking
Documentation
use super::*;

#[cfg(target_os = "windows")]
#[test]
fn test_append_windows_guest_stream_uses_shared_phase_and_keeps_partial_lines() {
    let tmp = tempfile::tempdir().unwrap();
    let stdout_source = tmp.path().join("stdout.source");
    let stderr_source = tmp.path().join("stderr.source");
    let stdout_destination = tmp.path().join("stdout.destination");
    let stderr_destination = tmp.path().join("stderr.destination");
    std::fs::write(
        &stdout_source,
        concat!(
            "init.krun: mount_filesystems ok\n",
            "init.krun: business\n",
            "init.krun: config parsed",
        ),
    )
    .unwrap();
    std::fs::write(
        &stderr_source,
        concat!(
            "init.krun: execvp(/bin/app) starting\n",
            "init.krun: mount_filesystems ok\n",
        ),
    )
    .unwrap();

    let filter = a3s_box_core::log::RuntimeConsoleFilter::new();
    append_windows_guest_stream(&stdout_source, &stdout_destination, &filter).unwrap();
    append_windows_guest_stream(&stderr_source, &stderr_destination, &filter).unwrap();

    assert_eq!(
        std::fs::read_to_string(stdout_destination).unwrap(),
        "init.krun: business\ninit.krun: config parsed"
    );
    assert_eq!(
        std::fs::read_to_string(stderr_destination).unwrap(),
        "init.krun: mount_filesystems ok\n"
    );
}

#[cfg(target_os = "windows")]
#[test]
fn test_collect_windows_guest_result_is_idempotent() {
    let tmp = tempfile::tempdir().unwrap();
    let box_dir = tmp.path().join("box");
    let rootfs = box_dir.join("rootfs");
    std::fs::create_dir_all(&rootfs).unwrap();
    std::fs::write(rootfs.join(WINDOWS_GUEST_STDOUT), "once\n").unwrap();
    std::fs::write(rootfs.join(WINDOWS_GUEST_STDERR), "error once\n").unwrap();
    std::fs::write(rootfs.join(WINDOWS_GUEST_EXIT_CODE), "7\n").unwrap();

    let config = a3s_box_core::log::LogConfig::default();
    assert_eq!(
        collect_windows_guest_result(&box_dir, &config, 0).unwrap(),
        7
    );
    assert_eq!(
        collect_windows_guest_result(&box_dir, &config, 0).unwrap(),
        7
    );

    let logs = box_dir.join("logs");
    assert_eq!(
        std::fs::read_to_string(logs.join("console.log")).unwrap(),
        "once\n"
    );
    assert_eq!(
        std::fs::read_to_string(logs.join("console.err.log")).unwrap(),
        "error once\n"
    );
    let json = std::fs::read_to_string(logs.join("container.json")).unwrap();
    assert_eq!(json.matches("\"log\":\"once\\n\"").count(), 1);
    assert_eq!(json.matches("\"log\":\"error once\\n\"").count(), 1);
}

#[cfg(target_os = "windows")]
#[test]
fn test_collect_windows_guest_result_does_not_replay_drained_live_logs() {
    let tmp = tempfile::tempdir().unwrap();
    let box_dir = tmp.path().join("box");
    let rootfs = box_dir.join("rootfs");
    let logs = box_dir.join("logs");
    std::fs::create_dir_all(&rootfs).unwrap();
    std::fs::create_dir_all(&logs).unwrap();
    std::fs::write(rootfs.join(WINDOWS_GUEST_STDOUT), "live once\n").unwrap();
    std::fs::write(rootfs.join(WINDOWS_GUEST_STDERR), "live error once\n").unwrap();
    std::fs::write(rootfs.join(WINDOWS_GUEST_EXIT_CODE), "4\n").unwrap();
    std::fs::write(rootfs.join(WINDOWS_LIVE_LOGS_DRAINED_MARKER), "drained\n").unwrap();
    let live_json =
        "{\"log\":\"live once\\n\",\"stream\":\"stdout\",\"time\":\"2026-01-01T00:00:00Z\"}\n";
    std::fs::write(logs.join("container.json"), live_json).unwrap();

    let config = a3s_box_core::log::LogConfig::default();
    assert_eq!(
        collect_windows_guest_result(&box_dir, &config, 0).unwrap(),
        4
    );

    assert_eq!(
        std::fs::read_to_string(logs.join("container.json")).unwrap(),
        live_json
    );
    assert_eq!(
        std::fs::read_to_string(logs.join("console.log")).unwrap(),
        "live once\n"
    );
    assert_eq!(
        std::fs::read_to_string(logs.join("console.err.log")).unwrap(),
        "live error once\n"
    );
    assert!(rootfs.join(WINDOWS_GUEST_RESULT_MARKER).exists());
}

#[cfg(target_os = "windows")]
#[test]
fn test_collect_windows_guest_result_replaces_marker_symlink_without_touching_target() {
    let tmp = tempfile::tempdir().unwrap();
    let box_dir = tmp.path().join("box");
    let rootfs = box_dir.join("rootfs");
    std::fs::create_dir_all(&rootfs).unwrap();
    std::fs::write(rootfs.join(WINDOWS_GUEST_STDOUT), "safe output\n").unwrap();
    std::fs::write(rootfs.join(WINDOWS_GUEST_STDERR), "").unwrap();
    std::fs::write(rootfs.join(WINDOWS_GUEST_EXIT_CODE), "0\n").unwrap();

    let host_target = tmp.path().join("host-target.txt");
    std::fs::write(&host_target, "host secret").unwrap();
    let marker = rootfs.join(WINDOWS_GUEST_RESULT_MARKER);
    match std::os::windows::fs::symlink_file(&host_target, &marker) {
        Ok(()) => {}
        Err(error) if error.raw_os_error() == Some(1314) => return,
        Err(error) => panic!("failed to create marker symlink: {error}"),
    }

    let config = a3s_box_core::log::LogConfig::default();
    assert_eq!(
        collect_windows_guest_result(&box_dir, &config, 0).unwrap(),
        0
    );
    assert_eq!(
        std::fs::read_to_string(&host_target).unwrap(),
        "host secret"
    );
    assert_eq!(std::fs::read_to_string(&marker).unwrap(), "collected\n");
    assert!(!std::fs::symlink_metadata(marker)
        .unwrap()
        .file_type()
        .is_symlink());
}

#[cfg(target_os = "windows")]
#[test]
fn test_collect_windows_guest_result_refuses_stream_symlink() {
    let tmp = tempfile::tempdir().unwrap();
    let box_dir = tmp.path().join("box");
    let rootfs = box_dir.join("rootfs");
    std::fs::create_dir_all(&rootfs).unwrap();
    let host_secret = tmp.path().join("host-secret.txt");
    std::fs::write(&host_secret, "must not be logged\n").unwrap();
    match std::os::windows::fs::symlink_file(&host_secret, rootfs.join(WINDOWS_GUEST_STDOUT)) {
        Ok(()) => {}
        Err(error) if error.raw_os_error() == Some(1314) => return,
        Err(error) => panic!("failed to create stream symlink: {error}"),
    }
    std::fs::write(rootfs.join(WINDOWS_GUEST_STDERR), "").unwrap();
    std::fs::write(rootfs.join(WINDOWS_GUEST_EXIT_CODE), "0\n").unwrap();

    let config = a3s_box_core::log::LogConfig::default();
    let error = collect_windows_guest_result(&box_dir, &config, 0)
        .unwrap_err()
        .to_string();

    assert!(error.contains("Failed to collect Windows guest output"));
    let console = box_dir.join("logs").join("console.log");
    assert!(
        !console.exists()
            || !std::fs::read_to_string(console)
                .unwrap()
                .contains("must not")
    );
}

#[cfg(target_os = "windows")]
#[test]
fn test_collect_windows_guest_result_rejects_false_success() {
    let tmp = tempfile::tempdir().unwrap();
    let box_dir = tmp.path().join("box");
    std::fs::create_dir_all(box_dir.join("rootfs")).unwrap();
    let config = a3s_box_core::log::LogConfig::default();

    let error = collect_windows_guest_result(&box_dir, &config, 0)
        .unwrap_err()
        .to_string();
    assert!(error.contains("before the guest persisted its exit code"));
    assert_eq!(
        collect_windows_guest_result(&box_dir, &config, 9).unwrap(),
        9
    );
}

#[cfg(target_os = "windows")]
#[tokio::test]
async fn test_windows_exit_file_waits_for_shim_log_relay() {
    let tmp = tempfile::tempdir().unwrap();
    let box_id = "box-windows-pending-relay".to_string();
    let mut vm =
        VmManager::with_box_id(BoxConfig::default(), EventEmitter::new(16), box_id.clone());
    vm.home_dir = tmp.path().to_path_buf();
    *vm.handler.write().await = Some(Box::new(RecordingHandler {
        stopped: Arc::new(AtomicBool::new(false)),
    }));

    let exit_path = tmp
        .path()
        .join("boxes")
        .join(&box_id)
        .join("rootfs")
        .join(".a3s_exit_code");
    std::fs::create_dir_all(exit_path.parent().unwrap()).unwrap();
    std::fs::write(exit_path, "0\n").unwrap();

    assert_eq!(vm.try_wait_exit().await.unwrap(), None);
    assert_eq!(vm.exit_code(), None);
    assert!(!vm.has_exited().await);
}

#[cfg(target_os = "windows")]
#[tokio::test]
async fn test_wait_for_exec_ready_classifies_persisted_windows_exit() {
    let tmp = tempfile::tempdir().unwrap();
    let box_id = "box-windows-completed-before-ready".to_string();
    let mut vm =
        VmManager::with_box_id(BoxConfig::default(), EventEmitter::new(16), box_id.clone());
    vm.home_dir = tmp.path().to_path_buf();
    *vm.handler.write().await = Some(Box::new(RecordingHandler {
        stopped: Arc::new(AtomicBool::new(false)),
    }));

    let rootfs = tmp.path().join("boxes").join(&box_id).join("rootfs");
    std::fs::create_dir_all(&rootfs).unwrap();
    std::fs::write(rootfs.join(WINDOWS_GUEST_EXIT_CODE), "42\n").unwrap();

    let error = tokio::time::timeout(
        std::time::Duration::from_secs(1),
        vm.wait_for_exec_ready(&tmp.path().join("missing-exec.sock")),
    )
    .await
    .unwrap()
    .unwrap_err()
    .to_string();

    assert!(error.contains("completed with exit code 42"));
    assert_eq!(vm.exit_code(), None, "log relay has not completed yet");
}

#[cfg(target_os = "windows")]
#[tokio::test]
async fn test_boot_cleanup_collects_windows_guest_completed_before_readiness() {
    let tmp = tempfile::tempdir().unwrap();
    let box_id = "box-windows-cleanup-completed-before-ready".to_string();
    let mut vm = VmManager::with_box_id(
        BoxConfig {
            persistent: true,
            ..BoxConfig::default()
        },
        EventEmitter::new(16),
        box_id.clone(),
    );
    vm.home_dir = tmp.path().to_path_buf();
    vm.set_rootfs_provider(Box::new(crate::rootfs::CopyProvider));
    let stopped = Arc::new(AtomicBool::new(false));
    *vm.handler.write().await = Some(Box::new(RecordingHandler {
        stopped: Arc::clone(&stopped),
    }));

    let box_dir = tmp.path().join("boxes").join(&box_id);
    let rootfs = box_dir.join("rootfs");
    std::fs::create_dir_all(&rootfs).unwrap();
    std::fs::write(rootfs.join(WINDOWS_GUEST_STDOUT), "guest output\n").unwrap();
    std::fs::write(rootfs.join(WINDOWS_GUEST_STDERR), "guest failure\n").unwrap();
    std::fs::write(rootfs.join(WINDOWS_GUEST_EXIT_CODE), "42\n").unwrap();

    vm.cleanup_boot_failure().await;

    assert!(stopped.load(Ordering::SeqCst));
    assert_eq!(vm.exit_code(), Some(42));
    assert!(vm.preserve_rootfs_on_boot_failure);
    assert_eq!(
        std::fs::read_to_string(box_dir.join("logs").join("console.log")).unwrap(),
        "guest output\n"
    );
    assert!(rootfs.is_dir());
}

#[cfg(unix)]
#[tokio::test]
async fn test_wait_for_exec_ready_rejects_provider_exit_without_guest_status() {
    let mut vm = VmManager::with_box_id(
        BoxConfig::default(),
        EventEmitter::new(16),
        "box-exec-exited".to_string(),
    );
    *vm.handler.write().await = Some(Box::new(CompletedHandler { code: 1 }));
    let tmp = tempfile::tempdir().unwrap();
    vm.home_dir = tmp.path().to_path_buf();

    let error = vm
        .wait_for_exec_ready(&tmp.path().join("missing-exec.sock"))
        .await
        .unwrap_err()
        .to_string();

    assert!(error.contains("exited with code 1"), "{error}");
    assert!(vm.exec_client.is_none());
}

#[cfg(unix)]
#[tokio::test]
async fn test_wait_for_exec_ready_returns_when_guest_exit_code_persisted() {
    let tmp = tempfile::tempdir().unwrap();
    let box_id = "box-exec-finished".to_string();
    let mut vm =
        VmManager::with_box_id(BoxConfig::default(), EventEmitter::new(16), box_id.clone());
    vm.home_dir = tmp.path().to_path_buf();

    let exit_path = tmp
        .path()
        .join("boxes")
        .join(&box_id)
        .join("upper")
        .join(".a3s_exit_code");
    std::fs::create_dir_all(exit_path.parent().unwrap()).unwrap();
    std::fs::write(&exit_path, "17\n").unwrap();

    tokio::time::timeout(
        std::time::Duration::from_secs(1),
        vm.wait_for_exec_ready(&tmp.path().join("missing-exec.sock")),
    )
    .await
    .unwrap()
    .unwrap();

    assert_eq!(vm.exit_code(), Some(17));
    assert!(vm.exec_client.is_none());
}

#[cfg(unix)]
#[tokio::test]
async fn test_probe_exec_ready_once_ignores_missing_socket() {
    let mut vm = VmManager::with_box_id(
        BoxConfig::default(),
        EventEmitter::new(16),
        "box-probe".to_string(),
    );
    let tmp = tempfile::tempdir().unwrap();

    vm.probe_exec_ready_once(&tmp.path().join("missing-exec.sock"))
        .await;

    assert!(vm.exec_client.is_none());
}

#[cfg(unix)]
#[tokio::test]
async fn test_attach_running_process_infers_port_forward_socket_path() {
    let mut vm = VmManager::with_box_id(
        BoxConfig::default(),
        EventEmitter::new(16),
        "box-test".to_string(),
    );
    let tmp = tempfile::tempdir().unwrap();
    let exec_socket_path = tmp.path().join("exec.sock");
    let pty_socket_path = Some(tmp.path().join("pty.sock"));

    vm.attach_running_process(
        std::process::id(),
        exec_socket_path.clone(),
        pty_socket_path.clone(),
    )
    .await
    .unwrap();

    assert_eq!(vm.exec_socket_path(), Some(exec_socket_path.as_path()));
    assert_eq!(vm.pty_socket_path(), pty_socket_path.as_deref());
    assert_eq!(
        vm.port_forward_socket_path(),
        Some(exec_socket_path.with_file_name("portfwd.sock").as_path())
    );
    assert_eq!(vm.state().await, BoxState::Ready);
}