diff --git a/robot-repo-automaton/src/hypatia.rs b/robot-repo-automaton/src/hypatia.rs index e0ab4ba8..adf51c69 100644 --- a/robot-repo-automaton/src/hypatia.rs +++ b/robot-repo-automaton/src/hypatia.rs @@ -619,13 +619,15 @@ fn recipe_to_rule(recipe: &serde_json::Value) -> Option { // Build pattern from recipe detection info let pattern = if let Some(glob) = recipe.get("file_glob").and_then(|v| v.as_str()) { RulePattern::FileGlob { glob: glob.to_string() } - } else if let Some(regex) = recipe.get("pattern").and_then(|v| v.as_str()) { + } else { + let regex = recipe.get("pattern").and_then(|v| v.as_str())?; RulePattern::ContentRegex { regex: regex.to_string(), - file_glob: recipe.get("applies_to").and_then(|v| v.as_str()).map(|s| s.to_string()), + file_glob: recipe + .get("applies_to") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()), } - } else { - return None; }; // Build fix from recipe diff --git a/shared-context/benches/fleet_benchmarks.rs b/shared-context/benches/fleet_benchmarks.rs index 82ffa20a..f70d710d 100644 --- a/shared-context/benches/fleet_benchmarks.rs +++ b/shared-context/benches/fleet_benchmarks.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MPL-2.0 //! Performance benchmarks for gitbot-fleet operations -use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; use gitbot_shared_context::{BotId, Context, Finding, Severity}; use std::hint::black_box; use std::path::PathBuf; @@ -102,7 +102,12 @@ fn bench_finding_queries(c: &mut Criterion) { 2 => Severity::Info, _ => Severity::Suggestion, }; - ctx.add_finding(Finding::new(bot, &format!("TEST-{:03}", i), severity, "Test")); + ctx.add_finding(Finding::new( + bot, + &format!("TEST-{:03}", i), + severity, + "Test", + )); } group.bench_function("query_by_bot", |b| { @@ -191,7 +196,11 @@ fn bench_health_check(c: &mut Criterion) { ctx.add_finding(Finding::new( BotId::Rhodibot, &format!("TEST-{:03}", i), - if i < 5 { Severity::Error } else { Severity::Warning }, + if i < 5 { + Severity::Error + } else { + Severity::Warning + }, "Test finding", )); } @@ -227,7 +236,11 @@ fn bench_report_generation(c: &mut Criterion) { ctx.add_finding(Finding::new( BotId::Rhodibot, &format!("TEST-{:03}", i), - if i % 4 == 0 { Severity::Error } else { Severity::Warning }, + if i % 4 == 0 { + Severity::Error + } else { + Severity::Warning + }, "Test finding with some detail", )); } diff --git a/shared-context/src/bot.rs b/shared-context/src/bot.rs index d8df54e2..6ba007cc 100644 --- a/shared-context/src/bot.rs +++ b/shared-context/src/bot.rs @@ -64,8 +64,14 @@ impl BotId { /// Get the tier this bot belongs to pub fn tier(&self) -> Tier { match self { - BotId::Rhodibot | BotId::Echidnabot | BotId::Sustainabot | BotId::Oikosbot | BotId::Panicbot => Tier::Verifier, - BotId::Glambot | BotId::Seambot | BotId::Finishbot | BotId::Accessibilitybot => Tier::Finisher, + BotId::Rhodibot + | BotId::Echidnabot + | BotId::Sustainabot + | BotId::Oikosbot + | BotId::Panicbot => Tier::Verifier, + BotId::Glambot | BotId::Seambot | BotId::Finishbot | BotId::Accessibilitybot => { + Tier::Finisher + } BotId::Cipherbot => Tier::Specialist, BotId::RobotRepoAutomaton => Tier::Executor, BotId::Hypatia => Tier::Engine, @@ -134,11 +140,11 @@ impl Tier { /// Get execution order (lower = earlier) pub fn execution_order(&self) -> u8 { match self { - Tier::Engine => 0, // Engine coordinates, runs first + Tier::Engine => 0, // Engine coordinates, runs first Tier::Verifier => 1, Tier::Finisher => 2, - Tier::Specialist => 3, // Specialist runs after verifiers/finishers - Tier::Executor => 4, // Executor runs after all analysis + Tier::Specialist => 3, // Specialist runs after verifiers/finishers + Tier::Executor => 4, // Executor runs after all analysis Tier::Custom => 5, } } diff --git a/shared-context/src/context.rs b/shared-context/src/context.rs index 0a3fc85b..7448fa21 100644 --- a/shared-context/src/context.rs +++ b/shared-context/src/context.rs @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MPL-2.0 //! Shared context for coordinating bot executions +use crate::Result; use crate::bot::{BotExecution, BotId, BotStatus, Tier}; use crate::finding::{Finding, FindingSet}; -use crate::Result; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -191,10 +191,7 @@ impl Context { } let info = BotInfo::standard(*bot); - let deps_satisfied = info - .depends_on - .iter() - .all(|dep| self.bot_completed(*dep)); + let deps_satisfied = info.depends_on.iter().all(|dep| self.bot_completed(*dep)); if deps_satisfied { ready.push(*bot); diff --git a/shared-context/src/exclusion_registry.rs b/shared-context/src/exclusion_registry.rs index 496eb839..c3f449fd 100644 --- a/shared-context/src/exclusion_registry.rs +++ b/shared-context/src/exclusion_registry.rs @@ -172,10 +172,7 @@ impl FromStr for ExclusionRegistry { .into_iter() .map(|v| { let p = Pattern::new(&v.pattern).map_err(|e| { - ExclusionError::Parse(format!( - "invalid vendored pattern {:?}: {e}", - v.pattern - )) + ExclusionError::Parse(format!("invalid vendored pattern {:?}: {e}", v.pattern)) })?; Ok(CompiledPattern { pattern: p, @@ -262,8 +259,12 @@ impl ExclusionRegistry { /// file wins. Covers the common layouts on this machine. fn conventional_paths() -> Vec { vec![ - PathBuf::from("/var/mnt/eclipse/repos/developer-ecosystem/standards/.machine_readable/bot_exclusion_registry.a2ml"), - PathBuf::from("/var/mnt/eclipse/repos/standards/.machine_readable/bot_exclusion_registry.a2ml"), + PathBuf::from( + "/var/mnt/eclipse/repos/developer-ecosystem/standards/.machine_readable/bot_exclusion_registry.a2ml", + ), + PathBuf::from( + "/var/mnt/eclipse/repos/standards/.machine_readable/bot_exclusion_registry.a2ml", + ), PathBuf::from("./standards/.machine_readable/bot_exclusion_registry.a2ml"), PathBuf::from("../standards/.machine_readable/bot_exclusion_registry.a2ml"), PathBuf::from("../../standards/.machine_readable/bot_exclusion_registry.a2ml"), @@ -300,10 +301,7 @@ impl ExclusionRegistry { if matches!(k.as_str(), "off" | "disabled" | "0" | "false" | "halt") { return Decision::Deny { axis: DenyAxis::KillSwitch, - reason: format!( - "HYPATIA_AUTOMATION={} — global kill switch engaged", - kill - ), + reason: format!("HYPATIA_AUTOMATION={} — global kill switch engaged", kill), }; } } @@ -664,7 +662,7 @@ mod real_registry_smoke { #[test] fn real_registry_file_parses_and_has_expected_axes() { let path = std::path::Path::new( - "/var/mnt/eclipse/repos/developer-ecosystem/standards/.machine_readable/bot_exclusion_registry.a2ml" + "/var/mnt/eclipse/repos/developer-ecosystem/standards/.machine_readable/bot_exclusion_registry.a2ml", ); if !path.exists() { eprintln!("skipping: real registry not at {:?}", path); @@ -672,17 +670,28 @@ mod real_registry_smoke { } let r = ExclusionRegistry::load(path).expect("parse real registry"); // Smoke: at least one of each axis. - assert!(!r.external_repos.is_empty(), "external_repos axis populated"); - assert!(!r.vendored_patterns.is_empty(), "vendored_patterns axis populated"); - assert!(!r.remote_origin_patterns.is_empty(), "remote_origin_patterns axis populated"); + assert!( + !r.external_repos.is_empty(), + "external_repos axis populated" + ); + assert!( + !r.vendored_patterns.is_empty(), + "vendored_patterns axis populated" + ); + assert!( + !r.remote_origin_patterns.is_empty(), + "remote_origin_patterns axis populated" + ); } #[test] fn real_registry_blocks_joshuajewell() { let path = std::path::Path::new( - "/var/mnt/eclipse/repos/developer-ecosystem/standards/.machine_readable/bot_exclusion_registry.a2ml" + "/var/mnt/eclipse/repos/developer-ecosystem/standards/.machine_readable/bot_exclusion_registry.a2ml", ); - if !path.exists() { return; } + if !path.exists() { + return; + } let r = ExclusionRegistry::load(path).unwrap(); let d = r.check(&ActionContext { repo_full_name: "JoshuaJewell/IDApTIK", @@ -690,15 +699,20 @@ mod real_registry_smoke { remote_origin: None, action: Action::CreatePr, }); - assert!(!d.is_allow(), "real registry must deny JoshuaJewell/IDApTIK writes"); + assert!( + !d.is_allow(), + "real registry must deny JoshuaJewell/IDApTIK writes" + ); } #[test] fn real_registry_blocks_rust_lang_origin() { let path = std::path::Path::new( - "/var/mnt/eclipse/repos/developer-ecosystem/standards/.machine_readable/bot_exclusion_registry.a2ml" + "/var/mnt/eclipse/repos/developer-ecosystem/standards/.machine_readable/bot_exclusion_registry.a2ml", ); - if !path.exists() { return; } + if !path.exists() { + return; + } let r = ExclusionRegistry::load(path).unwrap(); let d = r.check(&ActionContext { repo_full_name: "somewhere-locally/rust-clone", @@ -706,6 +720,9 @@ mod real_registry_smoke { remote_origin: Some("git@github.com:rust-lang/rust.git"), action: Action::CreatePr, }); - assert!(!d.is_allow(), "real registry must deny rust-lang origin writes"); + assert!( + !d.is_allow(), + "real registry must deny rust-lang origin writes" + ); } } diff --git a/shared-context/src/finding.rs b/shared-context/src/finding.rs index 5967f2c9..bbfb7301 100644 --- a/shared-context/src/finding.rs +++ b/shared-context/src/finding.rs @@ -196,12 +196,10 @@ impl Finding { /// Get location string for display pub fn location_string(&self) -> Option { - self.file.as_ref().map(|f| { - match (self.line, self.column) { - (Some(l), Some(c)) => format!("{}:{}:{}", f.display(), l, c), - (Some(l), None) => format!("{}:{}", f.display(), l), - _ => f.display().to_string(), - } + self.file.as_ref().map(|f| match (self.line, self.column) { + (Some(l), Some(c)) => format!("{}:{}:{}", f.display(), l, c), + (Some(l), None) => format!("{}:{}", f.display(), l), + _ => f.display().to_string(), }) } } @@ -231,7 +229,10 @@ impl FindingSet { /// Get findings by source bot pub fn by_source(&self, source: BotId) -> Vec<&Finding> { - self.findings.iter().filter(|f| f.source == source).collect() + self.findings + .iter() + .filter(|f| f.source == source) + .collect() } /// Get findings by severity @@ -272,7 +273,10 @@ impl FindingSet { /// Get fixable findings pub fn fixable(&self) -> Vec<&Finding> { - self.findings.iter().filter(|f| f.fixable && !f.fixed).collect() + self.findings + .iter() + .filter(|f| f.fixable && !f.fixed) + .collect() } /// Get unfixed findings diff --git a/shared-context/src/health.rs b/shared-context/src/health.rs index 52866c72..12d1cd99 100644 --- a/shared-context/src/health.rs +++ b/shared-context/src/health.rs @@ -172,13 +172,12 @@ impl Context { let anomalies = self.detect_bot_anomalies(*bot_id, execution); let bot_status = determine_bot_health_status(execution, &anomalies); - let duration_ms = if let (Some(start), Some(end)) = - (execution.started_at, execution.completed_at) - { - Some((end - start).num_milliseconds() as u64) - } else { - None - }; + let duration_ms = + if let (Some(start), Some(end)) = (execution.started_at, execution.completed_at) { + Some((end - start).num_milliseconds() as u64) + } else { + None + }; health.insert( format!("{:?}", bot_id), @@ -202,12 +201,7 @@ impl Context { /// Check health of tiers fn check_tier_health(&self) -> HashMap { let mut tier_health = HashMap::new(); - let tiers = [ - Tier::Engine, - Tier::Verifier, - Tier::Finisher, - Tier::Executor, - ]; + let tiers = [Tier::Engine, Tier::Verifier, Tier::Finisher, Tier::Executor]; for tier in tiers { let tier_bots: Vec<_> = self @@ -473,9 +467,7 @@ impl Context { /// Determine overall health status from score and alerts fn determine_overall_status(score: f64, alerts: &[HealthAlert]) -> HealthStatus { - let has_critical = alerts - .iter() - .any(|a| a.severity == AlertSeverity::Critical); + let has_critical = alerts.iter().any(|a| a.severity == AlertSeverity::Critical); let has_errors = alerts.iter().any(|a| a.severity == AlertSeverity::Error); if has_critical || score < 30.0 { @@ -525,9 +517,7 @@ impl FleetHealth { println!("╠════════════════════════════════════════════════════════════════╣"); println!( "║ Status: {} {:?} (Score: {:.1}/100) ║", - status_symbol, - self.status, - self.health_score + status_symbol, self.status, self.health_score ); println!( "║ Checked: {} ║", @@ -553,7 +543,10 @@ impl FleetHealth { // Alerts if !self.alerts.is_empty() { - println!("║ Active Alerts: {} ║", self.alerts.len()); + println!( + "║ Active Alerts: {} ║", + self.alerts.len() + ); for alert in self.alerts.iter().take(5) { let severity_str = match alert.severity { AlertSeverity::Info => "ℹ️ ", @@ -566,10 +559,16 @@ impl FleetHealth { } else { alert.message.clone() }; - println!("║ {} {} ║", severity_str, msg); + println!( + "║ {} {} ║", + severity_str, msg + ); } if self.alerts.len() > 5 { - println!("║ ... and {} more alerts ║", self.alerts.len() - 5); + println!( + "║ ... and {} more alerts ║", + self.alerts.len() - 5 + ); } println!("╠════════════════════════════════════════════════════════════════╣"); } @@ -652,9 +651,11 @@ mod tests { let bot_health = health.bot_health.get("Rhodibot").unwrap(); assert!(!bot_health.anomalies.is_empty()); - assert!(bot_health - .anomalies - .iter() - .any(|a| a.contains("High error rate"))); + assert!( + bot_health + .anomalies + .iter() + .any(|a| a.contains("High error rate")) + ); } } diff --git a/shared-context/src/lib.rs b/shared-context/src/lib.rs index d3fbea6f..df1e8bfc 100644 --- a/shared-context/src/lib.rs +++ b/shared-context/src/lib.rs @@ -64,13 +64,13 @@ pub use panel::{ IsolationTier, PanelContext, PanelFileExpectation, PanelId, PanelManifest, PanelPhase, PanelValidation, WiringCheck, WiringStatus, }; -pub use reporting::{FleetReport, ReportFormat}; -pub use state::{RepoState, SessionState}; -pub use storage::ContextStorage; pub use panel_checker::{ PccObligation, PccPanelResult, PccSummary, find_pcc_binary, pcc_result_to_manifest, pcc_results_to_findings, run_pcc_verify, }; +pub use reporting::{FleetReport, ReportFormat}; +pub use state::{RepoState, SessionState}; +pub use storage::ContextStorage; pub use triangle::{ConfidenceThresholds, DispatchStrategy, TriangleTier}; use thiserror::Error; diff --git a/shared-context/src/panel.rs b/shared-context/src/panel.rs index 022ece11..5b4795e8 100644 --- a/shared-context/src/panel.rs +++ b/shared-context/src/panel.rs @@ -39,7 +39,12 @@ impl PanelId { if name.is_empty() { return Err(PanelError::InvalidName("Panel name cannot be empty".into())); } - if !name.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) { + if !name + .chars() + .next() + .map(|c| c.is_uppercase()) + .unwrap_or(false) + { return Err(PanelError::InvalidName( "Panel name must start with uppercase (PascalCase)".into(), )); @@ -95,8 +100,21 @@ impl std::fmt::Display for PanelId { /// Names that cannot be used for panels — they collide with PanLL internals. pub const RESERVED_NAMES: &[&str] = &[ - "Model", "View", "Update", "Msg", "App", "Main", "Tea", "Panel", "Pane", - "PaneL", "PaneN", "PaneW", "PanelSwitcher", "Storage", "Connection", + "Model", + "View", + "Update", + "Msg", + "App", + "Main", + "Tea", + "Panel", + "Pane", + "PaneL", + "PaneN", + "PaneW", + "PanelSwitcher", + "Storage", + "Connection", ]; // ============================================================================= @@ -233,7 +251,7 @@ impl PanelManifest { !self.validations.iter().any(|v| v.has_errors) } PanelPhase::Provisioned => true, // Activation is a runtime decision - PanelPhase::Active => false, // Terminal phase + PanelPhase::Active => false, // Terminal phase } } @@ -529,10 +547,7 @@ impl PanelFileExpectation { }, Self { role: "Engine tests".into(), - path: PathBuf::from(format!( - "tests/{}_engine_test.js", - snake - )), + path: PathBuf::from(format!("tests/{}_engine_test.js", snake)), required: false, // Checked by finishbot, not rhodibot found: false, }, @@ -579,10 +594,7 @@ impl PanelFileExpectation { /// Get files that are required but missing. pub fn missing_required(files: &[Self]) -> Vec<&Self> { - files - .iter() - .filter(|f| f.required && !f.found) - .collect() + files.iter().filter(|f| f.required && !f.found).collect() } } @@ -666,10 +678,7 @@ pub enum PanelError { /// Panel name is reserved by PanLL internals. ReservedName(String), /// Panel cannot advance to the next phase (conditions not met). - CannotAdvance { - panel: PanelId, - phase: PanelPhase, - }, + CannotAdvance { panel: PanelId, phase: PanelPhase }, /// Panel is already in the Active phase. AlreadyActive(PanelId), /// Panel not found in context. @@ -714,7 +723,7 @@ pub trait PanelContext { /// Update a panel's wiring status. fn update_panel_wiring(&mut self, id: &PanelId, wiring: WiringStatus) - -> Result<(), PanelError>; + -> Result<(), PanelError>; /// Record a bot's validation result for a panel. fn record_panel_validation( @@ -877,9 +886,8 @@ mod tests { #[test] fn test_panel_manifest_lifecycle() { let id = PanelId::new("TestPanel").unwrap(); - let mut manifest = PanelManifest::new_minted( - id, "Test", "A test panel", "test-icon", false, - ); + let mut manifest = + PanelManifest::new_minted(id, "Test", "A test panel", "test-icon", false); assert_eq!(manifest.phase, PanelPhase::Minted); assert!(!manifest.can_advance()); // Wiring not complete @@ -918,7 +926,11 @@ mod tests { let mut ctx = crate::context::Context::new("panll", "/path/to/panll"); let id = PanelId::new("Wharf").unwrap(); let manifest = PanelManifest::new_minted( - id.clone(), "Wharf", "Container orchestration panel", "ship", true, + id.clone(), + "Wharf", + "Container orchestration panel", + "ship", + true, ); ctx.register_panel(manifest); diff --git a/shared-context/src/panel_checker.rs b/shared-context/src/panel_checker.rs index acc3559d..b655456f 100644 --- a/shared-context/src/panel_checker.rs +++ b/shared-context/src/panel_checker.rs @@ -261,8 +261,7 @@ pub fn pcc_results_to_findings(results: &[PccPanelResult], bot: BotId) -> Vec PanelManifest { - let id = PanelId::new(&result.panel_id) - .unwrap_or_else(|_| PanelId(result.panel_id.clone())); + let id = PanelId::new(&result.panel_id).unwrap_or_else(|_| PanelId(result.panel_id.clone())); let phase = match result.state.as_deref() { Some("releasable") => PanelPhase::Active, diff --git a/shared-context/src/reporting.rs b/shared-context/src/reporting.rs index 87411f9d..113cc4e6 100644 --- a/shared-context/src/reporting.rs +++ b/shared-context/src/reporting.rs @@ -102,7 +102,11 @@ impl Context { /// Build fleet summary fn build_summary(&self) -> FleetSummary { let total_bots = self.executions.len(); - let bots_completed = self.executions.values().filter(|e| e.completed_at.is_some()).count(); + let bots_completed = self + .executions + .values() + .filter(|e| e.completed_at.is_some()) + .count(); let bots_in_progress = self .executions .values() @@ -128,9 +132,8 @@ impl Context { // Calculate health score (0-100) let overall_health = if total_bots > 0 { let completion_score = (bots_completed as f64 / total_bots as f64) * 50.0; - let severity_penalty = (critical_findings as f64 * 10.0) - + (errors as f64 * 5.0) - + (warnings as f64 * 1.0); + let severity_penalty = + (critical_findings as f64 * 10.0) + (errors as f64 * 5.0) + (warnings as f64 * 1.0); let finding_score = (50.0 - severity_penalty.min(50.0)).max(0.0); completion_score + finding_score } else { @@ -164,11 +167,12 @@ impl Context { } .to_string(); - let duration_ms = if let (Some(start), Some(end)) = (exec.started_at, exec.completed_at) { - Some((end.timestamp_millis() - start.timestamp_millis()) as u64) - } else { - None - }; + let duration_ms = + if let (Some(start), Some(end)) = (exec.started_at, exec.completed_at) { + Some((end.timestamp_millis() - start.timestamp_millis()) as u64) + } else { + None + }; BotExecutionReport { bot_id: format!("{:?}", bot_id), @@ -226,24 +230,26 @@ impl Context { tier_stats .into_iter() - .map(|(tier, (bots_count, completed_count, total_findings, durations))| { - let avg_duration_ms = if !durations.is_empty() { - durations.iter().sum::() as f64 / durations.len() as f64 - } else { - 0.0 - }; - - ( - format!("{:?}", tier), - TierPerformance { - tier: format!("{:?}", tier), - bots_count, - completed_count, - total_findings, - avg_duration_ms, - }, - ) - }) + .map( + |(tier, (bots_count, completed_count, total_findings, durations))| { + let avg_duration_ms = if !durations.is_empty() { + durations.iter().sum::() as f64 / durations.len() as f64 + } else { + 0.0 + }; + + ( + format!("{:?}", tier), + TierPerformance { + tier: format!("{:?}", tier), + bots_count, + completed_count, + total_findings, + avg_duration_ms, + }, + ) + }, + ) .collect() } @@ -261,15 +267,30 @@ impl Context { // Summary md.push_str("## Summary\n\n"); - md.push_str(&format!("**Overall Health:** {:.1}/100\n\n", report.summary.overall_health)); + md.push_str(&format!( + "**Overall Health:** {:.1}/100\n\n", + report.summary.overall_health + )); md.push_str("| Metric | Value |\n"); md.push_str("|--------|-------|\n"); md.push_str(&format!("| Total Bots | {} |\n", report.summary.total_bots)); - md.push_str(&format!("| Completed | {} |\n", report.summary.bots_completed)); - md.push_str(&format!("| In Progress | {} |\n", report.summary.bots_in_progress)); + md.push_str(&format!( + "| Completed | {} |\n", + report.summary.bots_completed + )); + md.push_str(&format!( + "| In Progress | {} |\n", + report.summary.bots_in_progress + )); md.push_str(&format!("| Pending | {} |\n", report.summary.bots_pending)); - md.push_str(&format!("| Total Findings | {} |\n", report.summary.total_findings)); - md.push_str(&format!("| Critical | {} |\n", report.summary.critical_findings)); + md.push_str(&format!( + "| Total Findings | {} |\n", + report.summary.total_findings + )); + md.push_str(&format!( + "| Critical | {} |\n", + report.summary.critical_findings + )); md.push_str(&format!("| Errors | {} |\n", report.summary.errors)); md.push_str(&format!("| Warnings | {} |\n\n", report.summary.warnings)); @@ -299,7 +320,11 @@ impl Context { for perf in report.tier_performance.values() { md.push_str(&format!( "| {} | {} | {} | {} | {:.0} |\n", - perf.tier, perf.bots_count, perf.completed_count, perf.total_findings, perf.avg_duration_ms + perf.tier, + perf.bots_count, + perf.completed_count, + perf.total_findings, + perf.avg_duration_ms )); } diff --git a/shared-context/src/storage.rs b/shared-context/src/storage.rs index fdd37b73..cc853d36 100644 --- a/shared-context/src/storage.rs +++ b/shared-context/src/storage.rs @@ -123,7 +123,10 @@ impl ContextStorage { let path = self.repos_dir().join(&filename); if !path.exists() { - return Err(ContextError::NotFound(format!("Repo {} not found", repo_name))); + return Err(ContextError::NotFound(format!( + "Repo {} not found", + repo_name + ))); } let json = std::fs::read_to_string(&path)?; @@ -134,7 +137,11 @@ impl ContextStorage { } /// Get or create repository state - pub fn get_or_create_repo_state(&self, repo_name: &str, repo_path: PathBuf) -> Result { + pub fn get_or_create_repo_state( + &self, + repo_name: &str, + repo_path: PathBuf, + ) -> Result { match self.load_repo_state(repo_name) { Ok(state) => Ok(state), Err(ContextError::NotFound(_)) => Ok(RepoState::new(repo_name, repo_path)), diff --git a/shared-context/tests/context_tests.rs b/shared-context/tests/context_tests.rs index 29075ee7..4004ac68 100644 --- a/shared-context/tests/context_tests.rs +++ b/shared-context/tests/context_tests.rs @@ -256,14 +256,19 @@ fn test_bot_tier() { #[test] fn test_finding_builder() { - let finding = Finding::new(BotId::Glambot, "WCAG-1.1.1", Severity::Error, "Missing alt text") - .with_rule_name("Image Alternative Text") - .with_category("accessibility") - .with_file(PathBuf::from("index.html")) - .with_location(42, 15) - .with_element("") - .with_suggestion("Add alt attribute to describe the image") - .fixable(); + let finding = Finding::new( + BotId::Glambot, + "WCAG-1.1.1", + Severity::Error, + "Missing alt text", + ) + .with_rule_name("Image Alternative Text") + .with_category("accessibility") + .with_file(PathBuf::from("index.html")) + .with_location(42, 15) + .with_element("") + .with_suggestion("Add alt attribute to describe the image") + .fixable(); assert_eq!(finding.rule_id, "WCAG-1.1.1"); assert_eq!(finding.rule_name, "Image Alternative Text"); @@ -280,7 +285,10 @@ fn test_finding_location_string() { let finding1 = Finding::new(BotId::Glambot, "TEST-001", Severity::Info, "Test") .with_file(PathBuf::from("test.html")) .with_location(10, 5); - assert_eq!(finding1.location_string(), Some("test.html:10:5".to_string())); + assert_eq!( + finding1.location_string(), + Some("test.html:10:5".to_string()) + ); let finding2 = Finding::new(BotId::Glambot, "TEST-002", Severity::Info, "Test") .with_file(PathBuf::from("test.html")) diff --git a/shared-context/tests/e2e_fleet_coordination_test.rs b/shared-context/tests/e2e_fleet_coordination_test.rs index 032fa37d..fa04e3df 100644 --- a/shared-context/tests/e2e_fleet_coordination_test.rs +++ b/shared-context/tests/e2e_fleet_coordination_test.rs @@ -15,9 +15,7 @@ //! do not start real bot processes; they exercise the full state machine that //! real bots drive. -use gitbot_shared_context::{ - BotId, Context, ContextStorage, Finding, ReportFormat, Severity, -}; +use gitbot_shared_context::{BotId, Context, ContextStorage, Finding, ReportFormat, Severity}; use std::path::PathBuf; use tempfile::TempDir; @@ -33,7 +31,8 @@ fn e2e_single_bot_dispatch_process_collect() { ctx.register_bot(BotId::Rhodibot); // --- DISPATCH phase --- - ctx.start_bot(BotId::Rhodibot).expect("start_bot must succeed for registered bot"); + ctx.start_bot(BotId::Rhodibot) + .expect("start_bot must succeed for registered bot"); // Verify bot is running let execution = ctx.executions.get(&BotId::Rhodibot).unwrap(); @@ -44,12 +43,22 @@ fn e2e_single_bot_dispatch_process_collect() { // --- PROCESS phase (bot adds findings) --- ctx.add_finding( - Finding::new(BotId::Rhodibot, "RSR-MISSING-README", Severity::Error, "Missing README.adoc") - .with_category("structure"), + Finding::new( + BotId::Rhodibot, + "RSR-MISSING-README", + Severity::Error, + "Missing README.adoc", + ) + .with_category("structure"), ); ctx.add_finding( - Finding::new(BotId::Rhodibot, "RSR-MISSING-LICENSE", Severity::Warning, "Missing LICENSE file") - .with_category("legal"), + Finding::new( + BotId::Rhodibot, + "RSR-MISSING-LICENSE", + Severity::Warning, + "Missing LICENSE file", + ) + .with_category("legal"), ); // --- COLLECT phase --- @@ -68,7 +77,10 @@ fn e2e_single_bot_dispatch_process_collect() { assert_eq!(results.len(), 2, "Must be able to retrieve all 2 findings"); let readme_finding = results.iter().find(|f| f.rule_id == "RSR-MISSING-README"); - assert!(readme_finding.is_some(), "RSR-MISSING-README must be present"); + assert!( + readme_finding.is_some(), + "RSR-MISSING-README must be present" + ); assert_eq!(readme_finding.unwrap().severity, Severity::Error); } @@ -83,23 +95,41 @@ fn e2e_multi_bot_dispatch_all_results_aggregated() { let mut ctx = Context::new("e2e-multi-bot", PathBuf::from("/tmp/e2e-multi-bot")); ctx.register_all_bots(); - let verifiers = [BotId::Rhodibot, BotId::Echidnabot, BotId::Sustainabot, BotId::Panicbot]; + let verifiers = [ + BotId::Rhodibot, + BotId::Echidnabot, + BotId::Sustainabot, + BotId::Panicbot, + ]; let finishers = [BotId::Glambot, BotId::Seambot, BotId::Finishbot]; // Run verifiers first (no deps) for &bot in &verifiers { ctx.start_bot(bot).expect("start verifier"); - ctx.add_finding(Finding::new(bot, &format!("{}-001", bot), Severity::Warning, "Verifier finding")); + ctx.add_finding(Finding::new( + bot, + &format!("{}-001", bot), + Severity::Warning, + "Verifier finding", + )); ctx.complete_bot(bot, 1, 0, 10).expect("complete verifier"); } // Verify all verifiers complete before finishers start - assert!(ctx.verifiers_complete(), "All verifiers must be complete before running finishers"); + assert!( + ctx.verifiers_complete(), + "All verifiers must be complete before running finishers" + ); // Run finishers for &bot in &finishers { ctx.start_bot(bot).expect("start finisher"); - ctx.add_finding(Finding::new(bot, &format!("{}-001", bot), Severity::Info, "Finisher finding")); + ctx.add_finding(Finding::new( + bot, + &format!("{}-001", bot), + Severity::Info, + "Finisher finding", + )); ctx.complete_bot(bot, 1, 0, 5).expect("complete finisher"); } @@ -110,19 +140,31 @@ fn e2e_multi_bot_dispatch_all_results_aggregated() { let total_bots = verifiers.len() + finishers.len(); assert_eq!( summary.total_findings, total_bots, - "Summary must aggregate findings from all {} bots", total_bots + "Summary must aggregate findings from all {} bots", + total_bots ); assert_eq!( summary.bots_run, total_bots, - "bots_run must count all {} completing bots", total_bots + "bots_run must count all {} completing bots", + total_bots ); // No errors (only warnings and info) - assert_eq!(summary.total_errors, 0, "No error-severity findings were added"); - assert_eq!(summary.total_warnings, verifiers.len(), "Verifiers each contributed one warning"); + assert_eq!( + summary.total_errors, 0, + "No error-severity findings were added" + ); + assert_eq!( + summary.total_warnings, + verifiers.len(), + "Verifiers each contributed one warning" + ); // No release blocks (no error-severity findings) - assert!(!summary.blocks_release, "Warnings and info should not block release"); + assert!( + !summary.blocks_release, + "Warnings and info should not block release" + ); } // --------------------------------------------------------------------------- @@ -133,7 +175,10 @@ fn e2e_multi_bot_dispatch_all_results_aggregated() { /// complete. The failed bot should not corrupt the session. #[test] fn e2e_bot_failure_does_not_prevent_other_bots() { - let mut ctx = Context::new("e2e-failure-isolation", PathBuf::from("/tmp/e2e-failure-isolation")); + let mut ctx = Context::new( + "e2e-failure-isolation", + PathBuf::from("/tmp/e2e-failure-isolation"), + ); ctx.register_all_bots(); // Rhodibot fails @@ -143,18 +188,31 @@ fn e2e_bot_failure_does_not_prevent_other_bots() { let rhodibot_exec = ctx.executions.get(&BotId::Rhodibot).unwrap(); assert!( - matches!(rhodibot_exec.status, gitbot_shared_context::bot::BotStatus::Failed), + matches!( + rhodibot_exec.status, + gitbot_shared_context::bot::BotStatus::Failed + ), "Rhodibot execution status must be Failed" ); // Echidnabot can still run independently ctx.start_bot(BotId::Echidnabot).unwrap(); - ctx.add_finding(Finding::new(BotId::Echidnabot, "PROOF-VERIFIED", Severity::Info, "Proof verified")); + ctx.add_finding(Finding::new( + BotId::Echidnabot, + "PROOF-VERIFIED", + Severity::Info, + "Proof verified", + )); ctx.complete_bot(BotId::Echidnabot, 1, 0, 5).unwrap(); // Sustainabot can still run independently ctx.start_bot(BotId::Sustainabot).unwrap(); - ctx.add_finding(Finding::new(BotId::Sustainabot, "ECO-001", Severity::Warning, "Outdated deps")); + ctx.add_finding(Finding::new( + BotId::Sustainabot, + "ECO-001", + Severity::Warning, + "Outdated deps", + )); ctx.complete_bot(BotId::Sustainabot, 1, 0, 8).unwrap(); // Panicbot can still run independently @@ -162,16 +220,31 @@ fn e2e_bot_failure_does_not_prevent_other_bots() { ctx.complete_bot(BotId::Panicbot, 0, 0, 3).unwrap(); // Findings from successful bots are intact - assert_eq!(ctx.findings_from(BotId::Echidnabot).len(), 1, "Echidnabot finding must be present"); - assert_eq!(ctx.findings_from(BotId::Sustainabot).len(), 1, "Sustainabot finding must be present"); + assert_eq!( + ctx.findings_from(BotId::Echidnabot).len(), + 1, + "Echidnabot finding must be present" + ); + assert_eq!( + ctx.findings_from(BotId::Sustainabot).len(), + 1, + "Sustainabot finding must be present" + ); // Rhodibot findings are empty (it failed before adding any) - assert_eq!(ctx.findings_from(BotId::Rhodibot).len(), 0, "Failed bot contributed no findings"); + assert_eq!( + ctx.findings_from(BotId::Rhodibot).len(), + 0, + "Failed bot contributed no findings" + ); // The session summary should reflect the failure ctx.complete_session(); let summary = ctx.summary(); - assert_eq!(summary.total_findings, 2, "Only 2 findings from the successful bots"); + assert_eq!( + summary.total_findings, 2, + "Only 2 findings from the successful bots" + ); } // --------------------------------------------------------------------------- @@ -190,8 +263,13 @@ fn e2e_session_persistence_and_reload() { ctx.register_bot(BotId::Rhodibot); ctx.start_bot(BotId::Rhodibot).unwrap(); ctx.add_finding( - Finding::new(BotId::Rhodibot, "RSR-001", Severity::Error, "Persistent error finding") - .with_category("structure"), + Finding::new( + BotId::Rhodibot, + "RSR-001", + Severity::Error, + "Persistent error finding", + ) + .with_category("structure"), ); ctx.complete_bot(BotId::Rhodibot, 1, 1, 20).unwrap(); ctx.complete_session(); @@ -200,17 +278,28 @@ fn e2e_session_persistence_and_reload() { let expected_findings = ctx.findings.len(); // Persist to disk - storage.save_context(&ctx).expect("save_context must succeed"); + storage + .save_context(&ctx) + .expect("save_context must succeed"); // Reload and verify - let loaded = storage.load_context(&session_id).expect("load_context must succeed"); + let loaded = storage + .load_context(&session_id) + .expect("load_context must succeed"); - assert_eq!(loaded.repo_name, "persist-repo", "repo_name must survive round-trip"); assert_eq!( - loaded.findings.len(), expected_findings, + loaded.repo_name, "persist-repo", + "repo_name must survive round-trip" + ); + assert_eq!( + loaded.findings.len(), + expected_findings, "findings count must survive round-trip" ); - assert_eq!(loaded.session_id, session_id, "session_id must be identical after reload"); + assert_eq!( + loaded.session_id, session_id, + "session_id must be identical after reload" + ); // Verify execution records are intact let exec = loaded.executions.get(&BotId::Rhodibot).unwrap(); @@ -233,15 +322,25 @@ fn e2e_report_generation_pipeline() { // Run a subset of bots to populate the context ctx.start_bot(BotId::Rhodibot).unwrap(); ctx.add_finding( - Finding::new(BotId::Rhodibot, "RSR-001", Severity::Error, "Missing SPDX headers on 3 files") - .with_category("licensing"), + Finding::new( + BotId::Rhodibot, + "RSR-001", + Severity::Error, + "Missing SPDX headers on 3 files", + ) + .with_category("licensing"), ); ctx.complete_bot(BotId::Rhodibot, 1, 1, 30).unwrap(); ctx.start_bot(BotId::Glambot).unwrap(); ctx.add_finding( - Finding::new(BotId::Glambot, "SEO-001", Severity::Warning, "README missing meta keywords") - .with_category("seo"), + Finding::new( + BotId::Glambot, + "SEO-001", + Severity::Warning, + "README missing meta keywords", + ) + .with_category("seo"), ); ctx.complete_bot(BotId::Glambot, 1, 0, 5).unwrap(); @@ -259,8 +358,8 @@ fn e2e_report_generation_pipeline() { let json_report = ctx.generate_report(ReportFormat::Json); assert!(!json_report.is_empty(), "JSON report must not be empty"); // Must be valid JSON - let parsed: serde_json::Value = serde_json::from_str(&json_report) - .expect("JSON report must be valid JSON"); + let parsed: serde_json::Value = + serde_json::from_str(&json_report).expect("JSON report must be valid JSON"); assert!( parsed.is_object() || parsed.is_array(), "JSON report must be an object or array at the root" @@ -294,10 +393,18 @@ fn e2e_findings_severity_pipeline_release_gate() { let mut ctx = Context::new("warning-repo", PathBuf::from("/tmp/warning-repo")); ctx.register_bot(BotId::Rhodibot); ctx.start_bot(BotId::Rhodibot).unwrap(); - ctx.add_finding(Finding::new(BotId::Rhodibot, "WARN-001", Severity::Warning, "Minor issue")); + ctx.add_finding(Finding::new( + BotId::Rhodibot, + "WARN-001", + Severity::Warning, + "Minor issue", + )); ctx.complete_bot(BotId::Rhodibot, 1, 0, 5).unwrap(); ctx.complete_session(); - assert!(!ctx.blocks_release(), "Warnings alone must not block release"); + assert!( + !ctx.blocks_release(), + "Warnings alone must not block release" + ); } // Case 3: Error present → blocks release @@ -305,11 +412,19 @@ fn e2e_findings_severity_pipeline_release_gate() { let mut ctx = Context::new("error-repo", PathBuf::from("/tmp/error-repo")); ctx.register_bot(BotId::Rhodibot); ctx.start_bot(BotId::Rhodibot).unwrap(); - ctx.add_finding(Finding::new(BotId::Rhodibot, "ERR-001", Severity::Error, "Critical missing file")); + ctx.add_finding(Finding::new( + BotId::Rhodibot, + "ERR-001", + Severity::Error, + "Critical missing file", + )); ctx.complete_bot(BotId::Rhodibot, 1, 1, 5).unwrap(); ctx.complete_session(); assert!(ctx.blocks_release(), "Error severity must block release"); - assert!(ctx.has_errors(), "has_errors must return true when Error findings present"); + assert!( + ctx.has_errors(), + "has_errors must return true when Error findings present" + ); } // Case 4: Findings marked as fixable are tracked by the pipeline @@ -319,8 +434,13 @@ fn e2e_findings_severity_pipeline_release_gate() { ctx.register_bot(BotId::Rhodibot); ctx.start_bot(BotId::Rhodibot).unwrap(); - let finding = Finding::new(BotId::Rhodibot, "ERR-002", Severity::Error, "Can be auto-fixed") - .fixable(); + let finding = Finding::new( + BotId::Rhodibot, + "ERR-002", + Severity::Error, + "Can be auto-fixed", + ) + .fixable(); let finding_id = finding.id; ctx.add_finding(finding); ctx.complete_bot(BotId::Rhodibot, 1, 1, 5).unwrap(); @@ -328,7 +448,11 @@ fn e2e_findings_severity_pipeline_release_gate() { // Before fixing: blocks release, has errors, has fixable finding assert!(ctx.blocks_release()); assert!(ctx.has_errors()); - assert_eq!(ctx.findings.fixable().len(), 1, "One fixable finding must be tracked"); + assert_eq!( + ctx.findings.fixable().len(), + 1, + "One fixable finding must be tracked" + ); // Mark the finding as fixed via the FindingSet mutation API if let Some(f) = ctx.findings.find_mut(finding_id) { @@ -338,11 +462,13 @@ fn e2e_findings_severity_pipeline_release_gate() { // After fixing: fixable() returns only not-yet-fixed items (now 0), // and unfixed() returns 0 as well (the only finding is now marked fixed). assert_eq!( - ctx.findings.fixable().len(), 0, + ctx.findings.fixable().len(), + 0, "fixable() must return 0 after the finding is marked fixed" ); assert_eq!( - ctx.findings.unfixed().len(), 0, + ctx.findings.unfixed().len(), + 0, "unfixed() must return 0 after fixing" ); } diff --git a/shared-context/tests/fleet_coordination_test.rs b/shared-context/tests/fleet_coordination_test.rs index 5bd3b821..c8ac6cc8 100644 --- a/shared-context/tests/fleet_coordination_test.rs +++ b/shared-context/tests/fleet_coordination_test.rs @@ -15,7 +15,16 @@ fn test_bot_registration() { let mut ctx = Context::new("test-repo", PathBuf::from("/tmp/test-repo")); ctx.register_all_bots(); - let bots = [BotId::Rhodibot, BotId::Echidnabot, BotId::Sustainabot, BotId::Glambot, BotId::Seambot, BotId::Finishbot, BotId::RobotRepoAutomaton, BotId::Hypatia]; + let bots = [ + BotId::Rhodibot, + BotId::Echidnabot, + BotId::Sustainabot, + BotId::Glambot, + BotId::Seambot, + BotId::Finishbot, + BotId::RobotRepoAutomaton, + BotId::Hypatia, + ]; for bot in &bots { assert!(ctx.executions.contains_key(bot)); @@ -28,7 +37,12 @@ fn test_finding_publication() { ctx.register_all_bots(); ctx.start_bot(BotId::RobotRepoAutomaton).unwrap(); - let finding = Finding::new(BotId::RobotRepoAutomaton, "TEST-FINDING", Severity::Warning, "Test finding description"); + let finding = Finding::new( + BotId::RobotRepoAutomaton, + "TEST-FINDING", + Severity::Warning, + "Test finding description", + ); ctx.add_finding(finding); let findings = ctx.findings_from(BotId::RobotRepoAutomaton); @@ -43,8 +57,18 @@ fn test_cross_bot_findings() { ctx.start_bot(BotId::Echidnabot).unwrap(); ctx.start_bot(BotId::RobotRepoAutomaton).unwrap(); - ctx.add_finding(Finding::new(BotId::Echidnabot, "PROOF-VERIFIED", Severity::Info, "Contract verified successfully")); - ctx.add_finding(Finding::new(BotId::RobotRepoAutomaton, "COMPLIANCE-VIOLATION", Severity::Error, "Missing LICENSE file")); + ctx.add_finding(Finding::new( + BotId::Echidnabot, + "PROOF-VERIFIED", + Severity::Info, + "Contract verified successfully", + )); + ctx.add_finding(Finding::new( + BotId::RobotRepoAutomaton, + "COMPLIANCE-VIOLATION", + Severity::Error, + "Missing LICENSE file", + )); assert_eq!(ctx.findings_from(BotId::Echidnabot).len(), 1); assert_eq!(ctx.findings_from(BotId::RobotRepoAutomaton).len(), 1); @@ -57,7 +81,13 @@ fn test_session_lifecycle() { ctx.register_all_bots(); ctx.start_bot(BotId::Seambot).unwrap(); - assert!(ctx.executions.get(&BotId::Seambot).unwrap().started_at.is_some()); + assert!( + ctx.executions + .get(&BotId::Seambot) + .unwrap() + .started_at + .is_some() + ); ctx.complete_bot(BotId::Seambot, 5, 2, 10).unwrap(); let exec = ctx.executions.get(&BotId::Seambot).unwrap(); @@ -75,6 +105,18 @@ fn test_tier_hierarchy() { ctx.start_bot(BotId::Seambot).unwrap(); ctx.start_bot(BotId::RobotRepoAutomaton).unwrap(); - assert!(ctx.executions.get(&BotId::Hypatia).unwrap().started_at.is_some()); - assert!(ctx.executions.get(&BotId::RobotRepoAutomaton).unwrap().started_at.is_some()); + assert!( + ctx.executions + .get(&BotId::Hypatia) + .unwrap() + .started_at + .is_some() + ); + assert!( + ctx.executions + .get(&BotId::RobotRepoAutomaton) + .unwrap() + .started_at + .is_some() + ); } diff --git a/shared-context/tests/property_tests.rs b/shared-context/tests/property_tests.rs index 2f8c6fb1..bcbb4c81 100644 --- a/shared-context/tests/property_tests.rs +++ b/shared-context/tests/property_tests.rs @@ -18,7 +18,7 @@ //! carefully constructed set of representative values spanning boundary conditions. use gitbot_shared_context::{ - BotId, Context, ConfidenceThresholds, DispatchStrategy, Finding, Severity, + BotId, ConfidenceThresholds, Context, DispatchStrategy, Finding, Severity, }; use std::path::PathBuf; @@ -38,13 +38,19 @@ fn prop_any_bot_subset_produces_valid_state() { ctx.register_bot(bot); // Start and complete the single bot - ctx.start_bot(bot).expect("start_bot should not fail for registered bot"); - ctx.complete_bot(bot, 0, 0, 1).expect("complete_bot should not fail"); + ctx.start_bot(bot) + .expect("start_bot should not fail for registered bot"); + ctx.complete_bot(bot, 0, 0, 1) + .expect("complete_bot should not fail"); // State must be internally consistent - assert!(ctx.bot_completed(bot), "Bot should be marked complete after complete_bot"); + assert!( + ctx.bot_completed(bot), + "Bot should be marked complete after complete_bot" + ); assert_eq!( - ctx.findings.len(), 0, + ctx.findings.len(), + 0, "No findings added — findings collection should be empty" ); } @@ -117,7 +123,12 @@ fn prop_findings_are_partitioned_by_bot() { // Each bot's findings slice must only contain that bot's findings for &bot in &all_bots { let bot_findings = ctx.findings_from(bot); - assert_eq!(bot_findings.len(), 3, "Expected exactly 3 findings for {}", bot); + assert_eq!( + bot_findings.len(), + 3, + "Expected exactly 3 findings for {}", + bot + ); for f in &bot_findings { assert_eq!( f.source, bot, @@ -147,8 +158,7 @@ fn prop_confidence_scores_always_yield_valid_strategy() { // Test boundary values and representative points across [0.0, 1.0] let test_values: &[f64] = &[ - 0.0, 0.001, 0.1, 0.3, 0.5, 0.69, 0.70, 0.849, 0.85, 0.94, - 0.95, 0.96, 0.99, 1.0, + 0.0, 0.001, 0.1, 0.3, 0.5, 0.69, 0.70, 0.849, 0.85, 0.94, 0.95, 0.96, 0.99, 1.0, ]; for &confidence in test_values {