use rch_common::e2e::logging::{LogLevel, LogSource, TestLoggerBuilder};
use serde::{Deserialize, Serialize};
const DOCS_VALIDATION_SCHEMA_VERSION: &str = "1.0.0";
#[derive(Debug, Clone, Serialize, Deserialize)]
struct DocRequirement {
id: String,
section: String,
required_content: Vec<String>,
description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct DocValidationResult {
file_path: String,
exists: bool,
requirements_checked: usize,
requirements_met: usize,
failures: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct DocsValidationConfig {
schema_version: String,
requirements: Vec<DocRequirement>,
}
fn build_runbook_requirements() -> Vec<DocRequirement> {
vec![
DocRequirement {
id: "DOC-ARCH-001".into(),
section: "Architecture Overview".into(),
required_content: vec![
"Repo Convergence".into(),
"Storage Pressure".into(),
"Process Triage".into(),
"Circuit Breakers".into(),
"Fail-Open".into(),
],
description: "Runbook must describe all five reliability pillars".into(),
},
DocRequirement {
id: "DOC-POSTURE-001".into(),
section: "System Posture".into(),
required_content: vec![
"remote-ready".into(),
"degraded".into(),
"local-only".into(),
],
description: "Runbook must document all three system postures".into(),
},
DocRequirement {
id: "DOC-DIAG-001".into(),
section: "Quick Diagnosis".into(),
required_content: vec!["rch status".into(), "rch doctor".into()],
description: "Runbook must include diagnostic commands".into(),
},
DocRequirement {
id: "DOC-ERROR-001".into(),
section: "Error Taxonomy".into(),
required_content: vec![
"RCH-E".into(),
"Configuration".into(),
"Transfer".into(),
"Build execution".into(),
"Convergence".into(),
"Daemon".into(),
"Storage".into(),
],
description: "Runbook must document error code categories".into(),
},
DocRequirement {
id: "DOC-FAIL-001".into(),
section: "Worker Unreachable".into(),
required_content: vec![
"circuit_open".into(),
"worker_unreachable".into(),
"rch workers probe".into(),
"Risk:".into(),
],
description: "Worker unreachable failure mode documented with remediation and risk"
.into(),
},
DocRequirement {
id: "DOC-FAIL-002".into(),
section: "Disk Pressure".into(),
required_content: vec![
"pressure_critical".into(),
"cargo clean".into(),
"Risk:".into(),
],
description: "Disk pressure failure mode documented with remediation and risk".into(),
},
DocRequirement {
id: "DOC-FAIL-003".into(),
section: "Convergence Failed".into(),
required_content: vec![
"convergence_failed".into(),
"convergence_drifting".into(),
"repo-convergence repair".into(),
"--force".into(),
"Risk:".into(),
],
description: "Convergence failure mode documented with remediation and risk".into(),
},
DocRequirement {
id: "DOC-FAIL-004".into(),
section: "All Workers Down".into(),
required_content: vec!["local-only".into(), "fail-open".into()],
description: "Fail-open scenario documented".into(),
},
DocRequirement {
id: "DOC-FAIL-005".into(),
section: "Schema Mismatch".into(),
required_content: vec!["schema_mismatch".into(), "migration".into()],
description: "Schema mismatch failure mode documented".into(),
},
DocRequirement {
id: "DOC-FLAGS-001".into(),
section: "Feature Flags".into(),
required_content: vec![
"disabled".into(),
"dry_run".into(),
"canary".into(),
"enabled".into(),
],
description: "Feature flag states documented".into(),
},
DocRequirement {
id: "DOC-SLO-001".into(),
section: "SLO Guardrails".into(),
required_content: vec![
"Hook decision latency".into(),
"Convergence".into(),
"Fallback rate".into(),
],
description: "SLO budgets documented".into(),
},
DocRequirement {
id: "DOC-TRIAGE-001".into(),
section: "Incident Triage".into(),
required_content: vec!["posture".into(), "Escalation".into()],
description: "Incident triage flowchart documented".into(),
},
DocRequirement {
id: "DOC-DRYRUN-001".into(),
section: "Dry-Run".into(),
required_content: vec!["safe to run".into(), "--force".into(), "destructive".into()],
description: "Dry-run safety guidance documented".into(),
},
]
}
fn validate_runbook(content: &str, requirements: &[DocRequirement]) -> DocValidationResult {
let mut met = 0;
let mut failures = Vec::new();
for req in requirements {
let all_present = req
.required_content
.iter()
.all(|c| content.contains(c.as_str()));
if all_present {
met += 1;
} else {
let missing: Vec<_> = req
.required_content
.iter()
.filter(|c| !content.contains(c.as_str()))
.collect();
failures.push(format!(
"{}: missing {:?} in section '{}'",
req.id, missing, req.section
));
}
}
DocValidationResult {
file_path: "docs/runbooks/reliability-operations.md".into(),
exists: true,
requirements_checked: requirements.len(),
requirements_met: met,
failures,
}
}
#[test]
fn e2e_docs_runbook_exists() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("docs/runbooks/reliability-operations.md");
assert!(
path.exists(),
"reliability operations runbook must exist at {:?}",
path
);
}
#[test]
fn e2e_docs_runbook_not_empty() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("docs/runbooks/reliability-operations.md");
let content = std::fs::read_to_string(&path).unwrap();
assert!(
content.len() > 1000,
"runbook must have substantial content (got {} bytes)",
content.len()
);
}
#[test]
fn e2e_docs_runbook_passes_all_requirements() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("docs/runbooks/reliability-operations.md");
let content = std::fs::read_to_string(&path).unwrap();
let requirements = build_runbook_requirements();
let result = validate_runbook(&content, &requirements);
assert_eq!(
result.requirements_met,
result.requirements_checked,
"runbook validation failed:\n{}",
result.failures.join("\n")
);
}
#[test]
fn e2e_docs_runbook_architecture_section() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("docs/runbooks/reliability-operations.md");
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains("# Runbook: Reliability Operations"));
assert!(content.contains("## Architecture Overview"));
}
#[test]
fn e2e_docs_runbook_has_code_examples() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("docs/runbooks/reliability-operations.md");
let content = std::fs::read_to_string(&path).unwrap();
let code_block_count = content.matches("```bash").count();
assert!(
code_block_count >= 5,
"runbook must have at least 5 code examples (found {})",
code_block_count
);
}
#[test]
fn e2e_docs_runbook_mentions_risk_for_destructive_ops() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("docs/runbooks/reliability-operations.md");
let content = std::fs::read_to_string(&path).unwrap();
let risk_count = content.matches("**Risk:**").count();
assert!(
risk_count >= 3,
"runbook must document risks for at least 3 destructive operations (found {})",
risk_count
);
}
#[test]
fn e2e_docs_existing_runbooks_present() {
let docs_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("docs/runbooks");
let expected = [
"debugging-slow-builds.md",
"worker-recovery.md",
"reliability-operations.md",
];
for file in &expected {
assert!(
docs_dir.join(file).exists(),
"runbook '{}' must exist in docs/runbooks/",
file
);
}
}
#[test]
fn e2e_docs_error_taxonomy_matches_implementation() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("docs/runbooks/reliability-operations.md");
let content = std::fs::read_to_string(&path).unwrap();
let ranges = [
"RCH-E0xx", "RCH-E1xx", "RCH-E2xx", "RCH-E3xx", "RCH-E4xx", "RCH-E5xx",
];
for range in &ranges {
assert!(
content.contains(range),
"error range '{}' must be documented",
range
);
}
}
#[test]
fn e2e_docs_validation_schema_version() {
let config = DocsValidationConfig {
schema_version: DOCS_VALIDATION_SCHEMA_VERSION.into(),
requirements: build_runbook_requirements(),
};
assert_eq!(config.schema_version, DOCS_VALIDATION_SCHEMA_VERSION);
}
#[test]
fn e2e_docs_validation_serialization_roundtrip() {
let config = DocsValidationConfig {
schema_version: DOCS_VALIDATION_SCHEMA_VERSION.into(),
requirements: build_runbook_requirements(),
};
let json = serde_json::to_string_pretty(&config).unwrap();
let parsed: DocsValidationConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.requirements.len(), config.requirements.len());
}
#[test]
fn e2e_docs_logging_integration() {
let logger = TestLoggerBuilder::new("docs_validation").build();
let requirements = build_runbook_requirements();
logger.log(
LogLevel::Info,
LogSource::Custom("docs_validation".into()),
format!(
"Docs validation: {} requirements defined",
requirements.len(),
),
);
let entries = logger.entries();
assert_eq!(entries.len(), 1);
assert!(entries[0].message.contains("requirements"));
}
const SESSION_HISTORY_CONTRACT_PATH: &str = "docs/guides/session-history-remediation-validation.md";
const REQUIRED_SESSION_HISTORY_REMEDIATION_BEADS: &[&str] = &[
"bd-session-history-remediation-ocv9i",
"bd-session-history-remediation-ocv9i.1",
"bd-session-history-remediation-ocv9i.1.1",
"bd-session-history-remediation-ocv9i.1.2",
"bd-session-history-remediation-ocv9i.1.3",
"bd-session-history-remediation-ocv9i.2",
"bd-session-history-remediation-ocv9i.2.1",
"bd-session-history-remediation-ocv9i.2.2",
"bd-session-history-remediation-ocv9i.3",
"bd-session-history-remediation-ocv9i.3.1",
"bd-session-history-remediation-ocv9i.3.2",
"bd-session-history-remediation-ocv9i.3.3",
"bd-session-history-remediation-ocv9i.4",
"bd-session-history-remediation-ocv9i.4.1",
"bd-session-history-remediation-ocv9i.4.2",
"bd-session-history-remediation-ocv9i.4.3",
"bd-session-history-remediation-ocv9i.5",
"bd-session-history-remediation-ocv9i.5.1",
"bd-session-history-remediation-ocv9i.5.2",
"bd-session-history-remediation-ocv9i.5.3",
"bd-session-history-remediation-ocv9i.5.4",
"bd-session-history-remediation-ocv9i.6",
"bd-session-history-remediation-ocv9i.6.1",
"bd-session-history-remediation-ocv9i.6.2",
"bd-session-history-remediation-ocv9i.6.3",
"bd-session-history-remediation-ocv9i.6.4",
"bd-session-history-remediation-ocv9i.7",
"bd-session-history-remediation-ocv9i.7.1",
"bd-session-history-remediation-ocv9i.7.2",
"bd-session-history-remediation-ocv9i.7.3",
"bd-session-history-remediation-ocv9i.7.4",
"bd-session-history-remediation-ocv9i.8",
"bd-session-history-remediation-ocv9i.8.1",
"bd-session-history-remediation-ocv9i.8.2",
"bd-session-history-remediation-ocv9i.8.3",
"bd-session-history-remediation-ocv9i.9",
"bd-session-history-remediation-ocv9i.9.1",
"bd-session-history-remediation-ocv9i.9.2",
"bd-session-history-remediation-ocv9i.9.3",
"bd-session-history-remediation-ocv9i.10",
"bd-session-history-remediation-ocv9i.10.1",
"bd-session-history-remediation-ocv9i.10.2",
"bd-session-history-remediation-ocv9i.10.3",
"bd-session-history-remediation-ocv9i.10.4",
"bd-session-history-remediation-ocv9i.11",
"bd-session-history-remediation-ocv9i.11.1",
"bd-session-history-remediation-ocv9i.11.2",
"bd-session-history-remediation-ocv9i.11.3",
"bd-session-history-remediation-ocv9i.11.4",
"bd-session-history-remediation-ocv9i.12",
"bd-session-history-remediation-ocv9i.12.1",
"bd-session-history-remediation-ocv9i.12.2",
"bd-session-history-remediation-ocv9i.12.3",
"bd-session-history-remediation-ocv9i.12.4",
"bd-session-history-remediation-ocv9i.13",
"bd-session-history-remediation-ocv9i.13.1",
"bd-session-history-remediation-ocv9i.13.2",
"bd-session-history-remediation-ocv9i.13.3",
"bd-session-history-remediation-ocv9i.13.4",
"bd-session-history-remediation-ocv9i.13.5",
"bd-session-history-remediation-ocv9i.14",
"bd-session-history-remediation-ocv9i.14.1",
"bd-session-history-remediation-ocv9i.14.2",
"bd-session-history-remediation-ocv9i.14.3",
"bd-session-history-remediation-ocv9i.14.4",
"bd-session-history-remediation-ocv9i.14.5",
"bd-session-history-remediation-ocv9i.15",
"bd-session-history-remediation-ocv9i.16",
"bd-session-history-remediation-ocv9i.16.1",
"bd-session-history-remediation-ocv9i.16.2",
"bd-session-history-remediation-ocv9i.16.3",
"bd-session-history-remediation-ocv9i.16.4",
"bd-session-history-remediation-ocv9i.16.5",
"bd-session-history-remediation-ocv9i.16.6",
"bd-session-history-remediation-ocv9i.16.7",
"bd-session-history-remediation-ocv9i.16.8",
"bd-session-history-remediation-ocv9i.17",
"bd-session-history-remediation-ocv9i.17.1",
"bd-session-history-remediation-ocv9i.17.2",
"bd-session-history-remediation-ocv9i.17.3",
];
fn repo_root() -> std::path::PathBuf {
let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
manifest_dir.parent().unwrap_or(manifest_dir).to_path_buf()
}
fn read_session_history_contract() -> std::io::Result<String> {
let path = repo_root().join(SESSION_HISTORY_CONTRACT_PATH);
std::fs::read_to_string(path)
}
#[test]
fn e2e_docs_session_history_contract_exists() {
let path = repo_root().join(SESSION_HISTORY_CONTRACT_PATH);
assert!(
path.exists(),
"session-history validation contract must exist at {}",
path.display()
);
}
#[test]
fn e2e_docs_session_history_contract_covers_open_remediation_beads() -> std::io::Result<()> {
let contract = read_session_history_contract()?;
let missing: Vec<_> = REQUIRED_SESSION_HISTORY_REMEDIATION_BEADS
.iter()
.filter(|bead_id| !contract.contains(&format!("`{bead_id}`")))
.copied()
.collect();
assert!(
missing.is_empty(),
"session-history validation contract is missing P0/P1/P2 bead mappings:\n{}",
missing.join("\n")
);
Ok(())
}
#[test]
fn e2e_docs_session_history_contract_names_failure_classes_and_proof_fields() -> std::io::Result<()>
{
let contract = read_session_history_contract()?;
let required_snippets = [
"no admissible workers",
"critical pressure",
"insufficient slots",
"hard preflight",
"active project exclusion",
"missing runtime/toolchain/Rust target",
"OS/arch mismatch",
"telemetry stale/age unknown",
"circuit open",
"daemon socket refused",
"local fallback",
"proof refusal",
"rsync vanished file",
"artifact miss",
"queue ambiguity",
"disk full",
"wrong user/path worker binary",
"unit tests",
"integration tests",
"E2E script scenario",
"fault injection",
"golden JSON/schema checks",
"performance budget checks",
"close-reason evidence",
"`run_id`",
"`bead_id`",
"`scenario`",
"`event`",
"`status`",
"`reason_code`",
"`worker_id`",
"`command_fingerprint`",
"`duration_ms`",
"`detail`",
"`RCH_WORKER`",
"`RCH_VISIBILITY`",
"`RCH_QUEUE_WHEN_BUSY`",
"`RCH_FORCE_REMOTE`",
"`RCH_REQUIRE_REMOTE`",
"`RCH_NO_SELF_HEALING`",
"wait timeout controls",
"`--worker`",
"`--all`",
"`--timeout`",
"worker-scoped target-dir diagnostics",
"registry/package-cache availability",
"target availability reason codes",
];
let missing: Vec<_> = required_snippets
.iter()
.filter(|snippet| !contract.contains(**snippet))
.copied()
.collect();
assert!(
missing.is_empty(),
"session-history validation contract is missing required snippets:\n{}",
missing.join("\n")
);
Ok(())
}