use std::collections::{HashMap, HashSet};
use std::path::Path;
use tokio::sync::mpsc;
use super::core::AppEvent;
use super::task;
use crate::app::session_state::SessionState;
use crate::domain::agent::{AgentModel, AgentSelection, ReasoningLevel};
use crate::domain::review::FocusedReviewStatus;
use crate::domain::session::{Session, SessionId, Status};
use crate::domain::transient_message::{
TransientMessage, TransientMessageAnchor, TransientMessageBody, TransientMessageLifecycle,
TransientMessageSlot,
};
use crate::infra::db::SessionFocusedReviewRow;
#[derive(Debug)]
pub(crate) enum ReviewCacheEntry {
Loading {
diff_hash: u64,
},
Ready {
diff_hash: u64,
text: String,
},
Failed {
diff_hash: u64,
error: String,
},
Suppressed,
}
impl ReviewCacheEntry {
pub(crate) fn diff_hash(&self) -> Option<u64> {
match self {
Self::Loading { diff_hash }
| Self::Ready { diff_hash, .. }
| Self::Failed { diff_hash, .. } => Some(*diff_hash),
Self::Suppressed => None,
}
}
pub(crate) fn matches_persistence(&self, update: &FocusedReviewPersistence) -> bool {
let status = match self {
Self::Loading { .. } => FocusedReviewStatus::Pending,
Self::Ready { .. } => FocusedReviewStatus::Ready,
Self::Failed { .. } => FocusedReviewStatus::Failed,
Self::Suppressed => return false,
};
status == update.status && self.diff_hash() == update.diff_hash
}
pub(crate) fn from_result(diff_hash: u64, result: &Result<String, String>) -> Self {
match result {
Ok(review_text) => Self::Ready {
diff_hash,
text: review_text.clone(),
},
Err(error) => Self::Failed {
diff_hash,
error: error.clone(),
},
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct ReviewUpdate {
pub(crate) diff_hash: u64,
pub(crate) result: Result<String, String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct FocusedReviewPersistence {
pub(crate) diff_hash: Option<u64>,
pub(crate) session_id: SessionId,
pub(crate) status: FocusedReviewStatus,
pub(crate) text: Option<String>,
}
pub(crate) const MAX_FOCUSED_REVIEW_PERSISTENCE_RETRIES: u8 = 3;
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct FocusedReviewPersistenceRetry {
pub(crate) attempt: u8,
pub(crate) persistence_update: FocusedReviewPersistence,
}
impl FocusedReviewPersistenceRetry {
pub(crate) fn initial(persistence_update: FocusedReviewPersistence) -> Self {
Self {
attempt: 0,
persistence_update,
}
}
pub(crate) fn next(self) -> Option<Self> {
(self.attempt < MAX_FOCUSED_REVIEW_PERSISTENCE_RETRIES).then(|| Self {
attempt: self.attempt.saturating_add(1),
persistence_update: self.persistence_update,
})
}
}
const REVIEW_LOADING_MESSAGE_PREFIX: &str = "Reviewing changes with";
pub(crate) const REVIEW_NO_DIFF_MESSAGE: &str = "No diff changes found for review.";
pub(crate) fn diff_content_hash(diff: &str) -> u64 {
const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
diff.as_bytes().iter().fold(FNV_OFFSET_BASIS, |hash, byte| {
(hash ^ u64::from(*byte)).wrapping_mul(FNV_PRIME)
})
}
pub(crate) fn review_loading_message(review_model: AgentModel) -> String {
format!("{REVIEW_LOADING_MESSAGE_PREFIX} {}", review_model.as_str())
}
pub(crate) fn review_failure_message(error: &str) -> String {
format!("Review assist unavailable: {}", error.trim())
}
pub(crate) fn review_view_text<'a>(
review_cache: &'a HashMap<SessionId, ReviewCacheEntry>,
session_id: &str,
) -> Option<&'a str> {
let cache_entry = review_cache.get(session_id)?;
match cache_entry {
ReviewCacheEntry::Ready { text, .. } => Some(text.as_str()),
ReviewCacheEntry::Loading { .. }
| ReviewCacheEntry::Failed { .. }
| ReviewCacheEntry::Suppressed => None,
}
}
pub(crate) fn hydrate_review_transients(
review_cache: &HashMap<SessionId, ReviewCacheEntry>,
session_state: &mut SessionState,
review_model: AgentModel,
) {
for session in session_state.sessions_mut() {
hydrate_session_review_transient(review_cache, session, review_model);
}
}
pub(crate) fn hydrate_review_transient(
review_cache: &HashMap<SessionId, ReviewCacheEntry>,
session_state: &mut SessionState,
session_id: &str,
review_model: AgentModel,
) {
let Some(session) = session_state.session_mut_for_id(session_id) else {
return;
};
hydrate_session_review_transient(review_cache, session, review_model);
}
pub(crate) fn prune_review_cache(
review_cache: &mut HashMap<SessionId, ReviewCacheEntry>,
pending_persistence: &HashMap<SessionId, FocusedReviewPersistence>,
session_state: &SessionState,
) {
let active_session_ids = session_state
.sessions()
.iter()
.map(|session| session.id.as_str())
.collect::<HashSet<_>>();
review_cache.retain(|session_id, cache_entry| {
active_session_ids.contains(session_id.as_str())
|| matches!(cache_entry, ReviewCacheEntry::Loading { .. })
|| pending_persistence.contains_key(session_id)
});
}
pub(crate) fn focused_review_result_anchor(session: &Session) -> TransientMessageAnchor {
session
.transient_messages
.get(TransientMessageSlot::Review)
.map_or(TransientMessageAnchor::AfterCompletedTurn, |message| {
message.anchor
})
}
fn hydrate_session_review_transient(
review_cache: &HashMap<SessionId, ReviewCacheEntry>,
session: &mut Session,
review_model: AgentModel,
) {
if !matches!(
session.status,
Status::Review | Status::Question | Status::AgentReview
) {
session
.transient_messages
.retract(TransientMessageSlot::Review);
return;
}
let Some(cache_entry) = review_cache.get(&session.id) else {
session
.transient_messages
.retract(TransientMessageSlot::Review);
return;
};
let (anchor, body) = match cache_entry {
ReviewCacheEntry::Loading { .. } => (
TransientMessageAnchor::Tail,
TransientMessageBody::Loading(review_loading_message(review_model)),
),
ReviewCacheEntry::Ready { text, .. } => (
focused_review_result_anchor(session),
TransientMessageBody::Markdown(text.clone()),
),
ReviewCacheEntry::Failed { error, .. } => (
focused_review_result_anchor(session),
TransientMessageBody::Plain(review_failure_message(error)),
),
ReviewCacheEntry::Suppressed => {
session
.transient_messages
.retract(TransientMessageSlot::Review);
return;
}
};
session.transient_messages.upsert(TransientMessage {
anchor,
body,
lifecycle: TransientMessageLifecycle::ClearOnNewTurn,
slot: TransientMessageSlot::Review,
turn_position: session.latest_user_prompt_position(),
});
}
pub(crate) fn review_cache_from_rows(
focused_review_rows: Vec<SessionFocusedReviewRow>,
) -> HashMap<SessionId, ReviewCacheEntry> {
focused_review_rows
.into_iter()
.filter_map(|row| {
let diff_hash = row.diff_hash.parse::<u64>().ok()?;
Some((
SessionId::from(row.session_id),
ReviewCacheEntry::Ready {
diff_hash,
text: row.text,
},
))
})
.collect()
}
pub(crate) fn start_review_assist(
app_event_tx: mpsc::UnboundedSender<AppEvent>,
review_agent: (AgentSelection, ReasoningLevel),
session_id: &str,
session_folder: &Path,
diff_hash: u64,
review_diff: &str,
session_chat_history: Option<&str>,
) {
let (review_selection, reasoning_level) = review_agent;
task::TaskService::spawn_review_assist_task(task::ReviewAssistTaskInput {
app_event_tx,
diff_hash,
reasoning_level,
review_diff: review_diff.to_string(),
review_selection,
session_chat_history: session_chat_history.map(str::to_string),
session_folder: session_folder.to_path_buf(),
session_id: SessionId::from(session_id),
});
}
pub(crate) fn mark_session_agent_review(session_state: &mut SessionState, session_id: &str) {
update_transient_review_status(
session_state,
session_id,
Status::Review,
Status::AgentReview,
);
}
pub(crate) fn apply_review_updates(
review_cache: &mut HashMap<SessionId, ReviewCacheEntry>,
session_state: &mut SessionState,
review_updates: HashMap<SessionId, ReviewUpdate>,
) -> Vec<FocusedReviewPersistence> {
let mut persistence_updates = Vec::new();
for (session_id, review_update) in review_updates {
if let Some(persistence_update) =
apply_review_update(review_cache, session_state, &session_id, review_update)
{
persistence_updates.push(persistence_update);
}
}
persistence_updates
}
pub(crate) fn fail_review_preparation(
review_cache: &mut HashMap<SessionId, ReviewCacheEntry>,
session_state: &mut SessionState,
session_id: &SessionId,
error: String,
review_model: AgentModel,
) -> FocusedReviewPersistence {
let diff_hash = diff_content_hash("");
review_cache.insert(
session_id.clone(),
ReviewCacheEntry::Failed { diff_hash, error },
);
hydrate_review_transient(review_cache, session_state, session_id, review_model);
FocusedReviewPersistence {
diff_hash: Some(diff_hash),
session_id: session_id.clone(),
status: FocusedReviewStatus::Failed,
text: None,
}
}
fn apply_review_update(
review_cache: &mut HashMap<SessionId, ReviewCacheEntry>,
session_state: &mut SessionState,
session_id: &str,
review_update: ReviewUpdate,
) -> Option<FocusedReviewPersistence> {
let ReviewUpdate { diff_hash, result } = review_update;
let cache_entry = review_cache.get(session_id)?;
if !matches!(cache_entry, ReviewCacheEntry::Loading { .. })
|| cache_entry.diff_hash() != Some(diff_hash)
{
return None;
}
let persistence_update = FocusedReviewPersistence {
diff_hash: Some(diff_hash),
session_id: SessionId::from(session_id),
status: if result.is_ok() {
FocusedReviewStatus::Ready
} else {
FocusedReviewStatus::Failed
},
text: result.as_ref().ok().cloned(),
};
review_cache.insert(
SessionId::from(session_id),
ReviewCacheEntry::from_result(diff_hash, &result),
);
if let Some(session) = session_state
.sessions_mut()
.iter_mut()
.find(|session| session.id == session_id)
{
let anchor = focused_review_result_anchor(session);
let body = match &result {
Ok(review_text) => TransientMessageBody::Markdown(review_text.clone()),
Err(error) => TransientMessageBody::Plain(review_failure_message(error)),
};
session.transient_messages.upsert(TransientMessage {
anchor,
body,
lifecycle: TransientMessageLifecycle::ClearOnNewTurn,
slot: TransientMessageSlot::Review,
turn_position: session.latest_user_prompt_position(),
});
}
restore_session_review_status(session_state, session_id);
Some(persistence_update)
}
pub(crate) fn restore_session_review_status(session_state: &mut SessionState, session_id: &str) {
update_transient_review_status(
session_state,
session_id,
Status::AgentReview,
Status::Review,
);
}
fn update_transient_review_status(
session_state: &mut SessionState,
session_id: &str,
current_status: Status,
next_status: Status,
) {
session_state.transition_status_if_current(session_id, current_status, next_status);
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::sync::Arc;
use super::*;
use crate::app::session_state::SessionState;
use crate::domain::selection::SelectionState;
use crate::infra::clock::RealClock;
use crate::test_support::SessionFixtureBuilder;
fn empty_session_state() -> SessionState {
SessionState::new(
HashMap::new(),
Vec::new(),
SelectionState::default(),
Arc::new(RealClock),
0,
0,
)
}
fn loading_review_cache(
session_id: &SessionId,
diff_hash: u64,
) -> HashMap<SessionId, ReviewCacheEntry> {
HashMap::from([(session_id.clone(), ReviewCacheEntry::Loading { diff_hash })])
}
fn successful_review_update(
session_id: &SessionId,
diff_hash: u64,
review_text: &str,
) -> HashMap<SessionId, ReviewUpdate> {
HashMap::from([(
session_id.clone(),
ReviewUpdate {
diff_hash,
result: Ok(review_text.to_string()),
},
)])
}
fn session_state_with_stale_review(session_id: &SessionId) -> SessionState {
let mut session = SessionFixtureBuilder::new()
.id(session_id.as_str())
.status(Status::Review)
.build();
session.transient_messages.upsert(TransientMessage {
anchor: TransientMessageAnchor::AfterCompletedTurn,
body: TransientMessageBody::Markdown("stale review".to_string()),
lifecycle: TransientMessageLifecycle::ClearOnNewTurn,
slot: TransientMessageSlot::Review,
turn_position: None,
});
SessionState::new(
HashMap::new(),
vec![session],
SelectionState::default(),
Arc::new(RealClock),
0,
0,
)
}
#[test]
fn review_loading_message_uses_requested_model_name() {
let review_model = AgentModel::Gpt56Sol;
let message = review_loading_message(review_model);
assert_eq!(message, "Reviewing changes with gpt-5.6-sol");
}
#[test]
fn review_view_text_hides_cached_review_generation() {
let mut review_cache = HashMap::new();
review_cache.insert(
"session-id".into(),
ReviewCacheEntry::Loading { diff_hash: 7 },
);
let review_text = review_view_text(&review_cache, "session-id");
assert_eq!(review_text, None);
}
#[test]
fn review_view_text_hides_suppressed_auto_review() {
let mut review_cache = HashMap::new();
review_cache.insert("session-id".into(), ReviewCacheEntry::Suppressed);
let review_text = review_view_text(&review_cache, "session-id");
assert_eq!(review_text, None);
}
#[test]
fn review_cache_matches_only_current_persistence_state() {
let update = |status| FocusedReviewPersistence {
diff_hash: Some(42),
session_id: "session-id".into(),
status,
text: None,
};
let loading = ReviewCacheEntry::Loading { diff_hash: 42 };
let ready = ReviewCacheEntry::Ready {
diff_hash: 42,
text: "review".to_string(),
};
let failed = ReviewCacheEntry::Failed {
diff_hash: 42,
error: "failed".to_string(),
};
assert!(loading.matches_persistence(&update(FocusedReviewStatus::Pending)));
assert!(ready.matches_persistence(&update(FocusedReviewStatus::Ready)));
assert!(failed.matches_persistence(&update(FocusedReviewStatus::Failed)));
assert!(!ready.matches_persistence(&update(FocusedReviewStatus::Pending)));
assert!(
!ReviewCacheEntry::Suppressed.matches_persistence(&update(FocusedReviewStatus::Failed))
);
let mut stale = update(FocusedReviewStatus::Ready);
stale.diff_hash = Some(41);
assert!(!ready.matches_persistence(&stale));
}
#[test]
fn focused_review_persistence_retry_stops_after_limit() {
let persistence_update = FocusedReviewPersistence {
diff_hash: Some(42),
session_id: "session-id".into(),
status: FocusedReviewStatus::Ready,
text: Some("review".to_string()),
};
let first = FocusedReviewPersistenceRetry::initial(persistence_update)
.next()
.expect("first retry should exist");
let second = first.clone().next().expect("second retry should exist");
let third = second.clone().next().expect("third retry should exist");
let exhausted = third.clone().next();
assert_eq!((first.attempt, second.attempt, third.attempt), (1, 2, 3));
assert_eq!(exhausted, None);
}
#[test]
fn review_cache_from_rows_restores_persisted_ready_review() {
let focused_review_rows = vec![SessionFocusedReviewRow {
diff_hash: "42".to_string(),
session_id: "session-id".to_string(),
text: "## Review\nPersisted finding.".to_string(),
}];
let review_cache = review_cache_from_rows(focused_review_rows);
assert!(matches!(
review_cache.get("session-id"),
Some(ReviewCacheEntry::Ready { diff_hash: 42, text })
if text == "## Review\nPersisted finding."
));
}
#[test]
fn hydrate_review_transients_retracts_terminal_session_review() {
let session_id = SessionId::from("session-id");
let mut session = SessionFixtureBuilder::new()
.id(session_id.as_str())
.status(Status::Done)
.build();
session.transient_messages.upsert(TransientMessage {
anchor: TransientMessageAnchor::AfterCompletedTurn,
body: TransientMessageBody::Markdown("stale review".to_string()),
lifecycle: TransientMessageLifecycle::ClearOnNewTurn,
slot: TransientMessageSlot::Review,
turn_position: None,
});
let review_cache = HashMap::from([(
session_id,
ReviewCacheEntry::Ready {
diff_hash: 42,
text: "persisted review".to_string(),
},
)]);
let mut session_state = SessionState::new(
HashMap::new(),
vec![session],
SelectionState::default(),
Arc::new(RealClock),
0,
0,
);
hydrate_review_transients(&review_cache, &mut session_state, AgentModel::Gpt56Sol);
assert!(
session_state.sessions()[0]
.transient_messages
.get(TransientMessageSlot::Review)
.is_none()
);
}
#[test]
fn hydrate_review_transient_retracts_review_without_cache_entry() {
let session_id = SessionId::from("session-id");
let mut session_state = session_state_with_stale_review(&session_id);
hydrate_review_transient(
&HashMap::new(),
&mut session_state,
&session_id,
AgentModel::Gpt56Sol,
);
assert!(
session_state.sessions()[0]
.transient_messages
.get(TransientMessageSlot::Review)
.is_none()
);
}
#[test]
fn hydrate_review_transient_retracts_suppressed_review() {
let session_id = SessionId::from("session-id");
let review_cache = HashMap::from([(session_id.clone(), ReviewCacheEntry::Suppressed)]);
let mut session_state = session_state_with_stale_review(&session_id);
hydrate_review_transient(
&review_cache,
&mut session_state,
&session_id,
AgentModel::Gpt56Sol,
);
assert!(
session_state.sessions()[0]
.transient_messages
.get(TransientMessageSlot::Review)
.is_none()
);
}
#[test]
fn hydrate_review_transient_restores_failed_review() {
let session_id = SessionId::from("session-id");
let review_cache = HashMap::from([(
session_id.clone(),
ReviewCacheEntry::Failed {
diff_hash: 42,
error: "provider unavailable".to_string(),
},
)]);
let mut session_state = session_state_with_stale_review(&session_id);
hydrate_review_transient(
&review_cache,
&mut session_state,
&session_id,
AgentModel::Gpt56Sol,
);
assert_eq!(
session_state.sessions()[0]
.transient_messages
.get(TransientMessageSlot::Review)
.map(|message| &message.body),
Some(&TransientMessageBody::Plain(
"Review assist unavailable: provider unavailable".to_string()
))
);
}
#[test]
fn hydrate_review_transient_ignores_missing_session() {
let mut session_state = empty_session_state();
hydrate_review_transient(
&HashMap::new(),
&mut session_state,
"missing-session",
AgentModel::Gpt56Sol,
);
assert!(session_state.sessions().is_empty());
}
#[test]
fn prune_review_cache_retains_active_loading_and_pending_entries() {
let active_session_id = SessionId::from("active-session");
let loading_session_id = SessionId::from("loading-session");
let pending_session_id = SessionId::from("inactive-ready");
let mut review_cache = HashMap::from([
(
active_session_id.clone(),
ReviewCacheEntry::Ready {
diff_hash: 1,
text: "active review".to_string(),
},
),
(
"inactive-ready".into(),
ReviewCacheEntry::Ready {
diff_hash: 2,
text: "inactive review".to_string(),
},
),
(
"inactive-failed".into(),
ReviewCacheEntry::Failed {
diff_hash: 3,
error: "failed review".to_string(),
},
),
("inactive-suppressed".into(), ReviewCacheEntry::Suppressed),
(
loading_session_id.clone(),
ReviewCacheEntry::Loading { diff_hash: 4 },
),
]);
let pending_persistence = HashMap::from([(
pending_session_id.clone(),
FocusedReviewPersistence {
diff_hash: Some(2),
session_id: pending_session_id.clone(),
status: FocusedReviewStatus::Ready,
text: Some("inactive review".to_string()),
},
)]);
let session_state = session_state_with_stale_review(&active_session_id);
prune_review_cache(&mut review_cache, &pending_persistence, &session_state);
assert_eq!(review_cache.len(), 3);
assert!(review_cache.contains_key(&active_session_id));
assert!(review_cache.contains_key(&pending_session_id));
assert!(matches!(
review_cache.get(&loading_session_id),
Some(ReviewCacheEntry::Loading { diff_hash: 4 })
));
}
#[test]
fn apply_review_updates_retains_inactive_success_until_persistence() {
let session_id = SessionId::from("session-persist-review");
let diff_hash = 19;
let review_text = "## Review\nPersist this finding.";
let mut review_cache = loading_review_cache(&session_id, diff_hash);
let mut session_state = empty_session_state();
let review_updates = successful_review_update(&session_id, diff_hash, review_text);
let persistence_updates =
apply_review_updates(&mut review_cache, &mut session_state, review_updates);
assert_eq!(
persistence_updates,
vec![FocusedReviewPersistence {
diff_hash: Some(diff_hash),
session_id: session_id.clone(),
status: FocusedReviewStatus::Ready,
text: Some(review_text.to_string()),
}]
);
assert!(matches!(
review_cache.get(&session_id),
Some(ReviewCacheEntry::Ready { diff_hash: 19, text }) if text == review_text
));
}
#[test]
fn apply_review_updates_returns_clear_for_failed_regeneration() {
let session_id = SessionId::from("session-failed-review");
let diff_hash = 29;
let mut review_cache = loading_review_cache(&session_id, diff_hash);
let mut session_state = empty_session_state();
let review_updates = HashMap::from([(
session_id.clone(),
ReviewUpdate {
diff_hash,
result: Err("provider failed".to_string()),
},
)]);
let persistence_updates =
apply_review_updates(&mut review_cache, &mut session_state, review_updates);
assert_eq!(
persistence_updates,
vec![FocusedReviewPersistence {
diff_hash: Some(diff_hash),
session_id,
status: FocusedReviewStatus::Failed,
text: None,
}]
);
}
#[test]
fn apply_review_updates_writes_success_to_cache() {
let session_id = SessionId::from("session-cache-review");
let diff_hash = 11;
let review_text = "## Review\nCache-backed finding.";
let mut review_cache = loading_review_cache(&session_id, diff_hash);
let mut session_state = session_state_with_stale_review(&session_id);
let review_updates = successful_review_update(&session_id, diff_hash, review_text);
apply_review_updates(&mut review_cache, &mut session_state, review_updates);
assert!(matches!(
review_cache.get(session_id.as_str()),
Some(ReviewCacheEntry::Ready { text, .. }) if text == review_text
));
}
#[test]
fn apply_review_updates_preserves_loading_row_tail_position() {
let session_id = SessionId::from("session-tail-review");
let diff_hash = 17;
let review_text = "## Review\nChronological finding.";
let mut review_cache = loading_review_cache(&session_id, diff_hash);
let mut session = SessionFixtureBuilder::new()
.id(session_id.as_str())
.status(Status::AgentReview)
.build();
session.transient_messages.upsert(TransientMessage {
anchor: TransientMessageAnchor::Tail,
body: TransientMessageBody::Loading("Reviewing changes".to_string()),
lifecycle: TransientMessageLifecycle::ClearOnNewTurn,
slot: TransientMessageSlot::Review,
turn_position: None,
});
let mut session_state = SessionState::new(
HashMap::new(),
vec![session],
SelectionState::default(),
Arc::new(RealClock),
0,
0,
);
let review_updates = successful_review_update(&session_id, diff_hash, review_text);
apply_review_updates(&mut review_cache, &mut session_state, review_updates);
let review_message = session_state.sessions()[0]
.transient_messages
.get(TransientMessageSlot::Review)
.expect("completed review should remain visible");
assert_eq!(review_message.anchor, TransientMessageAnchor::Tail);
assert_eq!(
review_message.body,
TransientMessageBody::Markdown(review_text.to_string())
);
}
#[test]
fn apply_review_updates_ignores_suppressed_auto_review_entry() {
let session_id = SessionId::from("session-suppressed-review");
let diff_hash = 23;
let mut review_cache = HashMap::from([(session_id.clone(), ReviewCacheEntry::Suppressed)]);
let mut session_state = session_state_with_stale_review(&session_id);
let review_updates =
successful_review_update(&session_id, diff_hash, "## Review\nShould not be rendered.");
apply_review_updates(&mut review_cache, &mut session_state, review_updates);
assert!(matches!(
review_cache.get(session_id.as_str()),
Some(ReviewCacheEntry::Suppressed)
));
}
}