WIP red: a records coordinate rename, from the browser (improvise-fdk1)

This commit is contained in:
Edward Langley
2026-09-22 23:15:39 -07:00
parent 4e5299f6d7
commit dd51f4316c
2 changed files with 225 additions and 0 deletions
@@ -771,3 +771,164 @@ fn a_typed_number_crosses_the_serialized_wire_exactly() {
"the host must store the number the user typed, not its neighbour"
);
}
// ── a coordinate rename over the wire (improvise-fdk1) ───────────────────
/// Every frame the host addressed, to whichever of the two clients it names.
fn route(
frames: &[Addressed],
a: (improvise_command::ui::project::SessionId, &mut Client),
b: (improvise_command::ui::project::SessionId, &mut Client),
) {
for Addressed { to, msg } in frames {
let json = serde_json::to_string(msg).expect("a server frame serializes");
if *to == a.0 {
a.1.on_frame(&json).expect("a folds every frame");
}
if *to == b.0 {
b.1.on_frame(&json).expect("b folds every frame");
}
}
}
/// Client A renames North's `Region` to `Zed` in records mode; when `racing`,
/// client B writes North's Revenue while A's editor is open. Evidence file.
fn rename_north(racing: bool) -> (Host, Vec<Addressed>) {
let mut host = host();
let (mut a, mut b) = (Client::new(), Client::new());
let (a_id, a_snap) = host.connect(10, 10);
let (b_id, b_snap) = host.connect(10, 10);
a.set_viewport(10, 10);
b.set_viewport(10, 10);
deliver(&mut a, &[a_snap]);
deliver(&mut b, &[b_snap]);
let mut press_a = |host: &mut Host, a: &mut Client, b: &mut Client, key: &str| {
let Some(frame) = a.key(&BrowserKey::new(key)) else {
return Vec::new();
};
let json = serde_json::to_string(&frame).expect("a client frame serializes");
let frames = send(host, a_id, &json);
route(&frames, (a_id, &mut *a), (b_id, &mut *b));
frames
};
press_a(&mut host, &mut a, &mut b, "T");
for (category, key) in [("_Index", "r"), ("_Dim", "c")] {
let at = a
.state()
.tiles
.tiles
.iter()
.position(|t| t.category == category)
.unwrap_or_else(|| panic!("the tile bar must carry {category}"));
while a.state().view.tile_cat_idx < at {
press_a(&mut host, &mut a, &mut b, "l");
}
while a.state().view.tile_cat_idx > at {
press_a(&mut host, &mut a, &mut b, "h");
}
press_a(&mut host, &mut a, &mut b, key);
}
press_a(&mut host, &mut a, &mut b, "Escape");
assert!(a.state().grid.records, "A must be standing in records mode");
let coordinate = (0..a.state().grid.extent.cols)
.find(|&c| {
format!("{:?}", a.state().grid.cell(0, c).and_then(|x| x.id.clone()))
.starts_with("Some(Coordinate")
})
.expect("records mode projects a coordinate column");
while a.state().view.cursor().selected.1 < coordinate {
press_a(&mut host, &mut a, &mut b, "l");
}
while a.state().view.cursor().selected.1 > coordinate {
press_a(&mut host, &mut a, &mut b, "h");
}
press_a(&mut host, &mut a, &mut b, "i");
assert!(
a.state().view.mode.is_editing(),
"A's editor must open on the coordinate, or nothing here is renamed"
);
assert_eq!(
a.state().view.buffers.get("edit").map(String::as_str),
Some("North")
);
if racing {
let revenue = (0..b.state().grid.extent.cols)
.find(|&c| {
b.record_cell_at(0, c)
.is_some_and(|cell| cell.measure.as_deref() == Some("Revenue"))
})
.expect("B sees North's Revenue");
let frame = b.typed_cell_frame(0, revenue, "999").expect("B composes a write");
let frames = send(
&mut host,
b_id,
&serde_json::to_string(&frame).expect("json"),
);
assert!(
frames.iter().any(|f| f.to == b_id
&& matches!(f.msg, improvise_protocol::ServerFrame::Accepted)),
"B's write must land, or A has nothing to be stale against: {frames:?}"
);
route(&frames, (a_id, &mut a), (b_id, &mut b));
}
for key in ["Backspace"; 5] {
press_a(&mut host, &mut a, &mut b, key);
}
for key in ["Z", "e", "d"] {
press_a(&mut host, &mut a, &mut b, key);
}
let answers = press_a(&mut host, &mut a, &mut b, "Enter");
(host, answers)
}
fn region(host: &Host, item: &str) -> Option<CellValue> {
host.app()
.model_state
.workbook
.model
.get_cell(&CellKey::new(vec![
("Region".into(), item.into()),
("_Measure".into(), "Revenue".into()),
]))
.cloned()
}
/// improvise-fdk1: the rename moves the record, claimed at the version the
/// editor opened on. Evidence: `context/test-evidence/clients.md`.
#[test]
fn a_browser_renames_a_records_coordinate_and_the_host_moves_the_record() {
let (host, answers) = rename_north(false);
assert!(
answers
.iter()
.any(|f| matches!(f.msg, improvise_protocol::ServerFrame::Accepted)),
"the rename must be accepted: {answers:?}"
);
assert_eq!(region(&host, "Zed"), Some(CellValue::Number(1200.0)));
assert_eq!(region(&host, "North"), None, "the record moved, not copied");
}
/// improvise-fdk1 / improvise-0j0f: another client wrote the record while the
/// editor was open, so the rename is stale. Evidence file.
#[test]
fn a_coordinate_rename_another_client_moved_is_refused() {
let (host, answers) = rename_north(true);
assert!(
answers.iter().any(|f| matches!(
f.msg,
improvise_protocol::ServerFrame::Rejected(improvise_protocol::Rejection::RecordMoved(_))
)),
"a rename composed against a record someone else wrote must be refused: {answers:?}"
);
assert_eq!(
region(&host, "North"),
Some(CellValue::Number(999.0)),
"the other client's write must survive"
);
assert_eq!(region(&host, "Zed"), None, "nothing was renamed");
}
@@ -1950,3 +1950,67 @@ fn every_projected_edit_text_commits_back_to_its_value() {
}
}
}
// A coordinate rename (improvise-fdk1).
/// Two records, North renamed past South so the sorted records order moves.
/// Evidence: `context/test-evidence/clients.md`.
#[test]
fn a_locally_committed_coordinate_rename_agrees_with_the_host() {
let r = KeyEvent::new(KeyCode::Char('R'), KeyModifiers::NONE);
let two_records = || {
let mut app = furnished_host();
let south = north_revenue().with("Region", "South");
app.model_state
.workbook
.model
.set_cell(south, improvise_protocol::CellValue::Number(900.0));
app.rebuild_layout();
reduce_key(&mut app, r);
app
};
let (mut app, mut twin) = (two_records(), two_records());
let region = (0..app.layout.col_count())
.find(|&c| app.layout.col_label(c) == "Region")
.expect("a Region column");
let north = 0;
assert_eq!(
app.layout.record_row(north).unwrap().coords.get("Region"),
Some("North"),
"the fixture renames North, at row 0"
);
for host in [&mut app, &mut twin] {
host.cursor_mut().selected = (north, region);
}
let mut client = records_client(&app);
both_ends(&mut app, &mut twin, &mut client, "i");
assert_eq!(
client.view.buffers.get("edit").map(String::as_str),
Some("North"),
"the editor opens on the coordinate it renames"
);
for key in [
"Backspace",
"Backspace",
"Backspace",
"Backspace",
"Backspace",
"Z",
"e",
"d",
] {
both_ends(&mut app, &mut twin, &mut client, key);
}
both_ends(&mut app, &mut twin, &mut client, "Enter");
let text = document_text(&app);
assert!(
text.contains("Region=Zed") && !text.contains("Region=North"),
"the rename must move North's cells to Zed: {text}"
);
assert!(
text.contains("Region=South"),
"South is a bystander and must survive: {text}"
);
}