Files
improvise/tests/web_demo_model.rs
T
Edward Langley 0f14b2a39e fix(protocol): a formula-owned cell projects CellId::Derived, not Record
improvise-ir7l. project::cell_id decided Record vs Aggregate purely on whether
a regular category sat on Axis::None; it never asked whether the coordinate
named a formula target. So a formula cell was a Record, render.rs::editable
said true, and a write to it landed, was stored, and was then shadowed forever
by formula_cache — not a revert, a permanent silent shadow, on the demo's
front page.

Rather than a second "but not really" flag beside CellId (the two-answers
shape ADR 002 exists to forbid), a formula-owned cell now projects a different
kind of identity. Derived carries the formula's target and no surrogate, so
the write is unconstructible rather than refused.

The predicate is Model::formula_for(&CellKey), one home, which already existed
three times open-coded inside model/types.rs. The TUI lands in the same change:
EditOrDrill calls the same predicate and refuses, or the two front ends
diverge.

Two orderings are load-bearing and tested: Derived is asked before the record
branch (the coordinate is stored), and after the aggregate branch (gotcha 10 —
an aggregating formula cell is still drillable).

Two tests were passing while their names were false for the Profit column and
are renamed and two-sided now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 10:59:59 -07:00

81 lines
2.7 KiB
Rust

//! The demo document must be one a visitor can *type into* (improvise-bck),
//! asked as the browser asks it. `context/test-evidence/tui.md`.
use improvise::protocol::CellId;
use improvise_command::ui::app::App;
use improvise_command::ui::project::{Viewport, grid_of};
fn demo_app() -> App {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("web/demo.improv");
let workbook = improvise::persistence::load(&path).expect("web/demo.improv loads");
App::new(workbook, Some(path))
}
/// Every cell the page paints is either one the visitor may change or one a
/// formula owns — never an aggregate. `context/test-evidence/tui.md`.
#[test]
fn every_web_demo_cell_is_settable_except_the_column_a_formula_owns() {
let app = demo_app();
let (rows, cols) = (app.layout.row_count(), app.layout.col_count());
assert!(rows > 0 && cols > 0, "the demo grid must not be empty");
let grid = grid_of(
&app,
Viewport {
row_offset: 0,
col_offset: 0,
rows,
cols,
},
);
for r in 0..rows {
for c in 0..cols {
let cell = grid.cell(r, c).expect("a projected cell at every position");
let expected_derived = grid
.col_frame
.item(c)
.is_some_and(|t| t.as_slice() == ["Profit"]);
let ok = match (&cell.id, expected_derived) {
(Some(CellId::Record(_)), false) => true,
(Some(CellId::Derived { target }), true) => target == "Profit",
_ => false,
};
assert!(
ok,
"cell ({r},{c}) — row {:?}, column {:?} — is {:?}; the two raw columns must be \
Records the page offers, and Profit must be Derived so it does not",
grid.row_frame.item(r),
grid.col_frame.item(c),
cell.id,
);
}
}
}
/// The payoff a visitor should see without pressing a key: a formula column
/// beside the two columns that feed it, so an edit visibly moves it.
#[test]
fn the_web_demo_shows_a_formula_beside_its_inputs() {
let app = demo_app();
let grid = grid_of(
&app,
Viewport {
row_offset: 0,
col_offset: 0,
rows: app.layout.row_count(),
cols: app.layout.col_count(),
},
);
assert_eq!(
grid.col_frame.leaf_labels(),
vec!["Revenue", "Cost", "Profit"]
);
assert_eq!(
grid.row_frame.leaf_labels(),
vec!["Espresso", "Latte", "Cold Brew", "Pastry"]
);
let profit = grid.cell(0, 2).expect("the Profit cell");
assert_eq!(profit.text, "12,200", "18,400 - 6,200, recomputed on load");
}