huskies: merge 895
This commit is contained in:
@@ -60,6 +60,10 @@ export interface PipelineStageItem {
|
||||
review_hold: boolean | null;
|
||||
qa: string | null;
|
||||
depends_on: number[] | null;
|
||||
/** True when the item is in Stage::Blocked — awaiting human unblock. */
|
||||
blocked?: boolean | null;
|
||||
/** True when the item is in Stage::Frozen — paused at its current stage. */
|
||||
frozen?: boolean | null;
|
||||
}
|
||||
|
||||
/** Snapshot of all pipeline stages returned via WebSocket or REST. */
|
||||
|
||||
@@ -396,6 +396,44 @@ export function StagePanel({
|
||||
{typeLabel}
|
||||
</span>
|
||||
)}
|
||||
{item.blocked && !item.merge_failure && (
|
||||
<span
|
||||
data-testid={`blocked-badge-${item.story_id}`}
|
||||
title="Blocked — awaiting human unblock"
|
||||
style={{
|
||||
fontSize: "0.65em",
|
||||
fontWeight: 700,
|
||||
color: "#f85149",
|
||||
background: "#2a1010",
|
||||
border: "1px solid #6e1b1b",
|
||||
borderRadius: "4px",
|
||||
padding: "1px 4px",
|
||||
marginRight: "8px",
|
||||
letterSpacing: "0.05em",
|
||||
}}
|
||||
>
|
||||
⊘ BLOCKED
|
||||
</span>
|
||||
)}
|
||||
{item.frozen && (
|
||||
<span
|
||||
data-testid={`frozen-badge-${item.story_id}`}
|
||||
title="Frozen — auto-assign paused"
|
||||
style={{
|
||||
fontSize: "0.65em",
|
||||
fontWeight: 700,
|
||||
color: "#58a6ff",
|
||||
background: "#0d1f36",
|
||||
border: "1px solid #1a3a6e",
|
||||
borderRadius: "4px",
|
||||
padding: "1px 4px",
|
||||
marginRight: "8px",
|
||||
letterSpacing: "0.05em",
|
||||
}}
|
||||
>
|
||||
❄ FROZEN
|
||||
</span>
|
||||
)}
|
||||
{costs?.has(item.story_id) && (
|
||||
<span
|
||||
data-testid={`cost-badge-${item.story_id}`}
|
||||
|
||||
@@ -2,9 +2,36 @@
|
||||
|
||||
use crate::agents::{AgentPool, AgentStatus};
|
||||
use crate::config::ProjectConfig;
|
||||
use crate::pipeline_state::{PipelineItem, Stage};
|
||||
use crate::pipeline_state::{ArchiveReason, PipelineItem, Stage};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
/// Map a stage to its display section label, or `None` to skip it entirely.
|
||||
///
|
||||
/// This is the single source of truth for the "where does this item appear"
|
||||
/// decision. It mirrors the bucket routing in `http/workflow/pipeline.rs`
|
||||
/// so that chat output and the web UI are always consistent.
|
||||
///
|
||||
/// `Stage::Frozen { resume_to }` is handled recursively: a frozen story
|
||||
/// appears in the same section its `resume_to` stage would land in.
|
||||
pub(crate) fn display_section(s: &Stage) -> Option<&'static str> {
|
||||
match s {
|
||||
Stage::Upcoming | Stage::Backlog => Some("Backlog"),
|
||||
Stage::Coding
|
||||
| Stage::Blocked { .. }
|
||||
| Stage::Archived {
|
||||
reason: ArchiveReason::Blocked { .. },
|
||||
..
|
||||
} => Some("In Progress"),
|
||||
Stage::Qa | Stage::ReviewHold { .. } => Some("QA"),
|
||||
Stage::Merge { .. } | Stage::MergeFailure { .. } | Stage::MergeFailureFinal { .. } => {
|
||||
Some("Merge")
|
||||
}
|
||||
Stage::Done { .. } => Some("Done"),
|
||||
Stage::Frozen { resume_to } => display_section(resume_to),
|
||||
Stage::Archived { .. } => None, // other archived variants are hidden
|
||||
}
|
||||
}
|
||||
|
||||
/// Check which dependency numbers from `item.depends_on` are unmet.
|
||||
///
|
||||
/// A dependency is considered met if the dep is in `Done` or `Archived` stage
|
||||
@@ -95,26 +122,24 @@ pub(crate) fn build_status_from_items(
|
||||
|
||||
let mut out = String::from("**Pipeline Status**\n\n");
|
||||
|
||||
// Active pipeline stages to display (Archived is handled separately below).
|
||||
type StagePredicate = fn(&Stage) -> bool;
|
||||
let stage_filters: &[(&str, StagePredicate)] = &[
|
||||
("Backlog", |s| matches!(s, Stage::Backlog)),
|
||||
("In Progress", |s| matches!(s, Stage::Coding)),
|
||||
("QA", |s| matches!(s, Stage::Qa)),
|
||||
("Merge", |s| matches!(s, Stage::Merge { .. })),
|
||||
("Done", |s| matches!(s, Stage::Done { .. })),
|
||||
];
|
||||
// Render each display section in order. Blocked items appear in-place
|
||||
// under their stage section (determined by `display_section`); there is
|
||||
// no separate "Blocked" section. Frozen items appear under the section
|
||||
// their `resume_to` stage maps to.
|
||||
let sections = ["Backlog", "In Progress", "QA", "Merge", "Done"];
|
||||
|
||||
for (label, filter) in stage_filters {
|
||||
let mut stage_items: Vec<&PipelineItem> =
|
||||
items.iter().filter(|i| filter(&i.stage)).collect();
|
||||
stage_items.sort_by(|a, b| a.story_id.0.cmp(&b.story_id.0));
|
||||
let count = stage_items.len();
|
||||
for label in sections {
|
||||
let mut section_items: Vec<&PipelineItem> = items
|
||||
.iter()
|
||||
.filter(|i| display_section(&i.stage) == Some(label))
|
||||
.collect();
|
||||
section_items.sort_by(|a, b| a.story_id.0.cmp(&b.story_id.0));
|
||||
let count = section_items.len();
|
||||
out.push_str(&format!("**{label}** ({count})\n"));
|
||||
if stage_items.is_empty() {
|
||||
if section_items.is_empty() {
|
||||
out.push_str(" *(none)*\n");
|
||||
} else {
|
||||
for item in &stage_items {
|
||||
for item in §ion_items {
|
||||
out.push_str(&render_item_line(
|
||||
item,
|
||||
items,
|
||||
@@ -129,26 +154,6 @@ pub(crate) fn build_status_from_items(
|
||||
out.push('\n');
|
||||
}
|
||||
|
||||
// Blocked items: Archived { reason: Blocked } shown with 🔴 indicator.
|
||||
let mut blocked_items: Vec<&PipelineItem> =
|
||||
items.iter().filter(|i| i.stage.is_blocked()).collect();
|
||||
blocked_items.sort_by(|a, b| a.story_id.0.cmp(&b.story_id.0));
|
||||
if !blocked_items.is_empty() {
|
||||
out.push_str(&format!("**Blocked** ({})\n", blocked_items.len()));
|
||||
for item in &blocked_items {
|
||||
out.push_str(&render_item_line(
|
||||
item,
|
||||
items,
|
||||
&active_map,
|
||||
&cost_by_story,
|
||||
&config,
|
||||
&running_merges,
|
||||
&merge_failures,
|
||||
));
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
|
||||
// Free agents: configured agents not currently running or pending.
|
||||
out.push_str("**Free Agents**\n");
|
||||
if let Some(cfg) = &config {
|
||||
@@ -198,7 +203,8 @@ fn render_item_line(
|
||||
} else {
|
||||
Some(item.name.as_str())
|
||||
};
|
||||
let frozen = crate::io::story_metadata::is_story_frozen_in_store(story_id);
|
||||
// Use the typed CRDT stage as the sole source of truth (story 945).
|
||||
let frozen = item.stage.is_frozen();
|
||||
let base_label = super::story_short_label(story_id, name_opt);
|
||||
let display = if frozen {
|
||||
format!("\u{2744}\u{FE0F} {base_label}") // ❄️ prefix
|
||||
@@ -219,9 +225,24 @@ fn render_item_line(
|
||||
format!(" *(waiting on: {})*", nums.join(", "))
|
||||
};
|
||||
|
||||
// Merge-stage items get a dedicated breakdown indicator instead of the
|
||||
// generic traffic-light dot.
|
||||
if matches!(item.stage, Stage::Merge { .. }) {
|
||||
// Merge-stage items get dedicated breakdown indicators instead of the
|
||||
// generic traffic-light dot. MergeFailure / MergeFailureFinal items
|
||||
// now also appear in the Merge section (in-place) so they are handled
|
||||
// here alongside normal Merge items.
|
||||
if matches!(
|
||||
item.stage,
|
||||
Stage::Merge { .. } | Stage::MergeFailure { .. } | Stage::MergeFailureFinal { .. }
|
||||
) {
|
||||
// MergeFailure and MergeFailureFinal carry their reason directly on
|
||||
// the stage variant — always show ⛔ with the failure snippet.
|
||||
match &item.stage {
|
||||
Stage::MergeFailure { reason, .. } | Stage::MergeFailureFinal { reason } => {
|
||||
let snippet = first_non_empty_snippet(reason, 120);
|
||||
return format!(" \u{26D4} {display}{cost_suffix}{dep_suffix} — {snippet}\n");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let in_det_merge = running_merges.contains(story_id);
|
||||
let merge_failure = merge_failures.get(story_id);
|
||||
if in_det_merge {
|
||||
|
||||
@@ -682,3 +682,257 @@ fn snippet_skips_leading_empty_lines() {
|
||||
let result = first_non_empty_snippet(text, 120);
|
||||
assert_eq!(result, "actual error here");
|
||||
}
|
||||
|
||||
// -- AC1: blocked items appear in-place, not in a separate "Blocked" section --
|
||||
|
||||
#[test]
|
||||
fn blocked_item_appears_in_in_progress_not_separate_section() {
|
||||
use tempfile::TempDir;
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
let items = vec![make_item(
|
||||
"42_story_blocked",
|
||||
"Blocked Story",
|
||||
Stage::Blocked {
|
||||
reason: "retry limit exceeded".to_string(),
|
||||
},
|
||||
)];
|
||||
|
||||
let agents = AgentPool::new_test(3000);
|
||||
let output = build_status_from_items(tmp.path(), &agents, &items);
|
||||
|
||||
// Must NOT appear under a separate "Blocked" section.
|
||||
assert!(
|
||||
!output.contains("**Blocked**"),
|
||||
"output must not have a separate Blocked section: {output}"
|
||||
);
|
||||
|
||||
// Must appear under "In Progress".
|
||||
let in_progress_pos = output
|
||||
.find("**In Progress**")
|
||||
.expect("In Progress section must exist");
|
||||
let qa_pos = output.find("**QA**").expect("QA section must exist");
|
||||
let story_pos = output
|
||||
.find("42 [story]")
|
||||
.expect("story must appear in output");
|
||||
|
||||
assert!(
|
||||
story_pos > in_progress_pos && story_pos < qa_pos,
|
||||
"blocked story should be in In Progress section: in_progress={in_progress_pos} story={story_pos} qa={qa_pos}\n{output}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocked_item_shows_red_dot_in_in_progress_section() {
|
||||
use tempfile::TempDir;
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
let items = vec![make_item(
|
||||
"50_story_blocked_dot",
|
||||
"Blocked With Dot",
|
||||
Stage::Blocked {
|
||||
reason: "too many failures".to_string(),
|
||||
},
|
||||
)];
|
||||
|
||||
let agents = AgentPool::new_test(3000);
|
||||
let output = build_status_from_items(tmp.path(), &agents, &items);
|
||||
|
||||
assert!(
|
||||
output.contains("\u{1F534} "), // 🔴
|
||||
"blocked story should show red circle emoji: {output}"
|
||||
);
|
||||
}
|
||||
|
||||
// -- AC2: stage counts include blocked items --
|
||||
|
||||
#[test]
|
||||
fn in_progress_count_includes_blocked_items() {
|
||||
use tempfile::TempDir;
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
let items = vec![
|
||||
make_item("10_story_coding", "Coding Story", Stage::Coding),
|
||||
make_item(
|
||||
"11_story_blocked",
|
||||
"Blocked Story",
|
||||
Stage::Blocked {
|
||||
reason: "failed".to_string(),
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
let agents = AgentPool::new_test(3000);
|
||||
let output = build_status_from_items(tmp.path(), &agents, &items);
|
||||
|
||||
// "In Progress" header should show count of 2 (one coding + one blocked).
|
||||
assert!(
|
||||
output.contains("**In Progress** (2)"),
|
||||
"In Progress count should include blocked items: {output}"
|
||||
);
|
||||
}
|
||||
|
||||
// -- AC4: frozen items appear in-place, with ❄️ indicator --
|
||||
|
||||
#[test]
|
||||
fn frozen_coding_item_appears_in_in_progress_section() {
|
||||
use tempfile::TempDir;
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
let items = vec![make_item(
|
||||
"60_story_frozen",
|
||||
"Frozen Coding Story",
|
||||
Stage::Frozen {
|
||||
resume_to: Box::new(Stage::Coding),
|
||||
},
|
||||
)];
|
||||
|
||||
let agents = AgentPool::new_test(3000);
|
||||
let output = build_status_from_items(tmp.path(), &agents, &items);
|
||||
|
||||
let in_progress_pos = output
|
||||
.find("**In Progress**")
|
||||
.expect("In Progress section must exist");
|
||||
let qa_pos = output.find("**QA**").expect("QA section must exist");
|
||||
let story_pos = output
|
||||
.find("60 [story]")
|
||||
.expect("story must appear in output");
|
||||
|
||||
assert!(
|
||||
story_pos > in_progress_pos && story_pos < qa_pos,
|
||||
"frozen-coding story should appear in In Progress: in_progress={in_progress_pos} story={story_pos} qa={qa_pos}\n{output}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_qa_item_appears_in_qa_section() {
|
||||
use tempfile::TempDir;
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
let items = vec![make_item(
|
||||
"70_story_frozen_qa",
|
||||
"Frozen QA Story",
|
||||
Stage::Frozen {
|
||||
resume_to: Box::new(Stage::Qa),
|
||||
},
|
||||
)];
|
||||
|
||||
let agents = AgentPool::new_test(3000);
|
||||
let output = build_status_from_items(tmp.path(), &agents, &items);
|
||||
|
||||
let qa_pos = output.find("**QA**").expect("QA section must exist");
|
||||
let merge_pos = output.find("**Merge**").expect("Merge section must exist");
|
||||
let story_pos = output
|
||||
.find("70 [story]")
|
||||
.expect("story must appear in output");
|
||||
|
||||
assert!(
|
||||
story_pos > qa_pos && story_pos < merge_pos,
|
||||
"frozen-QA story should appear in QA section: qa={qa_pos} story={story_pos} merge={merge_pos}\n{output}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_item_shows_snowflake_indicator() {
|
||||
use tempfile::TempDir;
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
let items = vec![make_item(
|
||||
"80_story_frozen_flake",
|
||||
"Frozen Flake Story",
|
||||
Stage::Frozen {
|
||||
resume_to: Box::new(Stage::Coding),
|
||||
},
|
||||
)];
|
||||
|
||||
let agents = AgentPool::new_test(3000);
|
||||
let output = build_status_from_items(tmp.path(), &agents, &items);
|
||||
|
||||
assert!(
|
||||
output.contains("\u{2744}\u{FE0F}"), // ❄️
|
||||
"frozen story should show snowflake prefix: {output}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_and_blocked_use_distinct_indicators() {
|
||||
use tempfile::TempDir;
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
let items = vec![
|
||||
make_item(
|
||||
"90_story_blocked_ind",
|
||||
"Blocked Story",
|
||||
Stage::Blocked {
|
||||
reason: "failed".to_string(),
|
||||
},
|
||||
),
|
||||
make_item(
|
||||
"91_story_frozen_ind",
|
||||
"Frozen Story",
|
||||
Stage::Frozen {
|
||||
resume_to: Box::new(Stage::Coding),
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
let agents = AgentPool::new_test(3000);
|
||||
let output = build_status_from_items(tmp.path(), &agents, &items);
|
||||
|
||||
assert!(
|
||||
output.contains("\u{1F534} "), // 🔴 for blocked
|
||||
"blocked story should show red dot: {output}"
|
||||
);
|
||||
assert!(
|
||||
output.contains("\u{2744}\u{FE0F}"), // ❄️ for frozen
|
||||
"frozen story should show snowflake: {output}"
|
||||
);
|
||||
}
|
||||
|
||||
// -- merge-failure items appear in Merge section --
|
||||
|
||||
#[test]
|
||||
fn merge_failure_item_appears_in_merge_section_not_blocked() {
|
||||
use crate::pipeline_state::BranchName;
|
||||
use tempfile::TempDir;
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
let items = vec![make_item(
|
||||
"100_story_merge_fail",
|
||||
"Merge Failure Story",
|
||||
Stage::MergeFailure {
|
||||
reason: "conflict in lib.rs".to_string(),
|
||||
feature_branch: BranchName("feature/100".to_string()),
|
||||
commits_ahead: std::num::NonZeroU32::new(1).unwrap(),
|
||||
},
|
||||
)];
|
||||
|
||||
let agents = AgentPool::new_test(3000);
|
||||
let output = build_status_from_items(tmp.path(), &agents, &items);
|
||||
|
||||
// Must not be in a separate "Blocked" section.
|
||||
assert!(
|
||||
!output.contains("**Blocked**"),
|
||||
"output must not have a separate Blocked section: {output}"
|
||||
);
|
||||
|
||||
let merge_pos = output.find("**Merge**").expect("Merge section must exist");
|
||||
let done_pos = output.find("**Done**").expect("Done section must exist");
|
||||
let story_pos = output
|
||||
.find("100 [story]")
|
||||
.expect("story must appear in output");
|
||||
|
||||
assert!(
|
||||
story_pos > merge_pos && story_pos < done_pos,
|
||||
"merge-failure story should appear in Merge section: merge={merge_pos} story={story_pos} done={done_pos}\n{output}"
|
||||
);
|
||||
|
||||
assert!(
|
||||
output.contains("\u{26D4}"), // ⛔
|
||||
"merge failure should show stop sign: {output}"
|
||||
);
|
||||
assert!(
|
||||
output.contains("conflict in lib.rs"),
|
||||
"merge failure reason should be shown: {output}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,9 +33,12 @@ pub struct UpcomingStory {
|
||||
/// Number of retries at the current pipeline stage.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub retry_count: Option<u32>,
|
||||
/// True when the story has exceeded its retry limit and will not be auto-assigned.
|
||||
/// True when the item is in `Stage::Blocked` (awaiting human unblock).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub blocked: Option<bool>,
|
||||
/// True when the item is in `Stage::Frozen` (paused at its current stage).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub frozen: Option<bool>,
|
||||
/// Story numbers this story depends on.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub depends_on: Option<Vec<u32>>,
|
||||
@@ -63,6 +66,24 @@ pub struct PipelineState {
|
||||
pub deterministic_merges_in_flight: Vec<String>,
|
||||
}
|
||||
|
||||
/// Determine which pipeline bucket a frozen item's `resume_to` stage maps to.
|
||||
///
|
||||
/// Mirrors the routing in `load_pipeline_state` for non-frozen items so that
|
||||
/// a frozen story always appears under the same section it was in before freezing.
|
||||
fn frozen_resume_bucket(resume_to: &crate::pipeline_state::Stage) -> &'static str {
|
||||
use crate::pipeline_state::Stage;
|
||||
match resume_to {
|
||||
Stage::Upcoming | Stage::Backlog => "backlog",
|
||||
Stage::Coding | Stage::Blocked { .. } => "current",
|
||||
Stage::Qa | Stage::ReviewHold { .. } => "qa",
|
||||
Stage::Merge { .. } | Stage::MergeFailure { .. } | Stage::MergeFailureFinal { .. } => {
|
||||
"merge"
|
||||
}
|
||||
Stage::Frozen { resume_to: inner } => frozen_resume_bucket(inner),
|
||||
_ => "backlog", // Done, Archived → fall back to backlog (should not occur)
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the full pipeline state (all 5 active stages).
|
||||
///
|
||||
/// Reads from the CRDT document and enriches with content from the
|
||||
@@ -131,6 +152,11 @@ pub fn load_pipeline_state(ctx: &AppContext) -> Result<PipelineState, String> {
|
||||
} else {
|
||||
None
|
||||
},
|
||||
frozen: if item.stage.is_frozen() {
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
depends_on: if item.depends_on.is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -143,12 +169,6 @@ pub fn load_pipeline_state(ctx: &AppContext) -> Result<PipelineState, String> {
|
||||
},
|
||||
epic_id,
|
||||
};
|
||||
// Frozen items (CRDT flag) are routed to the backlog bucket regardless
|
||||
// of their underlying stage — they're paused, not progressing.
|
||||
if item.is_frozen() {
|
||||
state.backlog.push(story);
|
||||
continue;
|
||||
}
|
||||
match &item.stage {
|
||||
Stage::Upcoming => state.backlog.push(story), // upcoming shown with backlog
|
||||
Stage::Backlog => state.backlog.push(story),
|
||||
@@ -159,7 +179,16 @@ pub fn load_pipeline_state(ctx: &AppContext) -> Result<PipelineState, String> {
|
||||
Stage::MergeFailure { .. } => state.merge.push(story), // show merge failures with merge
|
||||
Stage::MergeFailureFinal { .. } => state.merge.push(story), // mergemaster gave up
|
||||
Stage::ReviewHold { .. } => state.qa.push(story), // review-held shown with QA
|
||||
Stage::Frozen { .. } => state.backlog.push(story), // paused, route to backlog
|
||||
Stage::Frozen { resume_to } => {
|
||||
// Route to the section matching the stage that was active when
|
||||
// the item was frozen, so it appears in-place.
|
||||
match frozen_resume_bucket(resume_to) {
|
||||
"current" => state.current.push(story),
|
||||
"qa" => state.qa.push(story),
|
||||
"merge" => state.merge.push(story),
|
||||
_ => state.backlog.push(story),
|
||||
}
|
||||
}
|
||||
Stage::Done { .. } => state.done.push(story),
|
||||
Stage::Archived { .. } => {} // skip archived
|
||||
}
|
||||
@@ -244,6 +273,11 @@ pub fn load_upcoming_stories(_ctx: &AppContext) -> Result<Vec<UpcomingStory>, St
|
||||
} else {
|
||||
None
|
||||
},
|
||||
frozen: if item.stage.is_frozen() {
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
depends_on: if item.depends_on.is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
||||
@@ -214,6 +214,7 @@ mod tests {
|
||||
qa: None,
|
||||
retry_count: None,
|
||||
blocked: None,
|
||||
frozen: None,
|
||||
depends_on: None,
|
||||
epic_id: None,
|
||||
}],
|
||||
@@ -227,6 +228,7 @@ mod tests {
|
||||
qa: None,
|
||||
retry_count: None,
|
||||
blocked: None,
|
||||
frozen: None,
|
||||
depends_on: None,
|
||||
epic_id: None,
|
||||
}],
|
||||
@@ -242,6 +244,7 @@ mod tests {
|
||||
qa: None,
|
||||
retry_count: None,
|
||||
blocked: None,
|
||||
frozen: None,
|
||||
depends_on: None,
|
||||
epic_id: None,
|
||||
}],
|
||||
@@ -298,6 +301,7 @@ mod tests {
|
||||
qa: None,
|
||||
retry_count: None,
|
||||
blocked: None,
|
||||
frozen: None,
|
||||
depends_on: None,
|
||||
epic_id: None,
|
||||
}],
|
||||
|
||||
@@ -209,6 +209,7 @@ mod tests {
|
||||
qa: None,
|
||||
retry_count: None,
|
||||
blocked: None,
|
||||
frozen: None,
|
||||
depends_on: None,
|
||||
epic_id: None,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user