huskies: merge 503_bug_depends_on_pointing_at_an_archived_story_is_silently_treated_as_deps_met_surprising_users
This commit is contained in:
@@ -431,6 +431,7 @@ fn extract_item_view(item: &PipelineItemCrdt) -> Option<PipelineItemView> {
|
||||
/// according to CRDT state.
|
||||
///
|
||||
/// Returns `true` if the dependency is satisfied (item found in a done stage).
|
||||
/// See `dep_is_archived_crdt` to distinguish archive-satisfied from cleanly-done.
|
||||
pub fn dep_is_done_crdt(dep_number: u32) -> bool {
|
||||
let prefix = format!("{dep_number}_");
|
||||
if let Some(items) = read_all_items() {
|
||||
@@ -443,6 +444,22 @@ pub fn dep_is_done_crdt(dep_number: u32) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether a dependency (by numeric ID prefix) is specifically in `6_archived`
|
||||
/// according to CRDT state.
|
||||
///
|
||||
/// Used to detect when a dependency is satisfied via archive rather than via a clean
|
||||
/// completion through `5_done`. Returns `false` when the CRDT layer is not initialised.
|
||||
pub fn dep_is_archived_crdt(dep_number: u32) -> bool {
|
||||
let prefix = format!("{dep_number}_");
|
||||
if let Some(items) = read_all_items() {
|
||||
items.iter().any(|item| {
|
||||
item.story_id.starts_with(&prefix) && item.stage == "6_archived"
|
||||
})
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Check unmet dependencies for a story by reading its `depends_on` from the
|
||||
/// CRDT document and checking each dependency against CRDT state.
|
||||
///
|
||||
@@ -461,6 +478,25 @@ pub fn check_unmet_deps_crdt(story_id: &str) -> Vec<u32> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Return the list of dependency numbers from `story_id`'s `depends_on` that are
|
||||
/// specifically in `6_archived` according to CRDT state.
|
||||
///
|
||||
/// Used to emit a warning when promotion fires because a dep is archived rather than
|
||||
/// cleanly completed. Returns an empty `Vec` when no deps are archived.
|
||||
pub fn check_archived_deps_crdt(story_id: &str) -> Vec<u32> {
|
||||
let item = match read_item(story_id) {
|
||||
Some(i) => i,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
let deps = match item.depends_on {
|
||||
Some(d) => d,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
deps.into_iter()
|
||||
.filter(|&dep| dep_is_archived_crdt(dep))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Hex-encode a byte slice (no external dep needed).
|
||||
mod hex {
|
||||
pub fn encode(bytes: &[u8]) -> String {
|
||||
@@ -793,4 +829,19 @@ mod tests {
|
||||
let result = check_unmet_deps_crdt("nonexistent_story");
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
// ── Bug 503: archived-dep visibility ─────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn dep_is_archived_crdt_returns_false_when_no_crdt_state() {
|
||||
// When the global CRDT state is not initialised, must not panic.
|
||||
let _ = dep_is_archived_crdt(9998);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_archived_deps_crdt_returns_empty_when_item_not_found() {
|
||||
// Non-existent story should return empty archived deps.
|
||||
let result = check_archived_deps_crdt("nonexistent_story_archived");
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user