//! The guard on the security-scan sweep. //! //! `phase_runner::scan_finished_security_phases` fires `security_scan::run` //! for finished `security_scan` phases. The scan needs Docker; the part that //! decides whether it runs twice, once, or never is pure SQL, and it is the //! part that fails silently in both directions — rescanning forever, or never //! scanning at all and leaving a phase that looks identical to a clean repo. use cm_domain::{Workspace, WorkspaceId}; use uuid::Uuid; async fn seed_mission(pool: &sqlx::PgPool) -> Uuid { let ws = Workspace { id: WorkspaceId::new(), name: "Security Sweep Test".into(), plan: "team".into(), }; cm_db::repo::workspaces::insert(pool, &ws).await.unwrap(); let id = Uuid::now_v7(); sqlx::query( "INSERT INTO missions (id, workspace_id, title, template_kind, status) VALUES ($1, $2, 'security test', 'security_hardening', 'running')", ) .bind(id) .bind(ws.id.as_uuid()) .execute(pool) .await .unwrap(); id } async fn seed_phase( pool: &sqlx::PgPool, mission_id: Uuid, kind: &str, status: &str, order_idx: i32, ) -> Uuid { let id = Uuid::now_v7(); sqlx::query( "INSERT INTO mission_phases (id, mission_id, kind, order_idx, status, completed_at) VALUES ($1, $2, $3, $4, $5, now())", ) .bind(id) .bind(mission_id) .bind(kind) .bind(order_idx) .bind(status) .execute(pool) .await .unwrap(); id } async fn mark_scanned(pool: &sqlx::PgPool, mission_id: Uuid, phase_id: Uuid) { cm_db::repo::missions::upsert_task( pool, cm_db::repo::missions::UpsertTask { mission_id, phase_id, external_id: cm_api::security_scan::SCAN_MARKER, title: "security scan complete — ran [gitleaks], 0 finding(s)", assigned_agent_id: None, status: "created", run_id: None, }, ) .await .unwrap(); } #[tokio::test] async fn a_finished_security_phase_is_selected_until_it_carries_a_scan_marker() { let pool = cm_testkit::test_pool().await; let mission = seed_mission(&pool).await; let phase = seed_phase(&pool, mission, "security_scan", "completed", 0).await; let selected = cm_api::phase_runner::unscanned_security_phases(&pool) .await .unwrap(); assert!( selected.iter().any(|(p, _)| *p == phase), "a finished security_scan phase with no marker must be selected — \ otherwise the scan never runs and the phase reports completed having \ scanned nothing" ); // A clean scan writes NO findings, so the marker is the only evidence the // scan happened. This is the case that would otherwise rescan forever. mark_scanned(&pool, mission, phase).await; let selected = cm_api::phase_runner::unscanned_security_phases(&pool) .await .unwrap(); assert!( !selected.iter().any(|(p, _)| *p == phase), "a phase carrying the scan marker must not be selected again, even \ though it has zero findings" ); } #[tokio::test] async fn only_finished_security_phases_are_selected() { let pool = cm_testkit::test_pool().await; let mission = seed_mission(&pool).await; let running = seed_phase(&pool, mission, "security_scan", "running", 0).await; let coding = seed_phase(&pool, mission, "coding", "completed", 1).await; let failed = seed_phase(&pool, mission, "security_scan", "failed", 2).await; let selected: Vec = cm_api::phase_runner::unscanned_security_phases(&pool) .await .unwrap() .into_iter() .map(|(p, _)| p) .collect(); assert!( !selected.contains(&running), "scanning a phase still running would scan a half-written checkout" ); assert!(!selected.contains(&coding), "only security_scan phases scan"); assert!( selected.contains(&failed), "a FAILED security phase is exactly the one worth scanning — the \ scanners are how we find out what state it left behind" ); }