diff --git a/crates/automation/tests/qa/affordances.rs b/crates/automation/tests/qa/affordances.rs new file mode 100644 index 00000000..4f4de8e0 --- /dev/null +++ b/crates/automation/tests/qa/affordances.rs @@ -0,0 +1,45 @@ +//! Agent affordances added during QA: the `batch` tool, ids and paths in replies, depth-limited +//! flat property lists, `@uid` paths. + +use serde_json::json; + +use crate::harness::Qa; + +#[test] +fn batch_tool_builds_in_one_call_and_one_undo_step() { + let mut qa = Qa::new("batch"); + qa.exec("comp.new", json!({"name": "B", "width": 64, "height": 64})); + let undo = qa.tool("get_project", json!({}))["undo"].as_array().unwrap().len(); + let r = qa.tool( + "batch", + json!({"label": "Ring", "steps": [ + {"command": "layer.newShape", "params": {"kind": "ellipse", "name": "Ring", "size": [40, 40]}}, + {"command": "layer.addShapeItem", "params": {"layer": "$1.layer", "kind": "trim"}}, + {"command": "prop.addKey", "params": {"layer": "$1.layer", "path": "transform/opacity", "time": 0, "value": 0}}, + {"command": "effect.apply", "params": {"layer": "$1.layer", "effect": "Glow"}}, + ]}), + ); + assert_eq!(r["steps"], 4); + assert_eq!(r["results"][1]["path"], "contents/trim"); + assert_eq!(r["results"][3]["paths"], json!(["effects/#1"])); + let p = qa.tool("get_project", json!({})); + assert_eq!(p["undo"].as_array().unwrap().len(), undo + 1); + assert_eq!(p["undo"].as_array().unwrap().last().unwrap(), "Ring"); + // A failing step: nothing changes, and the message names the step. + let e = qa + .try_tool( + "batch", + json!({"steps": [{"command": "layer.newNull", "params": {}}, {"command": "layer.rename", "params": {"layer": "Nope", "name": "x"}}]}), + ) + .unwrap_err(); + assert!(e.starts_with("step 2 (layer.rename)") && e.contains("rolled back"), "{e}"); + assert_eq!(qa.tool("get_comp", json!({}))["layers"].as_array().unwrap().len(), 1); + + // depth-limited flat listings keep cut-off groups visible; `@uid` reaches any property. + let flat = qa.tool("get_layer", json!({"layer": "Ring", "flat": true, "depth": 1})); + let props = flat["properties"].as_array().unwrap(); + assert!(props.iter().any(|p| p["path"] == "effects" && p["truncated"] == true), "{flat}"); + let uid = qa.tool("get_property", json!({"layer": "Ring", "path": "contents/trim/end"}))["uid"].as_u64().unwrap(); + let byuid = qa.tool("get_property", json!({"layer": "Ring", "path": format!("@{uid}")})); + assert_eq!(byuid["name"], "End"); +} diff --git a/crates/automation/tests/qa/main.rs b/crates/automation/tests/qa/main.rs index 0240a3d0..3b13286b 100644 --- a/crates/automation/tests/qa/main.rs +++ b/crates/automation/tests/qa/main.rs @@ -2,6 +2,7 @@ //! over JSON-RPC, the same commands the CLI and control channel run), asserting on rendered //! pixels and written files rather than on command success alone. +mod affordances; mod compositing; mod harness; mod kinetic;