huskies: merge 1035
This commit is contained in:
@@ -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