Files
alktty/tests/negotiation.rs
T
glm-5.2 b84c67bd8a phase 5: port integration tests (negotiation, pipe, pty)
Port the integration test suite from alknet-tty-local/tests/ into
tests/ at the crate root, plus the shared ClientSide harness. The
inline unit tests (wire, negotiation, control, adapter, session,
channels) were already ported in Phases 1-2 alongside the production
code; this completes Phase 5 step 2 (integration tests) — Phase 5
step 3 (channels integration tests) landed inline in src/channels.rs
mod tests in Phase 2.

Tests:
- tests/common/mod.rs — ClientSide wire-protocol harness +
  spawn_session helper + negotiate_pty_json / negotiate_pipe_json
  builders. Imports renamed alknet_core::auth::Identity ->
  alkcall::core::auth::Identity, alknet_tty::... -> alktty::...
- tests/negotiation.rs — 4 negotiation-error scenarios
  (unknown_backend, malformed_negotiation x3, allocate_failed)
- tests/pipe.rs — 6 pipe-mode scenarios (echo, separate stderr,
  SIGTERM, cancel cleanup, resize no-op, stdout sentinel). The 2
  cancel-cleanup / SIGTERM tests are #[cfg(unix)].
- tests/pty.rs — 8 PTY-mode scenarios (echo, interactive cat,
  resize, SIGINT, process-group signal, stdin-EOF sentinel,
  cancel cleanup, exit-chunk-is-last). The 4 signal /
  cancel-cleanup tests are #[cfg(unix)].

Each test file carries #![cfg(feature = "local")] so the default
crate (no features) skips the integration binaries and stays
wasm-buildable. The pty/pipe cancel-cleanup tests use
unsafe { libc::kill(pid, 0) } to probe the child — matching the
existing pattern in src/local/ (libc::kill is a safe libc crate
API wrapped in an unsafe block per Rust's foreign-function rules;
no new in-crate unsafe beyond what src/local/ already has).

Plan doc updated: Phase 5 marked landed 2026-08-17.

Verification:
- cargo test                         -> 80 lib tests pass
- cargo test --all-features          -> 99 tests pass (80 lib +
  5 negotiation + 6 pipe + 8 pty)
- cargo clippy --all-targets --all-features -- -D warnings -> clean
- cargo clippy --target wasm32-unknown-unknown -- -D warnings -> clean
- cargo fmt --check                  -> clean
2026-08-17 11:09:00 +00:00

117 lines
4.2 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#![cfg(feature = "local")]
//! Negotiation error tests — scenarios 1418 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;
}