Files
improvise/tests/bug_hunt_reproductions.rs
T
Edward Langley 1a2952058d Import asks what to do with records that share every coordinate (improvise-cgqt)
When source records collide on every coordinate, the import wizard now asks:
sum them, keep them separate with a generated occurrence category, or go
back. Collisions are found before the build by the same placement pass the
build uses, and are carried to the TUI as structured data (WizardCache) and
listed in full, scrollable. At the 12-category cap, "keep separate" is shown
disabled with its reason and remedy.

Every wizard warning and message now wraps to the panel instead of being
truncated, and long lists scroll.

Headless paths no longer sum silently: `import --no-wizard` takes
`--on-duplicate refuse|sum|separate` (default refuse), and `:import-json`
refuses and points at `:import`.

Reasoning: context/code/formula-and-io.md, "Colliding records are a
question, not a default"; context/code/tui.md; context/code/cli.md §11.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-24 18:43:19 -07:00

445 lines
15 KiB
Rust

//! Bug-hunt reproductions. An `#[ignore]`d one is still open; un-ignoring it is
//! part of the fix. Open ones: `cargo test --test bug_hunt_reproductions -- --ignored`.
use improvise_io::import::csv_parser::parse_csv_bytes_with_warnings;
use improvise_io::{
formula::AggFunc,
persistence::{format_md, parse_md},
workbook::Workbook,
};
#[test]
fn grouped_item_order_survives_save() {
let mut w = Workbook::new("test");
w.add_category("C").unwrap();
w.add_item_in_group("C", "first", "G").unwrap();
w.add_item("C", "second").unwrap();
let r = parse_md(&format_md(&w)).unwrap();
assert_eq!(
r.model.category("C").unwrap().ordered_item_names(),
w.model.category("C").unwrap().ordered_item_names()
);
}
#[test]
fn aggregation_survives_save() {
let mut w = Workbook::new("test");
w.add_item("_Measure", "Price").unwrap();
w.model.measure_agg.insert("Price".into(), AggFunc::Avg);
let r = parse_md(&format_md(&w)).unwrap();
assert!(
matches!(r.model.measure_agg.get("Price"), Some(AggFunc::Avg)),
"loaded aggregation: {:?}",
r.model.measure_agg
);
}
#[test]
fn deleted_default_stays_deleted() {
let mut w = Workbook::new("test");
w.create_view("Custom");
w.delete_view("Default").unwrap();
let r = parse_md(&format_md(&w)).unwrap();
assert_eq!(
r.views.keys().collect::<Vec<_>>(),
w.views.keys().collect::<Vec<_>>()
);
}
#[test]
fn csv_nonfinite_is_not_zero() {
for token in ["NaN", "inf", "-inf", "1e999"] {
let (r, _) = parse_csv_bytes_with_warnings(format!("Value\n{token}\n").as_bytes()).unwrap();
assert_ne!(r[0]["Value"].as_f64(), Some(0.0), "{token} became zero");
assert_eq!(
r[0]["Value"].as_str(),
Some(token),
"{token} must survive as its own text, not as some other number"
);
}
}
#[test]
fn duplicate_csv_headers_not_silent() {
let (records, warnings) = parse_csv_bytes_with_warnings(b"A,A\n1,2\n").unwrap();
let fields = records[0].as_object().unwrap();
assert_eq!(
fields.len(),
2,
"both columns must survive as separate fields: {fields:?}"
);
assert_eq!(fields["A"].as_f64(), Some(1.0), "{fields:?}");
assert_eq!(fields["A_2"].as_f64(), Some(2.0), "{fields:?}");
assert!(
warnings
.iter()
.any(|warning| warning.to_lowercase().contains("duplicate") && warning.contains("A")),
"the rename must be reported: {warnings:?}"
);
}
#[test]
fn blank_measure_still_infers_numeric() {
use improvise_io::import::{analyzer::FieldKind, wizard::ImportPipeline};
let (records, _) = parse_csv_bytes_with_warnings(b"Region,Revenue\nEast,10\nWest,\n").unwrap();
let p = ImportPipeline::new(serde_json::Value::Array(records));
let proposal = p.proposals.iter().find(|p| p.field == "Revenue").unwrap();
assert_eq!(proposal.kind, FieldKind::Measure);
}
#[test]
fn repeated_import_coordinates_not_silently_overwritten() {
use improvise_io::import::wizard::ImportPipeline;
let (records, _) =
parse_csv_bytes_with_warnings(b"Region,Revenue\nEast,10\nEast,20\n").unwrap();
let mut pipeline = ImportPipeline::new(serde_json::Value::Array(records));
pipeline.on_duplicate = improvise_io::import::wizard::DuplicatePolicy::Sum;
let (wb, warnings) = pipeline.build_model_with_warnings().unwrap();
let values: Vec<f64> = wb
.model
.data
.iter_cells()
.filter_map(|(_, v)| v.as_f64())
.collect();
assert_eq!(
values,
[30.0],
"the two records are one cell and neither may be dropped"
);
let warning = warnings
.iter()
.find(|w| w.to_lowercase().contains("duplicate"))
.unwrap_or_else(|| panic!("summing is a decision and is reported: {warnings:?}"));
assert!(warning.contains("Region=East"), "{warning}");
assert!(warning.contains("_Measure=Revenue"), "{warning}");
}
#[test]
#[ignore = "improvise-55v2: known bug reproduction"]
fn nonfinite_cell_value_survives_wire() {
use improvise::protocol::{CellValue, Command};
let cmd = Command::SetYanked(Some(CellValue::Number(f64::INFINITY)));
let json = serde_json::to_string(&cmd).unwrap();
assert!(
serde_json::from_str::<Command>(&json).is_ok(),
"unreadable wire: {json}"
);
}
#[test]
#[ignore = "improvise-8l0c: known bug reproduction"]
fn formula_panel_unicode_does_not_panic() {
use improvise::{
protocol::FormulaCache,
ui::{app::AppMode, formula_panel::FormulaContent, panel::Panel},
};
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
let cache = FormulaCache {
formulas: vec!["|é| = 1".into()],
};
let area = Rect::new(0, 0, 7, 5);
let mut buffer = Buffer::empty(area);
Panel::new(
FormulaContent::new(&cache, &AppMode::Normal),
&AppMode::Normal,
0,
)
.render(area, &mut buffer);
}
#[test]
#[ignore = "improvise-vw47: known bug reproduction"]
fn new_view_after_delete_does_not_overwrite_survivor() {
use improvise::{
command::{
cmd::{Cmd, panel::CreateAndSwitchView},
key::{KeyCode, KeyModifiers},
},
ui::app::App,
};
let mut w = Workbook::new("test");
w.create_view("View 2");
w.create_view("View 3").number_format = ".4".into();
w.delete_view("View 2").unwrap();
let mut app = App::new(w, None);
let effects =
CreateAndSwitchView.execute(&app.cmd_context(KeyCode::Char('n'), KeyModifiers::NONE));
app.apply_effects(effects);
assert_eq!(app.model_state.workbook.views["View 3"].number_format, ".4");
assert_eq!(app.model_state.workbook.views.len(), 3);
}
#[test]
#[ignore = "improvise-cj4u: known bug reproduction"]
fn label_stays_off_axes_in_new_view() {
use improvise_io::view::Axis;
let mut w = Workbook::new("test");
w.add_label_category("Description").unwrap();
assert_eq!(w.active_view().axis_of("Description"), Axis::None);
assert_eq!(w.create_view("Other").axis_of("Description"), Axis::None);
}
#[test]
fn long_csv_row_is_not_silently_dropped() {
let (records, warnings) = parse_csv_bytes_with_warnings(b"A,B\n1,2,3\n4,5\n1,2,3,4\n").unwrap();
assert_eq!(records.len(), 3, "no row is discarded outright");
let warning = warnings
.iter()
.find(|w| w.contains("more fields"))
.unwrap_or_else(|| panic!("extra values dropped silently: {warnings:?}"));
assert!(
warning.contains("2 row(s)") && warning.contains("2 header"),
"the warning counts the affected rows and the header width: {warning}"
);
assert!(
warning.contains("widest row: 4"),
"the warning says how much was dropped: {warning}"
);
let (_, clean) = parse_csv_bytes_with_warnings(b"A,B\n1,2\n").unwrap();
assert!(
clean.is_empty(),
"control: a full row is not long: {clean:?}"
);
}
// Passing control: embedded carriage returns already survive persistence.
#[test]
fn literal_cr_name_survives_roundtrip() {
let mut w = Workbook::new("test");
w.add_category("C").unwrap();
w.add_item("C", "x\ry").unwrap();
let text = format_md(&w);
let r = parse_md(&text).unwrap();
assert_eq!(
r.model.category("C").unwrap().ordered_item_names(),
vec!["x\ry"]
);
}
#[test]
fn scalar_cell_survives_roundtrip() {
use improvise_io::model::cell::{CellKey, CellValue};
let mut w = Workbook::new("test");
w.model
.set_cell(CellKey::new(vec![]), CellValue::Number(42.0));
let text = format_md(&w);
assert!(
parse_md(&text).is_ok(),
"writer emitted unreadable document: {text}"
);
}
fn revenue_book() -> Workbook {
use improvise_io::model::cell::{CellKey, CellValue};
let mut w = Workbook::new("T");
w.add_category("Region").unwrap();
w.add_item("Region", "East").unwrap();
w.add_item("_Measure", "Revenue").unwrap();
w.model.set_cell(
CellKey::new(vec![
("Region".into(), "East".into()),
("_Measure".into(), "Revenue".into()),
]),
CellValue::Number(10.0),
);
w
}
#[test]
fn sum_of_derived_measure_agrees_with_identity_expression() {
use improvise_io::{formula::parse_formula, model::cell::CellKey};
let mut w = revenue_book();
for raw in [
"Double = Revenue * 2",
"A = SUM(Double)",
"B = SUM(Double + 0)",
] {
w.model.add_formula(parse_formula(raw, "_Measure").unwrap());
}
let key = CellKey::new(vec![("Region".into(), "East".into())]);
let a = w.model.evaluate(&key.clone().with("_Measure", "A"));
let b = w.model.evaluate(&key.with("_Measure", "B"));
assert_eq!(a, b);
}
#[test]
fn unknown_aggregate_reference_is_not_all_measures() {
use improvise_io::{
formula::parse_formula,
model::cell::{CellKey, CellValue},
};
let mut w = revenue_book();
w.model
.add_formula(parse_formula("Total = SUM(Typo)", "_Measure").unwrap());
let key = CellKey::new(vec![
("Region".into(), "East".into()),
("_Measure".into(), "Total".into()),
]);
assert!(
matches!(w.model.evaluate(&key), Some(CellValue::Error(_))),
"actual {:?}",
w.model.evaluate(&key)
);
}
#[test]
#[ignore = "improvise-3b2b: known bug reproduction"]
fn category_panel_cannot_delete_required_virtual() {
use improvise::{
command::{
cmd::{Cmd, panel::DeleteCategoryAtCursor},
key::{KeyCode, KeyModifiers},
},
ui::app::App,
};
let mut app = App::new(revenue_book(), None);
app.view_state.cat_panel_cursor = 0;
let fx =
DeleteCategoryAtCursor.execute(&app.cmd_context(KeyCode::Char('d'), KeyModifiers::NONE));
app.apply_effects(fx);
let fx = improvise::command::cmd::grid::ToggleRecordsMode
.execute(&app.cmd_context(KeyCode::Char('r'), KeyModifiers::NONE));
app.apply_effects(fx);
assert!(
app.layout.is_records_mode(),
"deleting _Index prevents records mode; status {}",
app.view_state.status_msg
);
}
#[test]
#[ignore = "improvise-8l0c: known bug reproduction"]
fn formula_panel_handles_zero_inner_width() {
use improvise::{
protocol::FormulaCache,
ui::{app::AppMode, formula_panel::FormulaContent, panel::Panel},
};
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
let cache = FormulaCache {
formulas: vec!["A = 1".into()],
};
let area = Rect::new(0, 0, 2, 5);
let mut buffer = Buffer::empty(area);
Panel::new(
FormulaContent::new(&cache, &AppMode::Normal),
&AppMode::Normal,
0,
)
.render(area, &mut buffer);
}
#[test]
fn save_with_unfinished_record_is_readable() {
use improvise::{
command::{
cmd::{
Cmd,
grid::{AddRecordRow, ToggleRecordsMode},
},
key::{KeyCode, KeyModifiers},
},
ui::app::App,
};
let mut app = App::new(revenue_book(), None);
let fx = ToggleRecordsMode.execute(&app.cmd_context(KeyCode::Char('r'), KeyModifiers::NONE));
app.apply_effects(fx);
assert!(app.layout.is_records_mode());
let fx = AddRecordRow.execute(&app.cmd_context(KeyCode::Char('o'), KeyModifiers::NONE));
app.apply_effects(fx);
let text = format_md(&app.model_state.workbook);
assert!(parse_md(&text).is_ok(), "saved unparseable file: {text}");
}
#[test]
fn circular_formula_not_a_plausible_cached_number() {
use improvise_io::{
formula::parse_formula,
model::{
Aggregation,
cell::{CellKey, CellValue},
},
};
let mut w = revenue_book();
w.model
.add_formula(parse_formula("Revenue = Revenue + 1", "_Measure").unwrap());
let key = CellKey::new(vec![
("Region".into(), "East".into()),
("_Measure".into(), "Revenue".into()),
]);
assert!(matches!(w.model.evaluate(&key), Some(CellValue::Error(_))));
w.model
.recompute_formulas(&["_Index".into(), "_Dim".into()]);
let actual = w.model.evaluate_aggregated(&key, Aggregation::OverHidden);
assert!(
matches!(actual, Some(CellValue::Error(_))),
"circular formula became {actual:?}"
);
}
#[test]
fn wide_cell_clamps_without_overflow() {
use improvise::{
model::cell::{CellKey, CellValue},
ui::app::App,
};
// 65535 overflows the `+ 1` gap; 65536 truncates the `as u16` cast to 0.
// 20 is the ordinary case the cap must leave alone, gap included.
for (len, expect) in [
(20usize, 21u16),
(65535, improvise::ui::viewport::MAX_COL_WIDTH),
(65536, improvise::ui::viewport::MAX_COL_WIDTH),
] {
let mut w = revenue_book();
w.model.set_cell(
CellKey::new(vec![
("Region".into(), "East".into()),
("_Measure".into(), "Revenue".into()),
]),
CellValue::Text("x".repeat(len)),
);
let app = App::new(w, None);
let widths = improvise::ui::viewport::compute_col_widths(
&app.model_state.workbook.model,
&app.layout,
&improvise::format::FormatPlan::uniform(""),
);
assert_eq!(
widths,
vec![expect],
"a {len}-column value must yield exactly one column, of width {expect}"
);
}
}
#[test]
fn ordinary_controls() {
use improvise_io::import::{analyzer::FieldKind, wizard::ImportPipeline};
let (records, warnings) =
parse_csv_bytes_with_warnings(b"Region,Revenue\nEast,10\nWest,20\n").unwrap();
assert!(warnings.is_empty());
let p = ImportPipeline::new(serde_json::Value::Array(records));
assert_eq!(
p.proposals
.iter()
.find(|p| p.field == "Revenue")
.unwrap()
.kind,
FieldKind::Measure
);
let (w, warnings) = p.build_model_with_warnings().unwrap();
assert!(warnings.is_empty());
assert_eq!(
w.model
.data
.iter_cells()
.filter_map(|(_, v)| v.as_f64())
.sum::<f64>(),
30.0
);
assert!(parse_md(&format_md(&w)).is_ok());
}
// Passing control (improvise-nvq4): quote_name already pipe-quotes a name
// containing `/`, so hidden/collapsed round-trip. The report did not reproduce.
#[test]
fn slash_in_hidden_name_survives_roundtrip() {
let mut w = Workbook::new("test");
w.add_category("C").unwrap();
w.add_item("C", "A/B").unwrap();
w.add_item_in_group("C", "x", "G/H").unwrap();
w.active_view_mut().hide_item("C", "A/B");
w.active_view_mut().toggle_group_collapse("C", "G/H");
let text = format_md(&w);
let r = parse_md(&text).unwrap_or_else(|e| panic!("unparseable: {e}\n{text}"));
assert!(
r.active_view().is_hidden("C", "A/B"),
"hidden lost:\n{text}"
);
assert!(
r.active_view().is_group_collapsed("C", "G/H"),
"collapsed lost:\n{text}"
);
}