huskies: merge 517_story_remove_filesystem_shadow_fallback_paths_from_lifecycle_rs_finish_the_migration_to_crdt_only

This commit is contained in:
dave
2026-04-10 12:56:16 +00:00
parent fe405e81c6
commit 31388da609
12 changed files with 171 additions and 170 deletions
+34 -55
View File
@@ -24,7 +24,7 @@ pub(super) fn item_type_from_id(item_id: &str) -> &'static str {
/// `sources` stages, then updates the stage. Optionally clears front-matter
/// fields from the stored content. Returns the source stage on success.
fn move_item<'a>(
project_root: &Path,
_project_root: &Path,
story_id: &str,
sources: &'a [&'a str],
target_dir: &str,
@@ -86,42 +86,20 @@ fn move_item<'a>(
return Ok(Some(src_dir));
}
// Item not found in CRDT — check the content store as fallback.
if crate::db::read_content(story_id).is_some() {
// Content exists but not in CRDT yet — write it through.
let content = crate::db::read_content(story_id).unwrap();
// Item not found in CRDT — check the content store as a migration
// fallback. This handles items that were imported into the DB but
// haven't yet been replicated into the CRDT layer. Unlike the old
// filesystem fallback (removed — see story 517), this path reads
// from the authoritative in-memory DB and cannot cause state drift.
if let Some(mut content) = crate::db::read_content(story_id) {
for field in fields_to_clear {
content = clear_front_matter_field_in_content(&content, field);
}
crate::db::write_item_with_content(story_id, target_dir, &content);
slog!("[lifecycle] Moved '{story_id}' to work/{target_dir}/ (content store fallback)");
return Ok(Some(sources[0]));
}
// Try filesystem fallback for backwards compatibility during migration.
{
let sk = project_root.join(".huskies").join("work");
if let Some((src_dir, src_path)) = sources.iter().find_map(|&s| {
let p = sk.join(s).join(format!("{story_id}.md"));
p.exists().then_some((s, p))
}) && let Ok(mut content) = std::fs::read_to_string(&src_path) {
// Optionally clear front-matter fields.
for field in fields_to_clear {
content = clear_front_matter_field_in_content(&content, field);
}
// Import to DB.
crate::db::write_item_with_content(story_id, target_dir, &content);
// Also move on filesystem for backwards compat.
let target_path = sk.join(target_dir).join(format!("{story_id}.md"));
let _ = std::fs::create_dir_all(sk.join(target_dir));
let _ = std::fs::write(&target_path, &content);
// Only remove the source if it differs from the target (avoid
// deleting the file when src and target are the same directory).
if src_dir != target_dir {
let _ = std::fs::remove_file(&src_path);
}
slog!("[lifecycle] Moved '{story_id}' from work/{src_dir}/ to work/{target_dir}/");
return Ok(Some(src_dir));
}
}
if missing_ok {
slog!("[lifecycle] Work item '{story_id}' not found; skipping move to work/{target_dir}/");
return Ok(None);
@@ -135,11 +113,15 @@ fn move_item<'a>(
Err(format!("Work item '{story_id}' not found in {locs}."))
}
/// Move a work item (story, bug, or spike) from `work/1_backlog/` to `work/2_current/`.
/// Move a work item (story, bug, or spike) to `work/2_current/`.
///
/// Idempotent: if already in `2_current/`, returns Ok. If not found in `1_backlog/`, logs and returns Ok.
/// The source stage is read from the CRDT — any existing stage is accepted.
/// Idempotent: if already in `2_current/`, returns Ok. If not found, logs and returns Ok.
pub fn move_story_to_current(project_root: &Path, story_id: &str) -> Result<(), String> {
move_item(project_root, story_id, &["1_backlog"], "2_current", &[], true, &[]).map(|_| ())
const ALL_STAGES: &[&str] = &[
"1_backlog", "2_current", "3_qa", "4_merge", "5_done", "6_archived",
];
move_item(project_root, story_id, ALL_STAGES, "2_current", &[], true, &[]).map(|_| ())
}
/// Check whether a feature branch `feature/story-{story_id}` exists and has
@@ -306,28 +288,25 @@ mod tests {
// ── move_story_to_current tests ────────────────────────────────────────────
#[test]
fn move_story_to_current_from_filesystem() {
let tmp = tempfile::tempdir().unwrap();
let backlog = tmp.path().join(".huskies/work/1_backlog");
let current = tmp.path().join(".huskies/work/2_current");
std::fs::create_dir_all(&backlog).unwrap();
std::fs::create_dir_all(&current).unwrap();
std::fs::write(
backlog.join("10_story_foo.md"),
"---\nname: Test\n---\n# Story\n",
)
.unwrap();
move_story_to_current(tmp.path(), "10_story_foo").unwrap();
// Verify the story was moved to current.
assert!(
current.join("10_story_foo.md").exists(),
"story should be in 2_current/"
fn move_story_to_current_from_content_store() {
// Seed via the content store (the DB's in-memory representation).
// CRDT is not initialised in unit tests, so move_item uses the
// content-store fallback which re-imports to the target stage.
crate::db::ensure_content_store();
crate::db::write_content(
"99950_story_lifecycle",
"---\nname: Lifecycle Test\n---\n# Story\n",
);
let tmp = tempfile::tempdir().unwrap();
move_story_to_current(tmp.path(), "99950_story_lifecycle").unwrap();
// Verify the content store now has the item (imported at target stage).
let content = crate::db::read_content("99950_story_lifecycle")
.expect("item should be in content store after move");
assert!(
!backlog.join("10_story_foo.md").exists(),
"story should not still be in 1_backlog/"
content.contains("Lifecycle Test"),
"content should be preserved after move"
);
}