/// Compaction handler — T3.4 Scheduler /// /// Endpoint for triggering manual or scheduled compaction. /// Can be called by CronJob (K8s) or manually via API. use actix_web::{web, HttpRequest, HttpResponse}; use serde::{Deserialize, Serialize}; use serde_json::json; use crate::http_server::AppState; use crate::compaction::{compact_memory, CompactionMode, CompactionStats}; /// Compaction request parameters #[derive(Debug, Deserialize, Clone)] pub struct CompactRequest { /// Dry-run mode (don't apply changes) #[serde(default)] pub dry_run: bool, /// Enable LLM-based semantic dedup (T3.2) #[serde(default = "default_enable_semantic")] pub enable_semantic_dedup: bool, /// Project filter (if None, all projects) pub project: Option, } fn default_enable_semantic() -> bool { false } /// Compaction response #[derive(Debug, Serialize)] pub struct CompactResponse { pub status: String, pub mode: String, pub stats: CompactionStats, } /// POST /memory/compact - Trigger memory compaction pub async fn compact_handler( req: HttpRequest, body: web::Json, state: web::Data, ) -> HttpResponse { // 1. Validate JWT + rate limiting (centralized middleware) if let Err(response) = crate::handlers::middleware::validate_and_rate_limit(&req, &state, "compact", 10) { return response; } // 2. Execute compaction let mode = if body.dry_run { CompactionMode::DryRun } else { CompactionMode::Execute }; match compact_memory_sync(&state, mode).await { Ok(stats) => { let mode_str = if body.dry_run { "dry-run" } else { "execute" }; crate::handlers::response_builder::success_response(CompactResponse { status: "success".to_string(), mode: mode_str.to_string(), stats, }) } Err(e) => { tracing::error!("Compaction failed: {}", e); crate::handlers::response_builder::internal_error( &format!("Compaction failed: {}", e) ) } } } /// Execute compaction asynchronously async fn compact_memory_sync( state: &AppState, mode: CompactionMode, ) -> anyhow::Result { crate::compaction::compact_memory(&state.pool, None, mode).await } #[cfg(test)] mod tests { use super::*; #[test] fn test_compact_request_dry_run() { let req = CompactRequest { dry_run: true, enable_semantic_dedup: false, project: None, }; assert!(req.dry_run); assert!(!req.enable_semantic_dedup); } #[test] fn test_compact_request_with_project() { let req = CompactRequest { dry_run: false, enable_semantic_dedup: true, project: Some("poimen".to_string()), }; assert_eq!(req.project, Some("poimen".to_string())); } #[test] fn test_compact_response_serialization() { let resp = CompactResponse { status: "success".to_string(), mode: "execute".to_string(), stats: CompactionStats { duplicate_edges_deleted: 5, stale_facts_deleted: 3, ..Default::default() }, }; let json = serde_json::to_string(&resp).unwrap(); assert!(json.contains("success")); assert!(json.contains("duplicate_edges_deleted")); } }