huskies: merge 946

This commit is contained in:
dave
2026-05-13 07:54:50 +00:00
parent 4a0fbcaa95
commit a7840ea4b0
49 changed files with 378 additions and 314 deletions
+12 -6
View File
@@ -41,12 +41,15 @@ pub fn set_depends_on(story_id: &str, deps: &[u32]) -> bool {
/// Set the `item_type` CRDT register for a pipeline item (sub-story 933).
///
/// `Some(t)` writes the type string (e.g. `"story"`, `"epic"`, `"bug"`).
/// `Some(t)` writes the canonical type string (e.g. `"story"`, `"epic"`, `"bug"`).
/// `None` clears the register to an empty string, which means "use the
/// id-prefix heuristic" (see `item_type_from_id`).
///
/// Returns `true` if the item was found and the op was applied, `false` otherwise.
pub fn set_item_type(story_id: &str, item_type: Option<&str>) -> bool {
pub fn set_item_type(
story_id: &str,
item_type: Option<crate::io::story_metadata::ItemType>,
) -> bool {
let Some(state_mutex) = get_crdt() else {
return false;
};
@@ -56,18 +59,21 @@ pub fn set_item_type(story_id: &str, item_type: Option<&str>) -> bool {
let Some(&idx) = state.index.get(story_id) else {
return false;
};
let value = item_type.unwrap_or("").to_string();
let value = item_type
.map(|t| t.as_str().to_string())
.unwrap_or_default();
apply_and_persist(&mut state, |s| s.crdt.doc.items[idx].item_type.set(value));
true
}
/// Set the `epic` CRDT register for a pipeline item (sub-story 933).
///
/// `Some(epic_id)` links the item to its parent epic.
/// `Some(id)` links the item to its parent epic (stored as the numeric string,
/// e.g. `"9990"` for `EpicId(9990)`).
/// `None` clears the register to an empty string (no epic membership).
///
/// Returns `true` if the item was found and the op was applied, `false` otherwise.
pub fn set_epic(story_id: &str, epic_id: Option<&str>) -> bool {
pub fn set_epic(story_id: &str, epic_id: Option<crate::crdt_state::types::EpicId>) -> bool {
let Some(state_mutex) = get_crdt() else {
return false;
};
@@ -77,7 +83,7 @@ pub fn set_epic(story_id: &str, epic_id: Option<&str>) -> bool {
let Some(&idx) = state.index.get(story_id) else {
return false;
};
let value = epic_id.unwrap_or("").to_string();
let value = epic_id.map(|e| e.to_string()).unwrap_or_default();
apply_and_persist(&mut state, |s| s.crdt.doc.items[idx].epic.set(value));
true
}
+33 -34
View File
@@ -154,7 +154,18 @@ fn migrate_story_ids_to_numeric_skips_conflict() {
write_item_str(
"44_story_foo",
"1_backlog",
None,
Some("Foo slug"),
None,
None,
None,
None,
None,
None,
);
write_item_str(
"44",
"2_current",
Some("Foo numeric"),
None,
None,
None,
@@ -162,7 +173,6 @@ fn migrate_story_ids_to_numeric_skips_conflict() {
None,
None,
);
write_item_str("44", "2_current", None, None, None, None, None, None, None);
let result = migrate_story_ids_to_numeric();
// The slug entry must NOT be migrated because "44" is already occupied.
@@ -202,7 +212,7 @@ fn migrate_story_ids_to_numeric_preserves_stage_and_name() {
let item = read_item("45").expect("item must be accessible by numeric ID");
assert!(matches!(item.stage, crate::pipeline_state::Stage::Coding));
assert_eq!(item.name.as_deref(), Some("Crash Bug"));
assert_eq!(item.name, "Crash Bug");
assert_eq!(item.agent.as_deref(), Some("coder-1"));
}
@@ -223,20 +233,18 @@ fn migrate_names_from_slugs_fills_empty_names() {
None,
);
// Before migration the name should be empty.
let before = read_item("42_story_my_feature").unwrap();
// Before migration: nameless item is filtered by read_item (AC 5).
assert!(
before.name.as_deref().unwrap_or("").is_empty(),
"name should be empty before migration"
read_item("42_story_my_feature").is_none(),
"nameless item must not be returned by read_item before migration"
);
migrate_names_from_slugs();
// After migration the name should be derived from the slug.
// After migration the item has a name and is visible to read_item.
let after = read_item("42_story_my_feature").unwrap();
assert_eq!(
after.name.as_deref(),
Some("My feature"),
after.name, "My feature",
"name should be derived from slug after migration"
);
}
@@ -261,8 +269,7 @@ fn migrate_names_from_slugs_leaves_existing_names_unchanged() {
let after = read_item("43_story_named_item").unwrap();
assert_eq!(
after.name.as_deref(),
Some("Already Named"),
after.name, "Already Named",
"pre-existing name must not be overwritten"
);
}
@@ -300,7 +307,7 @@ fn set_depends_on_round_trip_and_clear() {
let view = read_item("872_test_target").unwrap();
assert_eq!(
view.depends_on,
Some(vec![837]),
vec![837u32],
"CRDT register should hold [837]"
);
@@ -308,8 +315,8 @@ fn set_depends_on_round_trip_and_clear() {
let ok = set_depends_on("872_test_target", &[]);
assert!(ok, "set_depends_on([]) should return true");
let view = read_item("872_test_target").unwrap();
assert_eq!(
view.depends_on, None,
assert!(
view.depends_on.is_empty(),
"clearing should leave register unset"
);
@@ -412,7 +419,7 @@ fn set_qa_mode_round_trip_server_then_human() {
write_item_str(
"869_story_qa_roundtrip",
"1_backlog",
None,
Some("Qa Roundtrip"),
None,
None,
None,
@@ -426,9 +433,9 @@ fn set_qa_mode_round_trip_server_then_human() {
assert!(ok, "set_qa_mode should return true for known item");
let view = read_item("869_story_qa_roundtrip").unwrap();
assert_eq!(
view.qa_mode.as_deref(),
Some("server"),
"CRDT register should hold \"server\""
view.qa_mode,
Some(QaMode::Server),
"CRDT register should hold Server"
);
// Set qa=human via typed path and assert CRDT register is updated.
@@ -436,9 +443,9 @@ fn set_qa_mode_round_trip_server_then_human() {
assert!(ok, "set_qa_mode should return true for known item");
let view = read_item("869_story_qa_roundtrip").unwrap();
assert_eq!(
view.qa_mode.as_deref(),
Some("human"),
"CRDT register should hold \"human\""
view.qa_mode,
Some(QaMode::Human),
"CRDT register should hold Human"
);
// Clear via None — register goes back to unset.
@@ -467,7 +474,7 @@ fn bump_retry_count_increments_by_one() {
write_item_str(
"9001_story_bump_test",
"2_current",
None,
Some("Bump Test"),
None,
None,
None,
@@ -483,11 +490,7 @@ fn bump_retry_count_increments_by_one() {
assert_eq!(v2, 2, "second bump should return 2");
let item = read_item("9001_story_bump_test").expect("item must exist");
assert_eq!(
item.retry_count,
Some(2),
"CRDT must reflect final bump value"
);
assert_eq!(item.retry_count, 2u32, "CRDT must reflect final bump value");
}
#[test]
@@ -496,7 +499,7 @@ fn set_retry_count_resets_to_zero() {
write_item_str(
"9002_story_set_test",
"2_current",
None,
Some("Set Test"),
None,
Some(5),
None,
@@ -508,11 +511,7 @@ fn set_retry_count_resets_to_zero() {
set_retry_count("9002_story_set_test", 0);
let item = read_item("9002_story_set_test").expect("item must exist");
assert_eq!(
item.retry_count,
Some(0),
"set_retry_count(0) must reset to 0"
);
assert_eq!(item.retry_count, 0u32, "set_retry_count(0) must reset to 0");
}
#[test]