//! Tests for the generated Mercury Toolbox AI assets. use std::fs; use std::path::{Path, PathBuf}; use serde_json::Value; fn workspace_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("..") .join("..") .canonicalize() .expect("workspace root") } fn workspace_path(root: &Path, relative_path: &[&str]) -> PathBuf { relative_path .iter() .fold(root.to_path_buf(), |path, component| path.join(component)) } fn read_workspace_text(root: &Path, relative_path: &[&str], label: &str) -> String { fs::read_to_string(workspace_path(root, relative_path)) .unwrap_or_else(|error| panic!("failed to read {label}: {error}")) } fn toolbox_commands(root: &Path) -> Vec { let commands = read_workspace_text( root, &["scripts", "toolbox-commands.ps1"], "scripts/toolbox-commands.ps1", ); let mut in_command_list = false; let mut consumed_command_list = false; let mut names = commands .lines() .filter_map(|line| { let trimmed = line.trim(); if trimmed == "return @(" && !in_command_list && !consumed_command_list { in_command_list = true; return None; } if in_command_list && trimmed == ")" { in_command_list = false; consumed_command_list = true; return None; } if !in_command_list { return None; } trimmed .strip_prefix('\'') .and_then(|rest| rest.split_once('\'')) .map(|(name, _)| name.to_string()) }) .collect::>(); names.sort(); names.dedup(); names } fn assert_lf_only(root: &Path, relative_path: &[&str]) { let path = workspace_path(root, relative_path); let bytes = fs::read(&path).unwrap_or_else(|error| panic!("read {}: {error}", path.display())); assert!( !bytes.windows(2).any(|window| window == b"\r\n"), "{} should use LF line endings", path.display() ); } #[test] fn ai_prompt_assets_exist_and_cover_every_tool() { let root = workspace_root(); let prompt = read_workspace_text( &root, &["docs", "ai", "mercury-toolbox-ai-prompt.md"], "generated AI prompt", ); let notes = read_workspace_text( &root, &["docs", "ai", "toolbox-ai-prompt-notes.json"], "AI prompt notes", ); let commands = toolbox_commands(&root); assert!(prompt.contains("Mercury Toolbox")); assert!(prompt.contains("PowerShell")); assert!(prompt.contains("`--json`")); assert!(prompt.contains("`--toon`")); assert!(prompt.contains("Available tools:")); assert!(prompt.contains("Rules:")); assert!(prompt.contains("Pipe external JSON into `toon`")); assert!(prompt.contains("Every tool has guided triage metadata")); assert!(prompt.contains("Guided: answer=")); assert!(prompt.contains("`report_quality`")); assert!(prompt.contains("`next_actions`")); assert!(!prompt.contains("toon --from json --to toon")); assert!(prompt.contains("Usage: `")); assert!(prompt.contains("Example:")); assert!(prompt.contains("msudo:")); assert!(prompt.contains("Top-level high-risk command")); assert!(prompt.contains( "msudo status --json | ConvertFrom-Json | Select-Object ok,host,supports_runas,is_elevated" )); assert!(!prompt.contains("## Tool Catalog")); assert!(!prompt.contains("### `")); assert!(!prompt.contains("$Fence")); assert!( prompt.lines().count() <= 120, "prompt should stay compact for AI consumption" ); assert_eq!(commands.len(), 59, "toolbox command inventory changed"); let notes_json = serde_json::from_str::(¬es).expect("AI prompt notes JSON"); let tools = notes_json["tools"].as_object().expect("notes tools object"); for command in commands { assert!( prompt.contains(&format!("{command}:")), "prompt should contain {command}" ); assert!( notes.contains(&format!("\"{command}\"")), "notes should contain {command}" ); let guided = tools .get(&command) .and_then(|tool| tool.get("guided_triage")) .unwrap_or_else(|| panic!("notes should contain guided_triage for {command}")); assert!( guided["answer"] .as_str() .is_some_and(|value| !value.is_empty()), "guided_triage.answer should be non-empty for {command}" ); assert!( guided["trust"] .as_str() .is_some_and(|value| !value.is_empty()), "guided_triage.trust should be non-empty for {command}" ); assert!( guided["next_actions"] .as_array() .is_some_and(|items| items.len() >= 2 && items .iter() .all(|item| item.as_str().is_some_and(|value| !value.is_empty()))), "guided_triage.next_actions should list at least two actions for {command}" ); } assert!( notes.contains("msudo status --json | ConvertFrom-Json | Select-Object ok,host,supports_runas,is_elevated"), "notes should document the stable msudo status discovery fields" ); } #[test] fn ai_skill_assets_exist_and_cover_every_tool() { let root = workspace_root(); let skill = read_workspace_text( &root, &["skills", "mercury-toolbox", "SKILL.md"], "generated skill", ); let catalog = read_workspace_text( &root, &[ "skills", "mercury-toolbox", "references", "command-catalog.md", ], "generated skill catalog", ); let openai_yaml = read_workspace_text( &root, &["skills", "mercury-toolbox", "agents", "openai.yaml"], "generated openai.yaml", ); assert!(skill.contains("Mercury Toolbox")); assert!(skill.contains("Prefer Mercury readers over `Get-Content`")); assert!(skill.contains("## Modern Pairings")); assert!(skill.contains("## Job Routing")); assert!(skill.contains("Windows driver: start with `drvshape `")); assert!(skill.contains("`report_quality` and `next_actions`")); assert!(skill.contains("Use `rg` over recursive `grep`")); assert!(skill.contains("Treat `msudo` as the top-level high-risk toolbox command")); assert!(skill.contains("`msudo status --json`")); assert!(skill.contains("references/command-catalog.md")); assert!(skill.contains("switch to `--toon` or `--format toon`")); assert!(!skill.contains("toon --from json --to toon")); assert!(skill.lines().count() <= 95, "skill should stay concise"); assert!(catalog.contains("# Mercury Toolbox Command Catalog")); assert!(catalog.contains("Keep output compact")); assert!(catalog.contains("TOON example:")); assert!(catalog.contains("Guided answer:")); assert!(catalog.contains("Trust basis:")); assert!(catalog.contains("Next actions:")); assert!(catalog.contains("--toon")); assert!(!catalog.contains("toon --from json --to toon")); assert!(catalog.contains("### `msudo`")); assert!(catalog.contains("Top-level high-risk command")); assert!(catalog.contains( "msudo status --json | ConvertFrom-Json | Select-Object ok,host,supports_runas,is_elevated" )); assert!( catalog.lines().count() <= 520, "catalog should stay compact" ); assert!(openai_yaml.contains("display_name: \"Mercury Toolbox\"")); assert!(openai_yaml.contains("icon_small: \"./assets/logo.png\"")); assert!(openai_yaml.contains("icon_large: \"./assets/logo.png\"")); assert!(openai_yaml.contains("brand_color: \"#35C2FF\"")); assert!(openai_yaml.contains("default_prompt: \"Use $mercury-toolbox first")); assert!( root.join("skills") .join("mercury-toolbox") .join("assets") .join("logo.png") .is_file(), "generated skill should include its logo asset" ); for command in toolbox_commands(&root) { assert!( catalog.contains(&format!("### `{command}`")), "catalog should contain {command}" ); } } #[test] fn generated_ai_markdown_assets_use_lf_line_endings() { let root = workspace_root(); assert_lf_only(&root, &["docs", "ai", "mercury-toolbox-ai-prompt.md"]); assert_lf_only(&root, &["skills", "mercury-toolbox", "SKILL.md"]); assert_lf_only( &root, &[ "skills", "mercury-toolbox", "references", "command-catalog.md", ], ); } #[test] fn readme_tool_map_covers_every_toolbox_command() { let root = workspace_root(); let readme = read_workspace_text(&root, &["README.md"], "README.md"); assert!(readme.contains("### Tool Map")); assert!(readme.contains("## Which Tool First")); assert!(readme.contains("Safe starter commands")); assert!(readme.contains("Every command has guided triage notes")); assert!(readme.contains("`report_quality` and `next_actions`")); assert!(readme.contains("Every command supports `--help`")); for command in toolbox_commands(&root) { assert!( readme.contains(&format!("| `{command}` |")), "README tool map should contain {command}" ); } assert!(readme.contains("### `asmflow`")); assert!(readme.contains("### `unityasset`")); assert!(readme.contains("### `unityprobe`")); assert!(readme.contains("### `unitydiag`")); }