huskies: merge 891
This commit is contained in:
@@ -36,22 +36,22 @@ impl fmt::Display for ProjectionError {
|
||||
|
||||
impl std::error::Error for ProjectionError {}
|
||||
|
||||
// ── Projection: PipelineItemView → PipelineItem ─────────────────────────────
|
||||
// ── Projection: WorkItem → PipelineItem ─────────────────────────────────────
|
||||
|
||||
impl TryFrom<&PipelineItemView> for PipelineItem {
|
||||
type Error = ProjectionError;
|
||||
|
||||
fn try_from(view: &PipelineItemView) -> Result<Self, ProjectionError> {
|
||||
let story_id = StoryId(view.story_id.clone());
|
||||
let name = view.name.clone().unwrap_or_default();
|
||||
let story_id = StoryId(view.story_id().to_string());
|
||||
let name = view.name().unwrap_or("").to_string();
|
||||
|
||||
let depends_on: Vec<StoryId> = view
|
||||
.depends_on
|
||||
.as_ref()
|
||||
.map(|deps| deps.iter().map(|d| StoryId(d.to_string())).collect())
|
||||
.unwrap_or_default();
|
||||
.depends_on()
|
||||
.iter()
|
||||
.map(|d| StoryId(d.to_string()))
|
||||
.collect();
|
||||
|
||||
let retry_count = view.retry_count.unwrap_or(0).max(0) as u32;
|
||||
let retry_count = view.retry_count();
|
||||
|
||||
let stage = project_stage(view)?;
|
||||
|
||||
@@ -65,11 +65,11 @@ impl TryFrom<&PipelineItemView> for PipelineItem {
|
||||
}
|
||||
}
|
||||
|
||||
/// Project the stage string + associated fields from a PipelineItemView into
|
||||
/// Project the stage string + associated fields from a WorkItem into
|
||||
/// a typed Stage enum. This is the one carefully-controlled boundary where
|
||||
/// loose CRDT data becomes typed.
|
||||
pub fn project_stage(view: &PipelineItemView) -> Result<Stage, ProjectionError> {
|
||||
match view.stage.as_str() {
|
||||
match view.stage_str() {
|
||||
"0_upcoming" => Ok(Stage::Upcoming),
|
||||
"1_backlog" => Ok(Stage::Backlog),
|
||||
"2_blocked" => Ok(Stage::Blocked {
|
||||
@@ -82,7 +82,7 @@ pub fn project_stage(view: &PipelineItemView) -> Result<Stage, ProjectionError>
|
||||
// commits_ahead — those are computed at transition time. For
|
||||
// projection from existing CRDT data, we synthesize defaults.
|
||||
// The feature branch follows the naming convention.
|
||||
let branch = format!("feature/story-{}", view.story_id);
|
||||
let branch = format!("feature/story-{}", view.story_id());
|
||||
// Existing CRDT data doesn't track commits_ahead, so we use 1 as
|
||||
// a safe non-zero default (the item is in merge, so there must be
|
||||
// at least one commit).
|
||||
@@ -105,7 +105,7 @@ pub fn project_stage(view: &PipelineItemView) -> Result<Stage, ProjectionError>
|
||||
// to UNIX_EPOCH, which makes them older than any retention window
|
||||
// and therefore eligible for immediate sweep to 6_archived.
|
||||
let merged_at = view
|
||||
.merged_at
|
||||
.merged_at()
|
||||
.map(|ts| {
|
||||
DateTime::from_timestamp(ts as i64, 0).unwrap_or(DateTime::<Utc>::UNIX_EPOCH)
|
||||
})
|
||||
@@ -117,7 +117,7 @@ pub fn project_stage(view: &PipelineItemView) -> Result<Stage, ProjectionError>
|
||||
}
|
||||
"6_archived" => {
|
||||
// Determine the archive reason from the CRDT fields.
|
||||
let reason = if view.blocked == Some(true) {
|
||||
let reason = if view.blocked() {
|
||||
ArchiveReason::Blocked {
|
||||
reason: "migrated from legacy blocked field".to_string(),
|
||||
}
|
||||
@@ -133,7 +133,7 @@ pub fn project_stage(view: &PipelineItemView) -> Result<Stage, ProjectionError>
|
||||
"7_frozen" => {
|
||||
// The stage to resume to is stored in front matter as `resume_to_stage`.
|
||||
// Fall back to Coding if the field is absent (e.g. legacy frozen items).
|
||||
let resume_to = crate::db::read_content(&view.story_id)
|
||||
let resume_to = crate::db::read_content(view.story_id())
|
||||
.and_then(|content| {
|
||||
crate::db::yaml_legacy::parse_front_matter(&content)
|
||||
.ok()
|
||||
@@ -186,7 +186,7 @@ pub fn read_all_typed() -> Vec<PipelineItem> {
|
||||
Err(e) => {
|
||||
crate::slog!(
|
||||
"[pipeline_state] projection error for '{}': {e}",
|
||||
v.story_id
|
||||
v.story_id()
|
||||
);
|
||||
None
|
||||
}
|
||||
@@ -221,42 +221,46 @@ mod tests {
|
||||
StoryId(s.to_string())
|
||||
}
|
||||
|
||||
fn make_view(story_id: &str, stage: &str, name: Option<&str>) -> PipelineItemView {
|
||||
PipelineItemView::for_test(
|
||||
story_id,
|
||||
stage,
|
||||
name.map(str::to_string),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_upcoming_item() {
|
||||
let view = PipelineItemView {
|
||||
story_id: "42_story_test".to_string(),
|
||||
stage: "0_upcoming".to_string(),
|
||||
name: Some("Test Story".to_string()),
|
||||
agent: None,
|
||||
retry_count: None,
|
||||
blocked: None,
|
||||
depends_on: None,
|
||||
claimed_by: None,
|
||||
claimed_at: None,
|
||||
merged_at: None,
|
||||
qa_mode: None,
|
||||
mergemaster_attempted: None,
|
||||
};
|
||||
let view = make_view("42_story_test", "0_upcoming", Some("Test Story"));
|
||||
let item = PipelineItem::try_from(&view).unwrap();
|
||||
assert!(matches!(item.stage, Stage::Upcoming));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_backlog_item() {
|
||||
let view = PipelineItemView {
|
||||
story_id: "42_story_test".to_string(),
|
||||
stage: "1_backlog".to_string(),
|
||||
name: Some("Test Story".to_string()),
|
||||
agent: None,
|
||||
retry_count: None,
|
||||
blocked: None,
|
||||
depends_on: Some(vec![10, 20]),
|
||||
claimed_by: None,
|
||||
claimed_at: None,
|
||||
merged_at: None,
|
||||
qa_mode: None,
|
||||
mergemaster_attempted: None,
|
||||
};
|
||||
let view = PipelineItemView::for_test(
|
||||
"42_story_test",
|
||||
"1_backlog",
|
||||
Some("Test Story".to_string()),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(vec![10, 20]),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let item = PipelineItem::try_from(&view).unwrap();
|
||||
assert_eq!(item.story_id, StoryId("42_story_test".to_string()));
|
||||
assert_eq!(item.name, "Test Story");
|
||||
@@ -267,20 +271,20 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn project_current_item() {
|
||||
let view = PipelineItemView {
|
||||
story_id: "42_story_test".to_string(),
|
||||
stage: "2_current".to_string(),
|
||||
name: Some("Test".to_string()),
|
||||
agent: Some("coder-1".to_string()),
|
||||
retry_count: Some(2),
|
||||
blocked: None,
|
||||
depends_on: None,
|
||||
claimed_by: None,
|
||||
claimed_at: None,
|
||||
merged_at: None,
|
||||
qa_mode: None,
|
||||
mergemaster_attempted: None,
|
||||
};
|
||||
let view = PipelineItemView::for_test(
|
||||
"42_story_test",
|
||||
"2_current",
|
||||
Some("Test".to_string()),
|
||||
Some("coder-1".to_string()),
|
||||
Some(2),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let item = PipelineItem::try_from(&view).unwrap();
|
||||
assert!(matches!(item.stage, Stage::Coding));
|
||||
assert_eq!(item.retry_count, 2);
|
||||
@@ -288,20 +292,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn project_merge_item() {
|
||||
let view = PipelineItemView {
|
||||
story_id: "42_story_test".to_string(),
|
||||
stage: "4_merge".to_string(),
|
||||
name: Some("Test".to_string()),
|
||||
agent: None,
|
||||
retry_count: None,
|
||||
blocked: None,
|
||||
depends_on: None,
|
||||
claimed_by: None,
|
||||
claimed_at: None,
|
||||
merged_at: None,
|
||||
qa_mode: None,
|
||||
mergemaster_attempted: None,
|
||||
};
|
||||
let view = make_view("42_story_test", "4_merge", Some("Test"));
|
||||
let item = PipelineItem::try_from(&view).unwrap();
|
||||
assert!(matches!(item.stage, Stage::Merge { .. }));
|
||||
if let Stage::Merge {
|
||||
@@ -316,40 +307,27 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn project_blocked_item() {
|
||||
let view = PipelineItemView {
|
||||
story_id: "42_story_test".to_string(),
|
||||
stage: "2_blocked".to_string(),
|
||||
name: Some("Test".to_string()),
|
||||
agent: None,
|
||||
retry_count: None,
|
||||
blocked: None,
|
||||
depends_on: None,
|
||||
claimed_by: None,
|
||||
claimed_at: None,
|
||||
merged_at: None,
|
||||
qa_mode: None,
|
||||
mergemaster_attempted: None,
|
||||
};
|
||||
let view = make_view("42_story_test", "2_blocked", Some("Test"));
|
||||
let item = PipelineItem::try_from(&view).unwrap();
|
||||
assert!(matches!(item.stage, Stage::Blocked { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_archived_blocked_item() {
|
||||
let view = PipelineItemView {
|
||||
story_id: "42_story_test".to_string(),
|
||||
stage: "6_archived".to_string(),
|
||||
name: Some("Test".to_string()),
|
||||
agent: None,
|
||||
retry_count: None,
|
||||
blocked: Some(true),
|
||||
depends_on: None,
|
||||
claimed_by: None,
|
||||
claimed_at: None,
|
||||
merged_at: None,
|
||||
qa_mode: None,
|
||||
mergemaster_attempted: None,
|
||||
};
|
||||
let view = PipelineItemView::for_test(
|
||||
"42_story_test",
|
||||
"6_archived",
|
||||
Some("Test".to_string()),
|
||||
None,
|
||||
None,
|
||||
Some(true),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let item = PipelineItem::try_from(&view).unwrap();
|
||||
assert!(matches!(
|
||||
item.stage,
|
||||
@@ -362,20 +340,20 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn project_archived_completed_item() {
|
||||
let view = PipelineItemView {
|
||||
story_id: "42_story_test".to_string(),
|
||||
stage: "6_archived".to_string(),
|
||||
name: Some("Test".to_string()),
|
||||
agent: None,
|
||||
retry_count: None,
|
||||
blocked: Some(false),
|
||||
depends_on: None,
|
||||
claimed_by: None,
|
||||
claimed_at: None,
|
||||
merged_at: None,
|
||||
qa_mode: None,
|
||||
mergemaster_attempted: None,
|
||||
};
|
||||
let view = PipelineItemView::for_test(
|
||||
"42_story_test",
|
||||
"6_archived",
|
||||
Some("Test".to_string()),
|
||||
None,
|
||||
None,
|
||||
Some(false),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let item = PipelineItem::try_from(&view).unwrap();
|
||||
assert!(matches!(
|
||||
item.stage,
|
||||
@@ -388,20 +366,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn project_unknown_stage_returns_error() {
|
||||
let view = PipelineItemView {
|
||||
story_id: "42_story_test".to_string(),
|
||||
stage: "9_invalid".to_string(),
|
||||
name: Some("Test".to_string()),
|
||||
agent: None,
|
||||
retry_count: None,
|
||||
blocked: None,
|
||||
depends_on: None,
|
||||
claimed_by: None,
|
||||
claimed_at: None,
|
||||
merged_at: None,
|
||||
qa_mode: None,
|
||||
mergemaster_attempted: None,
|
||||
};
|
||||
let view = make_view("42_story_test", "9_invalid", Some("Test"));
|
||||
let result = PipelineItem::try_from(&view);
|
||||
assert!(matches!(
|
||||
result,
|
||||
|
||||
Reference in New Issue
Block a user