Simplify queued-duration calculation to handle missing start times
Intent: The original `ci_timing_summary` function used a multi-arm match that did not account for the case where `started_at` is None but `finished_at` is Some — a CI run that terminates without ever starting. The rewrite collapses the logic: if `created_at` exists, the queued interval ends at whichever of `started_at`, `finished_at`, or `now` is available first, making the computation correct for all edge cases.
Affected files: crates/pika-news/src/web.rs
Evidence
@@ -2547,15 +2547,10 @@ fn ci_timing_summary(
- let queued = match (created_at, started_at) {
- (Some(created_at), Some(started_at)) => {
- compact_duration_part("queued", started_at.signed_duration_since(created_at))
- }
- (Some(created_at), None) if finished_at.is_none() => {
- compact_duration_part("queued", now.signed_duration_since(created_at))
- }
- _ => None,
- };
+ let queued = created_at.and_then(|created_at| {
+ let queued_end = started_at.or(finished_at).unwrap_or(now);
+ compact_duration_part("queued", queued_end.signed_duration_since(created_at))
+ });
The previous implementation had three match arms for computing the queued duration:
- Both
created_at and started_at present → queued = started_at - created_at.
created_at present, started_at absent, finished_at absent → queued = now - created_at (still waiting).
- Everything else →
None.
Arm 3 swallowed the case where a run has created_at and finished_at but no started_at (the job was terminated before it could start). The new code uses a single and_then chain:
let queued = created_at.and_then(|created_at| {
let queued_end = started_at.or(finished_at).unwrap_or(now);
compact_duration_part("queued", queued_end.signed_duration_since(created_at))
});
The fallback priority started_at > finished_at > now naturally covers all three original arms plus the missing edge case, with less code.
Add unit test for the finished-without-start timing edge case
Intent: Verify that `ci_timing_summary` returns a correct 'queued 14s' string when `started_at` is None but `finished_at` is provided, directly exercising the previously-uncovered code path.
Affected files: crates/pika-news/src/web.rs
Evidence
@@ -5214,6 +5209,16 @@ mod tests {
+ assert_eq!(
+ super::ci_timing_summary(
+ "2026-03-24T12:00:00Z",
+ None,
+ Some("2026-03-24T12:00:14Z"),
+ now,
+ )
+ .expect("finished-while-never-started summary"),
+ "queued 14s"
+ );
A new assertion is appended to the existing ci_timing_summary unit test. It calls the function with:
created_at = 2026-03-24T12:00:00Z
started_at = None
finished_at = 2026-03-24T12:00:14Z
The expected output is "queued 14s" with no ran segment, confirming the simplified logic correctly computes the queue wait even when the run never started.
Add integration test for rendered CI HTML with terminal lane lacking start time
Intent: Ensure that the full rendering pipeline (store → detail → HTML templates) correctly surfaces the 'queued 14s' duration for a CI lane that failed without ever starting, and does not erroneously append a 'ran' segment.
Affected files: crates/pika-news/src/web.rs
Evidence
@@ -7284,6 +7289,80 @@ paths = ["README.md", "feature.txt", "ci/forge-lanes.toml"]
+ #[test]
+ fn branch_ci_rendering_shows_queued_duration_for_terminal_lane_without_start_time() {
@@ +7289,80 @@
+ store
+ .with_connection(|conn| {
+ conn.execute(
+ "UPDATE branch_ci_runs
+ SET status = 'failed',
+ created_at = '2026-03-24T12:00:00Z',
+ started_at = NULL,
+ finished_at = '2026-03-24T12:00:14Z'
+ WHERE branch_id = ?1",
@@ +7289,80 @@
+ assert!(summary_html.contains("queued 14s"));
+ assert!(live_html.contains("queued 14s"));
+ assert!(!live_html.contains("queued 14s · ran"));
This test creates a full branch and CI run in a temporary SQLite store, then manually patches the run and lane rows to have status = 'failed', started_at = NULL, and finished_at set 14 seconds after created_at. It then renders both the summary and live CI HTML fragments and asserts:
- Both contain
"queued 14s".
- The live HTML does not contain
"queued 14s · ran", since the job never started.
This catches regressions in the full rendering path, not just the isolated timing function.
Clarify forge-wide health warning vs. branch-specific failure
Intent: Users were confused by a health banner that said 'the summary generator is unhealthy' appearing on a branch page alongside a branch-specific tutorial failure. The wording is revised to make the scope of each message explicit: forge-wide worker health is distinct from a per-branch generation failure.
Affected files: crates/pika-news/src/web.rs, crates/pika-news/templates/detail.html
Evidence
@@ -997,7 +997,7 @@ fn branch_page_notices(state: &AppState) -> Vec<PageNoticeView> {
- "The summary generator is unhealthy. New tutorials across the forge may be delayed until it recovers.",
+ "Forge health warning: the tutorial generator worker is unhealthy. New tutorials on any branch may be delayed until it recovers; this forge-wide warning does not mean this branch's last tutorial generation attempt failed.",
@@ -312,7 +312,8 @@
- <h2>Branch Tutorial Generation Failed</h2>
+ <h2>Branch-Specific Tutorial Generation Failed</h2>
+ <p>This failure is specific to the current branch head.</p>
@@ -355,7 +356,7 @@
- <p>This branch tutorial is unavailable because generation failed. Check the branch tutorial error above for details.</p>
+ <p>This branch tutorial is unavailable because generation for this branch head failed. That is separate from any forge-wide generator worker warning.</p>
Three pieces of user-facing text are updated:
-
Forge-wide health banner (web.rs:1000): The notice now begins with "Forge health warning:" and explicitly states "this forge-wide warning does not mean this branch's last tutorial generation attempt failed."
-
Branch tutorial error heading (detail.html:315): Changed from "Branch Tutorial Generation Failed" to "Branch-Specific Tutorial Generation Failed" with a new paragraph: "This failure is specific to the current branch head."
-
Summary panel failed message (detail.html:359): Reworded to "generation for this branch head failed. That is separate from any forge-wide generator worker warning."
These changes eliminate the ambiguity when both a global health warning and a branch-specific failure appear on the same page.
Update assertions in existing test to match revised wording
Intent: The existing test for the coexistence of forge-wide and branch-specific notices is updated to assert against the new, more descriptive message strings.
Affected files: crates/pika-news/src/web.rs
Evidence
@@ -7376,16 +7455,22 @@ paths = ["README.md", "feature.txt", "ci/forge-lanes.toml"]
- assert!(rendered.contains("The summary generator is unhealthy."));
- assert!(rendered.contains("Branch Tutorial Generation Failed"));
- assert!(rendered.contains("This branch tutorial is unavailable because generation failed."));
+ assert!(
+ rendered.contains("Forge health warning: the tutorial generator worker is unhealthy.")
+ );
+ assert!(rendered.contains("forge-wide warning does not mean"));
+ assert!(rendered.contains("Branch-Specific Tutorial Generation Failed"));
+ assert!(rendered.contains("This failure is specific to the current branch head."));
+ assert!(rendered.contains(
+ "This branch tutorial is unavailable because generation for this branch head failed."
+ ));
The test that renders a detail page with both a forge health warning and a branch tutorial error is updated to check for the new strings:
"Forge health warning: the tutorial generator worker is unhealthy."
"forge-wide warning does not mean"
"Branch-Specific Tutorial Generation Failed"
"This failure is specific to the current branch head."
"This branch tutorial is unavailable because generation for this branch head failed."
This ensures the template and Rust-side notice text stay in sync after the rewording.