#![allow(dead_code)]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum LexicalReadinessState {
Missing,
Repairing,
StaleButSearchable,
Ready,
CorruptQuarantined,
}
impl LexicalReadinessState {
pub(crate) fn is_searchable(self) -> bool {
matches!(
self,
Self::Ready | Self::StaleButSearchable | Self::Repairing
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum SemanticReadinessState {
Absent,
Backfilling,
FastTierReady,
HybridReady,
PolicyDisabled,
}
impl SemanticReadinessState {
pub(crate) fn can_refine(self) -> bool {
matches!(self, Self::FastTierReady | Self::HybridReady)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum SearchRefinementLevel {
LexicalOnly,
FastTierRefined,
FullyHybridRefined,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum RecommendedAction {
NothingRequired,
RepairLexicalNow,
WaitForLexicalRepair,
WaitForSemanticCatchUp,
RefreshLexicalSoon,
SemanticDisabledByPolicy,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct ReadinessSnapshot {
pub lexical: LexicalReadinessState,
pub semantic: SemanticReadinessState,
#[serde(default)]
pub last_search_refinement: Option<SearchRefinementLevel>,
}
impl ReadinessSnapshot {
pub(crate) fn new(lexical: LexicalReadinessState, semantic: SemanticReadinessState) -> Self {
Self {
lexical,
semantic,
last_search_refinement: None,
}
}
pub(crate) fn with_last_search_refinement(mut self, level: SearchRefinementLevel) -> Self {
self.last_search_refinement = Some(level);
self
}
pub(crate) fn recommended_action(&self) -> RecommendedAction {
match self.lexical {
LexicalReadinessState::Missing | LexicalReadinessState::CorruptQuarantined => {
RecommendedAction::RepairLexicalNow
}
LexicalReadinessState::Repairing => {
RecommendedAction::WaitForLexicalRepair
}
LexicalReadinessState::StaleButSearchable => RecommendedAction::RefreshLexicalSoon,
LexicalReadinessState::Ready => match self.semantic {
SemanticReadinessState::Absent | SemanticReadinessState::Backfilling => {
RecommendedAction::WaitForSemanticCatchUp
}
SemanticReadinessState::PolicyDisabled => {
RecommendedAction::SemanticDisabledByPolicy
}
SemanticReadinessState::FastTierReady | SemanticReadinessState::HybridReady => {
RecommendedAction::NothingRequired
}
},
}
}
pub(crate) fn is_searchable(&self) -> bool {
self.lexical.is_searchable()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lexical_states_serialize_as_snake_case() {
let pairs: &[(LexicalReadinessState, &str)] = &[
(LexicalReadinessState::Missing, "missing"),
(LexicalReadinessState::Repairing, "repairing"),
(
LexicalReadinessState::StaleButSearchable,
"stale_but_searchable",
),
(LexicalReadinessState::Ready, "ready"),
(
LexicalReadinessState::CorruptQuarantined,
"corrupt_quarantined",
),
];
for (state, expected) in pairs {
assert_eq!(
serde_json::to_string(state).unwrap(),
format!("\"{expected}\"")
);
}
}
#[test]
fn semantic_states_serialize_as_snake_case() {
let pairs: &[(SemanticReadinessState, &str)] = &[
(SemanticReadinessState::Absent, "absent"),
(SemanticReadinessState::Backfilling, "backfilling"),
(SemanticReadinessState::FastTierReady, "fast_tier_ready"),
(SemanticReadinessState::HybridReady, "hybrid_ready"),
(SemanticReadinessState::PolicyDisabled, "policy_disabled"),
];
for (state, expected) in pairs {
assert_eq!(
serde_json::to_string(state).unwrap(),
format!("\"{expected}\"")
);
}
}
#[test]
fn refinement_levels_serialize_as_snake_case() {
let pairs: &[(SearchRefinementLevel, &str)] = &[
(SearchRefinementLevel::LexicalOnly, "lexical_only"),
(SearchRefinementLevel::FastTierRefined, "fast_tier_refined"),
(
SearchRefinementLevel::FullyHybridRefined,
"fully_hybrid_refined",
),
];
for (level, expected) in pairs {
assert_eq!(
serde_json::to_string(level).unwrap(),
format!("\"{expected}\"")
);
}
}
#[test]
fn is_searchable_distinguishes_lexical_failure_modes() {
let cases = [
(LexicalReadinessState::Missing, false),
(LexicalReadinessState::CorruptQuarantined, false),
(LexicalReadinessState::Repairing, true),
(LexicalReadinessState::StaleButSearchable, true),
(LexicalReadinessState::Ready, true),
];
for (state, expected) in cases {
assert_eq!(state.is_searchable(), expected, "{state:?}");
}
}
#[test]
fn semantic_can_refine_only_when_at_least_fast_tier_ready() {
let cases = [
(SemanticReadinessState::Absent, false),
(SemanticReadinessState::Backfilling, false),
(SemanticReadinessState::PolicyDisabled, false),
(SemanticReadinessState::FastTierReady, true),
(SemanticReadinessState::HybridReady, true),
];
for (state, expected) in cases {
assert_eq!(state.can_refine(), expected, "{state:?}");
}
}
#[test]
fn recommended_actions_serialize_as_snake_case() {
let pairs: &[(RecommendedAction, &str)] = &[
(RecommendedAction::NothingRequired, "nothing_required"),
(RecommendedAction::RepairLexicalNow, "repair_lexical_now"),
(
RecommendedAction::WaitForLexicalRepair,
"wait_for_lexical_repair",
),
(
RecommendedAction::WaitForSemanticCatchUp,
"wait_for_semantic_catch_up",
),
(
RecommendedAction::RefreshLexicalSoon,
"refresh_lexical_soon",
),
(
RecommendedAction::SemanticDisabledByPolicy,
"semantic_disabled_by_policy",
),
];
for (action, expected) in pairs {
let expected_json = format!("\"{expected}\"");
assert!(
matches!(
serde_json::to_string(action).as_deref(),
Ok(actual) if actual == expected_json.as_str()
),
"action should serialize as {expected_json}"
);
}
}
#[test]
fn recommended_action_missing_lexical_always_repair_now() {
for sem in [
SemanticReadinessState::Absent,
SemanticReadinessState::Backfilling,
SemanticReadinessState::FastTierReady,
SemanticReadinessState::HybridReady,
SemanticReadinessState::PolicyDisabled,
] {
let snap = ReadinessSnapshot::new(LexicalReadinessState::Missing, sem);
assert_eq!(
snap.recommended_action(),
RecommendedAction::RepairLexicalNow
);
}
}
#[test]
fn recommended_action_corrupt_lexical_always_repair_now() {
let snap = ReadinessSnapshot::new(
LexicalReadinessState::CorruptQuarantined,
SemanticReadinessState::HybridReady,
);
assert_eq!(
snap.recommended_action(),
RecommendedAction::RepairLexicalNow
);
}
#[test]
fn recommended_action_active_lexical_repair_dominates_semantic_state() {
for sem in [
SemanticReadinessState::Absent,
SemanticReadinessState::Backfilling,
SemanticReadinessState::FastTierReady,
SemanticReadinessState::HybridReady,
SemanticReadinessState::PolicyDisabled,
] {
let snap = ReadinessSnapshot::new(LexicalReadinessState::Repairing, sem);
assert_eq!(
snap.recommended_action(),
RecommendedAction::WaitForLexicalRepair
);
assert!(snap.is_searchable());
}
}
#[test]
fn recommended_action_stale_lexical_requests_refresh() {
for sem in [
SemanticReadinessState::Absent,
SemanticReadinessState::HybridReady,
] {
let snap = ReadinessSnapshot::new(LexicalReadinessState::StaleButSearchable, sem);
assert_eq!(
snap.recommended_action(),
RecommendedAction::RefreshLexicalSoon
);
}
}
#[test]
fn recommended_action_ready_plus_hybrid_is_nothing_required() {
let snap = ReadinessSnapshot::new(
LexicalReadinessState::Ready,
SemanticReadinessState::HybridReady,
);
assert_eq!(
snap.recommended_action(),
RecommendedAction::NothingRequired
);
}
#[test]
fn recommended_action_ready_plus_policy_disabled_acknowledges_policy() {
let snap = ReadinessSnapshot::new(
LexicalReadinessState::Ready,
SemanticReadinessState::PolicyDisabled,
);
assert_eq!(
snap.recommended_action(),
RecommendedAction::SemanticDisabledByPolicy
);
}
#[test]
fn recommended_action_ready_plus_semantic_converging_waits() {
for sem in [
SemanticReadinessState::Absent,
SemanticReadinessState::Backfilling,
] {
let snap = ReadinessSnapshot::new(LexicalReadinessState::Ready, sem);
assert_eq!(
snap.recommended_action(),
RecommendedAction::WaitForSemanticCatchUp
);
}
}
#[test]
fn snapshot_with_last_search_refinement_round_trips_through_json() {
let snap = ReadinessSnapshot::new(
LexicalReadinessState::Ready,
SemanticReadinessState::FastTierReady,
)
.with_last_search_refinement(SearchRefinementLevel::FastTierRefined);
let json = serde_json::to_string(&snap).unwrap();
assert!(json.contains("\"lexical\":\"ready\""));
assert!(json.contains("\"semantic\":\"fast_tier_ready\""));
assert!(json.contains("\"last_search_refinement\":\"fast_tier_refined\""));
let parsed: ReadinessSnapshot = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, snap);
}
#[test]
fn snapshot_defaults_last_search_refinement_to_none() {
let snap = ReadinessSnapshot::new(
LexicalReadinessState::Ready,
SemanticReadinessState::HybridReady,
);
assert!(snap.last_search_refinement.is_none());
let json = serde_json::to_string(&snap).unwrap();
assert!(json.contains("\"last_search_refinement\":null"));
}
}