huskies: merge 1035
This commit is contained in:
@@ -144,6 +144,7 @@ async fn gateway_notification_poller_continues_when_one_project_unreachable() {
|
||||
|
||||
let event = vec![StoredEvent::StoryBlocked {
|
||||
story_id: "10_story_ok".to_string(),
|
||||
story_name: String::new(),
|
||||
reason: "retry limit".to_string(),
|
||||
timestamp_ms: 500,
|
||||
}];
|
||||
@@ -249,6 +250,7 @@ async fn gateway_notification_poller_sends_only_to_configured_gateway_rooms() {
|
||||
|
||||
let event = vec![StoredEvent::MergeFailure {
|
||||
story_id: "5_story_x".to_string(),
|
||||
story_name: String::new(),
|
||||
reason: "conflict".to_string(),
|
||||
timestamp_ms: 300,
|
||||
}];
|
||||
@@ -607,6 +609,7 @@ async fn gateway_notification_poller_delivers_events_from_two_projects_with_proj
|
||||
|
||||
let alpha_events = vec![StoredEvent::StageTransition {
|
||||
story_id: "1_story_alpha".to_string(),
|
||||
story_name: String::new(),
|
||||
from_stage: "2_current".to_string(),
|
||||
to_stage: "3_qa".to_string(),
|
||||
timestamp_ms: 100,
|
||||
@@ -632,6 +635,7 @@ async fn gateway_notification_poller_delivers_events_from_two_projects_with_proj
|
||||
|
||||
let beta_events = vec![StoredEvent::MergeFailure {
|
||||
story_id: "2_story_beta".to_string(),
|
||||
story_name: String::new(),
|
||||
reason: "merge conflict in lib.rs".to_string(),
|
||||
timestamp_ms: 200,
|
||||
}];
|
||||
@@ -765,6 +769,7 @@ async fn broadcaster_forwarder_forwards_events_with_project_tag() {
|
||||
project: "my-project".to_string(),
|
||||
event: StoredEvent::StageTransition {
|
||||
story_id: "7_story_x".to_string(),
|
||||
story_name: String::new(),
|
||||
from_stage: "2_current".to_string(),
|
||||
to_stage: "3_qa".to_string(),
|
||||
timestamp_ms: 100,
|
||||
@@ -841,6 +846,7 @@ async fn broadcaster_forwarder_resubscribes_on_lag() {
|
||||
project: "p".to_string(),
|
||||
event: StoredEvent::StageTransition {
|
||||
story_id: format!("{n}_story"),
|
||||
story_name: String::new(),
|
||||
from_stage: "2_current".to_string(),
|
||||
to_stage: "3_qa".to_string(),
|
||||
timestamp_ms: n,
|
||||
|
||||
@@ -138,26 +138,33 @@ fn status_to_stored(event: StatusEvent) -> Option<StoredEvent> {
|
||||
match event {
|
||||
StatusEvent::StageTransition {
|
||||
story_id,
|
||||
story_name,
|
||||
from_stage,
|
||||
to_stage,
|
||||
..
|
||||
} => Some(StoredEvent::StageTransition {
|
||||
story_id,
|
||||
story_name,
|
||||
from_stage,
|
||||
to_stage,
|
||||
timestamp_ms: now_ms,
|
||||
}),
|
||||
StatusEvent::MergeFailure {
|
||||
story_id, reason, ..
|
||||
story_id,
|
||||
story_name,
|
||||
reason,
|
||||
} => Some(StoredEvent::MergeFailure {
|
||||
story_id,
|
||||
story_name,
|
||||
reason,
|
||||
timestamp_ms: now_ms,
|
||||
}),
|
||||
StatusEvent::StoryBlocked {
|
||||
story_id, reason, ..
|
||||
story_id,
|
||||
story_name,
|
||||
reason,
|
||||
} => Some(StoredEvent::StoryBlocked {
|
||||
story_id,
|
||||
story_name,
|
||||
reason,
|
||||
timestamp_ms: now_ms,
|
||||
}),
|
||||
|
||||
@@ -56,11 +56,13 @@ mod tests {
|
||||
let buf = EventBuffer::new();
|
||||
buf.push(StoredEvent::MergeFailure {
|
||||
story_id: "42_story_x".to_string(),
|
||||
story_name: String::new(),
|
||||
reason: "conflict".to_string(),
|
||||
timestamp_ms: 1000,
|
||||
});
|
||||
buf.push(StoredEvent::StoryBlocked {
|
||||
story_id: "43_story_y".to_string(),
|
||||
story_name: String::new(),
|
||||
reason: "retry limit".to_string(),
|
||||
timestamp_ms: 2000,
|
||||
});
|
||||
@@ -79,6 +81,7 @@ mod tests {
|
||||
for i in 0..MAX_BUFFER_SIZE + 1 {
|
||||
buf.push(StoredEvent::MergeFailure {
|
||||
story_id: format!("{i}_story_x"),
|
||||
story_name: String::new(),
|
||||
reason: "x".to_string(),
|
||||
timestamp_ms: i as u64,
|
||||
});
|
||||
@@ -93,6 +96,7 @@ mod tests {
|
||||
fn stage_transition_timestamp_ms_accessor() {
|
||||
let e = StoredEvent::StageTransition {
|
||||
story_id: "1".to_string(),
|
||||
story_name: String::new(),
|
||||
from_stage: "2_current".to_string(),
|
||||
to_stage: "3_qa".to_string(),
|
||||
timestamp_ms: 9999,
|
||||
|
||||
@@ -18,6 +18,8 @@ pub enum StoredEvent {
|
||||
StageTransition {
|
||||
/// Work item ID (e.g. `"42_story_my_feature"`).
|
||||
story_id: String,
|
||||
/// Human-readable story name (empty string when unset).
|
||||
story_name: String,
|
||||
/// The stage the item moved FROM (display name, e.g. `"Current"`).
|
||||
from_stage: String,
|
||||
/// The stage the item moved TO (directory key, e.g. `"3_qa"`).
|
||||
@@ -29,6 +31,8 @@ pub enum StoredEvent {
|
||||
MergeFailure {
|
||||
/// Work item ID (e.g. `"42_story_my_feature"`).
|
||||
story_id: String,
|
||||
/// Human-readable story name (empty string when unset).
|
||||
story_name: String,
|
||||
/// Human-readable description of the failure.
|
||||
reason: String,
|
||||
/// Unix timestamp in milliseconds when this event was recorded.
|
||||
@@ -38,6 +42,8 @@ pub enum StoredEvent {
|
||||
StoryBlocked {
|
||||
/// Work item ID (e.g. `"42_story_my_feature"`).
|
||||
story_id: String,
|
||||
/// Human-readable story name (empty string when unset).
|
||||
story_name: String,
|
||||
/// Human-readable reason the story was blocked.
|
||||
reason: String,
|
||||
/// Unix timestamp in milliseconds when this event was recorded.
|
||||
@@ -104,11 +110,13 @@ mod tests {
|
||||
let buf = EventBuffer::new();
|
||||
buf.push(StoredEvent::MergeFailure {
|
||||
story_id: "42_story_x".to_string(),
|
||||
story_name: String::new(),
|
||||
reason: "conflict".to_string(),
|
||||
timestamp_ms: 1000,
|
||||
});
|
||||
buf.push(StoredEvent::StoryBlocked {
|
||||
story_id: "43_story_y".to_string(),
|
||||
story_name: String::new(),
|
||||
reason: "retry limit".to_string(),
|
||||
timestamp_ms: 2000,
|
||||
});
|
||||
@@ -127,6 +135,7 @@ mod tests {
|
||||
for i in 0..MAX_BUFFER_SIZE + 1 {
|
||||
buf.push(StoredEvent::MergeFailure {
|
||||
story_id: format!("{i}_story_x"),
|
||||
story_name: String::new(),
|
||||
reason: "x".to_string(),
|
||||
timestamp_ms: i as u64,
|
||||
});
|
||||
@@ -140,17 +149,20 @@ mod tests {
|
||||
let variants = [
|
||||
StoredEvent::StageTransition {
|
||||
story_id: "1".to_string(),
|
||||
story_name: String::new(),
|
||||
from_stage: "2_current".to_string(),
|
||||
to_stage: "3_qa".to_string(),
|
||||
timestamp_ms: 100,
|
||||
},
|
||||
StoredEvent::MergeFailure {
|
||||
story_id: "2".to_string(),
|
||||
story_name: String::new(),
|
||||
reason: "x".to_string(),
|
||||
timestamp_ms: 200,
|
||||
},
|
||||
StoredEvent::StoryBlocked {
|
||||
story_id: "3".to_string(),
|
||||
story_name: String::new(),
|
||||
reason: "y".to_string(),
|
||||
timestamp_ms: 300,
|
||||
},
|
||||
@@ -166,6 +178,7 @@ mod tests {
|
||||
for ts in [100u64, 200, 300] {
|
||||
buf.push(StoredEvent::MergeFailure {
|
||||
story_id: "x".to_string(),
|
||||
story_name: String::new(),
|
||||
reason: "r".to_string(),
|
||||
timestamp_ms: ts,
|
||||
});
|
||||
|
||||
@@ -33,6 +33,7 @@ pub fn subscribe_to_watcher(buffer: EventBuffer, mut rx: broadcast::Receiver<Wat
|
||||
if let Some(from) = from_stage {
|
||||
buffer.push(StoredEvent::StageTransition {
|
||||
story_id: item_id,
|
||||
story_name: String::new(),
|
||||
from_stage: from,
|
||||
to_stage: stage,
|
||||
timestamp_ms: now_ms(),
|
||||
@@ -42,6 +43,7 @@ pub fn subscribe_to_watcher(buffer: EventBuffer, mut rx: broadcast::Receiver<Wat
|
||||
Ok(WatcherEvent::MergeFailure { story_id, reason }) => {
|
||||
buffer.push(StoredEvent::MergeFailure {
|
||||
story_id,
|
||||
story_name: String::new(),
|
||||
reason,
|
||||
timestamp_ms: now_ms(),
|
||||
});
|
||||
@@ -49,6 +51,7 @@ pub fn subscribe_to_watcher(buffer: EventBuffer, mut rx: broadcast::Receiver<Wat
|
||||
Ok(WatcherEvent::StoryBlocked { story_id, reason }) => {
|
||||
buffer.push(StoredEvent::StoryBlocked {
|
||||
story_id,
|
||||
story_name: String::new(),
|
||||
reason,
|
||||
timestamp_ms: now_ms(),
|
||||
});
|
||||
|
||||
@@ -20,25 +20,33 @@ pub fn format_gateway_event(project_name: &str, event: &StoredEvent) -> (String,
|
||||
match event {
|
||||
StoredEvent::StageTransition {
|
||||
story_id,
|
||||
story_name,
|
||||
from_stage,
|
||||
to_stage,
|
||||
..
|
||||
} => {
|
||||
let from_typed = Stage::from_dir(from_stage).unwrap_or(Stage::Upcoming);
|
||||
let to_typed = Stage::from_dir(to_stage).unwrap_or(Stage::Upcoming);
|
||||
let (plain, html) = format_stage_notification(story_id, "", &from_typed, &to_typed);
|
||||
let (plain, html) =
|
||||
format_stage_notification(story_id, story_name, &from_typed, &to_typed);
|
||||
(format!("{prefix}{plain}"), format!("{prefix}{html}"))
|
||||
}
|
||||
StoredEvent::MergeFailure {
|
||||
story_id, reason, ..
|
||||
story_id,
|
||||
story_name,
|
||||
reason,
|
||||
..
|
||||
} => {
|
||||
let (plain, html) = format_error_notification(story_id, "", reason);
|
||||
let (plain, html) = format_error_notification(story_id, story_name, reason);
|
||||
(format!("{prefix}{plain}"), format!("{prefix}{html}"))
|
||||
}
|
||||
StoredEvent::StoryBlocked {
|
||||
story_id, reason, ..
|
||||
story_id,
|
||||
story_name,
|
||||
reason,
|
||||
..
|
||||
} => {
|
||||
let (plain, html) = format_blocked_notification(story_id, "", reason);
|
||||
let (plain, html) = format_blocked_notification(story_id, story_name, reason);
|
||||
(format!("{prefix}{plain}"), format!("{prefix}{html}"))
|
||||
}
|
||||
}
|
||||
@@ -54,6 +62,7 @@ mod tests {
|
||||
fn stage_transition_prefixes_project_name() {
|
||||
let event = StoredEvent::StageTransition {
|
||||
story_id: "42_story_my_feature".to_string(),
|
||||
story_name: String::new(),
|
||||
from_stage: "coding".to_string(),
|
||||
to_stage: "qa".to_string(),
|
||||
timestamp_ms: 1000,
|
||||
@@ -69,6 +78,7 @@ mod tests {
|
||||
fn merge_failure_prefixes_project_name() {
|
||||
let event = StoredEvent::MergeFailure {
|
||||
story_id: "42_story_my_feature".to_string(),
|
||||
story_name: String::new(),
|
||||
reason: "merge conflict".to_string(),
|
||||
timestamp_ms: 1000,
|
||||
};
|
||||
@@ -81,6 +91,7 @@ mod tests {
|
||||
fn story_blocked_prefixes_project_name() {
|
||||
let event = StoredEvent::StoryBlocked {
|
||||
story_id: "43_story_bar".to_string(),
|
||||
story_name: String::new(),
|
||||
reason: "retry limit exceeded".to_string(),
|
||||
timestamp_ms: 2000,
|
||||
};
|
||||
@@ -88,4 +99,93 @@ mod tests {
|
||||
assert!(plain.starts_with("[huskies] "));
|
||||
assert!(plain.contains("BLOCKED"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stage_transition_with_story_name_includes_title() {
|
||||
let event = StoredEvent::StageTransition {
|
||||
story_id: "10_story_feat".to_string(),
|
||||
story_name: "My New Feature".to_string(),
|
||||
from_stage: "backlog".to_string(),
|
||||
to_stage: "coding".to_string(),
|
||||
timestamp_ms: 100,
|
||||
};
|
||||
let (plain, html) = format_gateway_event("huskies", &event);
|
||||
assert!(plain.starts_with("[huskies] "));
|
||||
assert!(
|
||||
plain.contains("My New Feature"),
|
||||
"plain must include story title; got: {plain}"
|
||||
);
|
||||
assert!(
|
||||
html.contains("My New Feature"),
|
||||
"html must include story title; got: {html}"
|
||||
);
|
||||
assert!(plain.contains("#10"));
|
||||
assert!(plain.contains("Backlog"));
|
||||
assert!(plain.contains("Current"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_failure_with_story_name_includes_title() {
|
||||
let event = StoredEvent::MergeFailure {
|
||||
story_id: "7_story_bar".to_string(),
|
||||
story_name: "Bar Feature".to_string(),
|
||||
reason: "conflict in lib.rs".to_string(),
|
||||
timestamp_ms: 200,
|
||||
};
|
||||
let (plain, html) = format_gateway_event("robot-studio", &event);
|
||||
assert!(plain.starts_with("[robot-studio] "));
|
||||
assert!(
|
||||
plain.contains("Bar Feature"),
|
||||
"plain must include story title; got: {plain}"
|
||||
);
|
||||
assert!(
|
||||
html.contains("Bar Feature"),
|
||||
"html must include story title; got: {html}"
|
||||
);
|
||||
assert!(plain.contains("#7"));
|
||||
assert!(plain.contains("conflict in lib.rs"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn story_blocked_with_story_name_includes_title() {
|
||||
let event = StoredEvent::StoryBlocked {
|
||||
story_id: "3_story_baz".to_string(),
|
||||
story_name: "Baz Story".to_string(),
|
||||
reason: "retry limit exceeded".to_string(),
|
||||
timestamp_ms: 300,
|
||||
};
|
||||
let (plain, html) = format_gateway_event("huskies", &event);
|
||||
assert!(plain.starts_with("[huskies] "));
|
||||
assert!(
|
||||
plain.contains("Baz Story"),
|
||||
"plain must include story title; got: {plain}"
|
||||
);
|
||||
assert!(
|
||||
html.contains("Baz Story"),
|
||||
"html must include story title; got: {html}"
|
||||
);
|
||||
assert!(plain.contains("#3"));
|
||||
assert!(plain.contains("BLOCKED"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unnamed_story_falls_back_gracefully() {
|
||||
let event = StoredEvent::StageTransition {
|
||||
story_id: "5_story_unnamed".to_string(),
|
||||
story_name: String::new(),
|
||||
from_stage: "backlog".to_string(),
|
||||
to_stage: "qa".to_string(),
|
||||
timestamp_ms: 50,
|
||||
};
|
||||
let (plain, _html) = format_gateway_event("proj", &event);
|
||||
assert!(plain.starts_with("[proj] "));
|
||||
assert!(plain.contains("#5"));
|
||||
assert!(plain.contains("Backlog"));
|
||||
assert!(plain.contains("QA"));
|
||||
// Must NOT contain stray empty-name artefacts between # and —
|
||||
assert!(
|
||||
!plain.contains(" "),
|
||||
"should not have double spaces from empty name; got: {plain}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user