154 lines
8.0 KiB
Markdown
154 lines
8.0 KiB
Markdown
---
|
|
id: tty/integration-test
|
|
name: "End-to-end integration test: LocalTtyBackend + drive_session over real commands"
|
|
status: completed
|
|
depends_on: [tty/local-feature-reexport]
|
|
scope: broad
|
|
risk: medium
|
|
impact: phase
|
|
level: 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`)
|
|
|
|
1. **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.
|
|
2. **Interactive (cat)**: negotiate `cmd:["cat"]`, write stdin chunks, read
|
|
them back via stdout, send `eof` control chunk, await exit 0. Assert
|
|
stdin round-trips through the PTY.
|
|
3. **Resize**: negotiate `cmd:["cat"]`, send a `resize` control chunk, assert
|
|
no error (the PTY resizes). Send `eof`, await exit.
|
|
4. **Signal (SIGINT, Unix)**: negotiate `cmd:["sleep","60"]`, send a `signal`
|
|
control chunk with `name:"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).
|
|
5. **Process-group signal (Unix)**: negotiate `cmd:["bash","-c","sleep 60"]`,
|
|
send `signal:"INT"`, assert the `sleep` child also receives the signal
|
|
(the process group is targeted). This validates REQ-TTY-02 end-to-end.
|
|
6. **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.
|
|
7. **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., via `kill(pid, 0)` returning ESRCH, or a `ps` check, or a
|
|
`tokio::process` tracker).
|
|
8. **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`)
|
|
|
|
9. **Happy path (echo)**: negotiate `{backend:"local", tty:null, cmd:["echo","hello"]}`,
|
|
read stdout chunks, assert "hello", exit 0. Assert stderr is empty.
|
|
10. **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.
|
|
11. **Signal (SIGTERM, Unix)**: negotiate `cmd:["sleep","60"]`, send
|
|
`signal:"TERM"`, await exit, assert signal-terminated.
|
|
12. **Cancel cleanup (ADR-056)**: negotiate `cmd:["sleep","60"]`, drop the
|
|
duplex mid-session, assert the child is killed (no orphan).
|
|
13. **Resize no-op**: negotiate `cmd:["cat"]`, send a `resize` control chunk,
|
|
assert no error (PipeControl::resize is a no-op). Send `eof`, await exit.
|
|
|
|
#### Negotiation errors
|
|
|
|
14. **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 is `0x00`).
|
|
15. **malformed_negotiation (bad JSON)**: write garbage bytes as the
|
|
negotiation frame, assert `{"error":"malformed_negotiation",...}`.
|
|
16. **malformed_negotiation (carriage != raw)**: negotiate
|
|
`{carriage:"json",...}`, assert `malformed_negotiation`.
|
|
17. **malformed_negotiation (empty cmd)**: negotiate `{cmd:[]}`, assert
|
|
`malformed_negotiation`.
|
|
18. **allocate_failed**: (harder to trigger with the local backend — skip or
|
|
use a non-existent binary) negotiate `cmd:["/nonexistent"]`, assert
|
|
`allocate_failed` (the spawn fails).
|
|
|
|
### Test harness
|
|
|
|
Build a small test helper that wraps the client-side wire protocol:
|
|
- `write_negotiation(frame)` — serialize + length-prefix + write
|
|
- `write_chunk(stream_type, bytes)` — write a raw chunk
|
|
- `write_control(ControlMessage)` — serialize + write as stream_type 3
|
|
- `read_chunk()` — read a chunk, return `(stream_type, bytes)`
|
|
- `read_error_frame()` — read the length-prefixed JSON error (first byte `0x00`)
|
|
- `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/` (or `alknet-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 local` succeeds
|
|
- [ ] `cargo clippy -p alknet-tty --features local` succeeds 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
|
|
> `TtyBackend` trait seam. The `MockBackend` tests in `tty/adapter` validate
|
|
> 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 |