diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5bd929..547beca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,6 +89,37 @@ jobs: - name: Test compatibility harness run: cargo test -p splice-compat + # This is deliberately separate from the workspace suite: native Windows + # is the authority for ConPTY behavior, not a cross-compiled approximation. + - name: ConPTY runtime regression gate (authoritative Windows) + if: ${{ !cancelled() }} + run: cargo test -p splice-pty --test conpty_smoke + - name: Test if: ${{ !cancelled() }} run: cargo test --workspace + + ubuntu-runtime: + name: Native Ubuntu PTY runtime + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + ref: ${{ inputs.ref || github.ref }} + + - name: Set up Rust toolchain + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + + - name: Install Tauri build prerequisites + run: >- + sudo apt-get update && sudo apt-get install --yes + libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev + + - name: Run native Ubuntu PTY fixtures + run: cargo test -p splice-pty --test platform_runtime_matrix -- --nocapture + + - name: Run platform default-shell and authority integration + run: >- + cargo test -p splice-pty --test session_contract && + cargo test -p splice-shell-desktop --test platform_authority diff --git a/crates/splice-pty/tests/platform_runtime_matrix.rs b/crates/splice-pty/tests/platform_runtime_matrix.rs new file mode 100644 index 0000000..850ee82 --- /dev/null +++ b/crates/splice-pty/tests/platform_runtime_matrix.rs @@ -0,0 +1,129 @@ +use std::{ + fs, + path::PathBuf, + sync::mpsc, + time::{Duration, Instant}, +}; + +use splice_pty::{PtySession, TerminalSize}; + +fn size() -> TerminalSize { + TerminalSize::new(80, 24).expect("fixture terminal size is valid") +} + +fn supported_ubuntu_runtime() -> bool { + let release = fs::read_to_string("/etc/os-release").unwrap_or_default(); + release.lines().any(|line| line == "ID=ubuntu") + && release + .lines() + .any(|line| matches!(line, "VERSION_ID=\"22.04\"" | "VERSION_ID=\"24.04\"")) +} + +fn require_supported_ubuntu_runtime() -> bool { + if supported_ubuntu_runtime() { + true + } else { + eprintln!( + "SKIP: native Ubuntu runtime fixture requires Ubuntu 22.04 or 24.04; this host is unsupported" + ); + false + } +} + +fn receive_until(receiver: &mpsc::Receiver, marker: &str) -> String { + let deadline = Instant::now() + Duration::from_secs(5); + let mut output = String::new(); + while Instant::now() < deadline && !output.contains(marker) { + if let Ok(chunk) = receiver.recv_timeout(Duration::from_millis(250)) { + output.push_str(&chunk); + } + } + output +} + +#[test] +fn native_ubuntu_runtime_fixture_exercises_shell_utf8_input_resize_and_liveness() { + if !require_supported_ubuntu_runtime() { + return; + } + + let (sender, receiver) = mpsc::channel(); + let session = PtySession::spawn( + "/bin/sh", + &[ + "-c", + "printf 'ready utf8=café\\n'; stty size; read value; printf 'input=%s\\n' \"$value\"; stty size", + ], + size(), + move |_, output| { + let _ = sender.send(output); + }, + |_| {}, + ) + .expect("native Ubuntu default shell starts"); + + assert!(session.is_running().expect("liveness is available")); + assert_eq!(session.active_process_name().expect("process name"), "sh"); + assert!(receive_until(&receiver, "24 80").contains("ready utf8=café")); + + session + .resize(TerminalSize::new(132, 43).expect("resized fixture terminal size")) + .expect("resize succeeds"); + session.write("fixture-input\n").expect("input succeeds"); + let output = receive_until(&receiver, "43 132"); + assert!(output.contains("input=fixture-input")); + assert!(output.contains("43 132")); + session.close(); + println!("RECEIPT: native Ubuntu PTY startup, UTF-8, input, resize, and liveness passed"); +} + +#[test] +fn native_ubuntu_runtime_fixture_exercises_interrupt_teardown_and_idempotent_close() { + if !require_supported_ubuntu_runtime() { + return; + } + + let (sender, receiver) = mpsc::channel(); + let session = PtySession::spawn( + "/bin/sh", + &[ + "-c", + "trap 'printf interrupted; exit' INT; printf ready; while :; do sleep 1; done", + ], + size(), + move |_, output| { + let _ = sender.send(output); + }, + |_| {}, + ) + .expect("native Ubuntu default shell starts"); + + assert!(receive_until(&receiver, "ready").contains("ready")); + session.interrupt().expect("interrupt succeeds"); + assert!(receive_until(&receiver, "interrupted").contains("interrupted")); + session.close(); + session.close(); + assert!(!session.is_running().expect("liveness after close")); + println!("RECEIPT: native Ubuntu PTY interrupt, teardown, and idempotent close passed"); +} + +#[test] +fn ci_declares_authoritative_windows_and_native_ubuntu_runtime_gates() { + let workflow = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../.github/workflows/ci.yml"); + let workflow = fs::read_to_string(workflow).expect("CI workflow is readable"); + + for required_gate in [ + "ConPTY runtime regression gate (authoritative Windows)", + "cargo test -p splice-pty --test conpty_smoke", + "Native Ubuntu PTY runtime", + "runs-on: ubuntu-24.04", + "cargo test -p splice-pty --test platform_runtime_matrix", + "cargo test -p splice-pty --test session_contract", + "cargo test -p splice-shell-desktop --test platform_authority", + ] { + assert!( + workflow.contains(required_gate), + "CI workflow must retain runtime gate: {required_gate}" + ); + } +}