aboutsummaryrefslogtreecommitdiff
path: root/src/pane/terminal/migration_tests.rs
diff options
context:
space:
mode:
authorAinesh Chatterjee <ainesh@factory.ai>2026-09-21 06:38:56 -0700
committerGitHub <noreply@github.com>2026-09-21 17:38:56 +0400
commit309749ad65f3aa596f077ec23a1bf3ee428b7a04 (patch)
treeaa508259a4f33d930f93b4f68f3232327616b1e5 /src/pane/terminal/migration_tests.rs
parent3602c757e07645383dc092f8540dcb72ba0ed49d (diff)
fix: honor droid scrollback clear requests in panes (#4432)
Remove the droid-specific filter that discarded ED3 before PTY output reached the terminal parser. Keeping stale scrollback beneath replayed primary-screen frames duplicated the welcome header and old transcript. Pass the original bytes to the parser and trackers. Add process-aware regression coverage across PTY chunk boundaries and preserve ED2/ED3 screen-history separation and alternate-screen isolation. Co-authored-by: ain3sh <ainesh.chatterjee@gmail.com> Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Co-authored-by: JJ Liebig <jonathan.liebig@gmail.com>
Diffstat (limited to 'src/pane/terminal/migration_tests.rs')
-rw-r--r--src/pane/terminal/migration_tests.rs128
1 files changed, 127 insertions, 1 deletions
diff --git a/src/pane/terminal/migration_tests.rs b/src/pane/terminal/migration_tests.rs
index 94d78a77..09e5df01 100644
--- a/src/pane/terminal/migration_tests.rs
+++ b/src/pane/terminal/migration_tests.rs
@@ -46,9 +46,13 @@ impl Harness {
}
fn write(&mut self, bytes: &[u8]) {
+ self.write_from_process(0, bytes);
+ }
+
+ fn write_from_process(&mut self, shell_pid: u32, bytes: &[u8]) {
let result = self
.pane
- .process_pty_bytes(PaneId::from_raw(1), 0, bytes, &self.tx);
+ .process_pty_bytes(PaneId::from_raw(1), shell_pid, bytes, &self.tx);
for reply in result.terminal_responses {
self.effects.replies.extend_from_slice(&reply);
}
@@ -111,6 +115,128 @@ impl Harness {
}
}
+#[cfg(target_os = "linux")]
+#[test]
+fn primary_screen_replay_honors_ed3_for_droid_at_chunk_boundaries() {
+ // A zero PID would bypass the former process-specific filter entirely.
+ // Use the real foreground-job lookup, with cleanup even on assertion failure.
+ struct ChildGuard(Box<dyn portable_pty::Child + Send + Sync>);
+ impl Drop for ChildGuard {
+ fn drop(&mut self) {
+ let _ = self.0.kill();
+ let _ = self.0.wait();
+ }
+ }
+ let pair = portable_pty::native_pty_system()
+ .openpty(portable_pty::PtySize {
+ rows: 24,
+ cols: 80,
+ pixel_width: 0,
+ pixel_height: 0,
+ })
+ .unwrap();
+ let mut command = portable_pty::CommandBuilder::new("bash");
+ command.args(["-c", "exec -a droid sleep 999"]);
+ let child = ChildGuard(pair.slave.spawn_command(command).unwrap());
+ let pid = child.0.process_id().unwrap();
+ let deadline = Instant::now() + Duration::from_secs(5);
+ loop {
+ let ready = crate::detect::foreground_job(pid).is_some_and(|job| {
+ job.processes
+ .iter()
+ .any(|process| process.cmdline.as_deref() == Some("droid 999"))
+ });
+ if ready {
+ break;
+ }
+ assert!(Instant::now() < deadline, "Droid foreground job not ready");
+ std::thread::sleep(Duration::from_millis(10));
+ }
+
+ for clear in ["\x1b[3J", "\x1b[?3J"] {
+ let prefix = format!("\x1b[?2026h\x1b[2J{clear}\x1b[H");
+ let frame = |label: &str| {
+ let mut bytes = format!("{prefix}welcome\r\n");
+ for row in 0..55 {
+ bytes.push_str(&format!("{label}-{row:02}\r\n"));
+ }
+ bytes.push_str("\x1b[?2026l");
+ bytes
+ };
+ let old = frame("old");
+ let new = frame("new");
+ // Whole writes catch the old filter; bytewise writes and every split
+ // within the erase prefix prove that PTY chunking cannot change ED3.
+ for chunk_size in [usize::MAX, 1, 7] {
+ for split in 0..=prefix.len() {
+ let mut harness = Harness::new(80, 24);
+ for bytes in old.as_bytes().chunks(chunk_size) {
+ harness.write_from_process(pid, bytes);
+ }
+ assert!(harness
+ .pane
+ .recent_text_snapshot(256)
+ .text
+ .contains("old-00"));
+ harness.write_from_process(pid, &new.as_bytes()[..split]);
+ for bytes in new.as_bytes()[split..].chunks(chunk_size) {
+ harness.write_from_process(pid, bytes);
+ }
+ let recent = harness.pane.recent_text_snapshot(256).text;
+ assert!(recent.contains("new-54"), "redraw must complete");
+ assert_eq!(
+ recent.matches("welcome").count(),
+ 1,
+ "{clear:?}, split {split}, chunk {chunk_size}: {recent}"
+ );
+ assert!(!recent.contains("old-"), "{recent}");
+ assert!(harness.pane.visible_text().contains("new-54"));
+ harness.pane.scroll_up(256);
+ let top = harness.pane.visible_text();
+ assert!(top.contains("welcome") && top.contains("new-00"), "{top}");
+ assert!(!top.contains("old-"), "{top}");
+ }
+ }
+ }
+}
+
+#[test]
+fn erase_display_preserves_screen_and_history_boundaries() {
+ for clear in [b"\x1b[3J".as_slice(), b"\x1b[?3J"] {
+ let mut harness = Harness::new(80, 24);
+ for row in 0..55 {
+ harness.write(format!("history-{row:02}\r\n").as_bytes());
+ }
+ assert!(harness
+ .pane
+ .recent_text_snapshot(256)
+ .text
+ .contains("history-00"));
+ let primary = harness.pane.recent_text_snapshot(256);
+ let visible = harness.pane.visible_text();
+
+ harness.write(b"\x1b[?1049h\x1b[2J\x1b[Halternate");
+ harness.write(clear);
+ assert!(harness.pane.visible_text().contains("alternate"));
+ harness.write(b"\x1b[?1049l");
+ assert_eq!(harness.pane.recent_text_snapshot(256), primary);
+ assert_eq!(harness.pane.visible_text(), visible);
+
+ // ED2 clears only the display, not prior shell output in scrollback.
+ harness.write(b"\x1b[2J\x1b[Hprompt");
+ assert_eq!(harness.pane.visible_text().trim(), "prompt");
+ assert!(harness
+ .pane
+ .recent_text_snapshot(256)
+ .text
+ .contains("history-00"));
+ harness.write(clear);
+ // ED3 clears history without erasing the current display.
+ assert_eq!(harness.pane.visible_text().trim(), "prompt");
+ assert_eq!(harness.pane.recent_text_snapshot(256).text.trim(), "prompt");
+ }
+}
+
#[test]
fn short_streams_are_invariant_at_every_byte_boundary() {
let fixtures: &[&[u8]] = &[