ras-agent 2.6.0

Agent step loop, history, plan, rerun orchestration
Documentation
use ras_dom::BrowserStateSummary;

const CLICKABLE_LIMIT: usize = 80;
const NAME_BUDGET: usize = 80;

pub(crate) fn render_clickable_map(summary: &BrowserStateSummary) -> String {
    if summary.clickables.is_empty() {
        return String::new();
    }
    let mut buf = String::from("clickable_elements:\n");
    for c in summary.clickables.iter().take(CLICKABLE_LIMIT) {
        buf.push_str(&format!("  [{}] {}", c.index, c.tag));
        if let Some(name) = &c.ax_name {
            buf.push_str(&format!(" \"{}\"", truncate(name, NAME_BUDGET)));
        } else if let Some(label) = &c.label {
            buf.push_str(&format!(" \"{}\"", truncate(label, NAME_BUDGET)));
        }
        buf.push('\n');
    }
    if summary.clickables.len() > CLICKABLE_LIMIT {
        buf.push_str(&format!(
            "  …and {} more (truncated)\n",
            summary.clickables.len() - CLICKABLE_LIMIT
        ));
    }
    buf
}

fn truncate(s: &str, max: usize) -> String {
    if s.chars().count() <= max {
        s.to_string()
    } else {
        let mut out: String = s.chars().take(max).collect();
        out.push_str("");
        out
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use ras_dom::{BoundingBox, ClickableElement, PageStatistics};
    use ras_types::{BackendNodeId, TargetId};

    fn summary_with(clickables: Vec<ClickableElement>) -> BrowserStateSummary {
        BrowserStateSummary {
            target: TargetId("mock".into()),
            url: "https://example.com/".parse().expect("url"),
            title: "T".into(),
            tree: None,
            clickables,
            screenshot_b64: None,
            tabs: vec![],
            page_stats: PageStatistics::default(),
        }
    }

    fn click(idx: u32, tag: &str, ax_name: Option<&str>, label: Option<&str>) -> ClickableElement {
        ClickableElement {
            index: idx,
            backend_node_id: BackendNodeId(idx as i64),
            bbox: BoundingBox {
                x: 0.0,
                y: 0.0,
                width: 1.0,
                height: 1.0,
            },
            xpath: String::new(),
            stable_hash: String::new(),
            ax_name: ax_name.map(String::from),
            tag: tag.into(),
            label: label.map(String::from),
        }
    }

    #[test]
    fn empty_clickables_yields_empty_string() {
        assert_eq!(render_clickable_map(&summary_with(vec![])), "");
    }

    #[test]
    fn ax_name_takes_precedence_over_label() {
        let s = summary_with(vec![click(0, "button", Some("Sign in"), Some("submit"))]);
        let out = render_clickable_map(&s);
        assert!(out.contains("[0] button \"Sign in\""));
        assert!(!out.contains("submit"));
    }

    #[test]
    fn label_used_when_no_ax_name() {
        let s = summary_with(vec![click(0, "input", None, Some("user@example.com"))]);
        let out = render_clickable_map(&s);
        assert!(out.contains("[0] input \"user@example.com\""));
    }

    #[test]
    fn no_quotes_when_neither_ax_nor_label() {
        let s = summary_with(vec![click(0, "a", None, None)]);
        let out = render_clickable_map(&s);
        assert!(out.contains("[0] a\n"));
        assert!(!out.contains('"'));
    }

    #[test]
    fn truncates_beyond_limit_with_more_marker() {
        let many: Vec<ClickableElement> = (0..(CLICKABLE_LIMIT as u32 + 5))
            .map(|i| click(i, "div", None, None))
            .collect();
        let s = summary_with(many);
        let out = render_clickable_map(&s);
        assert!(out.contains(&format!("[{}]", CLICKABLE_LIMIT - 1)));
        assert!(!out.contains(&format!("[{}]", CLICKABLE_LIMIT)));
        assert!(out.contains("…and 5 more (truncated)"));
    }
}