strop-terminal 0.32.1

Owned local terminal sessions and bounded VT snapshots for strop
use super::*;
use strop_core::frontend_input::Input;

fn terminal(columns: u16, rows: u16) -> Vt {
    Vt::new(
        SessionId::from_request(strop_core::worker::WorkerId::new(1)),
        Geometry {
            columns,
            rows,
            revision: 1,
        },
    )
    .unwrap()
}

#[test]
fn soft_wrap_and_grapheme_cells_share_the_readonly_text_projection() {
    let mut vt = terminal(5, 3);
    vt.feed("\x1b[?2027hA👩\u{200d}💻BCx".as_bytes()).unwrap();
    let (frame, _) = vt.snapshot().unwrap();
    let first = &frame.rows[0].row;
    assert!(first.wrapped);
    assert_eq!(first.symbol(1), Some("👩\u{200d}💻"));
    assert_eq!(first.cells[1].width, 2);
    assert_eq!(first.cells[2].width, 0);
    assert_eq!(first.byte_at(1), first.byte_at(2));
    assert!(frame.projection.to_string().starts_with("A👩\u{200d}💻BCx"));
    assert_eq!(frame.cursor.row, 1);
    assert_eq!(frame.cursor.column, 1);
    assert_eq!(frame.cursor_byte(), "A👩\u{200d}💻BCx".len());
}

#[test]
fn continuing_output_cannot_mutate_an_inspection_snapshot() {
    let mut vt = terminal(12, 3);
    vt.feed(b"one\r\ntwo\r\nthree\r\n").unwrap();
    let (before, _) = vt.snapshot().unwrap();
    let before_text = before.projection.to_string();
    vt.feed(b"four\r\nfive\r\nlatest\r\n").unwrap();
    let (after, _) = vt.snapshot().unwrap();
    let after_text = after.projection.to_string();
    assert_eq!(before.projection.to_string(), before_text);
    assert!(!before_text.contains("latest"));
    assert!(after_text.contains("one"));
    assert!(after_text.contains("latest"));
    assert!(after.history_rows > before.history_rows);
}

#[test]
fn erased_cells_retain_their_terminal_background_color() {
    let mut vt = terminal(10, 2);
    vt.feed(b"\x1b[41m\x1b[2J").unwrap();
    let (frame, _) = vt.snapshot().unwrap();
    assert_eq!(frame.rows[0].row.symbol(0), Some(" "));
    assert_eq!(
        frame.rows[0].row.cells[0].style.background,
        Color::Indexed(1)
    );
}

#[test]
fn clipboard_reads_are_denied_and_multiline_paste_requires_explicit_consent() {
    let mut vt = terminal(20, 3);
    let query = vt.feed(b"\x1b]52;c;?\x07").unwrap();
    assert!(query.reply.is_empty());
    let paste = Input::Paste("first\nsecond".into());
    assert!(matches!(
        vt.input(&paste, false),
        Err(Error::PasteNeedsConfirmation)
    ));
    let accepted = vt.input(&paste, true).unwrap();
    assert!(String::from_utf8(accepted.reply)
        .unwrap()
        .contains("second"));
    vt.feed(b"\x1b[?2004h").unwrap();
    let bracketed = vt.input(&paste, false).unwrap().reply;
    assert!(bracketed.starts_with(b"\x1b[200~"));
    assert!(bracketed.ends_with(b"\x1b[201~"));
}

#[test]
fn nul_retains_modifiers_and_negotiated_release_semantics() {
    use strop_core::frontend_input::{KeyCode, KeyEvent, KeyKind};
    let mut vt = terminal(20, 3);
    let mut key = KeyEvent::press(KeyCode::Null);
    key.modifiers.alt = true;
    assert_eq!(vt.input(&Input::Key(key), false).unwrap().reply, b"\x1b\0");
    vt.feed(b"\x1b[>3u").unwrap();
    assert_eq!(
        vt.input(&Input::Key(key), false).unwrap().reply,
        b"\x1b[32;7u"
    );
    key.kind = KeyKind::Release;
    assert_eq!(
        vt.input(&Input::Key(key), false).unwrap().reply,
        b"\x1b[32;7:3u"
    );
}

#[test]
fn oversized_paste_refusal_leaves_the_terminal_encoder_usable() {
    let mut vt = terminal(20, 3);
    vt.feed(b"\x1b[?2004h").unwrap();
    let oversized = Input::Paste("x".repeat(MAX_INPUT_BYTES));
    assert!(matches!(
        vt.input(&oversized, false),
        Err(Error::Capacity(_))
    ));
    assert_eq!(
        vt.input(&Input::Paste("safe".into()), false).unwrap().reply,
        b"\x1b[200~safe\x1b[201~"
    );
}

#[test]
fn replay_admission_checks_real_grapheme_and_projection_correspondence() {
    let mut vt = terminal(5, 3);
    vt.feed("\x1b[?2027hA👩\u{200d}💻BCx".as_bytes()).unwrap();
    let (frame, _) = vt.snapshot().unwrap();
    frame.validate().unwrap();
    let update = Update {
        session: frame.session,
        phase: Phase::Running,
        frame: Some(frame.clone()),
        effects: Vec::new(),
        acknowledged_input: 0,
        warning: None,
    };
    let wire = serde_json::to_value(&update).unwrap();
    let decoded: Update = serde_json::from_value(wire.clone()).unwrap();
    let decoded = decoded.frame.unwrap();
    assert_eq!(decoded.projection, frame.projection);
    assert_eq!(decoded.cell_at_byte(1).unwrap().width, 2);
    let mut corrupt = wire;
    corrupt["frame"]["rows"][0]["row"]["cells"][0]["end"] = serde_json::json!(u32::MAX);
    assert!(serde_json::from_value::<Update>(corrupt).is_err());
}

#[test]
fn focus_reports_follow_the_child_negotiated_mode() {
    let mut vt = terminal(20, 3);
    assert!(vt.focus(false).unwrap().reply.is_empty());
    vt.feed(b"\x1b[?1004h").unwrap();
    assert_eq!(vt.focus(false).unwrap().reply, b"\x1b[O");
    assert_eq!(vt.focus(true).unwrap().reply, b"\x1b[I");
    vt.feed(b"\x1b[?1004l").unwrap();
    assert!(vt.focus(true).unwrap().reply.is_empty());
}

#[test]
fn keyboard_advertisement_matches_the_captured_frontend_profile() {
    let mut vt = terminal(20, 3);
    vt.keyboard_capabilities(0).unwrap();
    assert_eq!(vt.feed(b"\x1b[?u\x1b[c").unwrap().reply, b"\x1b[?62;22c");
    vt.feed(b"\x1b[>31u").unwrap();
    assert!(vt.feed(b"\x1b[?u").unwrap().reply.is_empty());
    let mut alt =
        strop_core::frontend_input::KeyEvent::press(strop_core::frontend_input::KeyCode::Char('x'));
    alt.modifiers.alt = true;
    assert_eq!(vt.input(&Input::Key(alt), false).unwrap().reply, b"\x1bx");
    vt.keyboard_capabilities(SUPPORTED_KEYBOARD_FLAGS).unwrap();
    assert_eq!(vt.feed(b"\x1b[?u").unwrap().reply, b"\x1b[?27u");
}