Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Preserve current core, source, REST, desktop, recovery, documentation and validation work, including AGENTS.md and the development summary. This is a work-in-progress checkpoint: the new compact palette has outstanding compilation errors and native acceptance; earlier passing gates retain their documented scope.
723 lines
23 KiB
Rust
723 lines
23 KiB
Rust
use super::{tests::*, *};
|
|
use crate::{app::App, document::Document};
|
|
use serde_json::json;
|
|
|
|
fn fixture(oa_kind: MessageKind, sa_kind: MessageKind) -> (Project, Id, Id, Id) {
|
|
let mut p = Project::empty();
|
|
let mut ids = vec![];
|
|
for (layer, message_kind) in [(Layer::Oa, oa_kind), (Layer::Sa, sa_kind)] {
|
|
let scenario = p
|
|
.add_element(Kind::Scenario, layer, "Scenario", None)
|
|
.unwrap();
|
|
let id = p
|
|
.add_element(Kind::Occurrence, layer, "Message", Some(scenario))
|
|
.unwrap();
|
|
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange {
|
|
operation: None,
|
|
exchange: None,
|
|
sender: None,
|
|
receiver: None,
|
|
message_kind,
|
|
reply_to: None,
|
|
values: vec![],
|
|
},
|
|
precondition: "retain precondition".into(),
|
|
postcondition: "retain postcondition".into(),
|
|
};
|
|
p.elements
|
|
.get_mut(&scenario)
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.push(id);
|
|
ids.push(id);
|
|
}
|
|
let trace = add_trace(&mut p, ids[0], ids[1], "Message-kind review");
|
|
(p, ids[0], ids[1], trace)
|
|
}
|
|
fn timer(value: &str) -> MessageKind {
|
|
MessageKind::Timer {
|
|
expression: value.into(),
|
|
}
|
|
}
|
|
fn path(layer: &str) -> String {
|
|
format!("/body/{layer}/Shared/Occurrence/body/Exchange/message_kind")
|
|
}
|
|
fn set_kind(p: &mut Project, id: Id, kind: MessageKind) {
|
|
let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange { message_kind, .. },
|
|
..
|
|
}) = p.elements.get_mut(&id).unwrap().shared_mut()
|
|
else {
|
|
panic!()
|
|
};
|
|
*message_kind = kind;
|
|
}
|
|
fn seed_history(p: &mut Project, trace: Id) {
|
|
let t = &p.traces[&trace];
|
|
let baseline = TraceBaseline {
|
|
oa: p.properties(t.oa).unwrap(),
|
|
sa: p.properties(t.sa).unwrap(),
|
|
semantic: Some(SemanticBaseline {
|
|
facets: [message_kind::selector(), message_kind::timer_selector()]
|
|
.iter()
|
|
.map(|selector| capture_facet(p, t.oa, t.sa, selector).unwrap())
|
|
.collect(),
|
|
}),
|
|
};
|
|
p.traces.get_mut(&trace).unwrap().baseline = Some(baseline);
|
|
}
|
|
fn acknowledged(app: &App) -> BTreeSet<FacetSelector> {
|
|
state(app).plan.as_ref().unwrap().reviews[0]
|
|
.acknowledged
|
|
.iter()
|
|
.cloned()
|
|
.collect()
|
|
}
|
|
fn failures(app: &App) -> String {
|
|
state(app)
|
|
.error
|
|
.iter()
|
|
.chain(state(app).plan.iter().flat_map(|p| &p.conflicts))
|
|
.cloned()
|
|
.collect::<Vec<_>>()
|
|
.join("; ")
|
|
}
|
|
|
|
#[test]
|
|
fn message_group_take_full_timer_and_scalar_candidate_are_one_source_history_change_both_directions()
|
|
{
|
|
for reverse in [false, true] {
|
|
let expression = " after β ≥ 5 s\n\tkeep exact whitespace ";
|
|
let (p, oa, sa, trace) = if reverse {
|
|
fixture(MessageKind::Asynchronous, timer(expression))
|
|
} else {
|
|
fixture(timer(expression), MessageKind::Asynchronous)
|
|
};
|
|
let (source, target) = if reverse { (sa, oa) } else { (oa, sa) };
|
|
let mut app = App::with_project(p);
|
|
let mut bundle = app.document.bundle().clone();
|
|
bundle
|
|
.files
|
|
.get_mut(&bundle.entry)
|
|
.unwrap()
|
|
.insert_str(0, "// Keep the engineering review rationale.\r\n");
|
|
app.document.apply_bundle(bundle).unwrap();
|
|
let before = app.document.snapshot();
|
|
let revision = app.document.revision();
|
|
start(&mut app, source);
|
|
decide(
|
|
&mut app,
|
|
message_kind::selector(),
|
|
SemanticDecision::TakeSource,
|
|
);
|
|
decide(
|
|
&mut app,
|
|
FacetSelector::Header(HeaderFacet::Name),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
send(
|
|
&mut app,
|
|
RowEvent::Input("/name".into(), "Reviewed message".into()),
|
|
);
|
|
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
|
|
assert_eq!(
|
|
acknowledged(&app),
|
|
BTreeSet::from([
|
|
message_kind::selector(),
|
|
message_kind::timer_selector(),
|
|
FacetSelector::Header(HeaderFacet::Name)
|
|
])
|
|
);
|
|
assert!(
|
|
state(&app).plan.as_ref().unwrap().reviews[0]
|
|
.retired
|
|
.is_empty()
|
|
);
|
|
assert!(state(&app).plan.as_ref().unwrap().impacts.is_empty());
|
|
let expected = state(&app).proposed.clone().unwrap();
|
|
assert_eq!(
|
|
message_kind::value(&expected, target),
|
|
Some(&timer(expression))
|
|
);
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
apply(&mut app);
|
|
assert_eq!(app.project(), &expected);
|
|
assert_eq!(app.document.revision(), revision + 1);
|
|
assert_eq!(
|
|
app.project().elements[&source],
|
|
before.decoded.project.elements[&source]
|
|
);
|
|
let saved = app.document.bundle().clone();
|
|
assert!(saved.files[&saved.entry].starts_with("// Keep the engineering review rationale."));
|
|
assert_eq!(
|
|
Document::from_bundle(saved.clone(), source_project::ProjectLimits::default())
|
|
.unwrap()
|
|
.snapshot()
|
|
.decoded
|
|
.project,
|
|
expected
|
|
);
|
|
let facts = &app.project().traces[&trace]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.as_ref()
|
|
.unwrap()
|
|
.facets;
|
|
assert_eq!(facts.len(), 3);
|
|
let _ = app.update(Message::Undo);
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
let _ = app.update(Message::Redo);
|
|
assert_eq!(app.document.bundle(), &saved);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn message_group_candidate_timer_switches_preserve_both_drafts_and_overlap_is_explicit() {
|
|
let (p, oa, _, _) = fixture(MessageKind::Asynchronous, timer("original target"));
|
|
let mut app = App::with_project(p);
|
|
let before = app.document.snapshot();
|
|
let kind = path("Sa");
|
|
let expression = format!("{kind}/Timer/expression");
|
|
start(&mut app, oa);
|
|
decide(
|
|
&mut app,
|
|
message_kind::timer_selector(),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
send(
|
|
&mut app,
|
|
RowEvent::Input(expression.clone(), "independent scalar timer".into()),
|
|
);
|
|
decide(
|
|
&mut app,
|
|
message_kind::selector(),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
assert_eq!(state(&app).decisions.len(), 2);
|
|
assert!(!state(&app).can_apply());
|
|
send(
|
|
&mut app,
|
|
RowEvent::Input(expression.clone(), " group β timer\n\t ".into()),
|
|
);
|
|
send(
|
|
&mut app,
|
|
RowEvent::Text(expression.clone(), text_editor::Action::SelectAll),
|
|
);
|
|
let cursor = state(&app)
|
|
.group_drafts
|
|
.get(&message_kind::selector())
|
|
.unwrap()
|
|
.inputs[&expression]
|
|
.editor
|
|
.cursor();
|
|
let selection = state(&app)
|
|
.group_drafts
|
|
.get(&message_kind::selector())
|
|
.unwrap()
|
|
.inputs[&expression]
|
|
.editor
|
|
.selection();
|
|
send(
|
|
&mut app,
|
|
RowEvent::Variant(kind.clone(), json!("Asynchronous")),
|
|
);
|
|
send(
|
|
&mut app,
|
|
RowEvent::Variant(kind, json!({"Timer":{"expression":""}})),
|
|
);
|
|
assert_eq!(
|
|
state(&app)
|
|
.group_drafts
|
|
.get(&message_kind::selector())
|
|
.unwrap()
|
|
.inputs[&expression]
|
|
.raw,
|
|
" group β timer\n\t "
|
|
);
|
|
assert_eq!(
|
|
state(&app)
|
|
.group_drafts
|
|
.get(&message_kind::selector())
|
|
.unwrap()
|
|
.inputs[&expression]
|
|
.editor
|
|
.cursor(),
|
|
cursor
|
|
);
|
|
assert_eq!(
|
|
state(&app)
|
|
.group_drafts
|
|
.get(&message_kind::selector())
|
|
.unwrap()
|
|
.inputs[&expression]
|
|
.editor
|
|
.selection(),
|
|
selection
|
|
);
|
|
assert_eq!(
|
|
state(&app).draft.as_ref().unwrap().inputs[&expression].raw,
|
|
"independent scalar timer"
|
|
);
|
|
assert!(!state(&app).can_apply());
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
// The visible overlap-resolution actions only change the explicitly chosen
|
|
// decision. Neither independent candidate buffer is discarded.
|
|
send(&mut app, RowEvent::Decide(message_kind::selector(), None));
|
|
assert!(state(&app).can_apply());
|
|
send(
|
|
&mut app,
|
|
RowEvent::Decide(message_kind::timer_selector(), None),
|
|
);
|
|
decide(
|
|
&mut app,
|
|
message_kind::selector(),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
|
|
assert_eq!(
|
|
acknowledged(&app),
|
|
BTreeSet::from([message_kind::selector(), message_kind::timer_selector()])
|
|
);
|
|
let _ = state(&app).view(app.project());
|
|
let expected = state(&app).proposed.clone().unwrap();
|
|
apply(&mut app);
|
|
assert_eq!(app.project(), &expected);
|
|
}
|
|
|
|
#[test]
|
|
fn message_group_keep_historical_switch_retires_timer_and_completed_no_switch_rejects() {
|
|
let (mut p, oa, sa, trace) = fixture(timer("old OA"), timer("old SA"));
|
|
seed_history(&mut p, trace);
|
|
set_kind(&mut p, oa, MessageKind::Asynchronous);
|
|
set_kind(&mut p, sa, MessageKind::Asynchronous);
|
|
let mut app = App::with_project(p);
|
|
start(&mut app, oa);
|
|
decide(
|
|
&mut app,
|
|
message_kind::selector(),
|
|
SemanticDecision::KeepTarget,
|
|
);
|
|
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
|
|
assert_eq!(
|
|
acknowledged(&app),
|
|
BTreeSet::from([message_kind::selector()])
|
|
);
|
|
assert_eq!(
|
|
state(&app).plan.as_ref().unwrap().reviews[0].retired,
|
|
vec![message_kind::timer_selector()]
|
|
);
|
|
apply(&mut app);
|
|
assert_eq!(
|
|
app.project().traces[&trace]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.as_ref()
|
|
.unwrap()
|
|
.facets
|
|
.len(),
|
|
1
|
|
);
|
|
start(&mut app, oa);
|
|
decide(
|
|
&mut app,
|
|
message_kind::selector(),
|
|
SemanticDecision::KeepTarget,
|
|
);
|
|
assert!(!state(&app).can_apply());
|
|
assert!(
|
|
failures(&app).contains("No constructor switch"),
|
|
"{}",
|
|
failures(&app)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn message_group_reply_and_reference_take_require_explicit_call_mapping_both_directions() {
|
|
for reverse in [false, true] {
|
|
let (mut p, oa, sa, _) = if reverse {
|
|
fixture(MessageKind::Asynchronous, MessageKind::Reply)
|
|
} else {
|
|
fixture(MessageKind::Reply, MessageKind::Asynchronous)
|
|
};
|
|
let (source, target, layer) = if reverse {
|
|
(sa, oa, "Oa")
|
|
} else {
|
|
(oa, sa, "Sa")
|
|
};
|
|
let mut calls = vec![];
|
|
for message in [oa, sa] {
|
|
let scenario = p.elements[&message].parent.unwrap();
|
|
let layer = p.elements[&message].layer();
|
|
let call = p
|
|
.add_element(Kind::Occurrence, layer, "Correlated call", Some(scenario))
|
|
.unwrap();
|
|
*p.elements.get_mut(&call).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange {
|
|
operation: None,
|
|
exchange: None,
|
|
sender: None,
|
|
receiver: None,
|
|
message_kind: MessageKind::Call,
|
|
reply_to: None,
|
|
values: vec![],
|
|
},
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
};
|
|
p.elements
|
|
.get_mut(&scenario)
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.insert(0, call);
|
|
if message == source {
|
|
let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange { reply_to, .. },
|
|
..
|
|
}) = p.elements.get_mut(&message).unwrap().shared_mut()
|
|
else {
|
|
panic!()
|
|
};
|
|
*reply_to = Some(call);
|
|
}
|
|
calls.push(call);
|
|
}
|
|
let target_call = if reverse { calls[0] } else { calls[1] };
|
|
let mut app = App::with_project(p);
|
|
let before = app.document.snapshot();
|
|
start(&mut app, source);
|
|
decide(
|
|
&mut app,
|
|
message_kind::selector(),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
send(&mut app, RowEvent::Variant(path(layer), json!("Reply")));
|
|
decide(
|
|
&mut app,
|
|
FacetSelector::Occurrence(OccurrenceFacet::ReplyTo),
|
|
SemanticDecision::TakeSource,
|
|
);
|
|
assert!(!state(&app).can_apply());
|
|
assert!(
|
|
failures(&app).contains("reference counterparts"),
|
|
"{}",
|
|
failures(&app)
|
|
);
|
|
send(
|
|
&mut app,
|
|
RowEvent::ReferenceOa(Choice {
|
|
id: calls[0],
|
|
label: "OA call".into(),
|
|
}),
|
|
);
|
|
send(
|
|
&mut app,
|
|
RowEvent::ReferenceSa(Choice {
|
|
id: calls[1],
|
|
label: "SA call".into(),
|
|
}),
|
|
);
|
|
send(&mut app, RowEvent::AddReference);
|
|
assert!(state(&app).can_apply(), "{}", failures(&app));
|
|
assert_eq!(
|
|
acknowledged(&app),
|
|
BTreeSet::from([
|
|
message_kind::selector(),
|
|
FacetSelector::Occurrence(OccurrenceFacet::ReplyTo)
|
|
])
|
|
);
|
|
let final_value =
|
|
serde_json::to_value(&state(&app).proposed.as_ref().unwrap().elements[&target])
|
|
.unwrap();
|
|
assert_eq!(
|
|
final_value.pointer(&format!(
|
|
"/body/{layer}/Shared/Occurrence/body/Exchange/reply_to"
|
|
)),
|
|
Some(&json!(target_call))
|
|
);
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
apply(&mut app);
|
|
assert_eq!(
|
|
message_kind::value(app.project(), target),
|
|
Some(&MessageKind::Reply)
|
|
);
|
|
for call in calls {
|
|
assert_eq!(
|
|
app.project().elements[&call],
|
|
before.decoded.project.elements[&call]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn message_group_candidate_closed_kinds_are_core_validated_in_both_directions() {
|
|
for reverse in [false, true] {
|
|
for kind in [
|
|
MessageKind::Asynchronous,
|
|
MessageKind::Call,
|
|
MessageKind::Reply,
|
|
MessageKind::Create,
|
|
MessageKind::Delete,
|
|
timer("after 2 s"),
|
|
] {
|
|
let (p, oa, sa, _) = fixture(MessageKind::Asynchronous, MessageKind::Asynchronous);
|
|
let (source, target, layer) = if reverse {
|
|
(sa, oa, "Oa")
|
|
} else {
|
|
(oa, sa, "Sa")
|
|
};
|
|
let mut app = App::with_project(p);
|
|
start(&mut app, source);
|
|
decide(
|
|
&mut app,
|
|
message_kind::selector(),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
send(
|
|
&mut app,
|
|
RowEvent::Variant(path(layer), serde_json::to_value(&kind).unwrap()),
|
|
);
|
|
assert!(state(&app).can_apply(), "{kind:?}: {}", failures(&app));
|
|
assert_eq!(
|
|
message_kind::value(state(&app).proposed.as_ref().unwrap(), target),
|
|
Some(&kind)
|
|
);
|
|
assert_eq!(
|
|
acknowledged(&app).contains(&message_kind::timer_selector()),
|
|
matches!(kind, MessageKind::Timer { .. })
|
|
);
|
|
assert!(state(&app).plan.as_ref().unwrap().impacts.is_empty());
|
|
}
|
|
}
|
|
let (mut p, oa, _, _) = fixture(MessageKind::Asynchronous, MessageKind::Asynchronous);
|
|
let mut review = State::new(&p, oa).unwrap();
|
|
let Some(SharedBody::Occurrence { body, .. }) = p.elements.get_mut(&oa).unwrap().shared_mut()
|
|
else {
|
|
panic!()
|
|
};
|
|
*body = OccurrenceBody::Action {
|
|
action: None,
|
|
lifeline: None,
|
|
};
|
|
review.refresh(&p);
|
|
assert!(
|
|
!review.editable(&p, &message_kind::selector()),
|
|
"An externally changed outer occurrence requires its own constructor review"
|
|
);
|
|
review.update(
|
|
&p,
|
|
RowEvent::Decide(message_kind::selector(), Some(SemanticDecision::Candidate)),
|
|
);
|
|
assert!(review.decisions.is_empty());
|
|
assert!(!review.can_apply());
|
|
}
|
|
|
|
#[test]
|
|
fn message_group_timer_does_not_erase_exchange_without_its_own_selected_decision() {
|
|
let (mut p, oa, sa, _) = fixture(MessageKind::Asynchronous, MessageKind::Asynchronous);
|
|
let sender = p
|
|
.add_element(Kind::Function, Layer::Sa, "Sender", None)
|
|
.unwrap();
|
|
let receiver = p
|
|
.add_element(Kind::Function, Layer::Sa, "Receiver", None)
|
|
.unwrap();
|
|
let exchange = p.relate(
|
|
RelationKind::FunctionalExchange,
|
|
"Message exchange",
|
|
sender,
|
|
receiver,
|
|
);
|
|
let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange {
|
|
exchange: selected, ..
|
|
},
|
|
..
|
|
}) = p.elements.get_mut(&sa).unwrap().shared_mut()
|
|
else {
|
|
panic!()
|
|
};
|
|
*selected = Some(exchange);
|
|
let mut app = App::with_project(p);
|
|
let before = app.document.snapshot();
|
|
start(&mut app, oa);
|
|
decide(
|
|
&mut app,
|
|
message_kind::selector(),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
send(
|
|
&mut app,
|
|
RowEvent::Variant(path("Sa"), json!({"Timer":{"expression":"after 2 s"}})),
|
|
);
|
|
assert!(!state(&app).can_apply());
|
|
assert!(
|
|
failures(&app).contains("timer_contract"),
|
|
"{}",
|
|
failures(&app)
|
|
);
|
|
assert_eq!(state(&app).decisions.len(), 1);
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
decide(
|
|
&mut app,
|
|
FacetSelector::Occurrence(OccurrenceFacet::Exchange),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
send(
|
|
&mut app,
|
|
RowEvent::Shape(
|
|
"/body/Sa/Shared/Occurrence/body/Exchange/exchange".into(),
|
|
Value::Null,
|
|
),
|
|
);
|
|
assert!(state(&app).can_apply(), "{}", failures(&app));
|
|
assert_eq!(
|
|
acknowledged(&app),
|
|
BTreeSet::from([
|
|
message_kind::selector(),
|
|
message_kind::timer_selector(),
|
|
FacetSelector::Occurrence(OccurrenceFacet::Exchange)
|
|
])
|
|
);
|
|
assert!(state(&app).plan.as_ref().unwrap().impacts.is_empty());
|
|
apply(&mut app);
|
|
assert_eq!(
|
|
app.project().relations[&exchange],
|
|
before.decoded.project.relations[&exchange]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn message_group_initial_equal_take_acknowledges_only_kind_and_checks_authored_timer_agreement() {
|
|
let (p, oa, sa, _) = fixture(timer("source timer"), timer("target timer"));
|
|
let mut app = App::with_project(p);
|
|
start(&mut app, oa);
|
|
decide(
|
|
&mut app,
|
|
message_kind::selector(),
|
|
SemanticDecision::TakeSource,
|
|
);
|
|
decide(
|
|
&mut app,
|
|
FacetSelector::Header(HeaderFacet::Name),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
send(
|
|
&mut app,
|
|
RowEvent::Input("/name".into(), "Final name".into()),
|
|
);
|
|
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
|
|
assert_eq!(
|
|
message_kind::value(state(&app).proposed.as_ref().unwrap(), sa),
|
|
Some(&timer("target timer"))
|
|
);
|
|
assert!(!acknowledged(&app).contains(&message_kind::timer_selector()));
|
|
let expression = format!("{}/Timer/expression", path("Sa"));
|
|
decide(
|
|
&mut app,
|
|
message_kind::selector(),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
send(
|
|
&mut app,
|
|
RowEvent::Input(expression.clone(), "authored group timer".into()),
|
|
);
|
|
assert!(
|
|
!state(&app).can_apply(),
|
|
"An unchanged initial kind cannot silently authorize a timer edit"
|
|
);
|
|
decide(
|
|
&mut app,
|
|
message_kind::timer_selector(),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
send(
|
|
&mut app,
|
|
RowEvent::Input(expression.clone(), "different scalar timer".into()),
|
|
);
|
|
assert!(!state(&app).can_apply());
|
|
assert!(
|
|
state(&app)
|
|
.error
|
|
.as_ref()
|
|
.unwrap()
|
|
.contains("different final values")
|
|
);
|
|
send(
|
|
&mut app,
|
|
RowEvent::Input(expression, "authored group timer".into()),
|
|
);
|
|
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
|
|
assert!(acknowledged(&app).contains(&message_kind::timer_selector()));
|
|
assert_eq!(
|
|
message_kind::value(state(&app).proposed.as_ref().unwrap(), sa),
|
|
Some(&timer("authored group timer"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn message_group_rejects_protected_edits_stale_plan_and_source_failure_without_losing_timer() {
|
|
for source_failure in [false, true] {
|
|
let (p, oa, _, _) = fixture(MessageKind::Asynchronous, timer("target"));
|
|
let mut app = App::with_project(p);
|
|
if source_failure {
|
|
let bundle = app.document.bundle().clone();
|
|
let mut limits = source_project::ProjectLimits::default();
|
|
limits.model.max_source_bytes =
|
|
bundle.files.values().map(String::len).sum::<usize>() + 10;
|
|
app.document = Document::from_bundle(bundle, limits).unwrap();
|
|
}
|
|
start(&mut app, oa);
|
|
decide(
|
|
&mut app,
|
|
message_kind::selector(),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
let expression = format!("{}/Timer/expression", path("Sa"));
|
|
let raw = "retain exact timer β\n".repeat(40);
|
|
send(&mut app, RowEvent::Input(expression.clone(), raw.clone()));
|
|
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
|
|
send(
|
|
&mut app,
|
|
RowEvent::Input("/name".into(), "Unselected protected scope".into()),
|
|
);
|
|
assert!(!state(&app).can_apply());
|
|
assert!(
|
|
state(&app)
|
|
.error
|
|
.as_ref()
|
|
.unwrap()
|
|
.contains("selected review")
|
|
);
|
|
send(&mut app, RowEvent::Preview);
|
|
assert!(state(&app).can_apply());
|
|
if !source_failure {
|
|
app.document
|
|
.execute(Command::Rename(oa, "External source edit".into()))
|
|
.unwrap();
|
|
}
|
|
let before = app.document.snapshot();
|
|
let revision = app.document.revision();
|
|
apply(&mut app);
|
|
assert!(state(&app).error.is_some());
|
|
assert_eq!(state(&app).decisions.len(), 1);
|
|
assert_eq!(
|
|
state(&app)
|
|
.group_drafts
|
|
.get(&message_kind::selector())
|
|
.unwrap()
|
|
.inputs[&expression]
|
|
.raw,
|
|
raw
|
|
);
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
assert_eq!(app.document.revision(), revision);
|
|
}
|
|
}
|