Files
OneCAD-Simulation/apps/fieldcraft-cli/tests/crashes.rs
T
jyvikoandClaude Opus 5.5 cb7e3a2711 CLI: fieldcraft-cli crashes [list|show|replay|dir|clear] (#107)
Lists, shows, replays and deletes the crash reports the desktop app
wrote. A replay opens the report's model, runs its commands with writes
confined to a scratch folder, and says whether the same internal error
happens again. Adapted from SolidCraft's solidcraft-cli crashes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 00:36:18 -04:00

76 lines
3.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `fieldcraft-cli crashes` on a crash folder of its own (`FIELDCRAFT_CRASH_DIR`).
use std::path::{Path, PathBuf};
use std::process::Command;
use fieldcraft_engine::Session;
use serde_json::{Value, json};
const ID: &str = "20261014-101500-00000000deadbeef";
/// A replayable report: a model with two nodes and a replay that adds a third.
fn folder() -> PathBuf {
let d = std::env::temp_dir().join(format!("fieldcraft-cli-crashes-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
let r = d.join(ID);
std::fs::create_dir_all(&r).unwrap();
let mut s = Session::default();
for l in 1..=2 {
s.execute("mesh.node.add", &json!({"label": l, "xyz": [l, 0, 0]})).unwrap();
}
std::fs::write(r.join("model.fieldcraft"), s.file_bytes(None).unwrap()).unwrap();
let replay = json!({"commands": [
{"command": "mesh.node.add", "params": {"label": 3, "xyz": [3, 0, 0]}},
{"command": "file.save", "params": {"path": d.join("overwritten.fieldcraft")}},
]});
std::fs::write(r.join("replay.json"), replay.to_string()).unwrap();
let report = json!({
"format": "fieldcraft-crash/1", "id": ID, "kind": "unclean_exit", "signature": "00000000deadbeef", "count": 2,
"first_seen": "2026-10-14T10:15:00Z", "message": "FieldCraft ended without a clean exit while running `mesh.node.add`",
"location": "unclean exit in mesh.node.add", "command": {"command": "mesh.node.add", "params": {}},
"app": {"version": "9.9.9"}, "backtrace": [],
});
std::fs::write(r.join("report.json"), report.to_string()).unwrap();
d
}
fn cli(dir: &Path, args: &[&str]) -> (String, bool) {
let out = Command::new(env!("CARGO_BIN_EXE_fieldcraft-cli")).args(args).env("FIELDCRAFT_CRASH_DIR", dir).output().unwrap();
(format!("{}{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr)), out.status.success())
}
#[test]
fn crashes_list_show_replay_and_clear() {
let d = folder();
let (out, ok) = cli(&d, &["crashes"]);
assert!(ok, "{out}");
assert!(out.contains(ID) && out.contains("unclean_exit ×2 in mesh.node.add") && out.contains("[replayable]"), "{out}");
let (out, ok) = cli(&d, &["crashes", "list", "--json"]);
assert!(ok);
let v: Value = serde_json::from_str(&out).unwrap();
assert_eq!(v["reports"][0]["id"], ID);
let (out, ok) = cli(&d, &["crashes", "show", ID]);
assert!(ok);
let v: Value = serde_json::from_str(&out).unwrap();
assert!(v["model_path"].as_str().unwrap().ends_with("model.fieldcraft"));
let (out, ok) = cli(&d, &["crashes", "replay", ID]);
assert!(ok, "{out}");
assert!(out.contains("1. mesh.node.add: ok"), "{out}");
// A command that writes a file is refused outside the scratch folder: the replay never
// writes over the user's files.
assert!(out.contains("2. file.save: ") && out.contains("outside the write root"), "{out}");
assert!(!d.join("overwritten.fieldcraft").exists());
assert!(out.contains("did not reproduce"), "{out}");
let (out, ok) = cli(&d, &["crashes", "show", "../etc"]);
assert!(!ok && out.contains("no crash report"), "{out}");
let (out, ok) = cli(&d, &["crashes", "dir"]);
assert!(ok && out.trim() == d.to_string_lossy(), "{out}");
let (out, ok) = cli(&d, &["crashes", "clear"]);
assert!(ok && out.contains("deleted 1 report(s)"), "{out}");
let (out, _) = cli(&d, &["crashes"]);
assert!(out.contains("(none)"), "{out}");
let (out, ok) = cli(&d, &["crashes", "nope"]);
assert!(!ok && out.contains("usage"), "{out}");
let _ = std::fs::remove_dir_all(&d);
}