Skip to content

Commit 024f016

Browse files
committed
fix: honor configured rework routing
1 parent 49db831 commit 024f016

3 files changed

Lines changed: 98 additions & 1 deletion

File tree

crates/orchestrator-core/src/services/tests.rs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ use super::*;
22
use crate::types::{
33
ArchitectureEntity, ListPageRequest, Priority, RequirementFilter, RequirementItem, RequirementPriority,
44
RequirementQuery, RequirementQuerySort, RequirementStatus, RequirementType, TaskCreateInput, TaskQuery,
5-
TaskQuerySort, TaskType, WorkflowFilter, WorkflowQuery, WorkflowQuerySort, WorkflowRunInput, WorkflowStatus,
5+
TaskQuerySort, TaskType, WorkflowDecisionRisk, WorkflowFilter, WorkflowQuery, WorkflowQuerySort, WorkflowRunInput,
6+
WorkflowStatus,
67
};
78

89
fn scoped_ao_root(project_root: &std::path::Path) -> std::path::PathBuf {
@@ -642,6 +643,94 @@ async fn file_hub_persists_workflows_with_machine_state() {
642643
assert_eq!(loaded.machine_state, crate::types::WorkflowMachineState::RunPhase);
643644
}
644645

646+
#[tokio::test]
647+
async fn file_hub_complete_phase_with_decision_honors_rework_routing() {
648+
let temp = tempfile::tempdir().expect("tempdir");
649+
let mut workflow_config = crate::load_workflow_config(temp.path()).expect("load workflow config");
650+
for (phase_id, label, category) in [
651+
("implement", "Implement", "build"),
652+
("qa-review", "QA Review", "qa"),
653+
("push-branch", "Push Branch", "git"),
654+
] {
655+
workflow_config.phase_catalog.insert(
656+
phase_id.to_string(),
657+
crate::PhaseUiDefinition {
658+
label: label.to_string(),
659+
description: String::new(),
660+
category: category.to_string(),
661+
icon: None,
662+
docs_url: None,
663+
tags: Vec::new(),
664+
visible: true,
665+
},
666+
);
667+
}
668+
workflow_config.workflows.push(crate::WorkflowDefinition {
669+
id: "routed-rework".to_string(),
670+
name: "Routed Rework".to_string(),
671+
description: "Regression test for file-backed rework routing".to_string(),
672+
phases: vec![
673+
"implement".to_string().into(),
674+
crate::WorkflowPhaseEntry::Rich(crate::WorkflowPhaseConfig {
675+
id: "qa-review".to_string(),
676+
skip_if: Vec::new(),
677+
max_rework_attempts: 3,
678+
on_verdict: std::collections::HashMap::from([(
679+
"rework".to_string(),
680+
crate::PhaseTransitionConfig {
681+
target: "implement".to_string(),
682+
guard: None,
683+
allow_agent_target: false,
684+
allowed_targets: Vec::new(),
685+
},
686+
)]),
687+
}),
688+
"push-branch".to_string().into(),
689+
],
690+
post_success: None,
691+
variables: Vec::new(),
692+
});
693+
crate::write_workflow_config(temp.path(), &workflow_config).expect("write workflow config");
694+
695+
let hub = file_hub(temp.path()).expect("create hub");
696+
let workflow = WorkflowServiceApi::run(
697+
&hub,
698+
WorkflowRunInput::for_task("TASK-routed-rework".to_string(), Some("routed-rework".to_string())),
699+
)
700+
.await
701+
.expect("run workflow");
702+
703+
let workflow =
704+
WorkflowServiceApi::complete_current_phase(&hub, &workflow.id).await.expect("complete implement phase");
705+
assert_eq!(workflow.current_phase.as_deref(), Some("qa-review"));
706+
assert_eq!(workflow.current_phase_index, 1);
707+
708+
let workflow = WorkflowServiceApi::complete_current_phase_with_decision(
709+
&hub,
710+
&workflow.id,
711+
Some(crate::PhaseDecision {
712+
kind: "phase_result".to_string(),
713+
phase_id: "qa-review".to_string(),
714+
verdict: crate::PhaseDecisionVerdict::Rework,
715+
reason: "needs fixes".to_string(),
716+
confidence: 0.9,
717+
risk: WorkflowDecisionRisk::Medium,
718+
evidence: Vec::new(),
719+
guardrail_violations: Vec::new(),
720+
commit_message: None,
721+
target_phase: None,
722+
}),
723+
)
724+
.await
725+
.expect("qa review should request rework");
726+
727+
assert_eq!(workflow.status, WorkflowStatus::Running);
728+
assert_eq!(workflow.current_phase.as_deref(), Some("implement"));
729+
assert_eq!(workflow.current_phase_index, 0);
730+
assert_eq!(workflow.phases[0].status, crate::types::WorkflowPhaseStatus::Running);
731+
assert_eq!(workflow.decision_history.last().and_then(|entry| entry.target_phase.as_deref()), Some("implement"));
732+
}
733+
645734
#[tokio::test]
646735
async fn file_hub_auto_prunes_checkpoints_on_completion_when_enabled() {
647736
let temp = tempfile::tempdir().expect("tempdir");

crates/orchestrator-core/src/services/workflow_impl.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,8 @@ impl WorkflowServiceApi for FileServiceHub {
432432
let state_machines = load_compiled_state_machines(self.project_root.as_path())?;
433433
let retry_configs = load_phase_retry_configs(self.project_root.as_path());
434434
let workflow_config = crate::load_workflow_config_or_default(self.project_root.as_path());
435+
let verdict_routing =
436+
crate::resolve_workflow_verdict_routing(&workflow_config.config, workflow.workflow_ref.as_deref());
435437
let skip_guards =
436438
crate::resolve_workflow_skip_guards(&workflow_config.config, workflow.workflow_ref.as_deref());
437439
let executor = WorkflowLifecycleExecutor::with_state_machines(
@@ -441,6 +443,7 @@ impl WorkflowServiceApi for FileServiceHub {
441443
)?,
442444
state_machines,
443445
)
446+
.with_verdict_routing_config(verdict_routing)
444447
.with_retry_configs(retry_configs)
445448
.with_skip_guards(skip_guards);
446449
executor.mark_current_phase_success_with_decision(&mut workflow, decision);

crates/orchestrator-core/src/workflow/lifecycle_executor.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,11 @@ impl WorkflowLifecycleExecutor {
154154
}
155155
}
156156

157+
pub fn with_verdict_routing_config(mut self, verdict_routing: VerdictRouting) -> Self {
158+
self.verdict_routing = verdict_routing;
159+
self
160+
}
161+
157162
pub fn with_retry_configs(mut self, configs: HashMap<String, PhaseRetryConfig>) -> Self {
158163
self.retry_configs = configs;
159164
self

0 commit comments

Comments
 (0)