get_style_guide answers "give me a guide matching this" and searches the shipped corpus only, so an agent choosing an asset could neither see what exists nor reach the DESIGN.md files the user imported — the material most worth picking was invisible to MCP entirely. list_style_guides merges both, imports first as the Asset Center paints them, and keeps them apart by id so an import cannot take a shipped guide's place. Passing id returns that one guide with its markdown, which is what closes the loop for imports. No data moved: the canonical registries already live in op-ai-skills, and only the swatch colours and summary line were ever widget-layer concerns. The tool reads the host's imported files, so it is a LocalFilesystem surface and denied online for the same reason list_theme_presets is.
259 lines
6.7 KiB
Rust
259 lines
6.7 KiB
Rust
use super::*;
|
|
use base64::Engine as _;
|
|
|
|
fn args(values: &[&str]) -> Vec<String> {
|
|
values.iter().map(|value| (*value).to_string()).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn export_item_maps_to_dedicated_command() {
|
|
let parsed = parse_args(&args(&[
|
|
"export",
|
|
"--item",
|
|
"page-2",
|
|
"--output",
|
|
"/tmp/page.png",
|
|
"--format",
|
|
"png",
|
|
"--scale",
|
|
"2",
|
|
]))
|
|
.expect("parse export");
|
|
assert_eq!(
|
|
parsed.command,
|
|
Command::Export {
|
|
item_id: Some("page-2".into()),
|
|
selection: false,
|
|
output: "/tmp/page.png".into(),
|
|
format: "png".into(),
|
|
scale: Some("2".into()),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn export_without_item_means_live_selection() {
|
|
let parsed = parse_args(&args(&[
|
|
"export",
|
|
"--output",
|
|
"/tmp/selected.png",
|
|
"--format",
|
|
"png",
|
|
]))
|
|
.expect("parse selection export");
|
|
assert!(matches!(
|
|
parsed.command,
|
|
Command::Export {
|
|
item_id: None,
|
|
selection: false,
|
|
..
|
|
}
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn export_selection_flag_means_live_selection() {
|
|
let parsed = parse_args(&args(&[
|
|
"export",
|
|
"--selection",
|
|
"--output",
|
|
"/tmp/selected.png",
|
|
]))
|
|
.expect("parse --selection export");
|
|
assert!(matches!(
|
|
parsed.command,
|
|
Command::Export {
|
|
item_id: None,
|
|
selection: true,
|
|
..
|
|
}
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn export_accepts_issue_formats_alias() {
|
|
let parsed = parse_args(&args(&[
|
|
"export",
|
|
"--item",
|
|
"page-1",
|
|
"--output",
|
|
"/tmp/page.png",
|
|
"--formats",
|
|
"png",
|
|
]))
|
|
.expect("parse --formats alias");
|
|
assert!(matches!(
|
|
parsed.command,
|
|
Command::Export { format, .. } if format == "png"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn export_rejects_conflicting_target_and_format_flags() {
|
|
let target = parse_args(&args(&[
|
|
"export",
|
|
"--item",
|
|
"n1",
|
|
"--selection",
|
|
"--output",
|
|
"/tmp/node.png",
|
|
]));
|
|
assert!(target
|
|
.unwrap_err()
|
|
.to_string()
|
|
.contains("--item and --selection"));
|
|
|
|
let format = parse_args(&args(&[
|
|
"export",
|
|
"--output",
|
|
"/tmp/node.png",
|
|
"--format",
|
|
"png",
|
|
"--formats",
|
|
"jpeg",
|
|
]));
|
|
assert!(format
|
|
.unwrap_err()
|
|
.to_string()
|
|
.contains("--format and --formats"));
|
|
}
|
|
|
|
#[test]
|
|
fn write_export_response_decodes_png_to_exact_path() {
|
|
let path =
|
|
std::env::temp_dir().join(format!("op-cli-export-{}-selected.png", std::process::id()));
|
|
let png = [0x89, b'P', b'N', b'G', 13, 10, 26, 10];
|
|
let response = serde_json::json!({
|
|
"itemId": "n1",
|
|
"itemType": "node",
|
|
"format": "png",
|
|
"bytes_base64": base64::engine::general_purpose::STANDARD.encode(png),
|
|
})
|
|
.to_string();
|
|
|
|
let output = export_cli::write_export_response(&response, &path).expect("write export");
|
|
assert_eq!(std::fs::read(&path).expect("read export"), png);
|
|
assert!(output.contains("\"itemType\":\"node\""), "{output}");
|
|
std::fs::remove_file(path).ok();
|
|
}
|
|
|
|
#[test]
|
|
fn write_export_response_rejects_invalid_payloads() {
|
|
let path = std::env::temp_dir().join("op-cli-export-invalid.png");
|
|
assert!(export_cli::write_export_response("not-json", &path).is_err());
|
|
assert!(export_cli::write_export_response(
|
|
r#"{"itemId":"n1","itemType":"node","format":"png","bytes_base64":"%%%"}"#,
|
|
&path,
|
|
)
|
|
.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn export_deck_defaults_to_pptx_and_requires_an_output() {
|
|
let parsed = parse_args(&args(&["export-deck", "--output", "/tmp/deck.pptx"]))
|
|
.expect("parse export-deck");
|
|
assert!(matches!(
|
|
parsed.command,
|
|
Command::ExportDeck { ref format, .. } if format == "pptx"
|
|
));
|
|
|
|
// PowerPoint is the format users reach for by name, so it is the default;
|
|
// the other two must still be selectable.
|
|
for format in ["html", "pdf"] {
|
|
let parsed = parse_args(&args(&[
|
|
"export-deck",
|
|
"--output",
|
|
"/tmp/deck.out",
|
|
"--format",
|
|
format,
|
|
]))
|
|
.expect("parse deck format");
|
|
assert!(matches!(
|
|
parsed.command,
|
|
Command::ExportDeck { format: ref parsed_format, .. } if parsed_format == format
|
|
));
|
|
}
|
|
|
|
assert!(parse_args(&args(&["export-deck"]))
|
|
.unwrap_err()
|
|
.to_string()
|
|
.contains("--output"));
|
|
}
|
|
|
|
#[test]
|
|
fn export_deck_rejects_a_node_export_format() {
|
|
// png is valid for `op export` and meaningless for a deck; accepting it
|
|
// would reach the daemon and fail there instead of at the typo.
|
|
let error = parse_args(&args(&[
|
|
"export-deck",
|
|
"--output",
|
|
"/tmp/deck.pptx",
|
|
"--format",
|
|
"png",
|
|
]))
|
|
.unwrap_err()
|
|
.to_string();
|
|
assert!(error.contains("unsupported deck format"), "{error}");
|
|
}
|
|
|
|
#[test]
|
|
fn use_template_accepts_a_bare_id_or_a_flag() {
|
|
for argv in [
|
|
vec!["use-template", "slide-deck"],
|
|
vec!["use-template", "--template", "slide-deck"],
|
|
] {
|
|
let parsed = parse_args(&args(&argv)).expect("parse use-template");
|
|
assert!(matches!(
|
|
parsed.command,
|
|
Command::UseTemplate { ref template_id } if template_id == "slide-deck"
|
|
));
|
|
}
|
|
assert!(parse_args(&args(&["use-template"]))
|
|
.unwrap_err()
|
|
.to_string()
|
|
.contains("template id"));
|
|
}
|
|
|
|
#[test]
|
|
fn templates_filters_are_optional() {
|
|
let bare = parse_args(&args(&["templates"])).expect("parse templates");
|
|
assert!(matches!(
|
|
bare.command,
|
|
Command::Templates {
|
|
scene: None,
|
|
tag: None
|
|
}
|
|
));
|
|
let filtered = parse_args(&args(&["templates", "--scene", "slides"])).expect("parse filter");
|
|
assert!(matches!(
|
|
filtered.command,
|
|
Command::Templates { scene: Some(ref scene), .. } if scene == "slides"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn styles_takes_a_bare_id_or_filters() {
|
|
let listing = parse_args(&args(&["styles"])).expect("parse styles");
|
|
assert!(matches!(
|
|
listing.command,
|
|
Command::Styles {
|
|
id: None,
|
|
tag: None,
|
|
platform: None
|
|
}
|
|
));
|
|
|
|
let one = parse_args(&args(&["styles", "user:my-brand"])).expect("parse styles id");
|
|
assert!(matches!(
|
|
one.command,
|
|
Command::Styles { id: Some(ref id), .. } if id == "user:my-brand"
|
|
));
|
|
|
|
let filtered =
|
|
parse_args(&args(&["styles", "--platform", "slides"])).expect("parse styles filter");
|
|
assert!(matches!(
|
|
filtered.command,
|
|
Command::Styles { platform: Some(ref platform), .. } if platform == "slides"
|
|
));
|
|
}
|