use std::collections::BTreeMap;
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use schemars::generate::SchemaSettings;
use schemars::{JsonSchema, Schema};
use serde::{Deserialize, Serialize};
use crate::incident::IncidentReasonCode;
use crate::schema_versions::{SchemaComponent, current_version};
pub const MAX_DIAGNOSTIC_CHARS: usize = 512;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum BypassFailureClass {
Ssh,
WorkerBinary,
RuntimeToolchain,
DiskInodePressure,
StaleTelemetry,
PathSync,
ArtifactRetrieval,
CircuitBreaker,
OsArchMismatch,
}
impl BypassFailureClass {
pub const ALL: &'static [BypassFailureClass] = &[
Self::Ssh,
Self::WorkerBinary,
Self::RuntimeToolchain,
Self::DiskInodePressure,
Self::StaleTelemetry,
Self::PathSync,
Self::ArtifactRetrieval,
Self::CircuitBreaker,
Self::OsArchMismatch,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Ssh => "ssh",
Self::WorkerBinary => "worker_binary",
Self::RuntimeToolchain => "runtime_toolchain",
Self::DiskInodePressure => "disk_inode_pressure",
Self::StaleTelemetry => "stale_telemetry",
Self::PathSync => "path_sync",
Self::ArtifactRetrieval => "artifact_retrieval",
Self::CircuitBreaker => "circuit_breaker",
Self::OsArchMismatch => "os_arch_mismatch",
}
}
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Ssh => "SSH connection failure",
Self::WorkerBinary => "worker binary missing/wrong path or user",
Self::RuntimeToolchain => "missing runtime/toolchain/target",
Self::DiskInodePressure => "disk/inode pressure",
Self::StaleTelemetry => "telemetry stale/age unknown",
Self::PathSync => "path/source sync failure",
Self::ArtifactRetrieval => "artifact retrieval failure",
Self::CircuitBreaker => "circuit breaker open",
Self::OsArchMismatch => "OS/arch mismatch",
}
}
#[must_use]
pub const fn incident_reason_code(self) -> IncidentReasonCode {
match self {
Self::Ssh => IncidentReasonCode::HardPreflight,
Self::WorkerBinary => IncidentReasonCode::WrongUserPathWorkerBinary,
Self::RuntimeToolchain => IncidentReasonCode::MissingRuntimeToolchainTarget,
Self::DiskInodePressure => IncidentReasonCode::DiskFull,
Self::StaleTelemetry => IncidentReasonCode::TelemetryStale,
Self::PathSync => IncidentReasonCode::RsyncVanishedFile,
Self::ArtifactRetrieval => IncidentReasonCode::ArtifactMiss,
Self::CircuitBreaker => IncidentReasonCode::CircuitOpen,
Self::OsArchMismatch => IncidentReasonCode::OsArchMismatch,
}
}
#[must_use]
pub fn from_str_opt(s: &str) -> Option<Self> {
Self::ALL.iter().copied().find(|c| c.as_str() == s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum BypassState {
TemporaryBypass,
RecoveredPendingCanary,
}
impl BypassState {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::TemporaryBypass => "temporary bypass",
Self::RecoveredPendingCanary => "recovered, pending canary",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct BypassBackoff {
pub current_ms: u64,
pub attempts: u32,
pub max_ms: u64,
}
impl BypassBackoff {
pub const DEFAULT_INITIAL_MS: u64 = 30_000;
pub const DEFAULT_MAX_MS: u64 = 900_000;
#[must_use]
pub const fn initial() -> Self {
Self {
current_ms: Self::DEFAULT_INITIAL_MS,
attempts: 0,
max_ms: Self::DEFAULT_MAX_MS,
}
}
#[must_use]
pub const fn advanced(self) -> Self {
let doubled = self.current_ms.saturating_mul(2);
let current_ms = if doubled > self.max_ms {
self.max_ms
} else {
doubled
};
Self {
current_ms,
attempts: self.attempts.saturating_add(1),
max_ms: self.max_ms,
}
}
}
impl Default for BypassBackoff {
fn default() -> Self {
Self::initial()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct AutoRejoinCriteria {
pub required_consecutive_passes: u32,
pub canary_required: bool,
}
impl Default for AutoRejoinCriteria {
fn default() -> Self {
Self {
required_consecutive_passes: 2,
canary_required: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct BypassRecord {
pub schema_version: String,
pub worker_id: String,
pub host: String,
pub user: String,
pub failure_class: BypassFailureClass,
#[schemars(with = "String")]
pub reason_code: IncidentReasonCode,
pub state: BypassState,
pub first_failure_unix_ms: u64,
pub last_failure_unix_ms: u64,
pub next_probe_unix_ms: u64,
pub backoff: BypassBackoff,
pub consecutive_failures: u32,
pub consecutive_passes: u32,
pub last_diagnostic: String,
pub auto_rejoin: AutoRejoinCriteria,
pub local_fallback_allowed: bool,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub details: BTreeMap<String, String>,
}
impl BypassRecord {
#[must_use]
pub fn new(
worker_id: impl Into<String>,
host: impl Into<String>,
user: impl Into<String>,
failure_class: BypassFailureClass,
first_failure_unix_ms: u64,
) -> Self {
let backoff = BypassBackoff::initial();
Self {
schema_version: bypass_record_schema_version().to_string(),
worker_id: worker_id.into(),
host: host.into(),
user: user.into(),
failure_class,
reason_code: failure_class.incident_reason_code(),
state: BypassState::TemporaryBypass,
first_failure_unix_ms,
last_failure_unix_ms: first_failure_unix_ms,
next_probe_unix_ms: first_failure_unix_ms.saturating_add(backoff.current_ms),
backoff,
consecutive_failures: 1,
consecutive_passes: 0,
last_diagnostic: String::new(),
auto_rejoin: AutoRejoinCriteria::default(),
local_fallback_allowed: true,
details: BTreeMap::new(),
}
}
#[must_use]
pub fn with_diagnostic(mut self, diagnostic: impl Into<String>) -> Self {
self.last_diagnostic = truncate_diagnostic(diagnostic.into());
self
}
#[must_use]
pub fn with_local_fallback_allowed(mut self, allowed: bool) -> Self {
self.local_fallback_allowed = allowed;
self
}
#[must_use]
pub fn with_auto_rejoin(mut self, criteria: AutoRejoinCriteria) -> Self {
self.auto_rejoin = criteria;
self
}
#[must_use]
pub fn with_detail(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.details.insert(key.into(), value.into());
self
}
pub fn record_failure(&mut self, now_unix_ms: u64, diagnostic: impl Into<String>) {
self.last_failure_unix_ms = now_unix_ms;
self.consecutive_failures = self.consecutive_failures.saturating_add(1);
self.consecutive_passes = 0;
self.backoff = self.backoff.advanced();
self.next_probe_unix_ms = now_unix_ms.saturating_add(self.backoff.current_ms);
self.state = BypassState::TemporaryBypass;
let diagnostic = diagnostic.into();
if !diagnostic.is_empty() {
self.last_diagnostic = truncate_diagnostic(diagnostic);
}
}
pub fn record_probe_pass(&mut self, now_unix_ms: u64) -> bool {
self.consecutive_passes = self.consecutive_passes.saturating_add(1);
self.consecutive_failures = 0;
self.next_probe_unix_ms = now_unix_ms.saturating_add(self.backoff.current_ms);
let met = self.consecutive_passes >= self.auto_rejoin.required_consecutive_passes;
if met && self.auto_rejoin.canary_required {
self.state = BypassState::RecoveredPendingCanary;
}
met
}
#[must_use]
pub const fn probe_due(&self, now_unix_ms: u64) -> bool {
now_unix_ms >= self.next_probe_unix_ms
}
}
#[must_use]
pub fn truncate_diagnostic(mut s: String) -> String {
if s.chars().count() <= MAX_DIAGNOSTIC_CHARS {
return s;
}
let cut = s
.char_indices()
.nth(MAX_DIAGNOSTIC_CHARS)
.map_or(s.len(), |(i, _)| i);
s.truncate(cut);
s.push('…');
s
}
#[must_use]
pub fn bypass_record_schema_version() -> &'static str {
current_version(SchemaComponent::WorkerBypassRecord)
}
#[must_use]
pub fn bypass_record_schema() -> Schema {
SchemaSettings::draft07()
.into_generator()
.into_root_schema_for::<BypassRecord>()
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
struct BypassRecordFile {
schema_version: String,
records: Vec<BypassRecord>,
}
#[must_use]
pub fn default_bypass_record_path() -> PathBuf {
let base = std::env::var_os("RCH_STATE_HOME")
.map(PathBuf::from)
.filter(|p| !p.as_os_str().is_empty())
.or_else(|| {
std::env::var_os("XDG_STATE_HOME")
.map(|d| PathBuf::from(d).join("rch"))
.filter(|p| p.as_os_str().len() > "rch".len())
})
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".local/state/rch")))
.unwrap_or_else(|| PathBuf::from("/tmp/rch"));
base.join("bypass_records.json")
}
#[derive(Debug, Clone)]
pub struct BypassRecordStore {
path: PathBuf,
records: BTreeMap<String, BypassRecord>,
}
impl BypassRecordStore {
#[must_use]
pub fn with_path(path: impl Into<PathBuf>) -> Self {
Self {
path: path.into(),
records: BTreeMap::new(),
}
}
#[must_use]
pub fn load(path: impl Into<PathBuf>) -> Self {
let path = path.into();
let mut records = BTreeMap::new();
if let Ok(bytes) = fs::read(&path)
&& let Ok(file) = serde_json::from_slice::<BypassRecordFile>(&bytes)
{
for record in file.records {
records.insert(record.worker_id.clone(), record);
}
}
Self { path, records }
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
#[must_use]
pub fn get(&self, worker_id: &str) -> Option<&BypassRecord> {
self.records.get(worker_id)
}
#[must_use]
pub fn contains(&self, worker_id: &str) -> bool {
self.records.contains_key(worker_id)
}
#[must_use]
pub fn all(&self) -> Vec<&BypassRecord> {
self.records.values().collect()
}
#[must_use]
pub fn len(&self) -> usize {
self.records.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
pub fn upsert(&mut self, record: BypassRecord) -> std::io::Result<()> {
self.records.insert(record.worker_id.clone(), record);
self.persist()
}
pub fn remove(&mut self, worker_id: &str) -> std::io::Result<Option<BypassRecord>> {
let removed = self.records.remove(worker_id);
if removed.is_some() {
self.persist()?;
}
Ok(removed)
}
fn persist(&self) -> std::io::Result<()> {
if let Some(parent) = self.path.parent()
&& !parent.as_os_str().is_empty()
{
fs::create_dir_all(parent)?;
}
let doc = BypassRecordFile {
schema_version: bypass_record_schema_version().to_string(),
records: self.records.values().cloned().collect(),
};
let body = serde_json::to_vec_pretty(&doc)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let parent = self.path.parent().unwrap_or_else(|| Path::new("."));
let tmp = parent.join(format!(".bypass_records.{}.tmp", std::process::id()));
{
let mut tmp_file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&tmp)?;
tmp_file.write_all(&body)?;
tmp_file.flush()?;
}
match fs::rename(&tmp, &self.path) {
Ok(()) => Ok(()),
Err(e) => {
let _ = fs::remove_file(&tmp);
Err(e)
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AdminDisableRecord {
pub worker_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
pub disabled_unix_ms: u64,
}
#[derive(Debug, Serialize, Deserialize)]
struct AdminDisableFile {
schema_version: u32,
records: Vec<AdminDisableRecord>,
}
const ADMIN_DISABLE_SCHEMA_VERSION: u32 = 1;
#[must_use]
pub fn default_admin_disable_path() -> PathBuf {
default_bypass_record_path().with_file_name("admin_disables.json")
}
#[derive(Debug)]
pub struct AdminDisableStore {
path: PathBuf,
records: BTreeMap<String, AdminDisableRecord>,
}
impl AdminDisableStore {
#[must_use]
pub fn with_path(path: impl Into<PathBuf>) -> Self {
Self {
path: path.into(),
records: BTreeMap::new(),
}
}
#[must_use]
pub fn load(path: impl Into<PathBuf>) -> Self {
let path = path.into();
let mut records = BTreeMap::new();
if let Ok(bytes) = fs::read(&path)
&& let Ok(file) = serde_json::from_slice::<AdminDisableFile>(&bytes)
{
for record in file.records {
records.insert(record.worker_id.clone(), record);
}
}
Self { path, records }
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
#[must_use]
pub fn get(&self, worker_id: &str) -> Option<&AdminDisableRecord> {
self.records.get(worker_id)
}
#[must_use]
pub fn all(&self) -> Vec<&AdminDisableRecord> {
self.records.values().collect()
}
pub fn upsert(&mut self, record: AdminDisableRecord) -> std::io::Result<()> {
self.records.insert(record.worker_id.clone(), record);
self.persist()
}
pub fn remove(&mut self, worker_id: &str) -> std::io::Result<Option<AdminDisableRecord>> {
let removed = self.records.remove(worker_id);
if removed.is_some() {
self.persist()?;
}
Ok(removed)
}
fn persist(&self) -> std::io::Result<()> {
if let Some(parent) = self.path.parent()
&& !parent.as_os_str().is_empty()
{
fs::create_dir_all(parent)?;
}
let doc = AdminDisableFile {
schema_version: ADMIN_DISABLE_SCHEMA_VERSION,
records: self.records.values().cloned().collect(),
};
let body = serde_json::to_vec_pretty(&doc)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let parent = self.path.parent().unwrap_or_else(|| Path::new("."));
let tmp = parent.join(format!(".admin_disables.{}.tmp", std::process::id()));
{
let mut tmp_file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&tmp)?;
tmp_file.write_all(&body)?;
tmp_file.flush()?;
}
match fs::rename(&tmp, &self.path) {
Ok(()) => Ok(()),
Err(e) => {
let _ = fs::remove_file(&tmp);
Err(e)
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DisabledWorkerSnapshot {
pub worker_id: String,
pub host: String,
pub user: String,
pub disabled_reason: Option<String>,
pub disabled_at_unix_ms: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DisabledMigration {
Migrate(Box<BypassRecord>),
KeepDisabled {
reason: String,
},
}
#[must_use]
pub fn classify_disable_reason(reason: &str) -> Option<BypassFailureClass> {
let r = reason.to_ascii_lowercase();
if r.trim().is_empty() {
return None;
}
const TABLE: &[(&[&str], BypassFailureClass)] = &[
(
&["no space", "enospc", "disk full", "inode", "disk pressure"],
BypassFailureClass::DiskInodePressure,
),
(
&["toolchain", "rustup", "nightly", "missing target", "rustc"],
BypassFailureClass::RuntimeToolchain,
),
(
&[
"rch-wkr",
"worker binary",
"wrong user",
"wrong path",
"no such file",
],
BypassFailureClass::WorkerBinary,
),
(&["circuit"], BypassFailureClass::CircuitBreaker),
(
&["telemetry", "stale", "heartbeat"],
BypassFailureClass::StaleTelemetry,
),
(
&["rsync", "vanished", "path sync", "source sync"],
BypassFailureClass::PathSync,
),
(&["artifact"], BypassFailureClass::ArtifactRetrieval),
(
&["os/arch", "architecture", "arch mismatch", "triple"],
BypassFailureClass::OsArchMismatch,
),
(
&[
"ssh",
"connection refused",
"connection reset",
"connection failed",
"connection timed out",
"unreachable",
"timed out",
"auth",
"permission denied",
],
BypassFailureClass::Ssh,
),
];
for (needles, class) in TABLE {
if needles.iter().any(|n| r.contains(n)) {
return Some(*class);
}
}
None
}
#[must_use]
pub fn migrate_disabled_worker(
snapshot: &DisabledWorkerSnapshot,
now_unix_ms: u64,
) -> DisabledMigration {
let Some(reason) = snapshot.disabled_reason.as_deref() else {
return DisabledMigration::KeepDisabled {
reason: "no disable reason recorded — treat as deliberate operator disable".to_string(),
};
};
let Some(class) = classify_disable_reason(reason) else {
return DisabledMigration::KeepDisabled {
reason: format!("disable reason does not look transient: {reason:?}"),
};
};
let first_failure = snapshot.disabled_at_unix_ms.unwrap_or(now_unix_ms);
let mut record = BypassRecord::new(
&snapshot.worker_id,
&snapshot.host,
&snapshot.user,
class,
first_failure,
)
.with_diagnostic(format!("migrated from admin-disabled: {reason}"))
.with_detail("migrated_from", "admin_disabled");
record.next_probe_unix_ms = now_unix_ms;
DisabledMigration::Migrate(Box::new(record))
}
#[cfg(test)]
mod tests {
use super::*;
const TS: u64 = 1_700_000_000_000;
fn record(class: BypassFailureClass) -> BypassRecord {
BypassRecord::new("css", "203.0.113.20", "ubuntu", class, TS)
}
#[test]
fn failure_class_wire_form_is_stable_snake_case() {
assert_eq!(BypassFailureClass::Ssh.as_str(), "ssh");
assert_eq!(
BypassFailureClass::DiskInodePressure.as_str(),
"disk_inode_pressure"
);
for c in BypassFailureClass::ALL {
let json = serde_json::to_string(c).unwrap();
assert_eq!(json, format!("\"{}\"", c.as_str()));
assert_eq!(BypassFailureClass::from_str_opt(c.as_str()), Some(*c));
}
assert_eq!(BypassFailureClass::from_str_opt("nonsense"), None);
}
#[test]
fn failure_class_maps_to_an_incident_reason_for_every_variant() {
for c in BypassFailureClass::ALL {
let code = c.incident_reason_code();
assert_eq!(
IncidentReasonCode::from_code_str(code.code()),
Some(code),
"reason code for {c:?} not in registry"
);
}
assert_eq!(
BypassFailureClass::Ssh.incident_reason_code(),
IncidentReasonCode::HardPreflight
);
assert_eq!(
BypassFailureClass::CircuitBreaker.incident_reason_code(),
IncidentReasonCode::CircuitOpen
);
assert_eq!(
BypassFailureClass::DiskInodePressure.incident_reason_code(),
IncidentReasonCode::DiskFull
);
}
#[test]
fn new_record_is_consistent_and_schema_stamped() {
let r = record(BypassFailureClass::Ssh);
assert_eq!(r.schema_version, bypass_record_schema_version());
assert_eq!(r.state, BypassState::TemporaryBypass);
assert_eq!(r.reason_code, IncidentReasonCode::HardPreflight);
assert_eq!(r.consecutive_failures, 1);
assert_eq!(r.consecutive_passes, 0);
assert_eq!(r.first_failure_unix_ms, TS);
assert_eq!(r.last_failure_unix_ms, TS);
assert_eq!(r.next_probe_unix_ms, TS + BypassBackoff::DEFAULT_INITIAL_MS);
assert!(r.local_fallback_allowed);
}
#[test]
fn record_serializes_required_fields_and_roundtrips() {
let r = record(BypassFailureClass::CircuitBreaker)
.with_diagnostic("circuit open after 5 failures")
.with_local_fallback_allowed(false)
.with_detail("attempts", "5");
let v = serde_json::to_value(&r).unwrap();
for key in [
"worker_id",
"host",
"user",
"failure_class",
"reason_code",
"state",
"first_failure_unix_ms",
"last_failure_unix_ms",
"next_probe_unix_ms",
"backoff",
"consecutive_failures",
"consecutive_passes",
"last_diagnostic",
"auto_rejoin",
"local_fallback_allowed",
] {
assert!(v.get(key).is_some(), "missing record field {key}");
}
assert_eq!(v["failure_class"], "circuit_breaker");
assert_eq!(v["reason_code"], "RCH-I009");
assert_eq!(v["local_fallback_allowed"], false);
let back: BypassRecord = serde_json::from_value(v).unwrap();
assert_eq!(back, r);
}
#[test]
fn record_never_carries_raw_command_or_secret_fields() {
let v = serde_json::to_value(record(BypassFailureClass::Ssh)).unwrap();
let obj = v.as_object().unwrap();
for forbidden in ["command", "raw_command", "secret", "password", "token"] {
assert!(
!obj.contains_key(forbidden),
"must not expose `{forbidden}`"
);
}
}
#[test]
fn diagnostic_is_truncated() {
let long = "x".repeat(MAX_DIAGNOSTIC_CHARS + 50);
let r = record(BypassFailureClass::Ssh).with_diagnostic(long);
assert!(r.last_diagnostic.chars().count() <= MAX_DIAGNOSTIC_CHARS + 1);
assert!(r.last_diagnostic.ends_with('…'));
let short = record(BypassFailureClass::Ssh).with_diagnostic("brief");
assert_eq!(short.last_diagnostic, "brief");
}
#[test]
fn backoff_doubles_up_to_ceiling() {
let mut b = BypassBackoff::initial();
assert_eq!(b.current_ms, BypassBackoff::DEFAULT_INITIAL_MS);
let mut last = b.current_ms;
for _ in 0..20 {
b = b.advanced();
assert!(b.current_ms >= last);
assert!(b.current_ms <= BypassBackoff::DEFAULT_MAX_MS);
last = b.current_ms;
}
assert_eq!(b.current_ms, BypassBackoff::DEFAULT_MAX_MS);
assert_eq!(b.attempts, 20);
}
#[test]
fn record_failure_advances_backoff_and_resets_passes() {
let mut r = record(BypassFailureClass::Ssh);
r.consecutive_passes = 1;
let before = r.backoff.current_ms;
r.record_failure(TS + 1000, "ssh timed out");
assert_eq!(r.consecutive_failures, 2);
assert_eq!(r.consecutive_passes, 0);
assert!(r.backoff.current_ms > before);
assert_eq!(r.next_probe_unix_ms, TS + 1000 + r.backoff.current_ms);
assert_eq!(r.last_diagnostic, "ssh timed out");
assert_eq!(r.state, BypassState::TemporaryBypass);
}
#[test]
fn probe_pass_advances_to_canary_when_criteria_met() {
let mut r = record(BypassFailureClass::Ssh);
assert!(!r.record_probe_pass(TS + 1));
assert_eq!(r.state, BypassState::TemporaryBypass);
assert!(r.record_probe_pass(TS + 2));
assert_eq!(r.state, BypassState::RecoveredPendingCanary);
assert_eq!(r.consecutive_passes, 2);
assert_eq!(r.consecutive_failures, 0);
}
#[test]
fn probe_due_respects_schedule() {
let r = record(BypassFailureClass::Ssh);
assert!(!r.probe_due(r.next_probe_unix_ms - 1));
assert!(r.probe_due(r.next_probe_unix_ms));
assert!(r.probe_due(r.next_probe_unix_ms + 1));
}
#[test]
fn schema_export_names_the_record_and_fields() {
let text = serde_json::to_string(&bypass_record_schema()).unwrap();
assert!(text.contains("BypassRecord"));
for f in [
"failure_class",
"reason_code",
"next_probe_unix_ms",
"auto_rejoin",
"local_fallback_allowed",
] {
assert!(text.contains(f), "schema omits {f}");
}
}
#[test]
fn store_upsert_get_remove_and_persist() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("bypass_records.json");
let mut store = BypassRecordStore::load(&path);
assert!(store.is_empty());
store.upsert(record(BypassFailureClass::Ssh)).unwrap();
store
.upsert(BypassRecord::new(
"bil",
"203.0.113.21",
"ubuntu",
BypassFailureClass::DiskInodePressure,
TS,
))
.unwrap();
assert_eq!(store.len(), 2);
assert!(store.contains("css"));
assert_eq!(
store.get("css").unwrap().failure_class,
BypassFailureClass::Ssh
);
let ids: Vec<&str> = store.all().iter().map(|r| r.worker_id.as_str()).collect();
assert_eq!(ids, vec!["bil", "css"]);
let removed = store.remove("css").unwrap();
assert_eq!(removed.unwrap().worker_id, "css");
assert!(!store.contains("css"));
assert!(store.remove("nope").unwrap().is_none());
}
#[test]
fn store_survives_reload_across_instances() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("bypass_records.json");
{
let mut store = BypassRecordStore::load(&path);
store
.upsert(record(BypassFailureClass::CircuitBreaker).with_diagnostic("circuit churn"))
.unwrap();
}
let reopened = BypassRecordStore::load(&path);
assert_eq!(reopened.len(), 1);
let r = reopened.get("css").unwrap();
assert_eq!(r.failure_class, BypassFailureClass::CircuitBreaker);
assert_eq!(r.last_diagnostic, "circuit churn");
}
#[test]
fn store_tolerates_corrupt_or_missing_file() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("nope.json");
assert!(BypassRecordStore::load(&missing).is_empty());
let corrupt = dir.path().join("corrupt.json");
fs::write(&corrupt, b"{ not valid json").unwrap();
assert!(BypassRecordStore::load(&corrupt).is_empty());
}
#[test]
fn default_path_ends_with_bypass_records_json() {
assert!(default_bypass_record_path().ends_with("bypass_records.json"));
}
#[test]
fn classify_disable_reason_recognizes_transient_classes() {
assert_eq!(
classify_disable_reason("ssh: connection refused"),
Some(BypassFailureClass::Ssh)
);
assert_eq!(
classify_disable_reason("No space left on device"),
Some(BypassFailureClass::DiskInodePressure)
);
assert_eq!(
classify_disable_reason("circuit breaker tripped"),
Some(BypassFailureClass::CircuitBreaker)
);
assert_eq!(
classify_disable_reason("missing rustup toolchain nightly"),
Some(BypassFailureClass::RuntimeToolchain)
);
assert_eq!(classify_disable_reason("decommissioned by ops"), None);
assert_eq!(classify_disable_reason(""), None);
assert_eq!(classify_disable_reason(" "), None);
}
#[test]
fn migrate_transient_disable_produces_bypass_record() {
let snap = DisabledWorkerSnapshot {
worker_id: "css".to_string(),
host: "203.0.113.20".to_string(),
user: "ubuntu".to_string(),
disabled_reason: Some("ssh connection timed out during probe".to_string()),
disabled_at_unix_ms: Some(TS),
};
match migrate_disabled_worker(&snap, TS + 100_000) {
DisabledMigration::Migrate(record) => {
assert_eq!(record.worker_id, "css");
assert_eq!(record.failure_class, BypassFailureClass::Ssh);
assert_eq!(record.first_failure_unix_ms, TS);
assert_eq!(record.next_probe_unix_ms, TS + 100_000);
assert_eq!(
record.details.get("migrated_from").map(String::as_str),
Some("admin_disabled")
);
assert!(
record
.last_diagnostic
.contains("migrated from admin-disabled")
);
}
other => panic!("expected migration, got {other:?}"),
}
}
#[test]
fn migrate_deliberate_or_unknown_disable_keeps_disabled() {
let no_reason = DisabledWorkerSnapshot {
worker_id: "css".to_string(),
host: "h".to_string(),
user: "u".to_string(),
disabled_reason: None,
disabled_at_unix_ms: Some(TS),
};
assert!(matches!(
migrate_disabled_worker(&no_reason, TS),
DisabledMigration::KeepDisabled { .. }
));
let deliberate = DisabledWorkerSnapshot {
disabled_reason: Some("retired hardware".to_string()),
..no_reason
};
assert!(matches!(
migrate_disabled_worker(&deliberate, TS),
DisabledMigration::KeepDisabled { .. }
));
}
#[test]
fn admin_disable_store_roundtrips_through_disk() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("admin_disables.json");
let mut store = AdminDisableStore::with_path(&path);
store
.upsert(AdminDisableRecord {
worker_id: "ovh-b".to_string(),
reason: Some("cpu-capability-fault (SIGILL)".to_string()),
disabled_unix_ms: 1_700_000_000_000,
})
.unwrap();
let reloaded = AdminDisableStore::load(&path);
let record = reloaded.get("ovh-b").expect("record survives reload");
assert_eq!(
record.reason.as_deref(),
Some("cpu-capability-fault (SIGILL)")
);
assert_eq!(record.disabled_unix_ms, 1_700_000_000_000);
assert_eq!(reloaded.all().len(), 1);
}
#[test]
fn admin_disable_store_remove_persists_and_reports() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("admin_disables.json");
let mut store = AdminDisableStore::with_path(&path);
store
.upsert(AdminDisableRecord {
worker_id: "w1".to_string(),
reason: None,
disabled_unix_ms: 1,
})
.unwrap();
assert!(store.remove("w1").unwrap().is_some());
assert!(store.remove("w1").unwrap().is_none());
assert!(AdminDisableStore::load(&path).get("w1").is_none());
}
#[test]
fn admin_disable_store_tolerates_missing_and_corrupt_files() {
let dir = tempfile::tempdir().unwrap();
let missing = AdminDisableStore::load(dir.path().join("nope.json"));
assert!(missing.all().is_empty());
let corrupt_path = dir.path().join("corrupt.json");
std::fs::write(&corrupt_path, b"{ not json").unwrap();
let corrupt = AdminDisableStore::load(&corrupt_path);
assert!(corrupt.all().is_empty());
}
#[test]
fn admin_disable_default_path_is_beside_bypass_records() {
let path = default_admin_disable_path();
assert!(path.ends_with("admin_disables.json"));
assert_eq!(path.parent(), default_bypass_record_path().parent());
}
}