8.0 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| tty/integration-test | End-to-end integration test: LocalTtyBackend + drive_session over real commands | completed |
|
broad | medium | phase | implementation |
Description
Write end-to-end integration tests that exercise the full stack:
LocalTtyBackend (PTY and pipe modes) + TtyAdapter::drive_session over a
tokio::io::duplex (transport stand-in for a QUIC bidi stream), running real
commands. These tests validate the two crates work together through the
TtyBackend trait seam and that the wire-format invariants (exit-chunk-is-last,
ADR-055; kill-on-Drop, ADR-056) hold with a real backend, not just the
MockBackend from tty/adapter.
These tests live in alknet-tty's test suite (with --features local so the
local backend is available) OR in alknet-tty-local's test suite (depending
on alknet-tty for the adapter). The natural home is alknet-tty's
tests/ directory with a local feature gate, since the adapter is in
alknet-tty and the test exercises the adapter + backend together. Use
#[cfg(feature = "local")] on the test module.
Test scenarios
All scenarios use tokio::io::duplex as the bidi stream stand-in (the POC's
pattern). The test acts as the client: writes the negotiation frame, sends
stdin/control chunks, reads stdout/stderr/control chunks, asserts the
exit chunk and stream close.
PTY mode (terminal: Some)
- Happy path (echo): negotiate
{backend:"local", tty:{cols:80,rows:24}, cmd:["echo","hello"]}, read stdout chunks until the exit chunk, assert stdout contains "hello", exit code is 0, exit chunk is the last chunk before stream close. - Interactive (cat): negotiate
cmd:["cat"], write stdin chunks, read them back via stdout, sendeofcontrol chunk, await exit 0. Assert stdin round-trips through the PTY. - Resize: negotiate
cmd:["cat"], send aresizecontrol chunk, assert no error (the PTY resizes). Sendeof, await exit. - Signal (SIGINT, Unix): negotiate
cmd:["sleep","60"], send asignalcontrol chunk withname:"INT", await the exit chunk. Assert exit code is signal-terminated (negative, e.g., -2 for SIGINT, or 130). Assert the child is reaped (no zombie). - Process-group signal (Unix): negotiate
cmd:["bash","-c","sleep 60"], sendsignal:"INT", assert thesleepchild also receives the signal (the process group is targeted). This validates REQ-TTY-02 end-to-end. - Stdin EOF (zero-length chunk): negotiate
cmd:["cat"], send a zero-length stdin chunk (the sentinel), assert the backend's stdin closes, stdout drains, exit chunk is sent. - Cancel cleanup (ADR-056): negotiate
cmd:["sleep","60"], drop the duplex (simulating connection drop) mid-session, assert the child is killed (no orphan). Use a short delay then check the process is gone (e.g., viakill(pid, 0)returning ESRCH, or apscheck, or atokio::processtracker). - Exit-chunk-is-last: in the happy path, assert no stdout chunk arrives after the exit chunk. Read all chunks, find the exit chunk, assert it is the last chunk before stream close.
Pipe mode (terminal: None)
- Happy path (echo): negotiate
{backend:"local", tty:null, cmd:["echo","hello"]}, read stdout chunks, assert "hello", exit 0. Assert stderr is empty. - Separate stderr: negotiate
cmd:["sh","-c","echo out; echo err >&2"], assert stdout stream receives "out", stderr stream receives "err" (as stderr chunks, stream_type 2), exit 0. - Signal (SIGTERM, Unix): negotiate
cmd:["sleep","60"], sendsignal:"TERM", await exit, assert signal-terminated. - Cancel cleanup (ADR-056): negotiate
cmd:["sleep","60"], drop the duplex mid-session, assert the child is killed (no orphan). - Resize no-op: negotiate
cmd:["cat"], send aresizecontrol chunk, assert no error (PipeControl::resize is a no-op). Sendeof, await exit.
Negotiation errors
- unknown_backend: negotiate
{backend:"kubernetes",...}, assert the error response{"error":"unknown_backend","backend":"kubernetes"}, stream closes, no raw mode (first byte of the error frame is0x00). - malformed_negotiation (bad JSON): write garbage bytes as the
negotiation frame, assert
{"error":"malformed_negotiation",...}. - malformed_negotiation (carriage != raw): negotiate
{carriage:"json",...}, assertmalformed_negotiation. - malformed_negotiation (empty cmd): negotiate
{cmd:[]}, assertmalformed_negotiation. - allocate_failed: (harder to trigger with the local backend — skip or
use a non-existent binary) negotiate
cmd:["/nonexistent"], assertallocate_failed(the spawn fails).
Test harness
Build a small test helper that wraps the client-side wire protocol:
write_negotiation(frame)— serialize + length-prefix + writewrite_chunk(stream_type, bytes)— write a raw chunkwrite_control(ControlMessage)— serialize + write as stream_type 3read_chunk()— read a chunk, return(stream_type, bytes)read_error_frame()— read the length-prefixed JSON error (first byte0x00)read_until_exit()— read chunks until the exit control chunk, return (stdout_bytes, stderr_bytes, exit_code)
This helper can live in a tests/common/ module or be reused from the
adapter's unit tests (the MockBackend tests in tty/adapter use a similar
pattern).
Acceptance Criteria
- Integration test module in
alknet-tty/tests/(oralknet-tty-local/tests/),#[cfg(feature = "local")]gated - PTY happy path (echo): stdout contains "hello", exit 0, exit chunk last
- PTY interactive (cat): stdin round-trips, eof → exit 0
- PTY resize: control chunk accepted, no error
- PTY signal (SIGINT): exit code signal-terminated, child reaped (Unix)
- PTY process-group signal: shell's child receives the signal (REQ-TTY-02, Unix)
- PTY stdin EOF (zero-length chunk): backend stdin closes, exit chunk sent
- PTY cancel cleanup: drop duplex → child killed, no orphan (ADR-056)
- PTY exit-chunk-is-last: no stdout chunk after exit chunk
- Pipe happy path (echo): stdout "hello", stderr empty, exit 0
- Pipe separate stderr: stdout "out", stderr "err", exit 0
- Pipe signal (SIGTERM): exit signal-terminated (Unix)
- Pipe cancel cleanup: drop duplex → child killed, no orphan (ADR-056)
- Pipe resize no-op: control chunk accepted, no error
- unknown_backend error: error response, stream closes, first byte
0x00 - malformed_negotiation (bad JSON, bad carriage, empty cmd): error response
- allocate_failed (nonexistent binary): error response
- Test helper for client-side wire protocol (negotiation, chunks, control, error frames)
cargo test -p alknet-tty --features localsucceedscargo clippy -p alknet-tty --features localsucceeds with no warnings
References
- docs/architecture/crates/tty/tty-adapter.md — session lifecycle, negotiation errors
- docs/architecture/crates/tty/tty-local.md — PTY and pipe mode behavior
- docs/architecture/decisions/055-exit-code-on-control-chunk.md — ADR-055 (exit-chunk-is-last)
- docs/architecture/decisions/056-backend-cleanup-on-session-cancel.md — ADR-056 (cancel cleanup)
- /workspace/alknet-tty-poc/tests/integration.rs — the POC's integration tests (reference)
- /workspace/alknet-tty-poc/tests/signal.rs — the POC's SIGINT-forwarding test (REQ-TTY-02)
Notes
These are the tests that validate the two crates work together through the
TtyBackendtrait seam. TheMockBackendtests intty/adaptervalidate the adapter in isolation; these tests validate the adapter + a real backend. The cancel-cleanup tests (ADR-056) are the most important — they confirm no orphaned processes when the connection drops mid-session. The process-group signal test (REQ-TTY-02) confirms Ctrl-C reaches a shell's children, not just the shell. Unix-only tests (signal, process-group) use#[cfg(unix)].
Summary
To be filled on completion