alktty 0.5.0

Terminal session protocol: wire format, TtyBackend trait, TtyAdapter, and typed consumer client. Producer/consumer protocol crate on top of alkcall channels.
Documentation
#![cfg(feature = "local")]

//! Negotiation error tests — scenarios 14–18 of
//! `tasks/tty/integration-test.md`.
//!
//!   14. unknown_backend: error response, first byte `0x00`
//!   15. malformed_negotiation (bad JSON): error response
//!   16. malformed_negotiation (carriage != raw): error response
//!   17. malformed_negotiation (empty cmd): error response
//!   18. allocate_failed (nonexistent binary): error response
//!
//! The test identity carries the `tty:open` scope, so the
//! `unknown_backend` test reaches the backend lookup; the scope-gate
//! is exercised in the adapter's unit tests. These tests assert the
//! server sends a length-prefixed JSON error frame (Phase 1 framing,
//! ADR-052) and closes the stream without entering raw mode.

mod common;

use std::sync::Arc;

use alktty::local::LocalTtyBackend;
use common::{negotiate_pipe_json, spawn_session};

/// 14. unknown_backend: negotiate `{backend:"kubernetes",...}`, assert
/// the error response `{"error":"unknown_backend","backend":"kubernetes"}`,
/// the first byte of the error frame is `0x00`, and the stream closes.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn unknown_backend_error() {
    let backend = Arc::new(LocalTtyBackend::new());
    let (mut client, server) = spawn_session("local", backend);
    client
        .write_negotiation(
            r#"{"carriage":"raw","backend":"kubernetes","tty":null,"cmd":["echo","hi"]}"#,
        )
        .await;

    let err = client.read_error_frame().await;
    assert_eq!(err["error"], "unknown_backend");
    assert_eq!(err["backend"], "kubernetes");

    client.assert_no_more_chunks().await;
    let _ = server.await;
}

/// 15. malformed_negotiation (bad JSON): write garbage bytes as the
/// negotiation frame, assert `malformed_negotiation` error response.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn malformed_negotiation_bad_json() {
    let backend = Arc::new(LocalTtyBackend::new());
    let (mut client, server) = spawn_session("local", backend);
    client.write_negotiation("not valid json").await;

    let err = client.read_error_frame().await;
    assert_eq!(err["error"], "malformed_negotiation");

    client.assert_no_more_chunks().await;
    let _ = server.await;
}

/// 16. malformed_negotiation (carriage != raw): negotiate
/// `{carriage:"json",...}`, assert `malformed_negotiation`.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn malformed_negotiation_carriage_not_raw() {
    let backend = Arc::new(LocalTtyBackend::new());
    let (mut client, server) = spawn_session("local", backend);
    client
        .write_negotiation(
            r#"{"carriage":"json","backend":"local","tty":null,"cmd":["echo","hi"]}"#,
        )
        .await;

    let err = client.read_error_frame().await;
    assert_eq!(err["error"], "malformed_negotiation");

    client.assert_no_more_chunks().await;
    let _ = server.await;
}

/// 17. malformed_negotiation (empty cmd): negotiate `{cmd:[]}`, assert
/// `malformed_negotiation`.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn malformed_negotiation_empty_cmd() {
    let backend = Arc::new(LocalTtyBackend::new());
    let (mut client, server) = spawn_session("local", backend);
    client
        .write_negotiation(r#"{"carriage":"raw","backend":"local","tty":null,"cmd":[]}"#)
        .await;

    let err = client.read_error_frame().await;
    assert_eq!(err["error"], "malformed_negotiation");

    client.assert_no_more_chunks().await;
    let _ = server.await;
}

/// 18. allocate_failed (nonexistent binary): negotiate with a
/// nonexistent command path, assert `allocate_failed` (the spawn
/// fails). The adapter sends the error response in negotiation
/// framing and closes.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn allocate_failed_nonexistent_binary() {
    let backend = Arc::new(LocalTtyBackend::new());
    let (mut client, server) = spawn_session("local", backend);
    client
        .write_negotiation(
            negotiate_pipe_json("local", &["/nonexistent/binary/that/does/not/exist"]).as_str(),
        )
        .await;

    let err = client.read_error_frame().await;
    assert_eq!(err["error"], "allocate_failed");

    client.assert_no_more_chunks().await;
    let _ = server.await;
}