Skip to content

Commit c100c3b

Browse files
branchseerclaude
andcommitted
Add e2e snapshot test for ctrl-c propagation to running tasks
Add a `vtt exit-on-ctrlc` subcommand and e2e fixture that verifies SIGINT propagates to concurrent tasks when the user presses Ctrl+C. The test runs two packages with `vt run -r dev`, synchronizes them via a filesystem barrier, then sends ctrl-c and verifies both tasks receive and handle it. Also adds `ctrl-c` as a new `write-key` interaction type for e2e snapshot tests. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent 1957b34 commit c100c3b

13 files changed

Lines changed: 107 additions & 13 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/pty_terminal_test/src/lib.rs

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use std::io::{BufReader, Read};
1+
use std::{
2+
collections::VecDeque,
3+
io::{BufReader, Read},
4+
};
25

36
pub use portable_pty::CommandBuilder;
47
use pty_terminal::terminal::{PtyReader, Terminal};
@@ -25,6 +28,8 @@ pub struct TestTerminal {
2528
pub struct Reader {
2629
pty: BufReader<PtyReader>,
2730
child_handle: ChildHandle,
31+
/// OSC sequences taken from the PTY but not yet consumed by `expect_milestone`.
32+
pending_osc: VecDeque<Vec<Vec<u8>>>,
2833
}
2934

3035
impl TestTerminal {
@@ -37,7 +42,11 @@ impl TestTerminal {
3742
let Terminal { pty_reader, pty_writer, child_handle, .. } = Terminal::spawn(size, cmd)?;
3843
Ok(Self {
3944
writer: pty_writer,
40-
reader: Reader { pty: BufReader::new(pty_reader), child_handle: child_handle.clone() },
45+
reader: Reader {
46+
pty: BufReader::new(pty_reader),
47+
child_handle: child_handle.clone(),
48+
pending_osc: VecDeque::new(),
49+
},
4150
child_handle,
4251
})
4352
}
@@ -71,15 +80,24 @@ impl Reader {
7180
let mut buf = [0u8; 4096];
7281

7382
loop {
74-
let found = self
75-
.pty
76-
.get_ref()
77-
.take_unhandled_osc_sequences()
78-
.into_iter()
79-
.filter_map(|params| {
80-
pty_terminal_test_client::decode_milestone_from_osc8_params(&params)
81-
})
82-
.any(|decoded| decoded == name);
83+
// Drain new sequences from the PTY into our local buffer.
84+
self.pending_osc.append(&mut self.pty.get_ref().take_unhandled_osc_sequences());
85+
86+
// Scan for the first matching milestone, keeping the rest.
87+
let mut found = false;
88+
let mut remaining = VecDeque::with_capacity(self.pending_osc.len());
89+
for params in self.pending_osc.drain(..) {
90+
if !found
91+
&& pty_terminal_test_client::decode_milestone_from_osc8_params(&params)
92+
.is_some_and(|decoded| decoded == name)
93+
{
94+
found = true;
95+
continue;
96+
}
97+
remaining.push_back(params);
98+
}
99+
self.pending_osc = remaining;
100+
83101
if found {
84102
return self.screen_contents();
85103
}

crates/vite_task_bin/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ path = "src/vtt/main.rs"
1616

1717
[dependencies]
1818
anyhow = { workspace = true }
19+
ctrlc = { workspace = true }
1920
libc = { workspace = true }
2021
notify = { workspace = true }
22+
pty_terminal_test_client = { workspace = true, features = ["testing"] }
2123
async-trait = { workspace = true }
2224
clap = { workspace = true, features = ["derive"] }
2325
jsonc-parser = { workspace = true }
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/// exit-on-ctrlc
2+
///
3+
/// Sets up a Ctrl+C handler, emits a "ready" milestone, then waits.
4+
/// When Ctrl+C is received, prints "ctrl-c received" and exits.
5+
pub fn run() -> Result<(), Box<dyn std::error::Error>> {
6+
ctrlc::set_handler(move || {
7+
use std::io::Write;
8+
let _ = write!(std::io::stdout(), "ctrl-c received");
9+
let _ = std::io::stdout().flush();
10+
std::process::exit(0);
11+
})?;
12+
13+
pty_terminal_test_client::mark_milestone("ready");
14+
15+
loop {
16+
std::thread::park();
17+
}
18+
}

crates/vite_task_bin/src/vtt/main.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
mod barrier;
1010
mod check_tty;
11+
mod exit_on_ctrlc;
1112
mod print;
1213
mod print_cwd;
1314
mod print_env;
@@ -21,7 +22,7 @@ fn main() {
2122
if args.len() < 2 {
2223
eprintln!("Usage: vtt <subcommand> [args...]");
2324
eprintln!(
24-
"Subcommands: barrier, check-tty, print, print-cwd, print-env, print-file, read-stdin, replace-file-content, touch-file"
25+
"Subcommands: barrier, check-tty, exit-on-ctrlc, print, print-cwd, print-env, print-file, read-stdin, replace-file-content, touch-file"
2526
);
2627
std::process::exit(1);
2728
}
@@ -32,6 +33,7 @@ fn main() {
3233
check_tty::run();
3334
Ok(())
3435
}
36+
"exit-on-ctrlc" => exit_on_ctrlc::run(),
3537
"print" => {
3638
print::run(&args[2..]);
3739
Ok(())
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "ctrl-c-test",
3+
"private": true
4+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "@ctrl-c/a",
3+
"scripts": {
4+
"dev": "vtt exit-on-ctrlc"
5+
}
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "@ctrl-c/b",
3+
"scripts": {
4+
"dev": "vtt exit-on-ctrlc"
5+
}
6+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
packages:
2+
- packages/*
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Tests that Ctrl+C (SIGINT) propagates to and terminates running tasks.
2+
# Two packages run concurrently; both set up ctrl-c handlers and emit
3+
# a "ready" milestone. After both are ready, ctrl-c is sent.
4+
5+
[[e2e]]
6+
name = "ctrl-c terminates running tasks"
7+
steps = [
8+
{ command = "vt run -r dev", interactions = [
9+
{ "expect-milestone" = "ready" },
10+
{ "expect-milestone" = "ready" },
11+
{ "write-key" = "ctrl-c" },
12+
] },
13+
]

0 commit comments

Comments
 (0)