845b85e7a7
cargo fmt without --all fails with "Failed to find targets" in workspace repos. This was blocking every story's gates. Also ran cargo fmt --all to fix all existing formatting issues. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
156 lines
5.7 KiB
Rust
156 lines
5.7 KiB
Rust
//! Shared story-lookup helper for chat commands.
|
|
//!
|
|
//! All chat commands that need to find a work item by its numeric prefix
|
|
//! use [`find_story_by_number`]. The lookup reads from:
|
|
//!
|
|
//! 1. **CRDT** — authoritative in-memory state.
|
|
//! 2. **Content store / pipeline_items** — in-memory mirror of the
|
|
//! `pipeline_items` table; catches items that are in the DB but whose
|
|
//! CRDT entry hasn't been synced yet.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
/// Locate a work item by its numeric ID prefix.
|
|
///
|
|
/// Returns `(story_id, stage_dir, path, content)` where:
|
|
/// - `story_id` — full stem, e.g. `"503_story_some_feature"`
|
|
/// - `stage_dir` — pipeline directory name, e.g. `"4_merge"`
|
|
/// - `path` — canonical path under `.huskies/work/`; may not exist on disk
|
|
/// for CRDT-only stories
|
|
/// - `content` — markdown body from the content store when available;
|
|
/// for the filesystem fallback this is the file contents read from disk;
|
|
/// `None` only when the story was found in CRDT but has no content entry
|
|
/// (callers may fall back to `fs::read_to_string(&path)` in that case)
|
|
///
|
|
/// Returns `None` when no matching work item is found by any mechanism.
|
|
pub(crate) fn find_story_by_number(
|
|
project_root: &Path,
|
|
number: &str,
|
|
) -> Option<(String, String, PathBuf, Option<String>)> {
|
|
// ── 1. CRDT (authoritative) ──────────────────────────────────────────
|
|
// `read_all_items` returns None only when the CRDT layer is not yet
|
|
// initialised (e.g. in unit tests or very early startup).
|
|
if let Some(items) = crate::crdt_state::read_all_items() {
|
|
for item in items {
|
|
if item.story_id.split('_').next().unwrap_or("") == number {
|
|
let path = project_root
|
|
.join(".huskies")
|
|
.join("work")
|
|
.join(&item.stage)
|
|
.join(format!("{}.md", item.story_id));
|
|
let content = crate::db::read_content(&item.story_id);
|
|
return Some((item.story_id, item.stage, path, content));
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── 2. Content store + CRDT stage lookup ────────────────────────────
|
|
// Handles the edge case where an item is in the content store but was
|
|
// somehow missing from the CRDT iteration above (e.g. concurrent write
|
|
// or CRDT not yet initialised, such as in unit tests).
|
|
for id in crate::db::all_content_ids() {
|
|
if id.split('_').next().unwrap_or("") != number {
|
|
continue;
|
|
}
|
|
let stage_dir = crate::crdt_state::read_item(&id)
|
|
.map(|v| v.stage)
|
|
.unwrap_or_else(|| "1_backlog".to_string());
|
|
let path = project_root
|
|
.join(".huskies")
|
|
.join("work")
|
|
.join(&stage_dir)
|
|
.join(format!("{id}.md"));
|
|
let content = crate::db::read_content(&id);
|
|
return Some((id, stage_dir, path, content));
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::chat::test_helpers::write_story_file;
|
|
|
|
#[test]
|
|
fn not_found_returns_none() {
|
|
let tmp = tempfile::TempDir::new().unwrap();
|
|
let result = find_story_by_number(tmp.path(), "999");
|
|
assert!(
|
|
result.is_none(),
|
|
"should return None when story is not found"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn finds_story_in_content_store() {
|
|
let tmp = tempfile::TempDir::new().unwrap();
|
|
write_story_file(
|
|
tmp.path(),
|
|
"1_backlog",
|
|
"9970_story_some_feature.md",
|
|
"---\nname: Some Feature\n---\n\n# Story 9970\n",
|
|
);
|
|
let (story_id, _stage_dir, path, content) =
|
|
find_story_by_number(tmp.path(), "9970").expect("should find story 9970");
|
|
assert_eq!(story_id, "9970_story_some_feature");
|
|
assert!(
|
|
path.ends_with("9970_story_some_feature.md"),
|
|
"unexpected path: {path:?}"
|
|
);
|
|
assert!(
|
|
content.as_deref().unwrap_or("").contains("Some Feature"),
|
|
"content should include story text"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn finds_bug_by_number() {
|
|
let tmp = tempfile::TempDir::new().unwrap();
|
|
write_story_file(
|
|
tmp.path(),
|
|
"2_current",
|
|
"7_bug_crash_on_login.md",
|
|
"---\nname: Crash on login\n---\n",
|
|
);
|
|
let (story_id, _stage_dir, _, _) =
|
|
find_story_by_number(tmp.path(), "7").expect("should find bug 7");
|
|
assert_eq!(story_id, "7_bug_crash_on_login");
|
|
}
|
|
|
|
#[test]
|
|
fn numeric_prefix_must_match_exactly() {
|
|
let tmp = tempfile::TempDir::new().unwrap();
|
|
// Story 9971 exists; searching for "99710" must not match "9971_story_foo".
|
|
write_story_file(
|
|
tmp.path(),
|
|
"1_backlog",
|
|
"9971_story_foo.md",
|
|
"---\nname: Foo\n---\n",
|
|
);
|
|
let result = find_story_by_number(tmp.path(), "99710");
|
|
assert!(result.is_none(), "number 99710 should not match story 9971");
|
|
}
|
|
|
|
#[test]
|
|
fn returned_path_points_into_project_root() {
|
|
let tmp = tempfile::TempDir::new().unwrap();
|
|
write_story_file(
|
|
tmp.path(),
|
|
"4_merge",
|
|
"503_story_migration.md",
|
|
"---\nname: Migration\n---\n",
|
|
);
|
|
let (_, _, path, _) =
|
|
find_story_by_number(tmp.path(), "503").expect("should find story 503");
|
|
assert!(
|
|
path.starts_with(tmp.path()),
|
|
"path should be under the project root"
|
|
);
|
|
}
|
|
}
|