feat(import): --strict refuses a skipped record, and the headless suite covers the reviewed gaps (improvise-8jk6.6, improvise-8jk6.17)

- 8jk6.6, the user's ruling on PR #321 ("Add a --strict flag"): the
  default stays exit 0 with the counted warning. With --strict, any
  skipped record is a refusal that exits 1 and writes nothing. The check
  runs before the build and reads ImportPipeline::skip_warning, the same
  function the build's warning now comes from.
- 8jk6.17: the headless suite now sees three faults that only io unit
  tests caught at efaa5050:
  - naming only the first bad formula (two bad --formula flags);
  - dropping CSV decimal measures;
  - import-json reading a CSV through the JSON constructor.
- Docs record the user's three answers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
Claude (agent-headless-import)
2026-09-24 23:27:45 -07:00
committed by Edward Langley
parent efaa505059
commit fbe83fcc57
7 changed files with 159 additions and 18 deletions
+72
View File
@@ -777,11 +777,14 @@ fn an_import_formula_that_does_not_parse_refuses_names_it_and_writes_nothing() {
"Y = x +",
"--formula",
"Z = x",
"--formula",
"W = * 2",
],
);
let err = stderr_of(&out);
assert!(!out.status.success(), "a dropped formula exited 0: {err}");
assert!(err.contains("'Y = x +'"), "names the formula: {err}");
assert!(err.contains("'W = * 2'"), "names every bad one: {err}");
assert!(err.contains("expected"), "carries the parse error: {err}");
assert!(!err.contains("'Z = x'"), "names only the bad one: {err}");
assert!(!out_path.exists(), "nothing is written");
@@ -823,6 +826,75 @@ fn an_import_that_skips_some_records_warns_with_a_count_and_still_writes() {
assert!(doc.contains("_Measure=x, a=m = 4"), "{doc}");
}
/// A JSON array with one stray scalar among its records.
const ONE_STRAY: &str = r#"[{"a": "k", "x": 1}, 2, {"a": "m", "x": 4}]"#;
/// improvise-8jk6.6, the user's ruling: `--strict` makes any skip a refusal
/// that writes nothing. Evidence: `context/test-evidence/cli.md`.
#[test]
fn a_strict_import_that_skips_a_record_refuses_and_writes_nothing() {
let scratch = Scratch::new("strict_skip");
let (out, out_path) = import_of(&scratch, "p.json", ONE_STRAY, &["--strict"]);
let err = stderr_of(&out);
assert!(!out.status.success(), "{err}");
assert!(
err.contains("--strict: Skipped 1 of 3 records that were not JSON objects"),
"{err}"
);
assert!(!out_path.exists(), "a strict refusal writes nothing");
}
/// The control: `--strict` refuses a skip, not every import.
#[test]
fn a_strict_import_that_skips_nothing_still_writes() {
let scratch = Scratch::new("strict_clean");
let body = r#"[{"a": "k", "x": 1}, {"a": "m", "x": 4}]"#;
let (out, out_path) = import_of(&scratch, "p.json", body, &["--strict"]);
assert!(out.status.success(), "{}", stderr_of(&out));
let doc = std::fs::read_to_string(&out_path).unwrap();
assert!(doc.contains("_Measure=x, a=m = 4"), "{doc}");
}
/// improvise-8jk6.17 (F3): a CSV decimal measure is its number, not dropped
/// text. Evidence: `context/test-evidence/cli.md`.
#[test]
fn a_csv_decimal_measure_imports_as_its_number() {
let scratch = Scratch::new("csv_decimal");
let body = "Region,Revenue\nEast,1000.50\nWest,-2.25\n";
let (out, out_path) = import_of(&scratch, "f.csv", body, &[]);
assert!(out.status.success(), "{}", stderr_of(&out));
let doc = std::fs::read_to_string(&out_path).unwrap();
for line in [
"Region=East, _Measure=Revenue = 1000.5",
"Region=West, _Measure=Revenue = -2.25",
] {
assert!(doc.lines().any(|l| l == line), "{line} missing from\n{doc}");
}
}
/// improvise-8jk6.17 (F1): `import-json` reads a CSV as CSV, so its numeric
/// column is a measure. Evidence: `context/test-evidence/cli.md`.
#[test]
fn import_json_on_a_csv_reads_its_numeric_column_as_a_measure() {
let scratch = Scratch::new("import_json_csv");
let csv = scratch.path("f.csv");
std::fs::write(&csv, "Region,Revenue\nEast,1000.50\nWest,7\n").unwrap();
let doc = scratch.path("m.improv");
let out = run_cmd(&[&format!("import-json {}", csv.display())], &doc);
assert!(out.status.success(), "{}", stderr_of(&out));
let text = std::fs::read_to_string(&doc).unwrap();
assert!(!text.contains("## Category: Revenue"), "{text}");
for line in [
"Region=East, _Measure=Revenue = 1000.5",
"Region=West, _Measure=Revenue = 7",
] {
assert!(
text.lines().any(|l| l == line),
"{line} missing from\n{text}"
);
}
}
/// improvise-lpzu, the user's ruling: a folder always merges, so even one CSV
/// keeps its file identity. Evidence: `context/test-evidence/cli.md`.
#[test]