Files

277 lines
9.6 KiB
Rust

//! 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<String> {
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::<Vec<_>>();
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::<Value>(&notes).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 <SYS>`"));
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`"));
}