use axterminator::copilot_format::{
FormatOptions, format_as_json, format_changes_for_llm, format_for_llm,
};
use axterminator::copilot_state::{
AppContext, ContentContext, CopilotState, NavigationContext, SelectionContext, diff_states,
};
fn full_state() -> CopilotState {
CopilotState {
app: AppContext {
name: Some("Safari".to_owned()),
focused_window: Some("Main Window".to_owned()),
active_tab: Some("Tab 1".to_owned()),
active_document: Some("index.html".to_owned()),
},
selection: SelectionContext {
selected_text: Some("Hello world".to_owned()),
selected_list_row: Some(2),
selected_table_cell: None,
selected_items: vec!["Item A".to_owned(), "Item B".to_owned()],
},
navigation: NavigationContext {
breadcrumb: vec!["Home".to_owned(), "Projects".to_owned()],
sidebar_selection: Some("Inbox".to_owned()),
tab_bar_selection: Some("Editor".to_owned()),
depth: 3,
},
content: ContentContext {
document_title: Some("My Project".to_owned()),
visible_text_excerpt: Some("Lorem ipsum dolor".to_owned()),
form_fields: vec![
("Username".to_owned(), "alice".to_owned()),
("Email".to_owned(), "alice@example.com".to_owned()),
],
focused_element_role: Some("AXTextField".to_owned()),
focused_element_title: Some("Search".to_owned()),
},
timestamp: 1_700_000_000,
}
}
#[test]
fn empty_state_all_fields_default() {
let state = CopilotState::empty();
assert!(state.app.name.is_none());
assert!(state.selection.selected_items.is_empty());
assert!(state.navigation.breadcrumb.is_empty());
assert!(state.content.form_fields.is_empty());
}
#[test]
fn full_state_carries_all_fields() {
let s = full_state();
assert_eq!(s.app.name.as_deref(), Some("Safari"));
assert_eq!(s.selection.selected_list_row, Some(2));
assert_eq!(s.navigation.depth, 3);
assert_eq!(s.content.form_fields.len(), 2);
}
#[test]
fn diff_identical_empty_states_empty_vec() {
let s = CopilotState::empty();
assert!(diff_states(&s, &s).is_empty());
}
#[test]
fn diff_identical_full_states_empty_vec() {
let s = full_state();
assert!(diff_states(&s, &s).is_empty());
}
#[test]
fn diff_app_name_none_to_some() {
let old = CopilotState::empty();
let mut new = CopilotState::empty();
new.app.name = Some("Finder".to_owned());
let changes = diff_states(&old, &new);
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].field, "app.name");
assert!(changes[0].new_value.contains("Finder"));
}
#[test]
fn diff_app_focused_window_changes() {
let mut old = full_state();
let mut new = full_state();
old.app.focused_window = Some("Win A".to_owned());
new.app.focused_window = Some("Win B".to_owned());
let changes = diff_states(&old, &new);
assert!(changes.iter().any(|c| c.field == "app.focused_window"));
}
#[test]
fn diff_selected_text_changes() {
let mut old = CopilotState::empty();
let mut new = CopilotState::empty();
old.selection.selected_text = Some("foo".to_owned());
new.selection.selected_text = Some("foo bar".to_owned());
let changes = diff_states(&old, &new);
assert!(changes.iter().any(|c| c.field == "selection.selected_text"));
}
#[test]
fn diff_selected_list_row_changes() {
let mut old = CopilotState::empty();
let mut new = CopilotState::empty();
old.selection.selected_list_row = Some(0);
new.selection.selected_list_row = Some(9);
let changes = diff_states(&old, &new);
assert!(
changes
.iter()
.any(|c| c.field == "selection.selected_list_row")
);
}
#[test]
fn diff_selected_items_vec_changes() {
let old = CopilotState::empty();
let mut new = CopilotState::empty();
new.selection.selected_items = vec!["X".to_owned()];
let changes = diff_states(&old, &new);
assert!(
changes
.iter()
.any(|c| c.field == "selection.selected_items")
);
}
#[test]
fn diff_breadcrumb_changes() {
let old = CopilotState::empty();
let mut new = CopilotState::empty();
new.navigation.breadcrumb = vec!["Root".to_owned()];
let changes = diff_states(&old, &new);
assert!(changes.iter().any(|c| c.field == "navigation.breadcrumb"));
}
#[test]
fn diff_navigation_depth_changes() {
let old = CopilotState::empty();
let mut new = CopilotState::empty();
new.navigation.depth = 5;
let changes = diff_states(&old, &new);
assert!(changes.iter().any(|c| c.field == "navigation.depth"));
}
#[test]
fn diff_document_title_changes() {
let old = CopilotState::empty();
let mut new = CopilotState::empty();
new.content.document_title = Some("Report.pdf".to_owned());
let changes = diff_states(&old, &new);
assert!(changes.iter().any(|c| c.field == "content.document_title"));
}
#[test]
fn diff_form_fields_changes() {
let old = CopilotState::empty();
let mut new = CopilotState::empty();
new.content.form_fields = vec![("Name".to_owned(), "Bob".to_owned())];
let changes = diff_states(&old, &new);
assert!(changes.iter().any(|c| c.field == "content.form_fields"));
}
#[test]
fn diff_multiple_changes_all_reported() {
let old = CopilotState::empty();
let mut new = CopilotState::empty();
new.app.name = Some("Xcode".to_owned());
new.navigation.depth = 2;
new.content.document_title = Some("main.rs".to_owned());
let changes = diff_states(&old, &new);
assert_eq!(changes.len(), 3);
}
#[test]
fn format_for_llm_full_state_contains_all_sections() {
let state = full_state();
let opts = FormatOptions::default();
let text = format_for_llm(&state, &opts);
assert!(text.contains("[App]"), "missing [App]: {text}");
assert!(text.contains("[Selection]"), "missing [Selection]: {text}");
assert!(
text.contains("[Navigation]"),
"missing [Navigation]: {text}"
);
assert!(text.contains("[Content]"), "missing [Content]: {text}");
}
#[test]
fn format_for_llm_breadcrumb_uses_arrow_separator() {
let mut state = CopilotState::empty();
state.navigation.breadcrumb = vec!["A".to_owned(), "B".to_owned(), "C".to_owned()];
let text = format_for_llm(&state, &FormatOptions::default());
assert!(text.contains("A > B > C"), "got: {text}");
}
#[test]
fn format_for_llm_selected_items_comma_joined() {
let mut state = CopilotState::empty();
state.selection.selected_items = vec!["One".to_owned(), "Two".to_owned()];
let text = format_for_llm(&state, &FormatOptions::default());
assert!(text.contains("One, Two"), "got: {text}");
}
#[test]
fn format_for_llm_token_budget_via_with_token_budget() {
let state = full_state();
let opts = FormatOptions::with_token_budget(32);
let text = format_for_llm(&state, &opts);
assert!(text.contains("[truncated]"), "got: {text}");
}
#[test]
fn format_as_json_full_state_all_keys_present() {
let state = full_state();
let opts = FormatOptions::default();
let json = format_as_json(&state, &opts);
assert_eq!(json["app"]["name"], "Safari");
assert_eq!(json["navigation"]["depth"], 3);
assert!(json["selection"]["selected_items"].is_array());
assert!(json["content"]["form_fields"].is_array());
}
#[test]
fn format_as_json_form_fields_as_object_array() {
let mut state = CopilotState::empty();
state.content.form_fields = vec![("User".to_owned(), "carol".to_owned())];
let json = format_as_json(&state, &FormatOptions::default());
let fields = &json["content"]["form_fields"];
assert!(fields.is_array());
assert_eq!(fields[0]["label"], "User");
assert_eq!(fields[0]["value"], "carol");
}
#[test]
fn format_as_json_visible_text_truncated_to_quarter_budget() {
let mut state = CopilotState::empty();
state.content.visible_text_excerpt = Some("z".repeat(1000));
let opts = FormatOptions::with_token_budget(100);
let json = format_as_json(&state, &opts);
let excerpt = json["content"]["visible_text_excerpt"]
.as_str()
.expect("should be string");
assert!(excerpt.len() <= 100, "len={}", excerpt.len());
}
#[test]
fn format_changes_empty_message() {
let text = format_changes_for_llm(&[], &FormatOptions::default());
assert!(text.contains("No state changes"));
}
#[test]
fn format_changes_lists_field_names() {
let old = CopilotState::empty();
let mut new = CopilotState::empty();
new.app.name = Some("Notes".to_owned());
new.navigation.depth = 1;
let changes = diff_states(&old, &new);
let text = format_changes_for_llm(&changes, &FormatOptions::default());
assert!(text.contains("app.name"), "got: {text}");
assert!(text.contains("navigation.depth"), "got: {text}");
}
#[test]
fn read_copilot_state_null_ref_does_not_crash() {
let null_ref: axterminator::AXUIElementRef = std::ptr::null();
let state = axterminator::copilot_state::read_copilot_state(null_ref);
assert!(state.app.name.is_none());
}
#[test]
fn live_read_copilot_state_finder() {
if !axterminator::check_accessibility_enabled() {
eprintln!("Skipping: accessibility not enabled");
return;
}
let output = std::process::Command::new("pgrep")
.args(["-x", "Finder"])
.output()
.expect("pgrep failed");
let pid_str = String::from_utf8_lossy(&output.stdout);
let pid: i32 = match pid_str.trim().parse() {
Ok(p) => p,
Err(_) => {
eprintln!("Skipping: Finder not running");
return;
}
};
let app_ref =
axterminator::create_application_element(pid).expect("create_application_element failed");
let state = axterminator::copilot_state::read_copilot_state(app_ref);
assert!(state.timestamp > 0);
let text = format_for_llm(&state, &FormatOptions::default());
assert!(!text.is_empty());
}