use super::*;
mod recordings;
use browser::{BrowserPort, Frame, Recording, Reply, SessionRequest};
use recordings::*;
use serde_json::{Map, json};
const ONE_RED_PIXEL: &str = concat!(
"iVBORw0KGgoAAAAN",
"SUhEUgAAAAEAAAAB",
"CAYAAAAfFcSJAAAA",
"DUlEQVR42mP8z8BQ",
"DwAEhQGAhKmMIQAA",
"AABJRU5ErkJggg=="
);
const PAST_THE_CAP: &str = concat!(
"iVBORw0KGgoAAAAN",
"SUhEUgAAB9AAAAXc",
"CAYAAAB6ZRUrAAAA",
"AElFTkSuQmCC",
);
#[derive(Default)]
struct Sequence {
answers: Vec<Value>,
}
impl BrowserPort for Sequence {
fn send(&mut self, frame: &Frame) -> Result<Reply, AxError> {
let result = match frame.method() {
"session.new" => json!({ "sessionId": "s1" }),
"browsingContext.getTree" => json!({ "contexts": [{ "context": "c1" }] }),
_ => self.answers.remove(0),
};
Ok(Reply::Success {
id: frame.id(),
result,
})
}
}
fn args(value: Value) -> Payload {
Payload::new(value.as_object().cloned().unwrap_or_else(Map::new)).expect("no floats")
}
fn call(value: Value) -> ToolCall {
ToolCall {
id: "tu_1".to_owned(),
name: ToolName::parse("browser").expect("a literal name"),
args: args(value),
}
}
fn string_result(text: &str) -> Value {
json!({ "result": { "type": "string", "value": text } })
}
#[test]
fn a_screenshot_past_the_cap_is_taken_again_and_the_smaller_one_kept() {
let dir = tempfile::tempdir().expect("a temporary directory");
let cas = Cas::open(&dir.path().join("cas")).expect("a cas opens in a fresh directory");
let port = Sequence {
answers: vec![
json!({ "data": PAST_THE_CAP }),
json!({}),
json!({ "data": ONE_RED_PIXEL }),
],
};
let tool =
BrowserTool::new(Role::Building, Box::new(port), cas, origin()).expect("the tool builds");
let shot = tool
.invoke(&call(json!({ "action": "screenshot" })))
.expect("the shot is taken again and kept");
assert_eq!(shot.attachments.len(), 1);
assert_eq!(
(shot.attachments[0].width, shot.attachments[0].height),
(1, 1)
);
}
#[test]
fn a_screenshot_by_reference_set_covers_every_element_it_named() {
let dir = tempfile::tempdir().expect("a temporary directory");
let tool = tool_with(&dir.path().join("cas"), recorded_by_union());
tool.invoke(&call(json!({ "action": "snapshot" })))
.expect("the page is looked at");
let shot = tool
.invoke(&call(
json!({ "action": "screenshot", "refs": ["e1", "e2"], "generation": 1 }),
))
.expect("the elements are photographed together");
let result = text(&shot);
assert!(
result.contains("cas:b3-"),
"a union shot becomes evidence too: {result}"
);
assert_eq!(shot.attachments.len(), 1);
}
#[test]
fn a_screenshot_by_reference_covers_the_element_the_page_named() {
let dir = tempfile::tempdir().expect("a temporary directory");
let tool = tool_with(&dir.path().join("cas"), recorded_by_reference());
tool.invoke(&call(json!({ "action": "snapshot" })))
.expect("the page is looked at");
let shot = tool
.invoke(&call(
json!({ "action": "screenshot", "ref": "e1", "generation": 1 }),
))
.expect("the element is photographed");
let result = text(&shot);
assert!(
result.contains("cas:b3-"),
"a clipped shot becomes evidence too: {result}"
);
assert_eq!(shot.attachments.len(), 1);
assert_eq!(
(shot.attachments[0].width, shot.attachments[0].height),
(1, 1)
);
}
fn text(outcome: &ToolOutcome) -> String {
serde_json::to_string(&outcome.result).expect("a payload serialises")
}
#[test]
fn one_conversation_replays_from_open_to_a_screenshot_that_is_evidence() {
let dir = tempfile::tempdir().expect("a temporary directory");
let tool = tool(&dir.path().join("cas"));
let opened = tool
.invoke(&call(
json!({ "action": "open", "url": "https://example.test/" }),
))
.expect("the page opens");
assert!(text(&opened).contains("https://example.test/"));
let looked = tool
.invoke(&call(json!({ "action": "snapshot" })))
.expect("the page is looked at");
let seen = text(&looked);
assert!(seen.contains("e1 button"), "{seen}");
assert!(
seen.contains("\"generation\":1"),
"a reference is a position in one snapshot: {seen}"
);
let acted = tool
.invoke(&call(
json!({ "action": "act", "generation": 1, "ref": "e1", "kind": "click" }),
))
.expect("the button is clicked");
assert!(!text(&acted).is_empty());
let shot = tool
.invoke(&call(json!({ "action": "screenshot" })))
.expect("the page is photographed");
let result = text(&shot);
assert!(
result.contains("cas:b3-"),
"a screenshot is stored: {result}"
);
assert!(result.contains("\"width\":1"), "{result}");
assert!(result.contains("\"height\":1"), "{result}");
assert_eq!(
shot.attachments.len(),
1,
"a picture a model cannot see is not evidence"
);
let picture = &shot.attachments[0];
assert_eq!((picture.width, picture.height), (1, 1));
assert_eq!(picture.media_type, kernel::ImageType::Png);
let kernel::Locator::Cas { hash, .. } = &picture.locator else {
panic!("a screenshot is stored by content, not by path");
};
let stored = Cas::open(&dir.path().join("cas"))
.expect("the same store reopens")
.get(hash)
.expect("the bytes are in the store");
assert_eq!(stored.len(), 70);
let origins = Cas::open(&dir.path().join("cas"))
.expect("the same store reopens")
.origins(hash)
.expect("the origins read");
assert_eq!(origins, vec![origin()], "pinned for the run that took it");
}
#[test]
fn acting_before_looking_is_refused_and_leaves_the_tool_usable() {
let dir = tempfile::tempdir().expect("a temporary directory");
let tool = tool(&dir.path().join("cas"));
let err = tool
.invoke(&call(
json!({ "action": "act", "generation": 1, "ref": "e1", "kind": "click" }),
))
.unwrap_err();
assert_eq!(err.code(), &AxCode::InvalidArgs);
assert!(err.recovery().contains("snapshot"));
assert_eq!(tool.meta().name.as_str(), "browser");
}
#[test]
fn a_call_bearing_another_tools_name_is_refused() {
let dir = tempfile::tempdir().expect("a temporary directory");
let tool = tool(&dir.path().join("cas"));
let err = tool
.invoke(&ToolCall {
id: "tu_2".to_owned(),
name: ToolName::parse("exec").expect("a literal name"),
args: args(json!({ "action": "close" })),
})
.unwrap_err();
assert_eq!(err.code(), &AxCode::InvalidArgs);
}
#[test]
fn a_second_look_at_the_same_page_settles_the_development_loop() {
let dir = tempfile::tempdir().expect("a temporary directory");
let tool = tool(&dir.path().join("cas"));
let _ = tool.invoke(&call(json!({ "action": "snapshot" })));
let mut steps = Vec::new();
for _ in 0..3 {
let outcome = tool
.invoke(&call(json!({ "action": "snapshot" })))
.expect("a page can be looked at again");
steps.push(text(&outcome));
}
assert!(
steps.iter().any(|step| step.contains("settled")),
"a page that stops changing settles: {steps:?}"
);
}
#[test]
fn the_tab_is_opened_once_and_the_frames_keep_one_numbering() {
let dir = tempfile::tempdir().expect("a temporary directory");
let tool = tool(&dir.path().join("cas"));
for _ in 0..2 {
tool.invoke(&call(json!({ "action": "snapshot" })))
.expect("a page is looked at");
}
assert!(
tool.browser.lock().unwrap().context.is_some(),
"the session a tool opened is the session it keeps"
);
}
#[test]
fn the_person_tool_asks_before_it_connects_and_names_itself() {
let dir = tempfile::tempdir().expect("a temporary directory");
let cas = Cas::open(&dir.path().join("cas")).expect("a cas opens in a fresh directory");
let waiting = BrowserTool::new(
Role::PersonWaiting,
Box::new(crate::browser_bidi::AttachedBrowser::waiting()),
cas,
origin(),
)
.expect("the tool builds");
assert_eq!(
waiting.meta().name.as_str(),
kernel::ToolName::USER_BROWSER,
"the person's browser is its own tool"
);
assert!(matches!(
waiting.meta().effect,
Effect::AttachUserBrowser { address: None }
));
assert!(
waiting.meta().disclosure.contains("Use `browser`"),
"the disclosure guides to the tool that needs nobody's permission"
);
let ask = ToolCall {
id: "tu_person".to_owned(),
name: ToolName::parse(kernel::ToolName::USER_BROWSER).expect("a literal name"),
args: args(json!({ "action": "snapshot" })),
};
let err = waiting
.invoke(&ask)
.expect_err("nothing connects without an address");
assert_eq!(err.code(), &AxCode::BrowserUnavailable);
}
#[test]
fn a_declared_address_is_the_effect_the_attach_door_judges() {
let dir = tempfile::tempdir().expect("a temporary directory");
let cas = Cas::open(&dir.path().join("cas")).expect("a cas opens in a fresh directory");
let tool = BrowserTool::new(
Role::PersonAt {
host: "127.0.0.1".to_owned(),
},
Box::new(crate::browser_bidi::AttachedBrowser::at(
"ws://127.0.0.1:9222/session",
)),
cas,
origin(),
)
.expect("the tool builds");
assert!(matches!(
tool.meta().effect,
Effect::AttachUserBrowser { address: Some(ref host) } if host == "127.0.0.1"
));
let subject = tool
.subject(&call(
json!({ "action": "open", "url": "https://example.com/" }),
))
.expect("the call reads");
assert_eq!(subject, GateSubject::Host("example.com".to_owned()));
let snapshot = tool
.subject(&call(json!({ "action": "snapshot" })))
.expect("the call reads");
assert_eq!(snapshot, GateSubject::None);
}
#[test]
fn a_fetch_answers_with_the_pages_own_reading_of_the_response() {
let dir = tempfile::tempdir().expect("a temporary directory");
let cas = Cas::open(&dir.path().join("cas")).expect("a cas opens in a fresh directory");
let port = Sequence {
answers: vec![
string_result(
r#"{"status":200,"type":"text/html","url":"https://example.test/a","text":"Title\n\nBody","cut":false,"chars":11}"#,
),
string_result(
r#"{"error":"TypeError: NetworkError when attempting to fetch resource."}"#,
),
],
};
let tool =
BrowserTool::new(Role::Building, Box::new(port), cas, origin()).expect("the tool builds");
let fetched = tool
.invoke(&call(
json!({ "action": "fetch", "url": "https://example.test/a" }),
))
.map(|outcome| outcome.result.as_map().clone());
let expected: Map<String, Value> = json!({
"status": 200, "type": "text/html", "url": "https://example.test/a",
"text": "Title\n\nBody", "cut": false, "chars": 11,
})
.as_object()
.cloned()
.unwrap_or_default();
assert_eq!(fetched.ok(), Some(expected));
let refused = tool
.invoke(&call(
json!({ "action": "fetch", "url": "https://elsewhere.test/b" }),
))
.map(|outcome| outcome.result.as_map().get("error").cloned());
assert_eq!(
refused.ok().flatten(),
Some(Value::String(
"TypeError: NetworkError when attempting to fetch resource.".to_owned()
))
);
}