2026-07-13 05:23:38 +07:00
|
|
|
//! Company-style workflow orchestrator: runs the complete division pipeline
|
2026-07-13 11:12:31 +07:00
|
|
|
//! (Strategy → Engineering → [Quality || Security || Documentation] in parallel)
|
|
|
|
|
//! with findings flowing between stages, then returns a consolidated executive
|
2026-07-13 05:23:38 +07:00
|
|
|
//! summary to the CEO (main agent).
|
|
|
|
|
//!
|
|
|
|
|
//! Flow:
|
|
|
|
|
//! ```
|
|
|
|
|
//! CEO Main Agent
|
|
|
|
|
//! │ delegates to run_company_pipeline(request)
|
|
|
|
|
//! ▼
|
|
|
|
|
//! ┌──────────────────────────────────────────────────┐
|
2026-07-13 11:12:31 +07:00
|
|
|
//! │ Strategy Division — plan + mermaid diagrams │ (runs sequentially first)
|
|
|
|
|
//! └─────────────────────────┬────────────────────────┘
|
|
|
|
|
//! ▼
|
|
|
|
|
//! ┌──────────────────────────────────────────────────┐
|
|
|
|
|
//! │ Engineering Division — implement per plan │ (runs sequentially second)
|
|
|
|
|
//! └─────────────────────────┬────────────────────────┘
|
|
|
|
|
//! ▼
|
|
|
|
|
//! ┌────────────┼────────────┐
|
|
|
|
|
//! ▼ ▼ ▼
|
|
|
|
|
//! ┌───────────┐┌───────────┐┌───────────┐
|
|
|
|
|
//! │ Quality ││ Security ││ Docs │ (run concurrently in parallel)
|
|
|
|
|
//! └───────────┘└───────────┘└───────────┘
|
|
|
|
|
//! │ │ │
|
|
|
|
|
//! └────────────┼────────────┘
|
|
|
|
|
//! ▼
|
|
|
|
|
//! CEO Main Agent delivers consolidated summary to user
|
2026-07-13 05:23:38 +07:00
|
|
|
//! ```
|
|
|
|
|
|
|
|
|
|
use std::collections::HashMap;
|
2026-07-13 08:12:02 +07:00
|
|
|
use std::fmt::Write;
|
2026-07-13 09:09:23 +07:00
|
|
|
use std::sync::{Arc, Mutex, atomic::AtomicBool};
|
2026-07-13 05:23:38 +07:00
|
|
|
use crate::app::workflow::script::{ScriptPrimitive, ScriptOptions, WorkflowScript};
|
2026-07-13 11:12:31 +07:00
|
|
|
use crate::app::workflow::engine::{execute_primitive, LiveStateFn, AgentStatus};
|
2026-07-13 05:23:38 +07:00
|
|
|
use crate::app::subagent::division;
|
|
|
|
|
|
2026-07-13 11:12:31 +07:00
|
|
|
/// Construct the 4 specialized agents for a division.
|
|
|
|
|
fn make_division_specialists(
|
|
|
|
|
div: &division::Division,
|
|
|
|
|
user_request: &str,
|
|
|
|
|
) -> Vec<ScriptPrimitive> {
|
|
|
|
|
let div_prompt = div.agent_def.system_prompt.as_deref().unwrap_or("");
|
|
|
|
|
|
|
|
|
|
let specializations = match div.name {
|
|
|
|
|
"Strategy" => vec![
|
|
|
|
|
("Architectural Analysis", "Focus on component tree, file layout, and module structure."),
|
|
|
|
|
("Data Flow Planning", "Focus on sequence of calls, interface definitions, and APIs."),
|
|
|
|
|
("Task Breakdown", "Focus on step-by-step TODO lists and implementation order."),
|
|
|
|
|
("Risk Evaluation", "Focus on edge cases, compatibility, and system constraints."),
|
|
|
|
|
],
|
|
|
|
|
"Engineering" => vec![
|
|
|
|
|
("Core Logic", "Focus on core algorithms, mathematical processing, and backend logic."),
|
|
|
|
|
("Interface & Endpoints", "Focus on implementing routes, IPC handlers, and struct mappings."),
|
|
|
|
|
("Error Handling & Logs", "Focus on implementing robust error handling, try/catch, tracing, and Result wrappers."),
|
|
|
|
|
("Utility & Helpers", "Focus on filesystem helpers, input sanitization, and parsing utilities."),
|
|
|
|
|
],
|
|
|
|
|
"Quality" => vec![
|
|
|
|
|
("Code Reviewer", "Focus on checking coding style, naming standards, and coding conventions."),
|
|
|
|
|
("Unit Testing", "Focus on writing and running unit tests for individual functions and modules."),
|
|
|
|
|
("Integration Testing", "Focus on writing and running integration tests for system interactions and state flows."),
|
|
|
|
|
("Performance Analyst", "Focus on efficiency check, bottleneck analysis, and time complexity."),
|
|
|
|
|
],
|
|
|
|
|
"Security" => vec![
|
|
|
|
|
("Dependency Auditor", "Focus on auditing cargo lock and checking dependencies for vulnerabilities."),
|
|
|
|
|
("Input Sanitizer", "Focus on auditing input validation, injection prevention, path traversal, and shell safety."),
|
|
|
|
|
("Access Control", "Focus on auditing authorization, filesystem access permissions, and API scopes."),
|
|
|
|
|
("Secrets Auditor", "Focus on auditing secrets leakage, credentials safety, and log auditing."),
|
|
|
|
|
],
|
|
|
|
|
"Documentation" => vec![
|
|
|
|
|
("README & Setup", "Focus on updating README, installation guides, usage examples, and high-level setup."),
|
|
|
|
|
("API Reference", "Focus on updating API reference, parameter details, and traits/functions documentation."),
|
|
|
|
|
("Changelog & Architecture", "Focus on updating CHANGELOG and describing system architecture/diagrams."),
|
|
|
|
|
("Inline Comments", "Focus on adding explanatory inline comments and documentation comments inside source files."),
|
|
|
|
|
],
|
|
|
|
|
_ => vec![
|
|
|
|
|
("Specialist 1", "Focus on general tasks and responsibilities of this division."),
|
|
|
|
|
("Specialist 2", "Focus on code review and validation."),
|
|
|
|
|
("Specialist 3", "Focus on error handling and reporting."),
|
|
|
|
|
("Specialist 4", "Focus on documentation and testing."),
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
specializations
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|(label, focus)| {
|
|
|
|
|
// Prepend [Division Name: Specialist Label] so the first 40 chars
|
|
|
|
|
// of the prompt become the agent_name in spawn_single_agent.
|
|
|
|
|
let prompt = format!(
|
|
|
|
|
"[{}: {}]\n\n{}\n\n{}\n\nUser request: {}\n\nFindings from previous divisions: {{findings}}",
|
|
|
|
|
div.name,
|
|
|
|
|
label,
|
|
|
|
|
focus,
|
|
|
|
|
div_prompt,
|
|
|
|
|
user_request,
|
|
|
|
|
);
|
|
|
|
|
ScriptPrimitive::Agent(prompt)
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Construct a named Phase wrapper containing a Parallel block of division specialists.
|
|
|
|
|
fn make_division_phase(
|
|
|
|
|
div: &division::Division,
|
|
|
|
|
user_request: &str,
|
|
|
|
|
) -> ScriptPrimitive {
|
|
|
|
|
let specialists = make_division_specialists(div, user_request);
|
|
|
|
|
ScriptPrimitive::Phase {
|
|
|
|
|
name: div.name.to_string(),
|
|
|
|
|
script: Box::new(ScriptPrimitive::Parallel(specialists)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-13 05:23:38 +07:00
|
|
|
/// Run the full company-style pipeline for a given user request.
|
|
|
|
|
///
|
2026-07-13 11:12:31 +07:00
|
|
|
/// This orchestrates all five divisions, running Strategy and Engineering
|
|
|
|
|
/// sequentially, followed by Quality, Security, and Documentation in parallel:
|
|
|
|
|
/// 1. **Strategy** — create plan with diagrams (4 parallel subagents)
|
|
|
|
|
/// 2. **Engineering** — implement code per the plan (4 parallel subagents)
|
|
|
|
|
/// 3. **Quality** || **Security** || **Documentation** (in parallel, up to 10 concurrent subagents total)
|
2026-07-13 05:23:38 +07:00
|
|
|
///
|
|
|
|
|
/// Each division receives findings from all previous divisions, enabling
|
|
|
|
|
/// context to flow through the pipeline.
|
|
|
|
|
///
|
|
|
|
|
/// Returns a consolidated executive summary string.
|
|
|
|
|
pub fn run_company_pipeline(
|
|
|
|
|
user_request: &str,
|
|
|
|
|
session_dir: &std::path::Path,
|
|
|
|
|
workspaces: &[std::path::PathBuf],
|
|
|
|
|
turn_events: Option<&Arc<Mutex<std::collections::VecDeque<crate::app::state::runtime::TurnEvent>>>>,
|
2026-07-13 09:09:23 +07:00
|
|
|
abort_flag: &Option<Arc<AtomicBool>>,
|
2026-07-13 05:23:38 +07:00
|
|
|
) -> anyhow::Result<String> {
|
|
|
|
|
let divisions = division::all_divisions();
|
|
|
|
|
|
2026-07-13 11:12:31 +07:00
|
|
|
let strategy_phase = make_division_phase(&divisions[0], user_request);
|
|
|
|
|
let engineering_phase = make_division_phase(&divisions[1], user_request);
|
|
|
|
|
|
|
|
|
|
let quality_phase = make_division_phase(&divisions[2], user_request);
|
|
|
|
|
let security_phase = make_division_phase(&divisions[3], user_request);
|
|
|
|
|
let documentation_phase = make_division_phase(&divisions[4], user_request);
|
|
|
|
|
|
|
|
|
|
let parallel_divisions = ScriptPrimitive::Parallel(vec![
|
|
|
|
|
quality_phase,
|
|
|
|
|
security_phase,
|
|
|
|
|
documentation_phase,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
let pipeline_primitive = ScriptPrimitive::Pipeline(vec![
|
|
|
|
|
strategy_phase,
|
|
|
|
|
engineering_phase,
|
|
|
|
|
parallel_divisions,
|
|
|
|
|
]);
|
2026-07-13 05:23:38 +07:00
|
|
|
|
|
|
|
|
let wf = WorkflowScript {
|
|
|
|
|
name: "company-pipeline".to_string(),
|
2026-07-13 11:12:31 +07:00
|
|
|
description: "Company Pipeline: Strategy → Engineering → (Quality || Security || Documentation)".to_string(),
|
|
|
|
|
script: pipeline_primitive,
|
2026-07-13 05:23:38 +07:00
|
|
|
options: ScriptOptions {
|
2026-07-13 11:12:31 +07:00
|
|
|
max_concurrency: 10, // Max concurrent agents in execution
|
2026-07-13 05:23:38 +07:00
|
|
|
continue_on_error: true, // one division failing shouldn't block the rest
|
|
|
|
|
timeout_ms: None,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Build a live callback for TUI updates if turn_events is available.
|
2026-07-13 11:12:31 +07:00
|
|
|
// Uses agent_name (division + specialist name) for the display label in the panel.
|
2026-07-13 05:23:38 +07:00
|
|
|
let live: Option<LiveStateFn> = turn_events.map(|events| {
|
|
|
|
|
let events = events.clone();
|
|
|
|
|
let f: LiveStateFn = Arc::new(move |_agent_id: String, agent_name: String, status: AgentStatus| {
|
|
|
|
|
let display_name = agent_name.chars().take(30).collect::<String>();
|
|
|
|
|
if let Ok(mut q) = events.lock() {
|
|
|
|
|
q.push_back(crate::app::state::runtime::TurnEvent::WorkflowAgentUpdate {
|
|
|
|
|
agent_id: display_name.clone(),
|
|
|
|
|
agent_name: display_name,
|
|
|
|
|
status,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
f
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let args: HashMap<String, String> = HashMap::new();
|
|
|
|
|
let live_ref = live.as_ref();
|
|
|
|
|
|
|
|
|
|
// Create a per-pipeline findings scope so divisions can pass data
|
|
|
|
|
let findings: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
|
|
|
|
|
|
|
|
|
|
let results = execute_primitive(
|
|
|
|
|
&wf.script,
|
|
|
|
|
&args,
|
2026-07-13 11:12:31 +07:00
|
|
|
wf.options.max_concurrency,
|
2026-07-13 05:23:38 +07:00
|
|
|
true,
|
2026-07-13 09:09:23 +07:00
|
|
|
abort_flag,
|
2026-07-13 05:23:38 +07:00
|
|
|
live_ref,
|
|
|
|
|
session_dir,
|
|
|
|
|
workspaces,
|
|
|
|
|
&findings,
|
|
|
|
|
None,
|
|
|
|
|
)?;
|
|
|
|
|
|
|
|
|
|
// Collect all findings for the executive summary
|
|
|
|
|
let all_findings = findings.lock()
|
|
|
|
|
.map(|f| f.clone())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
Ok(build_executive_summary(user_request, &results, &all_findings, &divisions))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Run a quick company pipeline that skips non-essential divisions
|
|
|
|
|
/// for simple tasks. Flow: Strategy → Engineering → Quality.
|
|
|
|
|
///
|
|
|
|
|
/// This is for smaller tasks where security audit and full docs are overkill.
|
|
|
|
|
pub fn run_company_pipeline_quick(
|
|
|
|
|
user_request: &str,
|
|
|
|
|
session_dir: &std::path::Path,
|
|
|
|
|
workspaces: &[std::path::PathBuf],
|
|
|
|
|
turn_events: Option<&Arc<Mutex<std::collections::VecDeque<crate::app::state::runtime::TurnEvent>>>>,
|
2026-07-13 09:09:23 +07:00
|
|
|
abort_flag: &Option<Arc<AtomicBool>>,
|
2026-07-13 05:23:38 +07:00
|
|
|
) -> anyhow::Result<String> {
|
|
|
|
|
let divisions = division::all_divisions();
|
|
|
|
|
// Only use first 3 divisions for quick pipeline: Strategy, Engineering, Quality
|
|
|
|
|
let quick_divisions = &divisions[..3];
|
|
|
|
|
|
2026-07-13 11:12:31 +07:00
|
|
|
let strategy_phase = make_division_phase(&quick_divisions[0], user_request);
|
|
|
|
|
let engineering_phase = make_division_phase(&quick_divisions[1], user_request);
|
|
|
|
|
let quality_phase = make_division_phase(&quick_divisions[2], user_request);
|
|
|
|
|
|
|
|
|
|
let pipeline_primitive = ScriptPrimitive::Pipeline(vec![
|
|
|
|
|
strategy_phase,
|
|
|
|
|
engineering_phase,
|
|
|
|
|
quality_phase,
|
|
|
|
|
]);
|
2026-07-13 05:23:38 +07:00
|
|
|
|
|
|
|
|
let wf = WorkflowScript {
|
|
|
|
|
name: "company-pipeline-quick".to_string(),
|
|
|
|
|
description: "Company Pipeline (quick): Strategy → Engineering → Quality".to_string(),
|
2026-07-13 11:12:31 +07:00
|
|
|
script: pipeline_primitive,
|
2026-07-13 05:23:38 +07:00
|
|
|
options: ScriptOptions {
|
2026-07-13 11:12:31 +07:00
|
|
|
max_concurrency: 10,
|
2026-07-13 05:23:38 +07:00
|
|
|
continue_on_error: true,
|
|
|
|
|
timeout_ms: None,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let live: Option<LiveStateFn> = turn_events.map(|events| {
|
|
|
|
|
let events = events.clone();
|
|
|
|
|
let f: LiveStateFn = Arc::new(move |_agent_id: String, agent_name: String, status: AgentStatus| {
|
|
|
|
|
let display_name = agent_name.chars().take(30).collect::<String>();
|
|
|
|
|
if let Ok(mut q) = events.lock() {
|
|
|
|
|
q.push_back(crate::app::state::runtime::TurnEvent::WorkflowAgentUpdate {
|
|
|
|
|
agent_id: display_name.clone(),
|
|
|
|
|
agent_name: display_name,
|
|
|
|
|
status,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
f
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let args: HashMap<String, String> = HashMap::new();
|
|
|
|
|
let findings: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
|
|
|
|
|
|
|
|
|
|
let results = execute_primitive(
|
2026-07-13 11:12:31 +07:00
|
|
|
&wf.script, &args, wf.options.max_concurrency, true,
|
2026-07-13 09:09:23 +07:00
|
|
|
abort_flag, live.as_ref(), session_dir, workspaces, &findings, None,
|
2026-07-13 05:23:38 +07:00
|
|
|
)?;
|
|
|
|
|
|
|
|
|
|
let all_findings = findings.lock()
|
|
|
|
|
.map(|f| f.clone())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
Ok(build_executive_summary(user_request, &results, &all_findings, quick_divisions))
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-13 05:33:04 +07:00
|
|
|
/// Build a compressed executive summary from pipeline results.
|
|
|
|
|
///
|
|
|
|
|
/// Keeps output brief to save context window space — just division verdicts
|
|
|
|
|
/// and key findings, not full outputs. Full results are accessible to the
|
|
|
|
|
/// CEO via the notes/findings that were archived during execution.
|
2026-07-13 05:23:38 +07:00
|
|
|
fn build_executive_summary(
|
|
|
|
|
request: &str,
|
|
|
|
|
results: &[String],
|
|
|
|
|
findings: &[String],
|
|
|
|
|
divisions: &[division::Division],
|
|
|
|
|
) -> String {
|
|
|
|
|
let mut summary = String::new();
|
2026-07-13 08:12:02 +07:00
|
|
|
writeln!(summary, "Pipeline for: {request}").unwrap();
|
2026-07-13 05:23:38 +07:00
|
|
|
|
|
|
|
|
for (i, div) in divisions.iter().enumerate() {
|
2026-07-13 11:12:31 +07:00
|
|
|
let mut division_verdicts = Vec::new();
|
|
|
|
|
for offset in 0..4 {
|
|
|
|
|
if let Some(r) = results.get(4 * i + offset) {
|
|
|
|
|
let first_line = r.lines().next().unwrap_or(r);
|
|
|
|
|
let trimmed = first_line.chars().take(40).collect::<String>();
|
|
|
|
|
division_verdicts.push(trimmed);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let verdict = if division_verdicts.is_empty() {
|
|
|
|
|
"—".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
division_verdicts.join(" | ")
|
|
|
|
|
};
|
2026-07-13 05:23:38 +07:00
|
|
|
|
2026-07-13 08:12:02 +07:00
|
|
|
writeln!(summary, " {}: {}", div.name, verdict).unwrap();
|
2026-07-13 05:23:38 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !findings.is_empty() {
|
2026-07-13 08:12:02 +07:00
|
|
|
writeln!(summary, " Notes: {} cross-division finding(s)", findings.len()).unwrap();
|
2026-07-13 05:23:38 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
summary
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Determine whether a request is complex enough for the full pipeline
|
|
|
|
|
/// or can use the quick version.
|
|
|
|
|
///
|
|
|
|
|
/// Simple = single file, minor fix, quick lookup, config change.
|
|
|
|
|
/// Complex = new feature, multi-file refactor, architecture change.
|
|
|
|
|
///
|
2026-07-13 08:12:02 +07:00
|
|
|
/// Used by the auto-CEO pipeline trigger in `run_agent_turn` to decide
|
2026-07-13 05:23:38 +07:00
|
|
|
/// whether to delegate to the full company pipeline or handle directly.
|
2026-07-13 05:33:04 +07:00
|
|
|
///
|
|
|
|
|
/// Heuristics:
|
|
|
|
|
/// - Very short requests (< 10 chars) are never complex.
|
|
|
|
|
/// - Negative keywords (simple/trivial/typo/quick) skip the pipeline.
|
|
|
|
|
/// - Positive keywords (refactor/api/implement/architecture) trigger it.
|
|
|
|
|
/// - Multi-line or multi-sentence requests are more likely complex.
|
2026-07-13 05:23:38 +07:00
|
|
|
pub fn is_complex_request(request: &str) -> bool {
|
2026-07-13 05:33:04 +07:00
|
|
|
let trimmed = request.trim();
|
|
|
|
|
// Very short requests are never complex
|
|
|
|
|
if trimmed.len() < 10 {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
// Single-line simple update patterns
|
|
|
|
|
let lower = trimmed.to_lowercase();
|
|
|
|
|
let negative_keywords = [
|
|
|
|
|
"simple", "trivial", "typo", "just a", "only a", "minor",
|
|
|
|
|
"quick", "tiny", "small fix", "rename", "nitpick",
|
|
|
|
|
"cosmetic", "formatting", "spelling", "grammar",
|
|
|
|
|
"bump", "version bump", "update comment",
|
|
|
|
|
];
|
|
|
|
|
if negative_keywords.iter().any(|k| lower.contains(k)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
// Multi-line/multi-sentence → likely complex
|
2026-07-13 08:12:02 +07:00
|
|
|
let sentences = trimmed.split(['.', '!', '?'])
|
2026-07-13 05:33:04 +07:00
|
|
|
.filter(|s| !s.trim().is_empty())
|
|
|
|
|
.count();
|
|
|
|
|
if sentences >= 3 {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
// Positive complexity keywords
|
2026-07-13 05:23:38 +07:00
|
|
|
let complexity_keywords = [
|
|
|
|
|
"refactor", "redesign", "architecture", "feature", "implement",
|
|
|
|
|
"migrate", "restructure", "rewrite", "new module", "new component",
|
|
|
|
|
"scaffold", "multi", "multiple files", "api", "endpoint",
|
|
|
|
|
"integration", "system", "workflow", "pipeline", "database",
|
2026-07-13 05:33:04 +07:00
|
|
|
"authentication", "authorization", "full stack",
|
2026-07-13 05:23:38 +07:00
|
|
|
];
|
|
|
|
|
complexity_keywords.iter().any(|k| lower.contains(k))
|
|
|
|
|
}
|
2026-07-13 11:12:31 +07:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_is_complex_request_too_short() {
|
|
|
|
|
assert!(!is_complex_request("abc"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_is_complex_request_simple_keywords() {
|
|
|
|
|
assert!(!is_complex_request("just a simple update to the readme"));
|
|
|
|
|
assert!(!is_complex_request("minor typo fix in main.rs"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_is_complex_request_multi_sentence() {
|
|
|
|
|
assert!(is_complex_request("This is sentence one. This is sentence two. This is sentence three."));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_is_complex_request_complex_keywords() {
|
|
|
|
|
assert!(is_complex_request("implement user authentication endpoint"));
|
|
|
|
|
assert!(is_complex_request("refactor the whole engine module"));
|
|
|
|
|
}
|
|
|
|
|
}
|