beam-worker 0.10.1

Per-session worker process for beam that owns terminal backends and CLI adapters
Documentation
use crate::worker_runtime::*;

#[test]
fn maybe_inject_term_codex_no_term_injects() {
    assert_eq!(
        maybe_inject_term("codex", None),
        Some(("TERM".to_string(), "xterm-256color".to_string()))
    );
}

#[test]
fn maybe_inject_term_codex_empty_term_injects() {
    assert_eq!(
        maybe_inject_term("codex", Some("")),
        Some(("TERM".to_string(), "xterm-256color".to_string()))
    );
}

#[test]
fn maybe_inject_term_codex_dumb_term_injects() {
    assert_eq!(
        maybe_inject_term("codex", Some("dumb")),
        Some(("TERM".to_string(), "xterm-256color".to_string()))
    );
}

#[test]
fn maybe_inject_term_traex_no_term_injects() {
    assert_eq!(
        maybe_inject_term("traex", None),
        Some(("TERM".to_string(), "xterm-256color".to_string()))
    );
}

#[test]
fn maybe_inject_term_traex_dumb_term_injects() {
    assert_eq!(
        maybe_inject_term("traex", Some("dumb")),
        Some(("TERM".to_string(), "xterm-256color".to_string()))
    );
}

#[test]
fn maybe_inject_term_codex_existing_xterm_does_not_inject() {
    assert_eq!(maybe_inject_term("codex", Some("xterm-256color")), None);
}

#[test]
fn maybe_inject_term_codex_existing_xterm_does_not_inject_any() {
    assert_eq!(maybe_inject_term("codex", Some("xterm")), None);
}

#[test]
fn maybe_inject_term_non_target_cli_never_injects() {
    assert_eq!(maybe_inject_term("claude", None), None);
    assert_eq!(maybe_inject_term("claude", Some("dumb")), None);
    assert_eq!(maybe_inject_term("opencode", None), None);
    assert_eq!(maybe_inject_term("gemini", None), None);
    assert_eq!(maybe_inject_term("coco", None), None);
    assert_eq!(maybe_inject_term("hermes", None), None);
    assert_eq!(maybe_inject_term("antigravity", None), None);
    assert_eq!(maybe_inject_term("kimi", None), None);
    assert_eq!(maybe_inject_term("generic", Some("dumb")), None);
}

#[test]
fn maybe_inject_term_valid_term_preserved_for_codex() {
    assert_eq!(maybe_inject_term("codex", Some("xterm")), None);
    assert_eq!(maybe_inject_term("codex", Some("screen-256color")), None);
    assert_eq!(maybe_inject_term("codex", Some("vt100")), None);
}

#[test]
fn maybe_inject_term_valid_term_preserved_for_traex() {
    assert_eq!(maybe_inject_term("traex", Some("xterm-256color")), None);
    assert_eq!(maybe_inject_term("traex", Some("xterm")), None);
}

#[test]
fn render_screen_for_display_mode_hides_or_shows_content() {
    assert_eq!(
        render_screen_for_display_mode("hello", DisplayMode::Hidden),
        "[screen hidden]"
    );
    assert_eq!(
        render_screen_for_display_mode("hello", DisplayMode::Screenshot),
        "hello"
    );
}

#[test]
fn render_screen_for_screenshot_mode_preserves_full_text() {
    let screen = (0..80)
        .map(|idx| format!("{idx:02}:{}", "x".repeat(140)))
        .collect::<Vec<_>>()
        .join("\n");
    let rendered = render_screen_for_display_mode(&screen, DisplayMode::Screenshot);
    let lines: Vec<&str> = rendered.lines().collect();

    assert_eq!(lines.len(), 80);
    assert!(lines[0].starts_with("00:"));
    assert!(lines[79].starts_with("79:"));
    assert!(lines[0].chars().count() > 120);
}

#[test]
fn term_action_keys_maps_supported_actions() {
    assert_eq!(term_action_keys(TermActionKey::Esc), vec!["Escape"]);
    assert_eq!(term_action_keys(TermActionKey::CtrlC), vec!["C-c"]);
    assert_eq!(term_action_keys(TermActionKey::Enter), vec!["Enter"]);
    assert_eq!(
        term_action_keys(TermActionKey::HalfPageDown),
        vec!["PageDown"]
    );
}

#[test]
fn detect_cli_usage_limit_finds_usage_and_rate_limits() {
    let usage = detect_cli_usage_limit(
        "You have hit your usage limit. Try again at 3:15 PM.",
        1_700_000_000_000,
    )
    .expect("usage limit detected");
    assert_eq!(usage.kind, CliUsageLimitKind::Usage);
    assert_eq!(usage.retry_label, "3:15 PM");
    assert!(usage.limited);

    let rate = detect_cli_usage_limit("Rate limited. Resets at 11:00 AM.", 1_700_000_000_000)
        .expect("rate limit detected");
    assert_eq!(rate.kind, CliUsageLimitKind::Rate);
    assert_eq!(rate.retry_label, "11:00 AM");
}

#[test]
fn usage_limit_tracker_suppresses_stale_retry_ready_banner_on_new_turn() {
    let now_ms = 1_700_000_000_000;
    let text = "Usage limit reached. Try again at 3:15 PM.";
    let mut tracker = UsageLimitTracker::default();
    let initial = detect_cli_usage_limit(text, now_ms).expect("limit");
    tracker.begin_turn(text, initial.retry_at_ms + 1);
    let (status, usage_limit) =
        tracker.classify(text, ScreenStatus::Working, initial.retry_at_ms + 1);
    assert_eq!(status, ScreenStatus::Working);
    assert_eq!(usage_limit, None);
}

#[test]
fn render_text_screenshot_png_produces_png_bytes() {
    let png = render_text_screenshot_png("hello\nworld").expect("png rendered");
    assert!(png.starts_with(&[0x89, b'P', b'N', b'G']));
    assert!(png.len() > 64);
}

#[test]
fn render_text_screenshot_png_uses_full_screenshot_input() {
    let screen = (0..80)
        .map(|_| "x".repeat(200))
        .collect::<Vec<_>>()
        .join("\n");
    let png = render_text_screenshot_png(&screen).expect("png rendered");
    let image = image::load_from_memory(&png).expect("png should decode");
    let expected_width = ((200f32 * CELL_W).ceil() as u32 + PADDING * 2).max(64);
    let expected_height = ((80f32 * CELL_H).ceil() as u32 + PADDING * 2).max(32);

    assert_eq!(image.width(), expected_width);
    assert_eq!(image.height(), expected_height);
}

#[test]
fn screen_analyzer_enablement_requires_complete_config() {
    let mut cfg = ScreenAnalyzerConfig::default();
    assert!(!screen_analyzer_enabled(&cfg));
    cfg.enabled = true;
    cfg.base_url = "https://example.com".to_string();
    cfg.api_key = "k".to_string();
    cfg.model = "m".to_string();
    assert!(screen_analyzer_enabled(&cfg));
}

#[test]
fn parse_screen_analyzer_response_accepts_markdown_wrapped_json() {
    let content = "```json\n{\"needsInteraction\":false,\"checkAgainWhen\":\"after_5s\"}\n```";
    assert_eq!(
        parse_screen_analyzer_response(content).check_again_when,
        "after_5s"
    );
    assert_eq!(
        parse_screen_analyzer_response("{\"needsInteraction\":false}").check_again_when,
        "content_changed"
    );
}

#[test]
fn parse_screen_analyzer_response_builds_tui_prompt_keys() {
    let content = r#"{
      "needsInteraction": true,
      "description": "pick one",
      "multiSelect": false,
      "confirmKey": "Enter",
      "options": [
        { "label": "1", "text": "alpha", "type": "select", "index": 0 },
        { "label": "2", "text": "beta", "type": "confirm", "index": 1 }
      ],
      "checkAgainWhen": "content_changed"
    }"#;
    let parsed = parse_screen_analyzer_response(content);
    assert!(parsed.needs_interaction);
    assert_eq!(parsed.description.as_deref(), Some("pick one"));
    assert_eq!(parsed.options.len(), 2);
    assert_eq!(parsed.options[0].keys, vec!["Enter"]);
    assert_eq!(parsed.options[1].keys, vec!["Down", "Enter"]);
}