Merge ghostlight-verse/grouped-scope-moved-label (cut-grouped-scope-moved-label r1, report h1, Soul s1 holds)

This commit is contained in:
Meta Vens 2026-10-17 15:55:57 +02:00
commit 73f26a5a32
2 changed files with 92 additions and 5 deletions

View file

@ -2917,11 +2917,12 @@ pub struct ConstituentRefusal {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ConstituentRefusalReason {
/// The subject's opportunity moved between the cover and the cell, so it was
/// not selected and nothing was submitted for it.
/// The subject holds no single decidable opportunity on its scope any more,
/// so it was not selected and nothing was submitted for it.
LeftBeforeSelection,
/// The kernel refused its proposal because the scope it was reasoned from
/// no longer held (a neighbour's act moved it).
/// The scope the member was bound to no longer held: found moved at
/// selection (still offered at a new digest, nothing submitted), or refused
/// by the kernel at admission (a neighbour's act moved it).
ScopeChanged,
/// The kernel refused its command for any other reason.
WorldRefused,
@ -3536,6 +3537,12 @@ impl ControllerRunner {
// cell was not active when the cell ran. It is dropped rather
// than declined: declining a stale opportunity would be refused
// anyway, and the drop is recorded where a reader can see it.
// Still offered at a new digest is a moved scope; anything else
// means the subject holds no single decidable opportunity.
Err(ControllerError::ScopeMoved) => refused.push(ConstituentRefusal {
subject: member.subject,
reason: ConstituentRefusalReason::ScopeChanged,
}),
Err(_) => refused.push(ConstituentRefusal {
subject: member.subject,
reason: ConstituentRefusalReason::LeftBeforeSelection,
@ -19908,7 +19915,7 @@ mod tests {
}
#[tokio::test]
async fn a_grouped_member_that_left_the_cell_is_named_in_refused() {
async fn a_grouped_member_whose_scope_moved_is_named_in_refused_as_scope_changed() {
let (_directory, mailbox, task) = active_cell_mailbox(vec![
NewController::OperationalAgent,
NewController::OperationalAgent,
@ -19949,6 +19956,62 @@ mod tests {
let CellRun::Grouped(run) = runner.run_cell(&cell).await.unwrap() else {
panic!("a coarse cell did not run the grouped lane")
};
assert_eq!(
run.refused,
vec![ConstituentRefusal {
subject: leaver,
reason: ConstituentRefusalReason::ScopeChanged,
}]
);
assert!(run.needs.is_empty(), "{:?}", run.needs);
drop(runner);
drop(mailbox);
task.await.unwrap();
}
#[tokio::test]
async fn a_grouped_member_that_truly_left_is_named_in_refused_as_left_before_selection() {
let (_directory, mailbox, task) = active_cell_mailbox(vec![
NewController::OperationalAgent,
NewController::OperationalAgent,
])
.await;
let cell = one_group(&mailbox).await;
let Cell::Group { id, tick, members } = cell else {
panic!("the cell is not coarse")
};
// One member's subject is no longer in the world at all; the other is
// bound to the world as it is.
let world = mailbox.snapshot().await.unwrap();
let leaver = SubjectId::issue();
let mut gone = members[1].opportunity.clone();
gone.scope.subject_id = leaver;
let bound = vec![
Constituent {
subject: members[0].subject,
opportunity: fresh_opportunity(&world, members[0].subject),
},
Constituent {
subject: leaver,
opportunity: gone,
},
];
let (runner, _store) = grouped_runner(
&mailbox,
vec![output(
vec![speak_call("c0", "c0__speak", "The hinge is flooding.")],
"lone-act",
)],
);
let cell = Cell::Group {
id,
tick,
members: bound,
};
let CellRun::Grouped(run) = runner.run_cell(&cell).await.unwrap() else {
panic!("a coarse cell did not run the grouped lane")
};
assert_eq!(
run.refused,
vec![ConstituentRefusal {

View file

@ -2371,6 +2371,30 @@ fn grouped_refusing(
}))
}
#[tokio::test]
async fn a_moved_scope_member_in_a_coarse_cell_is_recorded_as_scope_moved_at_its_position() {
let (_dir, verse) = Harness::fresh().await;
let group = three_member_group(&verse).await;
let Cell::Group { members, .. } = &group else {
panic!("a group")
};
let reasons = [
ConstituentRefusalReason::LeftBeforeSelection,
ConstituentRefusalReason::ScopeChanged,
ConstituentRefusalReason::LeftBeforeSelection,
];
let (status, committed, refusals) = fold(grouped_refusing(&group, &reasons), &group, 3);
assert!(status.is_none() && committed.is_empty());
assert_eq!(
refusals,
vec![
Refusal { cell: 3, subject: members[0].subject, reason: RefusalReason::OpportunityGone },
Refusal { cell: 3, subject: members[1].subject, reason: RefusalReason::ScopeMoved },
Refusal { cell: 3, subject: members[2].subject, reason: RefusalReason::OpportunityGone },
]
);
}
#[tokio::test]
async fn a_cell_the_driver_folds_records_each_refused_member_at_its_own_position() {
let (_dir, verse) = Harness::fresh().await;