Add 8 integration tests: HTTP lifecycle, SSE events, sub-pipeline, manager loop, graph merge, real LLM

- Change server registry_factory to accept Arc<dyn Interviewer> so WaitHumanHandler
  shares the same WebInterviewer as the REST API endpoints
- Add full HTTP lifecycle tests: approve-and-complete flow + cancel flow
- Add SSE event stream content parsing test with frame-level verification
- Add sub-pipeline E2E test through the engine with context propagation
- Add manager loop E2E test with SimulatingChildObserver
- Add graph merge E2E test verifying module prefixing and execution ordering
- Add 3 real LLM tests (#[ignore]) using claude-haiku via AnthropicAdapter
- Add dotenvy and http-body-util dev dependencies

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-02-22 13:07:26 -04:00
parent a6d5579d70
commit 08d76b93de
4 changed files with 1141 additions and 5 deletions

2
Cargo.lock generated
View file

@ -136,7 +136,9 @@ dependencies = [
"axum",
"chrono",
"coding-agent-loop",
"dotenvy",
"futures",
"http-body-util",
"nom",
"rand",
"serde",

View file

@ -36,6 +36,8 @@ tempfile = "3"
axum = "0.8"
tower = "0.5"
tokio-stream = { workspace = true, features = ["sync"] }
http-body-util = "0.1"
dotenvy.workspace = true
[lints]
workspace = true

View file

@ -18,7 +18,7 @@ use crate::engine::{PipelineEngine, RunConfig};
use crate::event::{EventEmitter, PipelineEvent};
use crate::handler::HandlerRegistry;
use crate::interviewer::web::WebInterviewer;
use crate::interviewer::{Answer, AnswerValue};
use crate::interviewer::{Answer, AnswerValue, Interviewer};
/// Status of a managed pipeline.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@ -52,7 +52,7 @@ struct ManagedPipeline {
/// Shared application state for the server.
pub struct AppState {
pipelines: Mutex<HashMap<String, ManagedPipeline>>,
registry_factory: Box<dyn Fn() -> HandlerRegistry + Send + Sync>,
registry_factory: Box<dyn Fn(Arc<dyn Interviewer>) -> HandlerRegistry + Send + Sync>,
}
/// Request body for POST /pipelines.
@ -106,8 +106,11 @@ pub fn build_router(state: Arc<AppState>) -> Router {
}
/// Create an `AppState` with the given registry factory.
///
/// The factory receives the pipeline's `WebInterviewer` so it can wire it
/// into handlers that need human-in-the-loop interaction (e.g., `WaitHumanHandler`).
pub fn create_app_state(
registry_factory: impl Fn() -> HandlerRegistry + Send + Sync + 'static,
registry_factory: impl Fn(Arc<dyn Interviewer>) -> HandlerRegistry + Send + Sync + 'static,
) -> Arc<AppState> {
Arc::new(AppState {
pipelines: Mutex::new(HashMap::new()),
@ -142,7 +145,7 @@ async fn start_pipeline(
let _ = tx_clone.send(event.clone());
});
let registry = (state.registry_factory)();
let registry = (state.registry_factory)(Arc::clone(&interviewer) as Arc<dyn Interviewer>);
let engine = PipelineEngine::new(registry, emitter);
{
@ -360,7 +363,7 @@ mod tests {
start -> exit
}"#;
fn test_registry() -> HandlerRegistry {
fn test_registry(_interviewer: Arc<dyn crate::interviewer::Interviewer>) -> HandlerRegistry {
let mut registry = HandlerRegistry::new(Box::new(StartHandler));
registry.register("start", Box::new(StartHandler));
registry.register("exit", Box::new(ExitHandler));

File diff suppressed because it is too large Load diff