M13.14: agent-interface audit (every command over MCP, control channel and exec --list; automation ids for every widget)

- command.describe / list_commands {schemas} give each command's params JSON Schema;
  MCP describe_command, add_effect, list_effects and get_state (app.capabilities)
- effectcraft-cli exec --list [--schemas]
- headless UI audit: every panel kind's interactive egui widgets are covered by
  automation ids (fixed: timeline navigator, label swatch, bar trim edges, group and
  property name cells)
- command lookup is indexed

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-03 09:59:23 -04:00
1 parent 5cf37921ef
commit 0d5f60c4b7
9 files changed
+493 -35

No files matched your search

+25 -18
View File
@@ -4,6 +4,7 @@
//! effectcraft-cli info project + engine summary
//! effectcraft-cli commands [--filter TEXT] [--enabled] list engine commands
//! effectcraft-cli exec <command-id> [--params JSON] run one command
//! effectcraft-cli exec --list [--filter TEXT] [--schemas] list engine commands (as `commands`)
//! effectcraft-cli run <id> <json> [<id> <json> …] run several commands in order
//! effectcraft-cli props <comp> <layer> [--flat] [--time S] a layer's property tree (with paths)
//! effectcraft-cli get <comp> <layer> <path> [--time S] read a property
@@ -36,7 +37,7 @@ use serde_json::{Value, json};
const USAGE: &str = "usage: effectcraft-cli <info|commands|exec|run|props|get|set|render-frame|render|script|mcp> [args] [--json]
info project + engine summary
commands [--filter TEXT] [--enabled] list engine commands
exec <command-id> [--params JSON] run one engine command
exec <command-id> [--params JSON] run one engine command (exec --list: list them, as `commands`)
run <id> <json> [<id> <json> ...] run several commands in order
props <comp> <layer> [--flat] [--time S] a layer's property tree with paths
get <comp> <layer> <path> [--time S] read a property
@@ -346,23 +347,8 @@ fn run(cmd: &str, args: &Args, json_out: bool) -> Result<(), Failure> {
}
}
}
"commands" => {
let mut b = backend(args, false)?;
let v = tool(&mut b, "list_commands", json!({"filter": args.opt("--filter"), "enabled_only": args.flag("--enabled")}))?;
if json_out {
emit(&v, true);
} else {
for c in v.as_array().into_iter().flatten() {
println!(
"{:<36} {:<36} {:<16} {}",
c["id"].as_str().unwrap_or(""),
c["label"].as_str().unwrap_or(""),
c["shortcut"].as_str().unwrap_or(""),
c["params"].as_str().unwrap_or("")
);
}
}
}
"commands" => list_commands_cmd(args, json_out)?,
"exec" if args.flag("--list") => list_commands_cmd(args, json_out)?,
"script" => script_cmd(args, json_out)?,
"exec" => {
let Some(id) = args.pos.first().cloned() else { return usage_err("exec needs a command id") };
@@ -605,6 +591,27 @@ fn render(args: &Args, json_out: bool) -> Result<(), Failure> {
Ok(())
}
/// `commands` / `exec --list`: every engine command (id, label, shortcut, params doc; with
/// `--schemas` also each command's params JSON Schema).
fn list_commands_cmd(args: &Args, json_out: bool) -> Result<(), Failure> {
let mut b = backend(args, false)?;
let v = tool(&mut b, "list_commands", json!({"filter": args.opt("--filter"), "enabled_only": args.flag("--enabled"), "schemas": args.flag("--schemas")}))?;
if json_out {
emit(&v, true);
} else {
for c in v.as_array().into_iter().flatten() {
println!(
"{:<36} {:<36} {:<16} {}",
c["id"].as_str().unwrap_or(""),
c["label"].as_str().unwrap_or(""),
c["shortcut"].as_str().unwrap_or(""),
c["params"].as_str().unwrap_or("")
);
}
}
Ok(())
}
/// `bench`: time one frame N times without the layer cache (per-layer and per-effect breakdown),
/// and optionally `--play N` consecutive frames with and without the cache.
fn bench_cmd(args: &Args) -> Result<(), Failure> {
+24
View File
@@ -44,6 +44,30 @@ fn info_and_commands() {
assert!(ids.iter().all(|i| i.contains("layer.new") || !i.is_empty()));
}
/// The agent-interface audit (CLI side): `exec --list` lists every registered engine command
/// with its label, params doc and (with `--schemas`) params JSON Schema, and `exec` routes every
/// id to its command (an unknown parameter is rejected by that command).
#[test]
fn exec_list_covers_every_engine_command() {
let v = ok_json(&["exec", "--list", "--schemas", "--empty"]);
let listed: std::collections::HashMap<&str, &Value> = v.as_array().unwrap().iter().map(|c| (c["id"].as_str().unwrap(), c)).collect();
let specs = effectcraft_engine::command_specs();
assert_eq!(listed.len(), specs.len());
for spec in specs {
let c = listed.get(spec.id).unwrap_or_else(|| panic!("`exec --list` misses {}", spec.id));
assert_eq!(c["label"], spec.label);
// Empty docs (`{}`) are dropped from the compact listing.
assert!(c["params"] == spec.params || (spec.params == "{}" && c["params"].is_null()), "{}: {}", spec.id, c["params"]);
assert_eq!(c["schema"]["type"], "object", "{}", spec.id);
}
// Routing: a few ids through a real process (all ids are checked in-process by the MCP audit).
for id in ["comp.new", "layer.newText", "effect.apply", "file.saveAs", "render.frame"].into_iter().filter(|i| listed.contains_key(i)) {
let (code, v) = run_json(&["exec", id, r#"{"__audit":1}"#, "--empty"]);
assert_eq!(code, 1, "{id}: {v}");
assert!(v["error"].as_str().unwrap().contains("unknown parameter"), "{id}: {v}");
}
}
#[test]
fn exec_set_get_with_saved_project() {
let proj = tmp("edit.ecproj");