fix(coordinator): clear focus_target on cancel regardless of phase

finish_cancel_session_state previously only cleared state.focus_target
when the cancelled phase was not Processing — Processing-phase cancel
left the stale window-resource handle in place until the next
begin_session_state overwrote it. Between cancel and next begin, any
reader of focus_target sees a value belonging to a session that is
about to be torn down.

Move state.focus_target = None ahead of the phase check so it always
fires when the cancel was accepted (i.e. not rejected for Idle /
Inserting phases). Phase transition stays conditional on the original
non-Processing branch, since end_session must still own the
Processing → Idle collapse to avoid racing with polish/insert.

Tighten cancel_session_state_machine_is_table_driven so it asserts
focus_target gets cleared for every accepted phase (including
Processing) and stays untouched on rejected cancels.

Audit ID 3.3.5.
This commit is contained in:
baiqing committed 2026-05-10 01:25:11 +08:00
1 parent b1c387265b
commit ce82fcd9fa
1 file changed
+18 -4
@@ -164,11 +164,14 @@ pub(crate) fn begin_cancel_session_state(state: &mut SessionState) -> Option<Can
})
}
/// cancel_session 外部资源清理后的锁内收尾。Processing 交给 end_session 自己收尾。
/// cancel_session 外部资源清理后的锁内收尾。Processing 阶段把 phase 留给 end_session
/// 自己收尾(防止与 polish/insert 路径竞争),但 focus_target 是当前 session 的窗口
/// 资源句柄,cancel 之后无论处于哪个 phase 都应当释放,避免下一个 session 之前的
/// 空档期被旧值污染。详见 audit 3.3.5。
pub(crate) fn finish_cancel_session_state(state: &mut SessionState, decision: CancelDecision) {
state.focus_target = None;
if decision.phase != SessionPhase::Processing {
state.phase = SessionPhase::Idle;
state.focus_target = None;
}
}
@@ -367,8 +370,19 @@ mod tests {
assert_eq!(state.phase, expected_phase, "initial={initial:?}");
assert_eq!(state.cancelled, expected_cancelled, "initial={initial:?}");
if matches!(initial, SessionPhase::Starting | SessionPhase::Listening) {
assert!(state.focus_target.is_none(), "initial={initial:?}");
// 任何被 begin_cancel_session_state 接受的 phase(即非 Idle/Inserting)
// 都应当清掉 focus_target,包括 Processing —— 这是 audit 3.3.5 的回归卡。
if expected_cancelled {
assert!(
state.focus_target.is_none(),
"focus_target should clear after cancel, initial={initial:?}"
);
} else {
assert_eq!(
state.focus_target,
Some(1),
"rejected cancel must not touch focus_target, initial={initial:?}"
);
}
}
}