# Conflicts: # context/test-evidence/clients.md # context/test-evidence/command.md
This commit is contained in:
@@ -1920,6 +1920,203 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A client connected to `host`, bootstrapped from its own `Snapshot`.
|
||||
fn connected_client(host: &mut Host) -> (SessionId, improvise_protocol::ClientState) {
|
||||
let (id, snapshot) = host.connect(10, 10);
|
||||
let mut client = improvise_protocol::ClientState::default();
|
||||
match snapshot.msg {
|
||||
ServerFrame::Snapshot(s) => client.bootstrap(*s),
|
||||
other => panic!("setup: connect answers a Snapshot, got {other:?}"),
|
||||
}
|
||||
(id, client)
|
||||
}
|
||||
|
||||
/// A frame sent the way the page sends one, and every answer folded the way the
|
||||
/// page folds it: accepted, refused or relayed.
|
||||
fn exchange(
|
||||
host: &mut Host,
|
||||
id: SessionId,
|
||||
client: &mut improvise_protocol::ClientState,
|
||||
frame: Vec<Command>,
|
||||
) -> Vec<Addressed> {
|
||||
client.fold_outgoing(&frame);
|
||||
let answer = host.handle(id, ClientFrame::Commands(frame));
|
||||
for msg in to(&answer, id) {
|
||||
match msg {
|
||||
ServerFrame::Commands(cmds) => cmds.iter().for_each(|c| client.fold(c)),
|
||||
ServerFrame::Accepted => client.accepted(),
|
||||
ServerFrame::Rejected(r) => client.refused(r),
|
||||
other => panic!("an unexpected answer: {other:?}"),
|
||||
}
|
||||
}
|
||||
answer
|
||||
}
|
||||
|
||||
/// Everything but projections one session heard, in arrival order.
|
||||
fn relayed_to(frames: &[Addressed], id: SessionId) -> Vec<Command> {
|
||||
to(frames, id)
|
||||
.into_iter()
|
||||
.flat_map(|msg| match msg {
|
||||
ServerFrame::Commands(cmds) => cmds,
|
||||
_ => Vec::new(),
|
||||
})
|
||||
.filter(|c| !matches!(c, Command::Project(_)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// improvise-hv0r: the echo is the asker's alone, is every placement AFTER the
|
||||
/// reset and nothing before it. Evidence: `context/test-evidence/command.md`.
|
||||
#[test]
|
||||
fn a_reframing_frame_echoes_only_its_later_placements_and_only_to_the_asker() {
|
||||
use improvise_protocol::AppMode;
|
||||
|
||||
let mut host = host();
|
||||
let (asker, mut client) = connected_client(&mut host);
|
||||
let (bystander, mut other) = connected_client(&mut host);
|
||||
// The bystander sits where an echo of the asker's placement would move it.
|
||||
exchange(
|
||||
&mut host,
|
||||
bystander,
|
||||
&mut other,
|
||||
vec![Command::SetSelected { row: 0, col: 1 }],
|
||||
);
|
||||
|
||||
let add = ModelCommand::AddCategory("Product".into());
|
||||
let frame = vec![
|
||||
Command::SetRowOffset(1),
|
||||
Command::ChangeMode(AppMode::Normal),
|
||||
crate::ui::reduce::caught_up(host.app(), add),
|
||||
Command::SetSelected { row: 1, col: 0 },
|
||||
Command::SetColOffset(1),
|
||||
];
|
||||
let answer = exchange(&mut host, asker, &mut client, frame);
|
||||
|
||||
assert_eq!(
|
||||
relayed_to(&answer, asker),
|
||||
vec![
|
||||
Command::ResetCursor,
|
||||
Command::SetSelected { row: 1, col: 0 },
|
||||
Command::SetColOffset(1),
|
||||
],
|
||||
"the asker is owed the reset, then exactly the placements that followed it"
|
||||
);
|
||||
assert_eq!(
|
||||
relayed_to(&answer, bystander),
|
||||
vec![Command::ResetCursor],
|
||||
"a bystander is told its cursor is stale, never where somebody else put theirs"
|
||||
);
|
||||
let mirror = host.sessions.get(asker).unwrap().view().cursor();
|
||||
assert_eq!(
|
||||
(mirror.selected, mirror.row_offset, mirror.col_offset),
|
||||
((1, 0), 0, 1)
|
||||
);
|
||||
assert_eq!(
|
||||
client.view.cursor(),
|
||||
mirror,
|
||||
"the asker's client and its mirror disagree"
|
||||
);
|
||||
}
|
||||
|
||||
/// improvise-jpba: a refusal rolled the client back past the relays it had
|
||||
/// heard, while its mirror kept them. Evidence: `context/test-evidence/command.md`.
|
||||
#[test]
|
||||
fn a_refusal_after_a_reframe_rolls_back_to_where_the_mirror_is() {
|
||||
use improvise_protocol::AppMode;
|
||||
|
||||
// A bare reframe, and the ticket's placed shape with a pre-reset offset.
|
||||
for placed in [false, true] {
|
||||
let mut host = host();
|
||||
let (asker, mut client) = connected_client(&mut host);
|
||||
exchange(
|
||||
&mut host,
|
||||
asker,
|
||||
&mut client,
|
||||
vec![Command::SetSelected { row: 1, col: 0 }],
|
||||
);
|
||||
// Composed now, sent after the reframe: stale, and so refused.
|
||||
let other = ModelCommand::AddCategory("Other".into());
|
||||
let stale = crate::ui::reduce::caught_up(host.app(), other);
|
||||
|
||||
let product = ModelCommand::AddCategory("Product".into());
|
||||
let reframe = crate::ui::reduce::caught_up(host.app(), product);
|
||||
let frame = match placed {
|
||||
false => vec![Command::ChangeMode(AppMode::Normal), reframe],
|
||||
true => vec![
|
||||
Command::SetRowOffset(1),
|
||||
Command::ChangeMode(AppMode::Normal),
|
||||
reframe,
|
||||
Command::SetSelected { row: 1, col: 0 },
|
||||
],
|
||||
};
|
||||
exchange(&mut host, asker, &mut client, frame);
|
||||
let answer = exchange(&mut host, asker, &mut client, vec![stale]);
|
||||
assert!(
|
||||
to(&answer, asker)
|
||||
.iter()
|
||||
.any(|m| matches!(m, ServerFrame::Rejected(_))),
|
||||
"setup, placed={placed}: the stale frame must be refused: {answer:?}"
|
||||
);
|
||||
|
||||
let mirror = host.sessions.get(asker).unwrap().view().cursor();
|
||||
assert_eq!(
|
||||
client.view.cursor(),
|
||||
mirror,
|
||||
"placed={placed}: the refusal rolled the client back past a relay"
|
||||
);
|
||||
assert_eq!(client.settled.cursor(), mirror, "placed={placed}: settled");
|
||||
}
|
||||
}
|
||||
|
||||
/// improvise-jpba: another client's reset, heard while my frame is in flight,
|
||||
/// outlives that frame's refusal. Evidence: `context/test-evidence/command.md`.
|
||||
#[test]
|
||||
fn a_relay_heard_while_a_frame_is_in_flight_survives_its_refusal() {
|
||||
use improvise_protocol::AppMode;
|
||||
|
||||
let mut host = host();
|
||||
let (asker, mut client) = connected_client(&mut host);
|
||||
let (other, mut theirs) = connected_client(&mut host);
|
||||
exchange(
|
||||
&mut host,
|
||||
asker,
|
||||
&mut client,
|
||||
vec![Command::SetSelected { row: 1, col: 0 }],
|
||||
);
|
||||
|
||||
let mine = ModelCommand::AddCategory("Other".into());
|
||||
let mine = vec![crate::ui::reduce::caught_up(host.app(), mine)];
|
||||
client.fold_outgoing(&mine);
|
||||
|
||||
let product = ModelCommand::AddCategory("Product".into());
|
||||
let reframe = crate::ui::reduce::caught_up(host.app(), product);
|
||||
let frame = vec![Command::ChangeMode(AppMode::Normal), reframe];
|
||||
let relayed = exchange(&mut host, other, &mut theirs, frame);
|
||||
for msg in to(&relayed, asker) {
|
||||
if let ServerFrame::Commands(cmds) = msg {
|
||||
cmds.iter().for_each(|c| client.fold(c));
|
||||
}
|
||||
}
|
||||
|
||||
let answer = host.handle(asker, ClientFrame::Commands(mine));
|
||||
let refusal = to(&answer, asker).into_iter().find_map(|m| match m {
|
||||
ServerFrame::Rejected(r) => Some(r),
|
||||
_ => None,
|
||||
});
|
||||
client.refused(refusal.expect("setup: my stale frame must be refused"));
|
||||
|
||||
let mirror = host.sessions.get(asker).unwrap().view().cursor();
|
||||
assert_eq!(
|
||||
mirror.selected,
|
||||
(0, 0),
|
||||
"setup: the other client's reframe reset me"
|
||||
);
|
||||
assert_eq!(
|
||||
client.view.cursor(),
|
||||
mirror,
|
||||
"the refusal undid a relay the mirror kept"
|
||||
);
|
||||
}
|
||||
|
||||
/// A refused batch is answered to the **asker and nobody else** (ADR 001):
|
||||
/// nothing happened, so there is nothing to tell the others.
|
||||
#[test]
|
||||
|
||||
@@ -51,6 +51,9 @@ pub struct ClientState {
|
||||
pub struct Sent {
|
||||
pub cmds: Vec<Command>,
|
||||
pub awaited: bool,
|
||||
/// Heard from the host rather than sent: it settles after the frame it
|
||||
/// arrived during, never with it (improvise-jpba, `clients.md`).
|
||||
pub relayed: bool,
|
||||
}
|
||||
|
||||
impl ClientState {
|
||||
@@ -78,9 +81,37 @@ impl ClientState {
|
||||
self.revision += 1;
|
||||
}
|
||||
|
||||
/// Fold one command. Total and deterministic: a command naming something
|
||||
/// the client does not have is dropped, never a panic.
|
||||
/// Fold one command **the host sent**, and keep it for the settled slice.
|
||||
/// Total: a command naming something absent is dropped, never a panic.
|
||||
pub fn fold(&mut self, cmd: &Command) {
|
||||
self.settle_relay(cmd);
|
||||
self.apply(cmd);
|
||||
}
|
||||
|
||||
/// A relay the host has already applied to this client's mirror, so a
|
||||
/// later refusal must not roll it back. Where it queues: `clients.md`.
|
||||
fn settle_relay(&mut self, cmd: &Command) {
|
||||
let behind = 1 + self
|
||||
.in_flight
|
||||
.iter()
|
||||
.skip(1)
|
||||
.take_while(|s| s.relayed)
|
||||
.count();
|
||||
match (cmd, self.in_flight.is_empty()) {
|
||||
(Command::Project(_), _) => {}
|
||||
(_, true) => reduce_view(&mut self.settled, cmd),
|
||||
(_, false) => self.in_flight.insert(
|
||||
behind,
|
||||
Sent {
|
||||
cmds: vec![cmd.clone()],
|
||||
awaited: false,
|
||||
relayed: true,
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply(&mut self, cmd: &Command) {
|
||||
match cmd {
|
||||
Command::Project(projection) => self.apply_projection(projection),
|
||||
other => reduce_view(&mut self.view, other),
|
||||
@@ -97,10 +128,11 @@ impl ClientState {
|
||||
self.in_flight.push_back(Sent {
|
||||
cmds: cmds.to_vec(),
|
||||
awaited,
|
||||
relayed: false,
|
||||
});
|
||||
self.settle_sent();
|
||||
for cmd in cmds {
|
||||
self.fold(cmd);
|
||||
self.apply(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +156,7 @@ impl ClientState {
|
||||
// one's entry — `Rejection::refused_an_edit` is that question.
|
||||
if rejection.refused_an_edit() {
|
||||
self.in_flight.pop_front();
|
||||
self.settle_sent();
|
||||
self.replay();
|
||||
}
|
||||
self.last_rejection = Some(rejection);
|
||||
@@ -417,6 +450,66 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// An awaited frame that also places the cursor at `row`.
|
||||
fn placing_commit(row: usize) -> Vec<Command> {
|
||||
let mut frame = commit_like("placed");
|
||||
frame.push(Command::SetSelected { row, col: 0 });
|
||||
frame
|
||||
}
|
||||
|
||||
/// improvise-3kfc: a relay queued behind a refused head must settle, or the
|
||||
/// next answer pops it instead of its frame. Evidence: clients.md.
|
||||
#[test]
|
||||
fn a_relay_behind_a_refused_frame_settles_before_the_next_answer() {
|
||||
let mut state = ClientState::default();
|
||||
state.fold_outgoing(&placing_commit(2));
|
||||
state.fold_outgoing(&placing_commit(3));
|
||||
state.fold(&Command::ResetCursor);
|
||||
|
||||
state.refused(Rejection::LayoutMoved);
|
||||
state.refused(Rejection::LayoutMoved);
|
||||
|
||||
assert_eq!(
|
||||
state.view.cursor().selected,
|
||||
(0, 0),
|
||||
"both placements were refused"
|
||||
);
|
||||
assert_eq!(
|
||||
state.view.cursor(),
|
||||
state.settled.cursor(),
|
||||
"view and settled split"
|
||||
);
|
||||
assert!(state.in_flight.is_empty(), "both frames were answered");
|
||||
}
|
||||
|
||||
/// improvise-3kfc, the root on gh/main: this client's own view-only frame
|
||||
/// behind a refused head. Evidence: clients.md.
|
||||
#[test]
|
||||
fn a_view_only_frame_behind_a_refused_frame_settles_before_the_next_answer() {
|
||||
let mut state = ClientState::default();
|
||||
state.fold_outgoing(&placing_commit(3));
|
||||
state.fold_outgoing(&[Command::SetSelected { row: 2, col: 0 }]);
|
||||
state.fold_outgoing(&placing_commit(4));
|
||||
|
||||
state.refused(Rejection::LayoutMoved);
|
||||
state.refused(Rejection::LayoutMoved);
|
||||
|
||||
assert_eq!(
|
||||
state.view.cursor().selected,
|
||||
(2, 0),
|
||||
"the unanswered move stands"
|
||||
);
|
||||
assert_eq!(
|
||||
state.view.cursor(),
|
||||
state.settled.cursor(),
|
||||
"view and settled split"
|
||||
);
|
||||
assert!(
|
||||
state.in_flight.is_empty(),
|
||||
"both awaited frames were answered"
|
||||
);
|
||||
}
|
||||
|
||||
/// A write, then the editor its advance reopens with the seed still to come.
|
||||
fn write_then_awaiting_editor() -> Vec<Command> {
|
||||
let mut frame = commit_like("written");
|
||||
|
||||
@@ -46,9 +46,7 @@ fn a_locally_typed_character_lands_where_the_host_would_put_it() {
|
||||
improvise_protocol::mode_label(&mode)
|
||||
);
|
||||
};
|
||||
for cmd in &cmds {
|
||||
client.fold(cmd);
|
||||
}
|
||||
client.fold_outgoing(&cmds);
|
||||
reduce_key(&mut app, event);
|
||||
|
||||
assert_eq!(
|
||||
@@ -92,9 +90,7 @@ fn a_locally_popped_character_lands_where_the_host_would_put_it() {
|
||||
improvise_protocol::mode_label(&mode)
|
||||
);
|
||||
};
|
||||
for cmd in &cmds {
|
||||
client.fold(cmd);
|
||||
}
|
||||
client.fold_outgoing(&cmds);
|
||||
reduce_key(&mut app, event);
|
||||
|
||||
assert_eq!(
|
||||
@@ -245,9 +241,7 @@ fn a_locally_resolved_cursor_move_lands_where_the_host_would_put_it() {
|
||||
let event = KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE);
|
||||
match resolve(&client, event) {
|
||||
KeyOutcome::Local(cmds) => {
|
||||
for cmd in &cmds {
|
||||
client.fold(cmd);
|
||||
}
|
||||
client.fold_outgoing(&cmds);
|
||||
}
|
||||
// At the bottom the move is a no-op and there is nothing to say.
|
||||
KeyOutcome::Ignored => {}
|
||||
@@ -318,9 +312,7 @@ fn a_locally_resolved_page_scroll_lands_where_the_host_would_put_it() {
|
||||
for (press, (event, want_row)) in presses.into_iter().enumerate() {
|
||||
match resolve(&client, event) {
|
||||
KeyOutcome::Local(cmds) => {
|
||||
for cmd in &cmds {
|
||||
client.fold(cmd);
|
||||
}
|
||||
client.fold_outgoing(&cmds);
|
||||
}
|
||||
other => panic!("press {press}: the client must answer {event:?}, got {other:?}"),
|
||||
}
|
||||
@@ -350,9 +342,7 @@ fn a_locally_resolved_jump_to_the_last_row_agrees_with_the_host() {
|
||||
let KeyOutcome::Local(cmds) = resolve(&client, event) else {
|
||||
panic!("G must resolve locally");
|
||||
};
|
||||
for cmd in &cmds {
|
||||
client.fold(cmd);
|
||||
}
|
||||
client.fold_outgoing(&cmds);
|
||||
reduce_key(&mut app, event);
|
||||
|
||||
assert_eq!(client.view.cursor().selected, (5, 0));
|
||||
@@ -375,9 +365,7 @@ fn the_g_chord_returns_to_the_first_row_on_both_ends() {
|
||||
let KeyOutcome::Local(cmds) = resolve(&client, event) else {
|
||||
panic!("{event:?} must resolve locally; an Upstream is a black hole");
|
||||
};
|
||||
for cmd in &cmds {
|
||||
client.fold(cmd);
|
||||
}
|
||||
client.fold_outgoing(&cmds);
|
||||
reduce_key(&mut app, event);
|
||||
assert_eq!(
|
||||
client.view.transient_prefix, app.view_state.transient_prefix,
|
||||
@@ -436,9 +424,7 @@ fn a_locally_resolved_panel_cursor_agrees_with_the_host() {
|
||||
let KeyOutcome::Local(cmds) = resolve(&client, event) else {
|
||||
panic!("press {press}: the category panel cursor must resolve locally");
|
||||
};
|
||||
for cmd in &cmds {
|
||||
client.fold(cmd);
|
||||
}
|
||||
client.fold_outgoing(&cmds);
|
||||
reduce_key(&mut app, event);
|
||||
assert_eq!(
|
||||
client.view.cat_panel_cursor, app.view_state.cat_panel_cursor,
|
||||
@@ -476,9 +462,7 @@ fn a_locally_resolved_tile_cursor_agrees_with_the_host() {
|
||||
let KeyOutcome::Local(cmds) = resolve(&client, event) else {
|
||||
panic!("press {press}: the tile cursor must resolve locally");
|
||||
};
|
||||
for cmd in &cmds {
|
||||
client.fold(cmd);
|
||||
}
|
||||
client.fold_outgoing(&cmds);
|
||||
reduce_key(&mut app, event);
|
||||
assert_eq!(
|
||||
client.view.tile_cat_idx, app.view_state.tile_cat_idx,
|
||||
@@ -558,9 +542,7 @@ fn entering_typing_and_cancelling_an_edit_agrees_with_the_host() {
|
||||
let KeyOutcome::Local(cmds) = resolve(&client, event) else {
|
||||
panic!("{key}: the client must answer this key");
|
||||
};
|
||||
for cmd in &cmds {
|
||||
client.fold(cmd);
|
||||
}
|
||||
client.fold_outgoing(&cmds);
|
||||
reduce_key(&mut app, event);
|
||||
|
||||
assert_eq!(
|
||||
@@ -626,9 +608,7 @@ fn the_buffer_i_seeds_is_the_value_the_host_would_seed() {
|
||||
let KeyOutcome::Local(cmds) = resolve(&client, event) else {
|
||||
panic!("i on a record cell must resolve locally");
|
||||
};
|
||||
for cmd in &cmds {
|
||||
client.fold(cmd);
|
||||
}
|
||||
client.fold_outgoing(&cmds);
|
||||
reduce_key(&mut app, event);
|
||||
|
||||
assert_eq!(
|
||||
@@ -656,9 +636,7 @@ fn a_locally_committed_edit_leaves_both_ends_in_the_same_place() {
|
||||
let KeyOutcome::Local(cmds) = resolve(&client, event) else {
|
||||
panic!("{key}: the client must answer this key");
|
||||
};
|
||||
for cmd in &cmds {
|
||||
client.fold(cmd);
|
||||
}
|
||||
client.fold_outgoing(&cmds);
|
||||
reduce_key(&mut app, event);
|
||||
|
||||
assert_eq!(
|
||||
@@ -815,9 +793,7 @@ fn both_ends(app: &mut App, twin: &mut App, client: &mut ClientState, key: &str)
|
||||
let KeyOutcome::Local(cmds) = resolve(client, event) else {
|
||||
panic!("{key}: the client must answer this key locally");
|
||||
};
|
||||
for cmd in &cmds {
|
||||
client.fold(cmd);
|
||||
}
|
||||
client.fold_outgoing(&cmds);
|
||||
apply_on(twin, &cmds);
|
||||
reduce_key(app, event);
|
||||
|
||||
@@ -1318,9 +1294,7 @@ fn a_formula_the_client_cannot_parse_diverges_on_the_status_line() {
|
||||
let KeyOutcome::Local(cmds) = resolve(&client, event) else {
|
||||
panic!("Enter in FormulaEdit resolves here");
|
||||
};
|
||||
for cmd in &cmds {
|
||||
client.fold(cmd);
|
||||
}
|
||||
client.fold_outgoing(&cmds);
|
||||
reduce_key(&mut app, event);
|
||||
|
||||
assert_eq!(
|
||||
@@ -1560,9 +1534,7 @@ fn in_lockstep(app: &mut App, client: &mut ClientState, key: KeyEvent, what: &st
|
||||
let KeyOutcome::Local(cmds) = resolve(client, key) else {
|
||||
panic!("{what}: the client must answer this key itself");
|
||||
};
|
||||
for cmd in &cmds {
|
||||
client.fold(cmd);
|
||||
}
|
||||
client.fold_outgoing(&cmds);
|
||||
reduce_key(app, key);
|
||||
let window = client.window;
|
||||
let fresh = grid_of(
|
||||
@@ -1619,9 +1591,7 @@ fn deliver(
|
||||
match addressed.msg {
|
||||
improvise_protocol::ServerFrame::Snapshot(snapshot) => client.bootstrap(*snapshot),
|
||||
improvise_protocol::ServerFrame::Commands(cmds) => {
|
||||
for cmd in &cmds {
|
||||
client.fold(cmd);
|
||||
}
|
||||
cmds.iter().for_each(|cmd| client.fold(cmd));
|
||||
}
|
||||
improvise_protocol::ServerFrame::Rejected(r) => client.refused(r),
|
||||
improvise_protocol::ServerFrame::Accepted => client.accepted(),
|
||||
@@ -1629,6 +1599,32 @@ fn deliver(
|
||||
}
|
||||
}
|
||||
|
||||
/// improvise-xoxh: `deliver` models the HOST, so a relay it hands over queues
|
||||
/// behind the awaited head. Evidence: `context/test-evidence/clients.md`.
|
||||
#[test]
|
||||
fn deliver_queues_a_host_relay_behind_the_awaited_head() {
|
||||
use improvise_protocol::Command;
|
||||
use improvise_protocol::client::Sent;
|
||||
let sent = |cmd: Command, awaited: bool, relayed: bool| Sent {
|
||||
cmds: vec![cmd],
|
||||
awaited,
|
||||
relayed,
|
||||
};
|
||||
let head = sent(Command::SetSelected { row: 1, col: 0 }, true, false);
|
||||
let own = sent(Command::SetSelected { row: 2, col: 0 }, false, false);
|
||||
let mut client = ClientState::default();
|
||||
client.in_flight.extend([head.clone(), own.clone()]);
|
||||
let session = improvise_command::ui::project::SessionId(0);
|
||||
let relay = Command::SetSelected { row: 3, col: 0 };
|
||||
let frame = improvise_command::ui::host::Addressed {
|
||||
to: session,
|
||||
msg: improvise_protocol::ServerFrame::Commands(vec![relay.clone()]),
|
||||
};
|
||||
deliver(vec![frame], session, &mut client);
|
||||
let queued: Vec<Sent> = client.in_flight.iter().cloned().collect();
|
||||
assert_eq!(queued, vec![head, sent(relay, false, true), own]);
|
||||
}
|
||||
|
||||
/// improvise-e5pf, the route the first fix missed: a **view switch** changes
|
||||
/// records-ness. See `context/test-evidence/clients.md`.
|
||||
#[test]
|
||||
@@ -1664,9 +1660,7 @@ fn switching_between_a_records_view_and_a_pivot_view_settles_both_ways() {
|
||||
improvise_protocol::Command::SwitchView(target.to_string()),
|
||||
improvise_protocol::Command::ChangeMode(stale),
|
||||
];
|
||||
for cmd in &composed {
|
||||
client.fold(cmd);
|
||||
}
|
||||
client.fold_outgoing(&composed);
|
||||
deliver(
|
||||
host.handle(session, ClientFrame::Commands(composed)),
|
||||
session,
|
||||
@@ -1980,9 +1974,7 @@ fn press(app: &mut App, twin: &mut App, client: &mut ClientState, key: char) {
|
||||
let KeyOutcome::Local(cmds) = resolve(client, event) else {
|
||||
panic!("{key}: the client must answer this key locally");
|
||||
};
|
||||
for cmd in &cmds {
|
||||
client.fold(cmd);
|
||||
}
|
||||
client.fold_outgoing(&cmds);
|
||||
apply_on(twin, &cmds);
|
||||
reduce_key(app, event);
|
||||
}
|
||||
@@ -2089,9 +2081,7 @@ fn yy_over_an_aggregate_copies_the_sum_the_host_copies() {
|
||||
let KeyOutcome::Local(cmds) = resolve(&client, event) else {
|
||||
panic!("y must resolve locally over an aggregate too");
|
||||
};
|
||||
for cmd in &cmds {
|
||||
client.fold(cmd);
|
||||
}
|
||||
client.fold_outgoing(&cmds);
|
||||
reduce_key(&mut app, event);
|
||||
}
|
||||
assert_eq!(
|
||||
@@ -2577,9 +2567,7 @@ fn untouched_browser_edit(value: improvise_protocol::CellValue) {
|
||||
let KeyOutcome::Local(cmds) = resolve(&client, event) else {
|
||||
panic!("{key}: the client must answer this key locally");
|
||||
};
|
||||
for cmd in &cmds {
|
||||
client.fold(cmd);
|
||||
}
|
||||
client.fold_outgoing(&cmds);
|
||||
apply_on(&mut host, &cmds);
|
||||
if key == "i" {
|
||||
assert_eq!(
|
||||
@@ -2934,12 +2922,12 @@ fn renaming_a_formula_from_the_panel_lands_the_same_document_as_the_host() {
|
||||
let KeyOutcome::Local(cmds) = resolve(&client, event) else {
|
||||
panic!("Enter in the formula editor resolves here");
|
||||
};
|
||||
for cmd in &cmds {
|
||||
client.fold(cmd);
|
||||
}
|
||||
client.fold_outgoing(&cmds);
|
||||
apply_on(&mut twin, &cmds);
|
||||
reduce_key(&mut app, event);
|
||||
|
||||
let awaiting: Vec<bool> = client.in_flight.iter().map(|s| s.awaited).collect();
|
||||
assert_eq!(awaiting, vec![true], "the commit awaits the host's answer");
|
||||
assert_eq!(document_text(&twin), document_text(&app));
|
||||
assert_eq!(
|
||||
raws(&app),
|
||||
|
||||
Reference in New Issue
Block a user