refactor(handlers): extract LearnParams + reusable RBAC helpers

learn_handler refactored:
- Extract LearnParams struct with validation + bounds clamping
- Extract store_compacted_memory helper
- Extract build_learn_response helper
- Reuse check_project_write_access for RBAC

ingest_handler refactored:
- Extract check_project_write_access (reusable)
- Extract execute_ingest helper

New tests (6 total):
- LearnParams validation tests

Total tests: 694 (was 688)
This commit is contained in:
2026-09-03 09:15:35 -07:00
parent 43778f730f
commit 0296cae6f4
3 changed files with 302 additions and 136 deletions
+172
View File
@@ -0,0 +1,172 @@
/// Learn Handler Helpers
///
/// Extracted to reduce learn_handler complexity.
use actix_web::HttpResponse;
use serde_json::{json, Value};
// ============================================================================
// Learn Parameters
// ============================================================================
/// Validated learn request parameters
#[derive(Debug, Clone)]
pub struct LearnParams {
pub project: String,
pub text: String,
pub question: String,
pub memory_budget: u32,
pub chunk_size: usize,
pub model: String,
}
impl LearnParams {
/// Parse and validate learn request body
pub fn from_body(body: &Value) -> Result<Self, LearnParamsError> {
let project = body["project"]
.as_str()
.unwrap_or("knowledge")
.to_string();
let text = body["text"]
.as_str()
.filter(|t| !t.trim().is_empty())
.ok_or(LearnParamsError::MissingText)?
.to_string();
let question = body["query"]
.as_str()
.unwrap_or("What are the key facts, patterns, and practices in this knowledge?")
.to_string();
let memory_budget = body["memory_budget"]
.as_u64()
.unwrap_or(4096)
.clamp(256, 32768) as u32;
let chunk_size = body["chunk_size"]
.as_u64()
.unwrap_or(2000)
.clamp(500, 10000) as usize;
let model = body["model"]
.as_str()
.unwrap_or("qwen2.5:3b-instruct")
.to_string();
Ok(Self { project, text, question, memory_budget, chunk_size, model })
}
}
/// Learn parameter validation errors
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LearnParamsError {
MissingText,
}
impl LearnParamsError {
pub fn to_response(&self) -> HttpResponse {
let reason = match self {
Self::MissingText => "missing required field: text",
};
HttpResponse::BadRequest().json(json!({
"error": "bad_request",
"reason": reason
}))
}
}
// ============================================================================
// Learn Response Builder
// ============================================================================
/// Build learn response JSON
pub fn build_learn_response(
project: &str,
model: &str,
chunks_seen: u32,
chunks_used: u32,
memory: &str,
stored: bool,
) -> HttpResponse {
HttpResponse::Ok().json(json!({
"project": project,
"status": "completed",
"chunks_seen": chunks_seen,
"chunks_used": chunks_used,
"memory": memory,
"memory_tokens": memory.len() / 4,
"stored": stored,
"model": model,
}))
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_learn_params_valid() {
let body = json!({
"project": "homelab",
"text": "Some knowledge to learn"
});
let params = LearnParams::from_body(&body).unwrap();
assert_eq!(params.project, "homelab");
assert_eq!(params.text, "Some knowledge to learn");
assert_eq!(params.memory_budget, 4096);
assert_eq!(params.chunk_size, 2000);
}
#[test]
fn test_learn_params_defaults() {
let body = json!({"text": "content"});
let params = LearnParams::from_body(&body).unwrap();
assert_eq!(params.project, "knowledge");
assert_eq!(params.model, "qwen2.5:3b-instruct");
}
#[test]
fn test_learn_params_custom_values() {
let body = json!({
"text": "content",
"memory_budget": 8192,
"chunk_size": 3000,
"model": "gpt-4"
});
let params = LearnParams::from_body(&body).unwrap();
assert_eq!(params.memory_budget, 8192);
assert_eq!(params.chunk_size, 3000);
assert_eq!(params.model, "gpt-4");
}
#[test]
fn test_learn_params_clamped_budget() {
let body = json!({"text": "x", "memory_budget": 999999});
let params = LearnParams::from_body(&body).unwrap();
assert_eq!(params.memory_budget, 32768);
let body = json!({"text": "x", "memory_budget": 10});
let params = LearnParams::from_body(&body).unwrap();
assert_eq!(params.memory_budget, 256);
}
#[test]
fn test_learn_params_missing_text() {
let body = json!({"project": "test"});
assert_eq!(LearnParams::from_body(&body).unwrap_err(), LearnParamsError::MissingText);
}
#[test]
fn test_learn_params_empty_text() {
let body = json!({"text": " "});
assert_eq!(LearnParams::from_body(&body).unwrap_err(), LearnParamsError::MissingText);
}
}
+2
View File
@@ -5,6 +5,8 @@
pub mod query;
pub mod ingest;
pub mod learn;
pub use query::*;
pub use ingest::*;
pub use learn::*;