Skip to content

Commit 107ad38

Browse files
authored
Merge pull request #2406 from guantw/fix/review-team-concurrency-config-persistence
fix(config): persist review team concurrency config fields
2 parents c8e466d + 9839785 commit 107ad38

2 files changed

Lines changed: 176 additions & 0 deletions

File tree

src/crates/assembly/core/src/service/config/service.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,90 @@ mod tests {
636636
(service, dir)
637637
}
638638

639+
#[tokio::test]
640+
async fn review_team_policy_config_survives_service_restart() {
641+
let dir = tempfile::tempdir().expect("tempdir");
642+
let path_manager = Arc::new(PathManager::with_user_root_for_tests(
643+
dir.path().join("review-team-concurrency"),
644+
));
645+
let settings = || ConfigManagerSettings {
646+
path_manager: Some(path_manager.clone()),
647+
auto_save: true,
648+
backup_count: 0,
649+
};
650+
651+
let service = ConfigService::with_settings(settings())
652+
.await
653+
.expect("config service should start");
654+
service
655+
.set_config(
656+
"ai.review_teams.default",
657+
serde_json::json!({
658+
"max_retries_per_role": 2,
659+
"max_parallel_reviewers": 1,
660+
"max_queue_wait_seconds": 45,
661+
"allow_provider_capacity_queue": false,
662+
"allow_bounded_auto_retry": true,
663+
"auto_retry_elapsed_guard_seconds": 240,
664+
}),
665+
)
666+
.await
667+
.expect("review team concurrency config should save");
668+
669+
let persisted: serde_json::Value = serde_json::from_str(
670+
&tokio::fs::read_to_string(path_manager.app_config_file())
671+
.await
672+
.expect("review team config should be persisted"),
673+
)
674+
.expect("persisted config should be valid JSON");
675+
let persisted_team = &persisted["ai"]["review_teams"]["default"];
676+
assert_eq!(persisted_team["max_retries_per_role"], serde_json::json!(2));
677+
assert_eq!(
678+
persisted_team["max_parallel_reviewers"],
679+
serde_json::json!(1)
680+
);
681+
assert_eq!(
682+
persisted_team["max_queue_wait_seconds"],
683+
serde_json::json!(45)
684+
);
685+
assert_eq!(
686+
persisted_team["allow_provider_capacity_queue"],
687+
serde_json::json!(false)
688+
);
689+
assert_eq!(
690+
persisted_team["allow_bounded_auto_retry"],
691+
serde_json::json!(true)
692+
);
693+
assert_eq!(
694+
persisted_team["auto_retry_elapsed_guard_seconds"],
695+
serde_json::json!(240)
696+
);
697+
698+
drop(service);
699+
let reloaded_service = ConfigService::with_settings(settings())
700+
.await
701+
.expect("config service should reload");
702+
let reloaded: serde_json::Value = reloaded_service
703+
.get_config(Some("ai.review_teams.default"))
704+
.await
705+
.expect("review team config should be readable after reload");
706+
assert_eq!(reloaded["max_retries_per_role"], serde_json::json!(2));
707+
assert_eq!(reloaded["max_parallel_reviewers"], serde_json::json!(1));
708+
assert_eq!(reloaded["max_queue_wait_seconds"], serde_json::json!(45));
709+
assert_eq!(
710+
reloaded["allow_provider_capacity_queue"],
711+
serde_json::json!(false)
712+
);
713+
assert_eq!(
714+
reloaded["allow_bounded_auto_retry"],
715+
serde_json::json!(true)
716+
);
717+
assert_eq!(
718+
reloaded["auto_retry_elapsed_guard_seconds"],
719+
serde_json::json!(240)
720+
);
721+
}
722+
639723
#[tokio::test]
640724
async fn compare_and_set_json_config_rejects_a_stale_snapshot() {
641725
let (service, _dir) = test_service("config-cas").await;

src/crates/assembly/core/src/service/config/types.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,18 @@ pub struct ReviewTeamConfig {
707707
pub reviewer_file_split_threshold: usize,
708708
/// Maximum number of same-role reviewer instances per role when file splitting is active.
709709
pub max_same_role_instances: usize,
710+
/// Maximum retries for a failed same-role reviewer instance.
711+
pub max_retries_per_role: usize,
712+
/// Maximum number of review instances that may run at the same time.
713+
pub max_parallel_reviewers: usize,
714+
/// Seconds to wait for provider capacity before skipping unstarted work. 0 skips immediately.
715+
pub max_queue_wait_seconds: u64,
716+
/// Whether unstarted review work may wait for provider capacity.
717+
pub allow_provider_capacity_queue: bool,
718+
/// Whether bounded automatic retry is allowed after a reviewer failure.
719+
pub allow_bounded_auto_retry: bool,
720+
/// Elapsed-seconds guard that blocks bounded automatic retry after this delay.
721+
pub auto_retry_elapsed_guard_seconds: u64,
710722
}
711723

712724
impl Default for ReviewTeamConfig {
@@ -720,6 +732,12 @@ impl Default for ReviewTeamConfig {
720732
auto_fix_enabled: false,
721733
reviewer_file_split_threshold: 20,
722734
max_same_role_instances: 3,
735+
max_retries_per_role: 1,
736+
max_parallel_reviewers: 2,
737+
max_queue_wait_seconds: 1200,
738+
allow_provider_capacity_queue: true,
739+
allow_bounded_auto_retry: false,
740+
auto_retry_elapsed_guard_seconds: 180,
723741
}
724742
}
725743
}
@@ -3051,6 +3069,80 @@ mod tests {
30513069
);
30523070
}
30533071

3072+
#[test]
3073+
fn preserves_review_team_concurrency_fields_through_config_round_trip() {
3074+
let config: AIConfig = serde_json::from_value(serde_json::json!({
3075+
"models": [],
3076+
"default_models": {},
3077+
"agent_profiles": {},
3078+
"review_teams": {
3079+
"default": {
3080+
"extra_subagent_ids": [],
3081+
"strategy_level": "normal",
3082+
"member_strategy_overrides": {},
3083+
"reviewer_timeout_seconds": 3600,
3084+
"judge_timeout_seconds": 2400,
3085+
"reviewer_file_split_threshold": 20,
3086+
"max_same_role_instances": 3,
3087+
"max_retries_per_role": 1,
3088+
"max_parallel_reviewers": 1,
3089+
"max_queue_wait_seconds": 0,
3090+
"allow_provider_capacity_queue": true,
3091+
"allow_bounded_auto_retry": false,
3092+
"auto_retry_elapsed_guard_seconds": 180
3093+
}
3094+
},
3095+
"proxy": {
3096+
"enabled": false,
3097+
"url": ""
3098+
}
3099+
}))
3100+
.expect("review team concurrency config should deserialize");
3101+
3102+
let serialized = serde_json::to_value(&config).expect("config should serialize");
3103+
let stored = &serialized["review_teams"]["default"];
3104+
assert_eq!(stored["max_retries_per_role"], serde_json::json!(1));
3105+
assert_eq!(stored["max_parallel_reviewers"], serde_json::json!(1));
3106+
assert_eq!(stored["max_queue_wait_seconds"], serde_json::json!(0));
3107+
assert_eq!(
3108+
stored["allow_provider_capacity_queue"],
3109+
serde_json::json!(true)
3110+
);
3111+
assert_eq!(stored["allow_bounded_auto_retry"], serde_json::json!(false));
3112+
assert_eq!(
3113+
stored["auto_retry_elapsed_guard_seconds"],
3114+
serde_json::json!(180)
3115+
);
3116+
}
3117+
3118+
#[test]
3119+
fn missing_review_team_concurrency_fields_use_product_defaults() {
3120+
let config: AIConfig = serde_json::from_value(serde_json::json!({
3121+
"models": [],
3122+
"review_teams": {
3123+
"default": {
3124+
"strategy_level": "normal"
3125+
}
3126+
}
3127+
}))
3128+
.expect("legacy review team config should deserialize");
3129+
3130+
let serialized = serde_json::to_value(&config).expect("config should serialize");
3131+
let stored = &serialized["review_teams"]["default"];
3132+
assert_eq!(stored["max_retries_per_role"], serde_json::json!(1));
3133+
assert_eq!(stored["max_parallel_reviewers"], serde_json::json!(2));
3134+
assert_eq!(stored["max_queue_wait_seconds"], serde_json::json!(1200));
3135+
assert_eq!(
3136+
stored["allow_provider_capacity_queue"],
3137+
serde_json::json!(true)
3138+
);
3139+
assert_eq!(stored["allow_bounded_auto_retry"], serde_json::json!(false));
3140+
assert_eq!(
3141+
stored["auto_retry_elapsed_guard_seconds"],
3142+
serde_json::json!(180)
3143+
);
3144+
}
3145+
30543146
#[test]
30553147
fn review_team_auxiliary_config_is_not_stored_inside_review_team_map() {
30563148
let config: AIConfig = serde_json::from_value(serde_json::json!({

0 commit comments

Comments
 (0)