Files
huskies/server/src/chat/lookup.rs
T
dave 845b85e7a7 fix: add --all to cargo fmt in script/test and autoformat codebase
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>
2026-04-13 14:07:08 +00:00

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"
);
}
}