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
117 lines
4.2 KiB
Rust
117 lines
4.2 KiB
Rust
#![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;
|
||
}
|