huskies: merge 1063
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
//! with `[project-name]` prefixes. The actual I/O (HTTP polling, spawning
|
||||
//! tasks, sending messages) lives in `io.rs`.
|
||||
|
||||
use crate::pipeline_state::Stage;
|
||||
use crate::pipeline_state::{Stage, stage_label};
|
||||
use crate::service::events::StoredEvent;
|
||||
use crate::service::notifications::{
|
||||
format_blocked_notification, format_error_notification, format_stage_notification,
|
||||
@@ -52,6 +52,46 @@ pub fn format_gateway_event(project_name: &str, event: &StoredEvent) -> (String,
|
||||
}
|
||||
}
|
||||
|
||||
/// Format a [`StoredEvent`] from a project as a compact audit line for LLM context.
|
||||
///
|
||||
/// Produces a structured one-line entry with stable `key=value` fields, including
|
||||
/// the project name, mirroring the sled-side `format_audit_entry` format.
|
||||
pub fn format_gateway_audit_line(project: &str, event: &StoredEvent) -> String {
|
||||
let ts_ms = event.timestamp_ms();
|
||||
let ts = chrono::DateTime::from_timestamp_millis(ts_ms as i64)
|
||||
.unwrap_or_else(chrono::Utc::now)
|
||||
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
|
||||
|
||||
match event {
|
||||
StoredEvent::StageTransition {
|
||||
story_id,
|
||||
from_stage,
|
||||
to_stage,
|
||||
..
|
||||
} => {
|
||||
let from_label = stage_label(&Stage::from_dir(from_stage).unwrap_or(Stage::Upcoming));
|
||||
let to_label = stage_label(&Stage::from_dir(to_stage).unwrap_or(Stage::Upcoming));
|
||||
format!(
|
||||
"audit ts={ts} project={project} id={story_id} from={from_label} to={to_label} event=stage_transition"
|
||||
)
|
||||
}
|
||||
StoredEvent::MergeFailure {
|
||||
story_id, reason, ..
|
||||
} => {
|
||||
format!(
|
||||
"audit ts={ts} project={project} id={story_id} event=merge_failure reason={reason}"
|
||||
)
|
||||
}
|
||||
StoredEvent::StoryBlocked {
|
||||
story_id, reason, ..
|
||||
} => {
|
||||
format!(
|
||||
"audit ts={ts} project={project} id={story_id} event=story_blocked reason={reason}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -188,4 +228,64 @@ mod tests {
|
||||
"should not have double spaces from empty name; got: {plain}"
|
||||
);
|
||||
}
|
||||
|
||||
// -- format_gateway_audit_line -------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn audit_line_stage_transition_contains_project_and_stages() {
|
||||
let event = StoredEvent::StageTransition {
|
||||
story_id: "42_story_feat".to_string(),
|
||||
story_name: String::new(),
|
||||
from_stage: "2_current".to_string(),
|
||||
to_stage: "3_qa".to_string(),
|
||||
timestamp_ms: 1_000_000,
|
||||
};
|
||||
let line = format_gateway_audit_line("huskies", &event);
|
||||
assert!(
|
||||
line.starts_with("audit ts="),
|
||||
"must start with audit ts=; got: {line}"
|
||||
);
|
||||
assert!(
|
||||
line.contains("project=huskies"),
|
||||
"must contain project; got: {line}"
|
||||
);
|
||||
assert!(
|
||||
line.contains("id=42_story_feat"),
|
||||
"must contain story id; got: {line}"
|
||||
);
|
||||
assert!(
|
||||
line.contains("event=stage_transition"),
|
||||
"must name event; got: {line}"
|
||||
);
|
||||
assert!(line.contains("from="), "must have from field; got: {line}");
|
||||
assert!(line.contains("to="), "must have to field; got: {line}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audit_line_merge_failure_contains_reason() {
|
||||
let event = StoredEvent::MergeFailure {
|
||||
story_id: "7_story_bar".to_string(),
|
||||
story_name: String::new(),
|
||||
reason: "conflict in main.rs".to_string(),
|
||||
timestamp_ms: 2_000_000,
|
||||
};
|
||||
let line = format_gateway_audit_line("robot-studio", &event);
|
||||
assert!(line.contains("project=robot-studio"), "got: {line}");
|
||||
assert!(line.contains("event=merge_failure"), "got: {line}");
|
||||
assert!(line.contains("reason=conflict in main.rs"), "got: {line}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audit_line_story_blocked_contains_reason() {
|
||||
let event = StoredEvent::StoryBlocked {
|
||||
story_id: "3_story_baz".to_string(),
|
||||
story_name: String::new(),
|
||||
reason: "retry limit exceeded".to_string(),
|
||||
timestamp_ms: 3_000_000,
|
||||
};
|
||||
let line = format_gateway_audit_line("proj", &event);
|
||||
assert!(line.contains("project=proj"), "got: {line}");
|
||||
assert!(line.contains("event=story_blocked"), "got: {line}");
|
||||
assert!(line.contains("reason=retry limit exceeded"), "got: {line}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user