Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions apps/decodex/src/orchestrator/operator_dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -5047,6 +5047,17 @@ <h2 id="recent-title">Run History</h2>
return Number.isFinite(numericCount) && numericCount > 0 ? String(numericCount) : "NONE";
}

function postReviewReadbackFacts(lane) {
const facts = [];
if (lane.readback_warning) {
facts.push(["Readback", lane.readback_warning]);
}
if (lane.readback_root_cause) {
facts.push(["Root cause", lane.readback_root_cause]);
}
return facts;
}

function renderAttentionFacts(candidate) {
const attention = candidate.attention;
if (!attention) {
Expand Down Expand Up @@ -8724,6 +8735,7 @@ <h2 id="recent-title">Run History</h2>
["Threads", reviewThreadToken(lane.unresolved_review_threads)],
["PR", optionalCardToken(lane.pr_url)],
["Branch", optionalCardToken(lane.branch_name)],
...postReviewReadbackFacts(lane),
],
});
continue;
Expand All @@ -8746,6 +8758,7 @@ <h2 id="recent-title">Run History</h2>
["Threads", reviewThreadToken(lane.unresolved_review_threads)],
["PR", optionalCardToken(lane.pr_url)],
["Branch", optionalCardToken(lane.branch_name)],
...postReviewReadbackFacts(lane),
],
});
continue;
Expand All @@ -8764,6 +8777,7 @@ <h2 id="recent-title">Run History</h2>
["Threads", reviewThreadToken(lane.unresolved_review_threads)],
["PR", optionalCardToken(lane.pr_url)],
["Branch", optionalCardToken(lane.branch_name)],
...postReviewReadbackFacts(lane),
],
});
continue;
Expand All @@ -8783,6 +8797,7 @@ <h2 id="recent-title">Run History</h2>
["Threads", reviewThreadToken(lane.unresolved_review_threads)],
["PR", optionalCardToken(lane.pr_url)],
["Branch", optionalCardToken(lane.branch_name)],
...postReviewReadbackFacts(lane),
],
});
}
Expand Down
49 changes: 45 additions & 4 deletions apps/decodex/src/orchestrator/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2370,6 +2370,11 @@ where
let review_state = review_state_inspector
.inspect_review_state(worktree.worktree_path(), review_handoff.pr_url())
.ok();
let readback_root_cause = Some(
PullRequestReadbackRootCause::TrackerIssueReadbackFailed
.as_str()
.to_owned(),
);
let (
pr_head_sha,
pr_state,
Expand Down Expand Up @@ -2412,6 +2417,7 @@ where
check_state,
unresolved_review_threads,
readback_warning: Some(String::from("tracker_issue_readback_degraded")),
readback_root_cause,
});
}

Expand Down Expand Up @@ -2622,6 +2628,7 @@ fn post_review_lane_status_from_classification(
check_state: classification.check_state,
unresolved_review_threads: classification.unresolved_review_threads,
readback_warning: classification.readback_warning,
readback_root_cause: classification.readback_root_cause,
}
}

Expand Down Expand Up @@ -3149,14 +3156,15 @@ where
},
};
let review_state = match review_state_inspector
.inspect_review_state(snapshot.worktree.worktree_path(), review_handoff.pr_url())
.inspect_review_state_readback(snapshot.worktree.worktree_path(), review_handoff.pr_url())
{
Ok(review_state) => review_state,
Err(_error) => {
Err(error) => {
return Ok(PostReviewLaneStateLoad::Classification(
readback_degraded_post_review_lane_from_handoff(
review_handoff,
"pull_request_state_read_failed",
error.root_cause(),
),
));
},
Expand Down Expand Up @@ -3506,6 +3514,7 @@ fn initial_post_review_lane_classification(
check_state: review_state.status_check_rollup_state.clone(),
unresolved_review_threads: Some(review_state.unresolved_review_threads),
readback_warning: None,
readback_root_cause: None,
}
}

Expand All @@ -3517,6 +3526,8 @@ fn blocked_post_review_lane_from_state(

classification.decision = PostReviewLaneDecision::Block;
classification.reason = reason.to_owned();
classification.readback_root_cause = post_review_readback_root_cause_for_reason(reason)
.map(|root_cause| root_cause.as_str().to_owned());

classification
}
Expand All @@ -3533,6 +3544,8 @@ fn blocked_post_review_lane(reason: &str) -> PostReviewLaneClassification {
check_state: None,
unresolved_review_threads: None,
readback_warning: None,
readback_root_cause: post_review_readback_root_cause_for_reason(reason)
.map(|root_cause| root_cause.as_str().to_owned()),
}
}

Expand All @@ -3551,6 +3564,7 @@ fn blocked_post_review_lane_from_handoff(
fn readback_degraded_post_review_lane_from_handoff(
review_handoff: &ReviewHandoffMarker,
reason: &str,
root_cause: PullRequestReadbackRootCause,
) -> PostReviewLaneClassification {
PostReviewLaneClassification {
decision: PostReviewLaneDecision::WaitForReview,
Expand All @@ -3563,6 +3577,7 @@ fn readback_degraded_post_review_lane_from_handoff(
check_state: None,
unresolved_review_threads: None,
readback_warning: Some(reason.to_owned()),
readback_root_cause: Some(root_cause.as_str().to_owned()),
}
}

Expand All @@ -3588,6 +3603,31 @@ fn blocked_post_review_lane_status(
check_state: None,
unresolved_review_threads: None,
readback_warning: None,
readback_root_cause: post_review_readback_root_cause_for_reason(reason)
.map(|root_cause| root_cause.as_str().to_owned()),
}
}

fn post_review_readback_root_cause_for_reason(
reason: &str,
) -> Option<PullRequestReadbackRootCause> {
match reason {
"pull_request_repository_parse_failed" => {
Some(PullRequestReadbackRootCause::PullRequestShapeReadFailed)
},
"pull_request_branch_mismatch"
| "pull_request_head_mismatch"
| "pull_request_head_repository_name_mismatch"
| "pull_request_head_repository_owner_mismatch"
| "pull_request_merge_commit_lineage_check_failed"
| "review_handoff_lineage_check_failed"
| "review_handoff_lineage_mismatch"
| "review_orchestration_branch_mismatch"
| "review_orchestration_head_mismatch"
| "review_orchestration_pr_mismatch" => {
Some(PullRequestReadbackRootCause::LineageValidationFailed)
},
_ => None,
}
}

Expand Down Expand Up @@ -4891,7 +4931,7 @@ fn render_operator_status(snapshot: &OperatorStatusSnapshot) -> String {
} else {
for lane in &snapshot.post_review_lanes {
output.push_str(&format!(
"- issue_id: {}\n issue: {}\n state: {}\n classification: {}\n reason: {}\n branch: {}\n worktree_path: {}\n pr_url: {}\n pr_head_sha: {}\n pr_state: {}\n review_decision: {}\n mergeable: {}\n check_state: {}\n unresolved_review_threads: {}\n readback_warning: {}\n",
"- issue_id: {}\n issue: {}\n state: {}\n classification: {}\n reason: {}\n branch: {}\n worktree_path: {}\n pr_url: {}\n pr_head_sha: {}\n pr_state: {}\n review_decision: {}\n mergeable: {}\n check_state: {}\n unresolved_review_threads: {}\n readback_warning: {}\n readback_root_cause: {}\n",
lane.issue_id,
lane.issue_identifier,
lane.issue_state,
Expand All @@ -4908,7 +4948,8 @@ fn render_operator_status(snapshot: &OperatorStatusSnapshot) -> String {
lane
.unresolved_review_threads
.map_or_else(|| String::from("none"), |value| value.to_string()),
lane.readback_warning.as_deref().unwrap_or("none")
lane.readback_warning.as_deref().unwrap_or("none"),
lane.readback_root_cause.as_deref().unwrap_or("none")
));
}
}
Expand Down
12 changes: 12 additions & 0 deletions apps/decodex/src/orchestrator/tests/operator/status/publishing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ fn live_operator_status_snapshot_preserves_retained_handoff_during_linear_backof
snapshot.post_review_lanes[0].readback_warning.as_deref(),
Some("tracker_issue_readback_degraded")
);
assert_eq!(
snapshot.post_review_lanes[0].readback_root_cause.as_deref(),
Some("tracker_issue_readback_failed")
);
assert!(
state_store
.connector_backoff(TEST_SERVICE_ID, "linear")
Expand Down Expand Up @@ -216,6 +220,14 @@ fn operator_state_snapshot_reports_tracker_rate_limit_as_backoff() {
assert_eq!(snapshot.post_review_lanes[0].reason, "tracker_issue_readback_degraded");
assert_eq!(snapshot.post_review_lanes[0].pr_url.as_deref(), Some(pr_url));
assert_eq!(snapshot.post_review_lanes[0].pr_head_sha.as_deref(), Some(head_sha));
assert_eq!(
snapshot.post_review_lanes[0].readback_root_cause.as_deref(),
Some("tracker_issue_readback_failed")
);
assert_eq!(
snapshot_json["post_review_lanes"][0]["readback_root_cause"],
"tracker_issue_readback_failed"
);
assert_eq!(snapshot.projects[0].connector_state, "backoff");
assert!(
tracker.label_queries.borrow().is_empty(),
Expand Down
2 changes: 2 additions & 0 deletions apps/decodex/src/orchestrator/tests/operator/status/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,12 +218,14 @@ fn operator_status_text_surfaces_cleanup_blocker_pr_url() {
check_state: Some(String::from("SUCCESS")),
unresolved_review_threads: Some(0),
readback_warning: None,
readback_root_cause: Some(String::from("lineage_validation_failed")),
}],
};
let rendered = orchestrator::render_operator_status(&snapshot);

assert!(rendered.contains("classification: cleanup_blocked"));
assert!(rendered.contains("reason: retry_budget_exhausted"));
assert!(rendered.contains("readback_root_cause: lineage_validation_failed"));
assert!(rendered.contains(&format!("pr_url: {pr_url}")));
assert!(!rendered.contains("pr_url: none"));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@ fn operator_status_text_post_review_lanes() -> Vec<OperatorPostReviewLaneStatus>
check_state: Some(String::from("SUCCESS")),
unresolved_review_threads: Some(0),
readback_warning: None,
readback_root_cause: None,
}]
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use orchestrator::PullRequestReadbackFailure;

#[test]
fn classify_post_review_lane_blocks_completed_issue_until_pull_request_is_merged() {
let temp_dir = TempDir::new().expect("temp dir should exist");
Expand Down Expand Up @@ -446,6 +448,10 @@ fn classify_post_review_lane_degrades_pull_request_state_read_failures_to_handof
classification.readback_warning.as_deref(),
Some("pull_request_state_read_failed")
);
assert_eq!(
classification.readback_root_cause.as_deref(),
Some("github_api_read_failed")
);
}

#[test]
Expand All @@ -459,6 +465,10 @@ fn classify_post_review_lane_degrades_missing_or_blank_github_token_env_var() {
missing.readback_warning.as_deref(),
Some("pull_request_state_read_failed")
);
assert_eq!(
missing.readback_root_cause.as_deref(),
Some("missing_github_token")
);

let env_var = format!("DECODEX_TEST_BLANK_STATUS_GITHUB_TOKEN_ENV_{}", process::id());
let _env_guard = TestEnvVarGuard::set(&env_var, "");
Expand All @@ -471,6 +481,30 @@ fn classify_post_review_lane_degrades_missing_or_blank_github_token_env_var() {
blank.readback_warning.as_deref(),
Some("pull_request_state_read_failed")
);
assert_eq!(
blank.readback_root_cause.as_deref(),
Some("missing_github_token")
);
}

#[test]
fn pull_request_readback_root_cause_classifier_maps_cli_and_shape_failures() {
let missing_cli_error = Command::new("decodex-test-missing-gh-command")
.output()
.expect_err("missing command should fail with an io error");
let missing_cli = PullRequestReadbackFailure::from_report(Report::from(missing_cli_error));
let shape_failure = PullRequestReadbackFailure::from_report(eyre::eyre!(
"GitHub GraphQL response for `https://github.com/hack-ink/decodex/pull/174` did not include a pull request."
));

assert_eq!(
missing_cli.root_cause(),
orchestrator::PullRequestReadbackRootCause::MissingGithubCli
);
assert_eq!(
shape_failure.root_cause(),
orchestrator::PullRequestReadbackRootCause::PullRequestShapeReadFailed
);
}

fn classify_post_review_lane_with_github_token_env_var(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ fn build_post_review_lane_statuses_preserves_handoff_marker_when_pr_readback_fai
lanes[0].readback_warning.as_deref(),
Some("pull_request_state_read_failed")
);
assert_eq!(
lanes[0].readback_root_cause.as_deref(),
Some("github_api_read_failed")
);
assert_eq!(lanes[0].pr_state, None);
}

Expand Down Expand Up @@ -923,6 +927,10 @@ fn build_post_review_lane_statuses_blocks_review_handoff_lineage_rewrite() {
assert_eq!(lanes.len(), 1);
assert_eq!(lanes[0].classification, "blocked");
assert_eq!(lanes[0].reason, "review_handoff_lineage_mismatch");
assert_eq!(
lanes[0].readback_root_cause.as_deref(),
Some("lineage_validation_failed")
);
assert_eq!(lanes[0].pr_url.as_deref(), Some(pr_url));
}

Expand Down
Loading