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
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
#![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;
|
||||
}
|
||||
Reference in New Issue
Block a user