forked from Crockan/MercuryToolbox
277 lines
9.6 KiB
Rust
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>(¬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 <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`"));
|
|
}
|