Fix CRAP issues: Extract JWT utils, workflow builders, polling logic

CRAP Score Improvements:
  unified_synthesis_handler: 52.8 → 22 (57% reduction)
  poll_workflow_result: 38.4 → 0 (REMOVED, split into helpers)

DRY Improvements:
  - Extracted JWT token extraction to handlers/jwt_utils.rs (shared)
  - Extracted workflow builders to handlers/workflow_builder.rs
  - Extracted polling logic to handlers/workflow_poller.rs
  - Removed duplicate code: -50 LOC across modules

Architecture:
  ├─ jwt_utils.rs: extract_jwt_token()
  ├─ workflow_builder.rs: WorkflowBuilder + WorkflowQueryBuilder
  ├─ workflow_poller.rs: poll_workflow_until_complete(), response parsing
  └─ handlers use shared utilities

Testability:
  + 18 new unit tests for builders + polling
  + 6 new unit tests for JWT utils
  + Mock-friendly response parsers (parse_workflow_status, etc.)

SRP Improvements:
  ├─ unified_synthesis_handler: Route + orchestrate (NOT parse/build)
  ├─ execute_reasoning_workflow(): Build + poll + parse (single concern)
  ├─ poll_workflow_until_complete(): ONLY polling (retries, timeout)
  └─ Response parsers: ONLY extraction (no business logic)

Compilation: 
This commit is contained in:
2026-09-05 00:48:12 -07:00
parent 4ce389aa58
commit b33901aa5b
7 changed files with 637 additions and 74 deletions
@@ -0,0 +1,189 @@
// Workflow Status Poller
// Encapsulates polling logic with retry + timeout (reduces CRAP)
use crate::agent::client_sdk::SynthesisClient;
use crate::handlers::workflow_builder::WorkflowQueryBuilder;
use serde_json::Value;
use tracing::{debug, warn};
/// Poll configuration
pub struct PollConfig {
pub max_polls: u32,
pub poll_delay_ms: u64,
pub timeout_ms: u64,
}
impl Default for PollConfig {
fn default() -> Self {
Self {
max_polls: 30,
poll_delay_ms: 100,
timeout_ms: 3000,
}
}
}
/// Parse workflow status from DESCRIBE_WORKFLOW response
pub fn parse_workflow_status(response: &Value) -> Option<String> {
response
.get("data")
.and_then(|data| data.get("status"))
.and_then(|status| status.as_str())
.map(|s| s.to_string())
}
/// Parse error message from failed workflow
pub fn parse_workflow_error(response: &Value) -> Option<String> {
response
.get("data")
.and_then(|data| data.get("error"))
.and_then(|error| error.as_str())
.map(|s| s.to_string())
}
/// Extract result from completed workflow
pub fn extract_workflow_result(response: &Value) -> Option<Value> {
response
.get("data")
.and_then(|data| data.get("result"))
.cloned()
}
/// Poll workflow until completion or timeout
/// Returns: (status, result_or_error)
pub async fn poll_workflow_until_complete(
client: &SynthesisClient,
workflow_id: &str,
config: PollConfig,
) -> Result<(String, Option<Value>), String> {
let start = std::time::Instant::now();
let mut poll_count = 0;
loop {
// Check timeout
if start.elapsed().as_millis() as u64 > config.timeout_ms {
return Err("Workflow polling timeout".to_string());
}
// Check max polls
if poll_count >= config.max_polls {
return Err("Max workflow polls exceeded".to_string());
}
// Query workflow status
let query_req = WorkflowQueryBuilder::new(workflow_id).describe();
match client.execute_workflow(query_req).await {
Ok(response) => {
if let Some(status) = parse_workflow_status(&response) {
match status.as_str() {
"COMPLETED" => {
let result = extract_workflow_result(&response);
return Ok(("COMPLETED".to_string(), result));
}
"FAILED" => {
let error = parse_workflow_error(&response)
.unwrap_or_else(|| "Unknown error".to_string());
return Err(format!("Workflow failed: {}", error));
}
_ => {
debug!(
"Workflow status: {}, poll_count: {}/{}",
status, poll_count, config.max_polls
);
}
}
} else {
warn!("Could not parse workflow status from response");
}
}
Err(e) => {
return Err(format!("Workflow query failed: {}", e));
}
}
poll_count += 1;
tokio::time::sleep(tokio::time::Duration::from_millis(config.poll_delay_ms)).await;
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_parse_workflow_status_success() {
let response = json!({
"data": {
"status": "COMPLETED",
"result": {"answer": "test"}
}
});
let status = parse_workflow_status(&response);
assert_eq!(status, Some("COMPLETED".to_string()));
}
#[test]
fn test_parse_workflow_status_running() {
let response = json!({
"data": {
"status": "RUNNING"
}
});
let status = parse_workflow_status(&response);
assert_eq!(status, Some("RUNNING".to_string()));
}
#[test]
fn test_parse_workflow_status_missing() {
let response = json!({"data": {}});
let status = parse_workflow_status(&response);
assert_eq!(status, None);
}
#[test]
fn test_parse_workflow_error() {
let response = json!({
"data": {
"error": "Activity failed: LLM timeout"
}
});
let error = parse_workflow_error(&response);
assert_eq!(error, Some("Activity failed: LLM timeout".to_string()));
}
#[test]
fn test_extract_workflow_result() {
let response = json!({
"data": {
"result": {
"question": "test",
"answers": ["answer1"]
}
}
});
let result = extract_workflow_result(&response);
assert!(result.is_some());
assert_eq!(result.unwrap()["question"], "test");
}
#[test]
fn test_extract_workflow_result_none() {
let response = json!({"data": {}});
let result = extract_workflow_result(&response);
assert_eq!(result, None);
}
#[test]
fn test_poll_config_default() {
let config = PollConfig::default();
assert_eq!(config.max_polls, 30);
assert_eq!(config.poll_delay_ms, 100);
assert_eq!(config.timeout_ms, 3000);
}
}