Files
cadcraft/apps/cadcraft-cli/tests/read_notes.rs
T
STE1200andClaude Opus 5.5 5ec0d215d8 CLI: info and convert report what a damaged DWG left out (read notes, #859) (#2337)
A damaged DWG opens partially since #859 and the drawing's read notes
say how many object records were left out. OPEN echoes them and MCP
`open` returns them as `warnings`, but `cadcraft-cli info` and
`convert` stayed silent: a corpus round trip wrote thousands of skipped
records away without a word.

- convert: each read note goes to stderr as `IN: <note>`, next to the
  skipped-records warning (#872). The exit code stays 0.
- drawing.inspect (and so info, MCP inspect_drawing and the control
  channel): new field `readNotes`, next to `skipped`. Existing fields
  are unchanged.
- run already prints OPEN's notes with the session log.
- README and docs/control-protocol.md say where the notes appear.

Part of #1039.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 15:01:51 -07:00

70 lines
3.3 KiB
Rust

//! A damaged DWG opens partially (#859); `info` lists the reader's notes on what it left out
//! (`readNotes`) and `convert` warns with them before writing the copy, so a script or agent sees
//! the loss. The exit code stays 0.
use std::process::{Command, Output};
fn cli(args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_cadcraft-cli")).args(args).output().unwrap()
}
/// A DWG of 200 lines, and a copy damaged so that the reader skips some of its records (found the
/// way the reader's own test finds one: flipping a few bytes at a time across the objects area).
fn dwg_and_damaged_copy() -> (Vec<u8>, Vec<u8>) {
let mut dxf = String::from("0\nSECTION\n2\nENTITIES\n");
for i in 0..200 {
dxf += &format!("0\nLINE\n8\n0\n10\n{i}\n20\n0\n11\n{i}\n21\n10\n");
}
dxf += "0\nENDSEC\n0\nEOF\n";
let d = cadcraft_io::read(dxf.as_bytes(), "lines.dxf").unwrap();
let opts = cadcraft_io::SaveOptions { version: Some(cadcraft_io::Version::R2000), binary: false };
let dwg = cadcraft_io::write_with(&d, "lines.dwg", &opts).unwrap();
let damaged = (dwg.len() / 4..dwg.len() * 3 / 5)
.step_by(97)
.map(|at| {
let mut b = dwg.clone();
b[at..at + 6].iter_mut().for_each(|x| *x ^= 0xA5);
b
})
.find(|b| cadcraft_io::read(b, "damaged.dwg").is_ok_and(|d| !d.read_notes.is_empty()))
.expect("no damaged copy read partially");
(dwg, damaged)
}
#[test]
fn info_lists_and_convert_warns_about_read_notes() {
let dir = std::env::temp_dir().join(format!("cc-cli-read-notes-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let (whole, damaged) = dwg_and_damaged_copy();
let path = dir.join("damaged.dwg");
std::fs::write(&path, &damaged).unwrap();
let path = path.to_str().unwrap();
let out = cli(&["info", path]);
assert!(out.status.success(), "{:?}: {}", out.status, String::from_utf8_lossy(&out.stderr));
let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
let notes = v["readNotes"].as_array().unwrap();
assert_eq!(notes.len(), 1, "{v}");
assert!(notes[0].as_str().unwrap().contains(" couldn't be read and "), "{v}");
let copy = dir.join("copy.dxf");
let out = cli(&["convert", path, copy.to_str().unwrap()]);
assert!(out.status.success(), "{:?}: {}", out.status, String::from_utf8_lossy(&out.stderr));
let err = String::from_utf8_lossy(&out.stderr);
let warnings: Vec<&str> = err.lines().filter(|l| l.contains(" couldn't be read and ")).collect();
assert_eq!(warnings.len(), 1, "{err}");
assert!(warnings[0].starts_with(&format!("{path}: DWG: ")), "{err}");
assert!(copy.exists());
// A complete read: no notes, and no warning.
let intact = dir.join("whole.dwg");
std::fs::write(&intact, &whole).unwrap();
let out = cli(&["info", intact.to_str().unwrap()]);
let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(v["readNotes"], serde_json::json!([]));
let out = cli(&["convert", intact.to_str().unwrap(), copy.to_str().unwrap()]);
assert!(out.status.success());
assert!(!String::from_utf8_lossy(&out.stderr).contains("couldn't be read"), "{}", String::from_utf8_lossy(&out.stderr));
let _ = std::fs::remove_dir_all(&dir);
}