use crate::mcp::protocol::{
Prompt, PromptArgument, PromptContent, PromptGetParams, PromptGetResult, PromptListResult,
PromptMessage, PromptRole,
};
#[must_use]
pub fn all_prompts() -> PromptListResult {
PromptListResult {
prompts: vec![
prompt_test_app(),
prompt_navigate_to(),
prompt_extract_data(),
prompt_accessibility_audit(),
prompt_troubleshooting(),
prompt_app_guide(),
prompt_automate_workflow(),
prompt_debug_ui(),
prompt_cross_app_copy(),
prompt_analyze_app(),
],
}
}
pub fn get_prompt(params: &PromptGetParams) -> Result<PromptGetResult, String> {
match params.name.as_str() {
"test-app" => build_test_app(params),
"navigate-to" => build_navigate_to(params),
"extract-data" => build_extract_data(params),
"accessibility-audit" => build_accessibility_audit(params),
"troubleshooting" => build_troubleshooting(params),
"app-guide" => build_app_guide(params),
"automate-workflow" => build_automate_workflow(params),
"debug-ui" => build_debug_ui(params),
"cross-app-copy" => build_cross_app_copy(params),
"analyze-app" => build_analyze_app(params),
other => Err(format!("Unknown prompt: {other}")),
}
}
fn prompt_test_app() -> Prompt {
Prompt {
name: "test-app",
title: "Test a macOS Application",
description: "Step-by-step guide to test a macOS application. \
Connects, explores the UI, runs interactions, and reports findings.",
arguments: vec![
PromptArgument {
name: "app_name",
description: "Name of the app to test (e.g. Safari, Finder)",
required: true,
},
PromptArgument {
name: "focus_area",
description: "Specific area to test (e.g. toolbar, sidebar). Omit to test all.",
required: false,
},
],
}
}
fn prompt_navigate_to() -> Prompt {
Prompt {
name: "navigate-to",
title: "Navigate to a Screen",
description: "Navigate to a specific screen, dialog, or state within a macOS application.",
arguments: vec![
PromptArgument {
name: "app_name",
description: "Name of the app",
required: true,
},
PromptArgument {
name: "target_screen",
description: "Where to navigate (e.g. Settings > General, File > New)",
required: true,
},
],
}
}
fn prompt_extract_data() -> Prompt {
Prompt {
name: "extract-data",
title: "Extract Data from Application",
description: "Extract structured data from a running macOS application. \
Reads element values, table contents, or form fields.",
arguments: vec![
PromptArgument {
name: "app_name",
description: "Name of the connected app",
required: true,
},
PromptArgument {
name: "data_description",
description: "What data to extract (e.g. list of contacts, form fields)",
required: true,
},
],
}
}
fn prompt_accessibility_audit() -> Prompt {
Prompt {
name: "accessibility-audit",
title: "Accessibility Audit",
description: "Audit a macOS application for accessibility issues: \
missing labels, incorrect roles, keyboard navigation, and WCAG compliance.",
arguments: vec![PromptArgument {
name: "app_name",
description: "Name of the app to audit",
required: true,
}],
}
}
fn prompt_troubleshooting() -> Prompt {
Prompt {
name: "troubleshooting",
title: "Troubleshooting Guide",
description: "Detailed guidance when something fails: element not found, \
click not working, text not appearing, screenshot failing. \
Request this prompt when you encounter an error.",
arguments: vec![PromptArgument {
name: "error",
description: "The error message or symptom you encountered",
required: true,
}],
}
}
fn prompt_app_guide() -> Prompt {
Prompt {
name: "app-guide",
title: "App-Specific Playbook",
description: "Detailed per-app instructions: which query syntax works, \
which interaction methods to use, known quirks. \
Available for: Calculator, TextEdit, Safari, Chrome, Finder, Notes.",
arguments: vec![PromptArgument {
name: "app",
description: "App name (e.g. Calculator, TextEdit, Safari)",
required: true,
}],
}
}
fn prompt_automate_workflow() -> Prompt {
Prompt {
name: "automate-workflow",
title: "Automate a Workflow",
description: "Plan and track a multi-step workflow. Guides the agent through defining \
steps and using ax_workflow_create/step/status alongside the matching ax_* actions.",
arguments: vec![
PromptArgument {
name: "app_name",
description: "Primary app the workflow runs in (e.g. Safari, TextEdit)",
required: true,
},
PromptArgument {
name: "goal",
description: "Natural language description of what the workflow should accomplish",
required: true,
},
],
}
}
fn prompt_debug_ui() -> Prompt {
Prompt {
name: "debug-ui",
title: "Debug UI Element",
description: "Debug why an element cannot be found. Walks through the accessibility tree, \
checks attribute values, suggests alternative queries.",
arguments: vec![
PromptArgument {
name: "app_name",
description: "App to debug",
required: true,
},
PromptArgument {
name: "query",
description: "The query that failed (e.g. title:Submit, role:AXButton)",
required: true,
},
],
}
}
fn prompt_cross_app_copy() -> Prompt {
Prompt {
name: "cross-app-copy",
title: "Copy Data Between Apps",
description: "Copy data between two macOS applications. Reads from source, \
transforms if needed, writes to destination.",
arguments: vec![
PromptArgument {
name: "source_app",
description: "App to read data from",
required: true,
},
PromptArgument {
name: "dest_app",
description: "App to write data to",
required: true,
},
PromptArgument {
name: "data_description",
description: "What data to copy (e.g. 'selected text', 'table contents')",
required: true,
},
],
}
}
fn prompt_analyze_app() -> Prompt {
Prompt {
name: "analyze-app",
title: "Analyze App UI",
description: "Comprehensive analysis of an app's UI: detect patterns, infer state, \
suggest actions, audit accessibility.",
arguments: vec![PromptArgument {
name: "app_name",
description: "Name of the app to analyze",
required: true,
}],
}
}
fn build_test_app(params: &PromptGetParams) -> Result<PromptGetResult, String> {
let app = require_arg(params, "app_name")?;
let focus_hint = optional_arg(params, "focus_area")
.map(|f| format!(" Focus your testing on the {f} area."))
.unwrap_or_default();
let user_msg = format!(
"Test the macOS application \"{app}\".{focus_hint}\n\
Follow these steps:\n\
1. Call ax_is_accessible to verify accessibility permissions are enabled.\n\
2. Call ax_connect with app=\"{app}\" to connect.\n\
3. Call ax_list_windows with app=\"{app}\" to see open windows.\n\
4. Call ax_screenshot with app=\"{app}\" for visual context.\n\
5. Find key interactive elements using ax_find and document what you discover.\n\
6. Test each interactive element: click buttons, fill text fields, \
verify expected state changes.\n\
7. Report your findings: what works, what looks broken, what is confusing."
);
let assistant_msg = format!(
"I will test {app} systematically. \
Starting with accessibility verification, \
then connecting and exploring the UI visually and through the element tree."
);
Ok(PromptGetResult {
description: format!("Guided testing workflow for {app}"),
messages: vec![user_message(user_msg), assistant_message(assistant_msg)],
})
}
fn build_navigate_to(params: &PromptGetParams) -> Result<PromptGetResult, String> {
let app = require_arg(params, "app_name")?;
let target = require_arg(params, "target_screen")?;
let user_msg = format!(
"Navigate to \"{target}\" in {app}.\n\
Steps:\n\
1. Call ax_connect with app=\"{app}\" to connect (if not already connected).\n\
2. Call ax_screenshot to see the current state.\n\
3. Use ax_find to locate navigation elements (menu items, buttons, tabs).\n\
4. Click the required sequence of elements to reach \"{target}\".\n\
5. Take a final ax_screenshot to confirm you have arrived at the right screen."
);
let assistant_msg = format!(
"I will navigate to \"{target}\" in {app} step by step, \
verifying each step with a screenshot."
);
Ok(PromptGetResult {
description: format!("Navigate to {target} in {app}"),
messages: vec![user_message(user_msg), assistant_message(assistant_msg)],
})
}
fn build_extract_data(params: &PromptGetParams) -> Result<PromptGetResult, String> {
let app = require_arg(params, "app_name")?;
let description = require_arg(params, "data_description")?;
let user_msg = format!(
"Extract \"{description}\" from {app}.\n\
Steps:\n\
1. Call ax_connect with app=\"{app}\" to connect (if not already connected).\n\
2. Call ax_screenshot to see the current screen.\n\
3. Use ax_find to locate the elements that contain the data.\n\
4. Call ax_get_value for each relevant element to read its content.\n\
5. Structure the extracted data as JSON and present it clearly."
);
let assistant_msg = format!(
"I will extract \"{description}\" from {app} by reading the accessibility \
element values and structuring the output as JSON."
);
Ok(PromptGetResult {
description: format!("Extract {description} from {app}"),
messages: vec![user_message(user_msg), assistant_message(assistant_msg)],
})
}
fn build_accessibility_audit(params: &PromptGetParams) -> Result<PromptGetResult, String> {
let app = require_arg(params, "app_name")?;
let user_msg = format!(
"Audit {app} for accessibility compliance.\n\
Steps:\n\
1. Call ax_connect with app=\"{app}\" to connect.\n\
2. Call ax_screenshot for visual context.\n\
3. Read axterminator://app/{app}/tree to inspect the full element hierarchy.\n\
4. Check every interactive element for:\n\
- A descriptive title or label (missing label = WCAG 1.3.1 failure)\n\
- A correct semantic role (e.g. AXButton, AXTextField, not AXUnknown)\n\
- Keyboard focus support (AXFocused attribute present)\n\
5. Report issues by element with severity: critical / warning / info."
);
let assistant_msg = format!(
"I will audit {app} for accessibility issues, checking labels, \
roles, and keyboard navigation, and report findings by severity."
);
Ok(PromptGetResult {
description: format!("Accessibility audit for {app}"),
messages: vec![user_message(user_msg), assistant_message(assistant_msg)],
})
}
fn build_troubleshooting(params: &PromptGetParams) -> Result<PromptGetResult, String> {
let error = require_arg(params, "error")?;
let user_msg = format!(
"I encountered this error while using AXTerminator: \"{error}\"\n\n\
Help me resolve it."
);
let assistant_msg = "\
Here's a troubleshooting guide based on common AXTerminator issues:\n\
\n\
## Element not found\n\
1. Run ax_get_tree with depth=6-8 to see the actual element hierarchy\n\
2. Check if the element uses AXDescription instead of AXTitle (common in Calculator, custom controls)\n\
3. Try query=\"description:YourText\" instead of simple text\n\
4. If multiple windows are open, close extras — search may find elements in the wrong window\n\
5. Increase timeout_ms (default 5000) for elements that appear after animation\n\
\n\
## AXPress unsupported / Click not working\n\
1. Some apps don't support AXPress on certain elements (Calculator buttons, text areas)\n\
2. Use ax_click_at with coordinates instead:\n\
- First: ax_find to get the element bounds [x, y, w, h]\n\
- Calculate center: click_x = x + w/2, click_y = y + h/2\n\
- Then: ax_click_at x=click_x y=click_y\n\
3. The target app MUST be in the foreground for coordinate clicks\n\
\n\
## Text not appearing after ax_type\n\
1. The target app must be active/focused — run osascript to activate it first\n\
2. The text area must have keyboard focus\n\
3. Try ax_set_value instead — it sets the value directly without keystroke simulation\n\
4. ax_set_value works even in background mode\n\
\n\
## Screenshot fails silently\n\
1. ax_screenshot uses a window-id lookup that may fail on some apps\n\
2. Fallback: use screencapture CLI with region coordinates from ax_list_windows\n\
3. Example: screencapture -R\"x,y,w,h\" -x /tmp/shot.png\n\
\n\
## App not found\n\
1. Run ax_list_apps to see exact running app names\n\
2. Use bundle ID (most reliable): ax_connect app=\"com.apple.calculator\"\n\
3. App names are case-sensitive and must match exactly\n\
\n\
## Search returns wrong element\n\
1. Searches are scoped to windows (menus excluded) but may hit wrong window\n\
2. Close other windows of the same app\n\
3. Use more specific queries: role:AXButton title:Save instead of just Save\n\
4. Use ax_get_tree to understand the hierarchy before searching"
.to_string();
Ok(PromptGetResult {
description: format!("Troubleshooting: {error}"),
messages: vec![user_message(user_msg), assistant_message(assistant_msg)],
})
}
fn build_app_guide(params: &PromptGetParams) -> Result<PromptGetResult, String> {
let app = require_arg(params, "app")?;
let guide = match app.to_lowercase().as_str() {
"calculator" => "\
## Calculator (macOS 26)\n\
\n\
**Critical**: macOS 26 Calculator is ALWAYS RPN mode. There is no '=' button.\n\
\n\
### Button attributes\n\
- Buttons have title=null — use description: prefix for queries\n\
- Button names: 'All Clear', 'Clear', 'Percent', 'Divide', '7', '8', '9', 'Multiply',\n\
'4', '5', '6', 'Subtract', '1', '2', '3', 'Add', 'Change Sign', '0', 'Point', 'Enter'\n\
\n\
### Interaction method\n\
- AXPress does NOT work on Calculator buttons\n\
- Use ax_click_at with coordinates:\n\
1. ax_find query=\"description:7\" → get bounds\n\
2. Calculate center: x = bounds[0] + bounds[2]/2, y = bounds[1] + bounds[3]/2\n\
3. ax_click_at x=center_x y=center_y\n\
- App MUST be in foreground for coordinate clicks\n\
\n\
### RPN sequence for 7 + 3 = 10\n\
1. Click '7'\n\
2. Click 'Enter' (pushes 7 to stack)\n\
3. Click '3'\n\
4. Click 'Add' (pops 7 and 3, pushes 10)\n\
\n\
### Reading the display\n\
- ax_get_value query=\"role:AXStaticText\" → returns the display value\n\
\n\
### Clearing\n\
- ax_click_at on 'All Clear' button (or 'Clear' if mid-entry)"
.to_string(),
"textedit" => "\
## TextEdit\n\
\n\
### Setting text\n\
- Best method: ax_set_value query=\"role:AXTextArea\" value=\"Your text\"\n\
- This sets the entire content instantly, works in background mode\n\
\n\
### Typing text (simulated keystrokes)\n\
- ax_type query=\"role:AXTextArea\" text=\"chars\" mode=\"focus\"\n\
- Requires app to be active: run osascript to activate first\n\
- Appends to existing content (doesn't replace)\n\
\n\
### Known issues\n\
- ax_click on AXTextArea returns 'AXPress unsupported' — this is normal\n\
- Multiple open documents: search may find AXTextArea in wrong window\n\
→ Close other documents first\n\
- Rich text mode may behave differently than plain text\n\
\n\
### Reading text\n\
- ax_get_value query=\"role:AXTextArea\" → returns full text content"
.to_string(),
"safari" => "\
## Safari\n\
\n\
### Connection\n\
- ax_connect app=\"com.apple.Safari\" alias=\"s\"\n\
\n\
### URL bar\n\
- ax_find query=\"id:WEB_BROWSER_ADDRESS_AND_SEARCH_FIELD\"\n\
- Click with focus mode, then ax_type to enter URL\n\
\n\
### Web content\n\
- Most web content is NOT in the accessibility tree\n\
- Use ax_get_tree with depth=8+ to find what's exposed\n\
- For full web interaction, use ax_screenshot + vision AI, or Chrome DevTools Protocol\n\
\n\
### Navigation\n\
- Back/Forward: ax_click query=\"description:Go Back\" or \"description:Go Forward\"\n\
- New Tab: ax_key_press keys=\"t\" with command modifier\n\
- Reload: ax_key_press keys=\"r\" with command modifier"
.to_string(),
"chrome" | "google chrome" => "\
## Google Chrome\n\
\n\
### Connection\n\
- ax_connect app=\"com.google.Chrome\" alias=\"c\"\n\
\n\
### URL bar\n\
- ax_find query=\"role:AXTextField\" — usually the first text field\n\
\n\
### Web content\n\
- Chrome's AX tree is deep — use ax_get_tree with depth=8+\n\
- Electron apps (VS Code, Slack) use Chrome's accessibility layer\n\
- For precise web automation, prefer Chrome DevTools Protocol\n\
\n\
### Tabs\n\
- Tabs are in an AXTabGroup\n\
- New tab: ax_key_press keys=\"t\" with command modifier\n\
- Close tab: ax_key_press keys=\"w\" with command modifier"
.to_string(),
"finder" => "\
## Finder\n\
\n\
### Connection\n\
- ax_connect app=\"com.apple.finder\" alias=\"f\"\n\
- Finder is always running — connection always succeeds\n\
\n\
### Interaction\n\
- Standard AX support — ax_click works in background mode\n\
- Buttons and menu items support AXPress\n\
\n\
### Common operations\n\
- New Folder: ax_click query=\"New Folder\" or ax_key_press keys=\"n\" with shift+command\n\
- Sidebar items: ax_find query=\"role:AXRow\" — rows in the sidebar\n\
- File list: ax_get_tree depth=6 to see file listing structure"
.to_string(),
"notes" => "\
## Notes\n\
\n\
### Connection\n\
- ax_connect app=\"com.apple.Notes\" alias=\"n\"\n\
\n\
### Creating a note\n\
- ax_click query=\"New Note\" or ax_key_press keys=\"n\" with command modifier\n\
\n\
### Typing into a note\n\
- ax_find query=\"role:AXTextArea\" to locate the note body\n\
- ax_type or ax_set_value to enter text\n\
\n\
### Reading note content\n\
- ax_get_value query=\"role:AXTextArea\""
.to_string(),
_ => format!(
"## {app}\n\
\n\
No specific playbook available for this app. General approach:\n\
1. ax_connect with the app name or bundle ID\n\
2. ax_get_tree depth=6 to explore the UI hierarchy\n\
3. Look at element roles and attributes to determine query syntax\n\
4. Try ax_click first; if 'AXPress unsupported', switch to ax_click_at\n\
5. For text input, try ax_set_value first, ax_type as fallback\n\
\n\
Tip: Run ax_list_apps to verify the exact app name."
),
};
let user_msg = format!("How do I automate {app} with AXTerminator?");
Ok(PromptGetResult {
description: format!("App-specific playbook for {app}"),
messages: vec![user_message(user_msg), assistant_message(guide)],
})
}
fn build_automate_workflow(params: &PromptGetParams) -> Result<PromptGetResult, String> {
let app = require_arg(params, "app_name")?;
let goal = require_arg(params, "goal")?;
let user_msg = format!(
"Automate the following workflow in {app}: \"{goal}\"\n\
Steps:\n\
1. Call ax_connect with app=\"{app}\" to connect (if not already connected).\n\
2. Call ax_get_tree and ax_screenshot to capture the current state.\n\
3. Decompose the goal into atomic click, type, wait, assert, or checkpoint steps.\n\
4. Call ax_workflow_create with the composed steps and a stable name.\n\
5. For each planned step, run the matching ax_* tool yourself when UI execution is needed.\n\
6. After the UI action succeeds, call ax_workflow_step to record progress and emit notifications.\n\
7. Use ax_workflow_status to inspect progress between steps or before resuming.\n\
8. Take a final ax_screenshot and call ax_assert to verify the goal was reached."
);
let assistant_msg = format!(
"I will automate \"{goal}\" in {app} by planning the steps, executing the matching \
ax_* tools, and using ax_workflow_create/step/status to keep workflow progress tracked."
);
Ok(PromptGetResult {
description: format!("Automate workflow: {goal} in {app}"),
messages: vec![user_message(user_msg), assistant_message(assistant_msg)],
})
}
fn build_debug_ui(params: &PromptGetParams) -> Result<PromptGetResult, String> {
let app = require_arg(params, "app_name")?;
let query = require_arg(params, "query")?;
let user_msg = format!(
"The query \"{query}\" returns no results in {app}. Help me find the element.\n\
Debug steps:\n\
1. Call ax_connect with app=\"{app}\" (if not already connected).\n\
2. Call ax_screenshot to see what is currently visible on screen.\n\
3. Call ax_get_tree with depth=8 to inspect the full element hierarchy.\n\
4. Look for elements that match the intent of \"{query}\" using different attributes:\n\
- Try query=\"description:{query}\" if the original used a title search\n\
- Try query=\"role:AXButton\" then narrow by other attributes\n\
- Try query=\"id:...\" if the element has an accessibility identifier\n\
5. Check whether the element is inside a scroll view, sheet, or popover \
that may not be in the foreground window.\n\
6. Check whether the element appears only after an action (e.g. hover, focus).\n\
7. Report: the exact element found (role, title, description, identifier, bounds) \
and the corrected query syntax."
);
let assistant_msg = format!(
"I will diagnose why \"{query}\" fails in {app} by inspecting the full \
accessibility tree and trying alternative locators."
);
Ok(PromptGetResult {
description: format!("Debug UI element '{query}' in {app}"),
messages: vec![user_message(user_msg), assistant_message(assistant_msg)],
})
}
fn build_cross_app_copy(params: &PromptGetParams) -> Result<PromptGetResult, String> {
let source = require_arg(params, "source_app")?;
let dest = require_arg(params, "dest_app")?;
let data = require_arg(params, "data_description")?;
let user_msg = format!(
"Copy \"{data}\" from {source} to {dest}.\n\
Steps:\n\
1. Call ax_connect for both {source} and {dest}.\n\
2. Call ax_screenshot on {source} to confirm the data is visible.\n\
3. Locate the source element with ax_find in {source} and read it with ax_get_value.\n\
4. Read the current clipboard via the axterminator://clipboard resource.\n\
5. If the source supports AXPress on a Copy button or cmd+C, use ax_key_press;\n\
otherwise use ax_set_value / ax_clipboard write to place the value on the clipboard.\n\
6. Switch to {dest}: call ax_screenshot to see its current state.\n\
7. Locate the destination element in {dest} and use ax_set_value or ax_key_press \
cmd+V to paste.\n\
8. Call ax_get_value on the destination element to verify the data was transferred.\n\
9. Report success with the transferred value and any transformation applied."
);
let assistant_msg = format!(
"I will copy \"{data}\" from {source} to {dest} using the clipboard \
as the transfer channel, verifying the data at each stage."
);
Ok(PromptGetResult {
description: format!("Copy {data} from {source} to {dest}"),
messages: vec![user_message(user_msg), assistant_message(assistant_msg)],
})
}
fn build_analyze_app(params: &PromptGetParams) -> Result<PromptGetResult, String> {
let app = require_arg(params, "app_name")?;
let user_msg = format!(
"Perform a comprehensive UI analysis of {app}.\n\
Steps:\n\
1. Call ax_connect with app=\"{app}\" to connect.\n\
2. Call ax_screenshot for a visual snapshot.\n\
3. Read axterminator://app/{app}/state to get structured window and focus info.\n\
4. Read axterminator://app/{app}/tree to get the full element hierarchy.\n\
5. Detect UI patterns: identify the main interaction model (form, table, \
document, browser, media player, settings panel, etc.).\n\
6. Infer application state: idle, loading, error, editing, modal dialog, etc.\n\
7. Suggest the 3-5 most useful next actions an agent could take.\n\
8. Run an accessibility pre-check: list any elements missing labels or roles.\n\
9. Report findings as a structured summary with sections: \
Visual State | UI Pattern | Suggested Actions | Accessibility Issues."
);
let assistant_msg = format!(
"I will analyze {app} comprehensively — visual state, UI patterns, \
inferred application state, next-action suggestions, and an accessibility pre-check."
);
Ok(PromptGetResult {
description: format!("Comprehensive UI analysis of {app}"),
messages: vec![user_message(user_msg), assistant_message(assistant_msg)],
})
}
fn require_arg<'a>(params: &'a PromptGetParams, name: &str) -> Result<&'a str, String> {
params
.arguments
.as_ref()
.and_then(|args| args.get(name))
.and_then(|v| v.as_str())
.ok_or_else(|| format!("Missing required argument: {name}"))
}
fn optional_arg<'a>(params: &'a PromptGetParams, name: &str) -> Option<&'a str> {
params
.arguments
.as_ref()
.and_then(|args| args.get(name))
.and_then(|v| v.as_str())
}
fn user_message(text: impl Into<String>) -> PromptMessage {
PromptMessage {
role: PromptRole::User,
content: PromptContent::text(text),
}
}
fn assistant_message(text: impl Into<String>) -> PromptMessage {
PromptMessage {
role: PromptRole::Assistant,
content: PromptContent::text(text),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn args(pairs: &[(&str, &str)]) -> Option<serde_json::Map<String, serde_json::Value>> {
let mut map = serde_json::Map::new();
for (k, v) in pairs {
map.insert((*k).into(), json!(*v));
}
Some(map)
}
fn params(name: &str, pairs: &[(&str, &str)]) -> PromptGetParams {
PromptGetParams {
name: name.into(),
arguments: args(pairs),
}
}
#[test]
fn all_prompts_returns_ten_prompts() {
let list = all_prompts();
assert_eq!(list.prompts.len(), 10);
}
#[test]
fn all_prompts_names_are_unique() {
let list = all_prompts();
let names: std::collections::HashSet<&str> = list.prompts.iter().map(|p| p.name).collect();
assert_eq!(names.len(), list.prompts.len());
}
#[test]
fn all_prompts_serialise_without_panic() {
let list = all_prompts();
let json = serde_json::to_string(&list).unwrap();
assert!(json.contains("test-app"));
assert!(json.contains("accessibility-audit"));
}
#[test]
fn unknown_prompt_returns_error() {
let p = PromptGetParams {
name: "does-not-exist".into(),
arguments: None,
};
assert!(get_prompt(&p).is_err());
}
#[test]
fn test_app_with_valid_args_returns_two_messages() {
let p = params("test-app", &[("app_name", "Safari")]);
let result = get_prompt(&p).unwrap();
assert_eq!(result.messages.len(), 2);
}
#[test]
fn test_app_user_message_contains_app_name() {
let p = params("test-app", &[("app_name", "Finder")]);
let result = get_prompt(&p).unwrap();
assert!(result.messages[0].content.text.contains("Finder"));
}
#[test]
fn test_app_with_focus_area_includes_it_in_message() {
let p = params(
"test-app",
&[("app_name", "Safari"), ("focus_area", "toolbar")],
);
let result = get_prompt(&p).unwrap();
assert!(result.messages[0].content.text.contains("toolbar"));
}
#[test]
fn test_app_missing_app_name_returns_error() {
let p = PromptGetParams {
name: "test-app".into(),
arguments: None,
};
let err = get_prompt(&p).unwrap_err();
assert!(err.contains("app_name"));
}
#[test]
fn navigate_to_with_valid_args_returns_two_messages() {
let p = params(
"navigate-to",
&[("app_name", "Safari"), ("target_screen", "Settings")],
);
let result = get_prompt(&p).unwrap();
assert_eq!(result.messages.len(), 2);
}
#[test]
fn navigate_to_user_message_contains_target() {
let p = params(
"navigate-to",
&[
("app_name", "Finder"),
("target_screen", "File > New Folder"),
],
);
let result = get_prompt(&p).unwrap();
assert!(result.messages[0]
.content
.text
.contains("File > New Folder"));
}
#[test]
fn navigate_to_missing_target_screen_returns_error() {
let p = params("navigate-to", &[("app_name", "Safari")]);
let err = get_prompt(&p).unwrap_err();
assert!(err.contains("target_screen"));
}
#[test]
fn extract_data_with_valid_args_returns_two_messages() {
let p = params(
"extract-data",
&[
("app_name", "Contacts"),
("data_description", "all contact names"),
],
);
let result = get_prompt(&p).unwrap();
assert_eq!(result.messages.len(), 2);
}
#[test]
fn extract_data_description_contains_data_hint() {
let p = params(
"extract-data",
&[("app_name", "Notes"), ("data_description", "note titles")],
);
let result = get_prompt(&p).unwrap();
assert!(result.description.contains("note titles"));
}
#[test]
fn extract_data_missing_data_description_returns_error() {
let p = params("extract-data", &[("app_name", "Notes")]);
assert!(get_prompt(&p).is_err());
}
#[test]
fn accessibility_audit_with_valid_app_returns_two_messages() {
let p = params("accessibility-audit", &[("app_name", "Mail")]);
let result = get_prompt(&p).unwrap();
assert_eq!(result.messages.len(), 2);
}
#[test]
fn accessibility_audit_user_message_mentions_wcag() {
let p = params("accessibility-audit", &[("app_name", "Mail")]);
let result = get_prompt(&p).unwrap();
assert!(result.messages[0].content.text.contains("WCAG"));
}
#[test]
fn accessibility_audit_mentions_resource_uri() {
let p = params("accessibility-audit", &[("app_name", "Mail")]);
let result = get_prompt(&p).unwrap();
assert!(result.messages[0]
.content
.text
.contains("axterminator://app/Mail/tree"));
}
#[test]
fn accessibility_audit_missing_app_name_returns_error() {
let p = PromptGetParams {
name: "accessibility-audit".into(),
arguments: None,
};
assert!(get_prompt(&p).is_err());
}
#[test]
fn automate_workflow_with_valid_args_returns_two_messages() {
let p = params(
"automate-workflow",
&[("app_name", "TextEdit"), ("goal", "save the document")],
);
let result = get_prompt(&p).unwrap();
assert_eq!(result.messages.len(), 2);
}
#[test]
fn automate_workflow_user_message_contains_goal() {
let p = params(
"automate-workflow",
&[("app_name", "Safari"), ("goal", "open Settings")],
);
let result = get_prompt(&p).unwrap();
assert!(result.messages[0].content.text.contains("open Settings"));
}
#[test]
fn automate_workflow_user_message_mentions_workflow_tools() {
let p = params(
"automate-workflow",
&[("app_name", "Finder"), ("goal", "create a folder")],
);
let result = get_prompt(&p).unwrap();
assert!(result.messages[0]
.content
.text
.contains("ax_workflow_create"));
assert!(result.messages[0].content.text.contains("ax_workflow_step"));
assert!(result.messages[0]
.content
.text
.contains("ax_workflow_status"));
}
#[test]
fn automate_workflow_missing_app_name_returns_error() {
let p = params("automate-workflow", &[("goal", "do something")]);
let err = get_prompt(&p).unwrap_err();
assert!(err.contains("app_name"));
}
#[test]
fn automate_workflow_missing_goal_returns_error() {
let p = params("automate-workflow", &[("app_name", "TextEdit")]);
let err = get_prompt(&p).unwrap_err();
assert!(err.contains("goal"));
}
#[test]
fn debug_ui_with_valid_args_returns_two_messages() {
let p = params(
"debug-ui",
&[("app_name", "Notes"), ("query", "title:New Note")],
);
let result = get_prompt(&p).unwrap();
assert_eq!(result.messages.len(), 2);
}
#[test]
fn debug_ui_user_message_contains_failed_query() {
let p = params(
"debug-ui",
&[("app_name", "Safari"), ("query", "role:AXButton title:Go")],
);
let result = get_prompt(&p).unwrap();
assert!(result.messages[0]
.content
.text
.contains("role:AXButton title:Go"));
}
#[test]
fn debug_ui_user_message_mentions_ax_get_tree() {
let p = params(
"debug-ui",
&[("app_name", "Finder"), ("query", "New Folder")],
);
let result = get_prompt(&p).unwrap();
assert!(result.messages[0].content.text.contains("ax_get_tree"));
}
#[test]
fn debug_ui_missing_query_returns_error() {
let p = params("debug-ui", &[("app_name", "Safari")]);
let err = get_prompt(&p).unwrap_err();
assert!(err.contains("query"));
}
#[test]
fn cross_app_copy_with_valid_args_returns_two_messages() {
let p = params(
"cross-app-copy",
&[
("source_app", "Notes"),
("dest_app", "TextEdit"),
("data_description", "note body text"),
],
);
let result = get_prompt(&p).unwrap();
assert_eq!(result.messages.len(), 2);
}
#[test]
fn cross_app_copy_user_message_contains_both_apps() {
let p = params(
"cross-app-copy",
&[
("source_app", "Contacts"),
("dest_app", "Sheets"),
("data_description", "email addresses"),
],
);
let result = get_prompt(&p).unwrap();
let text = &result.messages[0].content.text;
assert!(text.contains("Contacts"));
assert!(text.contains("Sheets"));
}
#[test]
fn cross_app_copy_description_references_clipboard_resource() {
let p = params(
"cross-app-copy",
&[
("source_app", "Notes"),
("dest_app", "Mail"),
("data_description", "note content"),
],
);
let result = get_prompt(&p).unwrap();
assert!(result.messages[0]
.content
.text
.contains("axterminator://clipboard"));
}
#[test]
fn cross_app_copy_missing_dest_app_returns_error() {
let p = params(
"cross-app-copy",
&[("source_app", "Notes"), ("data_description", "text")],
);
let err = get_prompt(&p).unwrap_err();
assert!(err.contains("dest_app"));
}
#[test]
fn analyze_app_with_valid_args_returns_two_messages() {
let p = params("analyze-app", &[("app_name", "Safari")]);
let result = get_prompt(&p).unwrap();
assert_eq!(result.messages.len(), 2);
}
#[test]
fn analyze_app_user_message_mentions_state_resource() {
let p = params("analyze-app", &[("app_name", "Finder")]);
let result = get_prompt(&p).unwrap();
assert!(result.messages[0]
.content
.text
.contains("axterminator://app/Finder/state"));
}
#[test]
fn analyze_app_user_message_mentions_tree_resource() {
let p = params("analyze-app", &[("app_name", "Mail")]);
let result = get_prompt(&p).unwrap();
assert!(result.messages[0]
.content
.text
.contains("axterminator://app/Mail/tree"));
}
#[test]
fn analyze_app_description_contains_analysis_summary_sections() {
let p = params("analyze-app", &[("app_name", "Notes")]);
let result = get_prompt(&p).unwrap();
let text = &result.messages[0].content.text;
assert!(text.contains("UI Pattern"));
assert!(text.contains("Suggested Actions"));
assert!(text.contains("Accessibility"));
}
#[test]
fn analyze_app_missing_app_name_returns_error() {
let p = PromptGetParams {
name: "analyze-app".into(),
arguments: None,
};
assert!(get_prompt(&p).is_err());
}
#[test]
fn prompt_content_text_kind_is_text() {
let c = PromptContent::text("hello");
assert_eq!(c.kind, "text");
assert_eq!(c.text, "hello");
}
#[test]
fn prompt_message_serialises_role_lowercase() {
let msg = user_message("hi");
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains(r#""role":"user""#));
}
}