Add affected CI and replayable crash repros (#1224)

* AG4: xtask ci --affected

cargo xtask ci --affected [--base REV] [--dry-run] runs fmt, clippy, tests (nextest + doctests when available), layers, assets (only on asset changes) and wasm (only for affected L0-L4/web crates) for the packages a change can affect: changed files map to the owning package and to packages whose sources read them via ../ paths, expanded to reverse dependents. Workspace-wide inputs (root Cargo.toml, Cargo.lock, .cargo/, xtask/, toolchain, clippy.toml) fall back to full CI. Documented in docs/testing.md, AGENTS.md and CLAUDE.md.

(cherry picked from commit f3ca84fc2ee3948582cbc94e4c99ee510e829540)

(cherry picked from commit 8a2cdcadf447eafa57ab14016beae4420f002af0)
(cherry picked from commit d8a4301bc1)

(cherry picked from commit 36402c61ac2f7e29ecd76a6cfc89365cc2572e0a)

* AO1: fast CI profile, looser timing tests

- [profile.ci] (release without LTO, 16 codegen units); `xtask ci` runs clippy
  and tests with --profile ci instead of --release.
- Wall-clock asserts in engine memory_tick, gpu OpenGL decline and script
  socket tests keep their intent with bounds that survive a loaded machine.
- docs/testing.md and CONTRIBUTING.md mention the profile.

(cherry picked from commit b45c8a379351ebdfda0d7076ad4b740baf6f2087)

(cherry picked from commit fa1ea6a107c4c1936cde89b518a05adc73f2b4b0)
(cherry picked from commit e5d5ed80e9)

(cherry picked from commit d2c67660c70764f9c56880a642ad7f0d004a7f92)

* AG6: journal replay repros

Bug reports become replay scripts and crash regression tests become data files:
- engine::replay: a JSON-lines repro format (start line, command steps with
  expect_error, expect queries with JSON pointer + equals, expect_no_error),
  a never-panicking runner that reports per-step ok/error and the first failure,
  and journal export (from the last project open; saves left out; size-capped).
- journal.export {path?} and help.exportRepro {path?} (not journaled).
- Crash capture: when the last-resort guard catches a panic in the outermost
  command, the journal plus that command is saved to Repros/crash-*.jsonl in the
  settings store (newest 20 kept) and the error names the file.
- Commands run by a journaled command (script steps) are no longer journaled twice.
- effectcraft-cli replay SCRIPT.jsonl [--json] [--keep-going]: exits 1 on a failed
  step, unmet expectation or panic.
- crates/host/tests/repros.rs replays every tests/repros/*.jsonl; three samples
  from past crash fixes (zero frame rate, Layer Settings with no selection,
  New Comp from Selection with hostile overlap).
- Docs: docs/agents.md "Repro scripts"; AGENTS.md: crash fix regression test =
  a .jsonl in tests/repros.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

(cherry picked from commit c38509d7dafe507856c48c78db65e00b94b0cebc)

(cherry picked from commit e55b56dbf76716adfcbfe4cc773df19a74edc419)
(cherry picked from commit 96a63855a9)

(cherry picked from commit c59351ab3bd697f0868e41230dd883960d94a540)

* xtask: ci and ci --affected run cargo test and nextest with --no-fail-fast

One run now lists every failing crate instead of stopping at the first.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019qXL3GPNnY9BSFF2XoU2Yu

(cherry picked from commit e46c7debe738eca62b35bf7392ab77637f6a33ae)

* Agents: confinement write-checks journal.export, help.exportRepro and file.exportTimeline

Review on #859 (split into this PR): their output paths were only read-checked (relative ones
not at all). Test: confinement_covers_every_command_that_writes_a_file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014d65YdNxQmjRkbXRtP2NwH

* #1224: handle bin-only CI targets and BOM repros without weakening timing tests

* #1224: run the full gate when nextest policy changes

---------

Co-authored-by: Elias <hakifx91@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
This commit is contained in:
eliasx45
2026-11-10 14:57:32 +01:00
committed by GitHub
co-authored by Claude Opus 5.5 Elias FredApps FredApps
parent f88e7980ae
commit 13470ca161
20 changed files with 1970 additions and 24 deletions
+60 -1
View File
@@ -31,6 +31,7 @@
//! effectcraft-cli bench --roto [--frames N] [--sam2 model.safetensors] [--mobilesam mobile_sam.pt] [--fixes] [--clip FRAMES --truth MASKS] [--json] Roto Brush trackers on synthetic clips with known mattes
//! (classical engine, MobileSAM and SAM 2.1 (any installed size) when installed; IoU, flicker, re-appearance, seconds per frame)
//! effectcraft-cli script FILE.jsx [F.ecproj] | --eval CODE run an After Effects-style script
//! effectcraft-cli replay SCRIPT.jsonl [--keep-going] replay a repro script (docs/agents.md)
//! effectcraft-cli mcp [--autosave | --bridge PORT] MCP server on stdio
//!
//! Project: --project F.ecproj (or a positional *.ecproj) | --demo | --empty (default: demo; mcp: empty)
@@ -97,7 +98,7 @@ fn stdout_broken() -> bool {
STDOUT_FAILED.get().is_some_and(|k| *k != std::io::ErrorKind::BrokenPipe)
}
const USAGE: &str = "usage: effectcraft-cli <info|commands|exec|run|props|get|set|render-frame|render|script|mcp> [args] [--json]
const USAGE: &str = "usage: effectcraft-cli <info|commands|exec|run|props|get|set|render-frame|render|script|replay|mcp> [args] [--json]
info project + engine summary
commands [--filter TEXT] [--enabled] list engine commands
exec <command-id> [--params JSON] run one engine command (exec --list: list them, as `commands`)
@@ -138,6 +139,9 @@ const USAGE: &str = "usage: effectcraft-cli <info|commands|exec|run|props|get|se
script FILE.jsx [F.ecproj] | --eval CODE run JavaScript with the After Effects-style object model
(app.project, comps, layers, properties…); prints writeLn
output and the result; errors exit 1 with file:line:col
replay SCRIPT.jsonl [--keep-going] replay a repro script headless (journal.export, crash repros):
per-step ok/error and the first failure; exits 1 when a step
fails, an expectation is not met or a command panics
mcp [--autosave | --bridge PORT] MCP server (JSON-RPC over stdio); --autosave preserves unsaved headless work
[--automation-read-root DIR] [--automation-write-root DIR]
confine the agent's file access: reads under a read or write root,
@@ -222,6 +226,7 @@ const FLAGS: &[&str] = &[
"--small",
"--adv3d",
"--autosave",
"--keep-going",
"--progress",
];
@@ -552,6 +557,59 @@ fn script_cmd(args: &Args, json_out: bool) -> Result<(), Failure> {
Ok(())
}
/// `replay SCRIPT.jsonl`: run a repro script against a headless session (its start line picks
/// the project) and report every step. Exits 1 when the replay fails.
fn replay_cmd(args: &Args, json_out: bool) -> Result<(), Failure> {
use effectcraft_engine::replay;
let Some(file) = args.pos.first() else { return usage_err("replay needs a .jsonl script") };
if args.opt("--bridge").is_some() || args.project.is_some() || args.flag("--demo") || args.flag("--empty") {
return usage_err("replay runs headless from the script's start line ({\"start\": \"new\" | \"demo\" | {\"project\": path}})");
}
use std::io::Read;
let path = std::path::Path::new(file);
let input = std::fs::File::open(path).map_err(|e| Failure::Error(format!("cannot read {file}: {e}")))?;
// Bound the read itself: metadata can be stale or describe a pipe rather than its contents.
let mut script = String::new();
input.take(replay::MAX_SCRIPT_BYTES as u64 + 1).read_to_string(&mut script).map_err(|e| Failure::Error(format!("cannot read {file}: {e}")))?;
if script.len() > replay::MAX_SCRIPT_BYTES {
return Err(Failure::Error(format!("{file} is larger than {} MB", replay::MAX_SCRIPT_BYTES >> 20)));
}
let mut s = session(args)?;
let opts = replay::Options { keep_going: args.flag("--keep-going"), base_dir: path.parent().map(std::path::Path::to_path_buf) };
let report = replay::run(&mut s, &script, &opts);
if json_out {
emit(&serde_json::to_value(&report).unwrap_or_else(|e| json!({"error": e.to_string()})), true);
} else {
for st in &report.steps {
let what = st.command.as_deref().unwrap_or(st.kind);
match (&st.error, st.ok) {
(None, _) => say!("line {:>4} ok {what}", st.line),
(Some(e), true) => say!("line {:>4} ok {what} (tolerated error: {e})", st.line),
(Some(e), false) => say!("line {:>4} FAIL {what}: {e}", st.line),
}
}
}
let Some(first) = &report.first_failure else {
if !json_out {
say!("replay ok: {} steps", report.steps.len());
}
return Ok(());
};
let msg = format!(
"failed at line {} ({}){}: {}{}",
first.line,
first.command.as_deref().unwrap_or(first.kind),
if report.panicked { ", a command panicked" } else { "" },
first.error.as_deref().unwrap_or(""),
if report.skipped > 0 { format!(" ({} later steps not run)", report.skipped) } else { String::new() }
);
if json_out {
note!("effectcraft-cli replay: {msg}");
std::process::exit(1);
}
Err(Failure::Error(msg))
}
/// Print a result: compact JSON with `--json`, else pretty JSON.
fn emit(v: &Value, json_out: bool) {
if json_out {
@@ -614,6 +672,7 @@ fn run(cmd: &str, args: &Args, json_out: bool) -> Result<(), Failure> {
"commands" => list_commands_cmd(args, json_out)?,
"exec" if args.flag("--list") => list_commands_cmd(args, json_out)?,
"script" => script_cmd(args, json_out)?,
"replay" => replay_cmd(args, json_out)?,
"exec" => {
let Some(id) = args.pos.first().cloned() else { return usage_err("exec needs a command id") };
let params = match (args.opt("--params"), args.pos.get(1).map(String::as_str)) {
+36
View File
@@ -1223,3 +1223,39 @@ fn render_exr_compression_and_depth() {
assert!(reply["error"].as_str().unwrap().contains("exrBitDepth: half|float"), "{reply}");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn replay_reports_steps_and_the_first_failure() {
let repro = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/repros/comp-zero-frame-rate.jsonl");
let v = ok_json(&["replay", repro.to_str().unwrap()]);
assert_eq!(v["ok"], true, "{v}");
assert!(v["steps"].as_array().unwrap().len() > 5);
let bad = tmp("bad-repro.jsonl");
std::fs::write(
&bad,
"{\"start\": \"new\"}\n{\"command\": \"comp.new\"}\n{\"expect\": {\"query\": \"comp.info\", \"pointer\": \"/width\", \"equals\": 1}}\nnot json\n",
)
.unwrap();
let (code, v) = run_json(&["replay", bad.to_str().unwrap()]);
assert_eq!(code, 1);
assert_eq!(v["ok"], false);
assert_eq!(v["first_failure"]["line"], 3, "{v}");
assert_eq!(v["skipped"], 1);
let out = bin().args(["replay", bad.to_str().unwrap(), "--keep-going"]).output().unwrap();
assert_eq!(out.status.code(), Some(1));
let text = String::from_utf8_lossy(&out.stdout);
assert!(text.contains("line 2 ok comp.new") && text.contains("line 4 FAIL invalid: not JSON"), "{text}");
assert!(String::from_utf8_lossy(&out.stderr).contains("failed at line 3 (comp.info)"));
// The start line picks the project.
assert_eq!(bin().args(["replay", bad.to_str().unwrap(), "--demo"]).output().unwrap().status.code(), Some(2));
}
/// Text editors can prefix an exported JSON-lines repro with a UTF-8 BOM.
#[test]
fn replay_accepts_a_utf8_bom() {
let path = tmp("bom-repro.jsonl");
std::fs::write(&path, "\u{feff}{\"command\":\"comp.new\"}\n").unwrap();
let (code, report) = run_json(&["replay", path.to_str().unwrap()]);
assert_eq!(code, 0, "{report}");
assert_eq!(report["ok"], true);
}