huskies: merge 557_refactor_remove_all_filesystem_fallback_paths_crdt_is_the_only_source_of_truth
This commit is contained in:
@@ -10,7 +10,8 @@ export const SLASH_COMMANDS: SlashCommand[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "/backlog",
|
name: "/backlog",
|
||||||
description: "Show all items in the backlog with dependency satisfaction status.",
|
description:
|
||||||
|
"Show all items in the backlog with dependency satisfaction status.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "/status",
|
name: "/status",
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|||||||
|
|
||||||
echo "=== Checking Rust formatting ==="
|
echo "=== Checking Rust formatting ==="
|
||||||
if cargo fmt --version &>/dev/null; then
|
if cargo fmt --version &>/dev/null; then
|
||||||
cargo fmt --manifest-path "$PROJECT_ROOT/Cargo.toml" --check
|
cargo fmt --manifest-path "$PROJECT_ROOT/Cargo.toml" --all --check
|
||||||
else
|
else
|
||||||
echo "Skipping Rust formatting check (rustfmt not installed)"
|
echo "Skipping Rust formatting check (rustfmt not installed)"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -300,15 +300,15 @@ mod tests {
|
|||||||
async fn auto_assign_picks_up_story_queued_in_current() {
|
async fn auto_assign_picks_up_story_queued_in_current() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let sk = tmp.path().join(".huskies");
|
let sk = tmp.path().join(".huskies");
|
||||||
let current = sk.join("work/2_current");
|
std::fs::create_dir_all(&sk).unwrap();
|
||||||
std::fs::create_dir_all(¤t).unwrap();
|
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
sk.join("project.toml"),
|
sk.join("project.toml"),
|
||||||
"[[agent]]\nname = \"coder-1\"\nstage = \"coder\"\n",
|
"[[agent]]\nname = \"coder-1\"\nstage = \"coder\"\n",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// Place the story in 2_current/ (simulating the "queued" state).
|
// Place the story in 2_current/ via CRDT (the only source of truth).
|
||||||
std::fs::write(current.join("story-3.md"), "---\nname: Story 3\n---\n").unwrap();
|
crate::db::ensure_content_store();
|
||||||
|
crate::db::write_item_with_content("story-3", "2_current", "---\nname: Story 3\n---\n");
|
||||||
|
|
||||||
let pool = AgentPool::new_test(3001);
|
let pool = AgentPool::new_test(3001);
|
||||||
// No agents are running — coder-1 is free.
|
// No agents are running — coder-1 is free.
|
||||||
@@ -549,23 +549,22 @@ mod tests {
|
|||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let root = tmp.path();
|
let root = tmp.path();
|
||||||
let sk = root.join(".huskies");
|
let sk = root.join(".huskies");
|
||||||
let current = sk.join("work/2_current");
|
std::fs::create_dir_all(&sk).unwrap();
|
||||||
let done = sk.join("work/5_done");
|
|
||||||
std::fs::create_dir_all(¤t).unwrap();
|
|
||||||
std::fs::create_dir_all(&done).unwrap();
|
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
sk.join("project.toml"),
|
sk.join("project.toml"),
|
||||||
"[[agent]]\nname = \"coder-1\"\nstage = \"coder\"\n",
|
"[[agent]]\nname = \"coder-1\"\nstage = \"coder\"\n",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
// Seed stories via CRDT (the only source of truth).
|
||||||
|
crate::db::ensure_content_store();
|
||||||
// Dep 999 is now done.
|
// Dep 999 is now done.
|
||||||
std::fs::write(done.join("999_story_dep.md"), "---\nname: Dep\n---\n").unwrap();
|
crate::db::write_item_with_content("999_story_dep", "5_done", "---\nname: Dep\n---\n");
|
||||||
// Story 10 depends on 999 which is done.
|
// Story 10 depends on 999 which is done.
|
||||||
std::fs::write(
|
crate::db::write_item_with_content(
|
||||||
current.join("10_story_unblocked.md"),
|
"10_story_unblocked",
|
||||||
|
"2_current",
|
||||||
"---\nname: Unblocked\ndepends_on: [999]\n---\n",
|
"---\nname: Unblocked\ndepends_on: [999]\n---\n",
|
||||||
)
|
);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let pool = AgentPool::new_test(3001);
|
let pool = AgentPool::new_test(3001);
|
||||||
pool.auto_assign_available_work(root).await;
|
pool.auto_assign_available_work(root).await;
|
||||||
|
|||||||
@@ -690,14 +690,9 @@ mod tests {
|
|||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let root = tmp.path();
|
let root = tmp.path();
|
||||||
|
|
||||||
// Set up story in 2_current/
|
// Seed story via CRDT (the only source of truth).
|
||||||
let current = root.join(".huskies/work/2_current");
|
crate::db::ensure_content_store();
|
||||||
fs::create_dir_all(¤t).unwrap();
|
crate::db::write_item_with_content("173_story_test", "2_current", "---\nname: test\n---\n");
|
||||||
fs::write(current.join("173_story_test.md"), "test").unwrap();
|
|
||||||
// Ensure 3_qa/ exists for the move target
|
|
||||||
fs::create_dir_all(root.join(".huskies/work/3_qa")).unwrap();
|
|
||||||
// Ensure 1_backlog/ exists (start_agent calls move_story_to_current)
|
|
||||||
fs::create_dir_all(root.join(".huskies/work/1_backlog")).unwrap();
|
|
||||||
|
|
||||||
// Write a project.toml with a qa agent so start_agent can resolve it.
|
// Write a project.toml with a qa agent so start_agent can resolve it.
|
||||||
fs::create_dir_all(root.join(".huskies")).unwrap();
|
fs::create_dir_all(root.join(".huskies")).unwrap();
|
||||||
@@ -880,8 +875,7 @@ stage = "qa"
|
|||||||
let root = tmp.path();
|
let root = tmp.path();
|
||||||
|
|
||||||
let sk = root.join(".huskies");
|
let sk = root.join(".huskies");
|
||||||
let qa_dir = sk.join("work/3_qa");
|
fs::create_dir_all(&sk).unwrap();
|
||||||
fs::create_dir_all(&qa_dir).unwrap();
|
|
||||||
|
|
||||||
// Configure a single QA agent.
|
// Configure a single QA agent.
|
||||||
fs::write(
|
fs::write(
|
||||||
@@ -894,19 +888,21 @@ stage = "qa"
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
// Seed stories via CRDT (the only source of truth).
|
||||||
|
crate::db::ensure_content_store();
|
||||||
// Story 292 is in QA with QA agent running (will "complete" via
|
// Story 292 is in QA with QA agent running (will "complete" via
|
||||||
// run_pipeline_advance below). Story 293 is in QA with NO agent —
|
// run_pipeline_advance below). Story 293 is in QA with NO agent —
|
||||||
// simulating the "stuck" state from bug 295.
|
// simulating the "stuck" state from bug 295.
|
||||||
fs::write(
|
crate::db::write_item_with_content(
|
||||||
qa_dir.join("292_story_first.md"),
|
"292_story_first",
|
||||||
|
"3_qa",
|
||||||
"---\nname: First\nqa: human\n---\n",
|
"---\nname: First\nqa: human\n---\n",
|
||||||
)
|
);
|
||||||
.unwrap();
|
crate::db::write_item_with_content(
|
||||||
fs::write(
|
"293_story_second",
|
||||||
qa_dir.join("293_story_second.md"),
|
"3_qa",
|
||||||
"---\nname: Second\nqa: human\n---\n",
|
"---\nname: Second\nqa: human\n---\n",
|
||||||
)
|
);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let pool = AgentPool::new_test(3001);
|
let pool = AgentPool::new_test(3001);
|
||||||
// QA is currently running on story 292.
|
// QA is currently running on story 292.
|
||||||
|
|||||||
@@ -35,16 +35,11 @@ pub fn extract_story_number(item_id: &str) -> Option<&str> {
|
|||||||
.filter(|s| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit()))
|
.filter(|s| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the story name from the work item file's YAML front matter.
|
/// Read the story name from the CRDT content store's YAML front matter.
|
||||||
///
|
///
|
||||||
/// Returns `None` if the file doesn't exist or has no parseable name.
|
/// Returns `None` if the item is not in the content store or has no parseable name.
|
||||||
pub fn read_story_name(project_root: &Path, stage: &str, item_id: &str) -> Option<String> {
|
pub fn read_story_name(_project_root: &Path, _stage: &str, item_id: &str) -> Option<String> {
|
||||||
let path = project_root
|
let contents = crate::db::read_content(item_id)?;
|
||||||
.join(".huskies")
|
|
||||||
.join("work")
|
|
||||||
.join(stage)
|
|
||||||
.join(format!("{item_id}.md"));
|
|
||||||
let contents = std::fs::read_to_string(&path).ok()?;
|
|
||||||
let meta = parse_front_matter(&contents).ok()?;
|
let meta = parse_front_matter(&contents).ok()?;
|
||||||
meta.name
|
meta.name
|
||||||
}
|
}
|
||||||
@@ -85,18 +80,11 @@ pub fn format_error_notification(
|
|||||||
(plain, html)
|
(plain, html)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Search all pipeline stages for a story name.
|
/// Look up a story name from the CRDT content store.
|
||||||
///
|
///
|
||||||
/// Tries each known pipeline stage directory in order and returns the first
|
/// Used for events (like rate-limit warnings) that arrive without a known stage.
|
||||||
/// name found. Used for events (like rate-limit warnings) that arrive without
|
|
||||||
/// a known stage.
|
|
||||||
fn find_story_name_any_stage(project_root: &Path, item_id: &str) -> Option<String> {
|
fn find_story_name_any_stage(project_root: &Path, item_id: &str) -> Option<String> {
|
||||||
for stage in &["2_current", "3_qa", "4_merge", "1_backlog", "5_done"] {
|
read_story_name(project_root, "", item_id)
|
||||||
if let Some(name) = read_story_name(project_root, stage, item_id) {
|
|
||||||
return Some(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format a blocked-story notification message.
|
/// Format a blocked-story notification message.
|
||||||
@@ -432,13 +420,13 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn rate_limit_warning_sends_notification_with_agent_and_story() {
|
async fn rate_limit_warning_sends_notification_with_agent_and_story() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let stage_dir = tmp.path().join(".huskies").join("work").join("2_current");
|
// Seed story via CRDT (the only source of truth).
|
||||||
std::fs::create_dir_all(&stage_dir).unwrap();
|
crate::db::ensure_content_store();
|
||||||
std::fs::write(
|
crate::db::write_item_with_content(
|
||||||
stage_dir.join("365_story_rate_limit.md"),
|
"365_story_rate_limit",
|
||||||
|
"2_current",
|
||||||
"---\nname: Rate Limit Test Story\n---\n",
|
"---\nname: Rate Limit Test Story\n---\n",
|
||||||
)
|
);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let (watcher_tx, watcher_rx) = broadcast::channel::<WatcherEvent>(16);
|
let (watcher_tx, watcher_rx) = broadcast::channel::<WatcherEvent>(16);
|
||||||
let (transport, calls) = MockTransport::new();
|
let (transport, calls) = MockTransport::new();
|
||||||
@@ -560,13 +548,9 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn stage_notification_uses_dynamic_room_ids() {
|
async fn stage_notification_uses_dynamic_room_ids() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let stage_dir = tmp.path().join(".huskies").join("work").join("3_qa");
|
// Seed story via CRDT (the only source of truth).
|
||||||
std::fs::create_dir_all(&stage_dir).unwrap();
|
crate::db::ensure_content_store();
|
||||||
std::fs::write(
|
crate::db::write_item_with_content("10_story_foo", "3_qa", "---\nname: Foo Story\n---\n");
|
||||||
stage_dir.join("10_story_foo.md"),
|
|
||||||
"---\nname: Foo Story\n---\n",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let (watcher_tx, watcher_rx) = broadcast::channel::<WatcherEvent>(16);
|
let (watcher_tx, watcher_rx) = broadcast::channel::<WatcherEvent>(16);
|
||||||
let (transport, calls) = MockTransport::new();
|
let (transport, calls) = MockTransport::new();
|
||||||
@@ -681,38 +665,37 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn read_story_name_reads_from_front_matter() {
|
fn read_story_name_reads_from_front_matter() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
crate::db::ensure_content_store();
|
||||||
let stage_dir = tmp.path().join(".huskies").join("work").join("2_current");
|
crate::db::write_item_with_content(
|
||||||
std::fs::create_dir_all(&stage_dir).unwrap();
|
"9942_story_my_feature",
|
||||||
std::fs::write(
|
"2_current",
|
||||||
stage_dir.join("42_story_my_feature.md"),
|
|
||||||
"---\nname: My Cool Feature\n---\n# Story\n",
|
"---\nname: My Cool Feature\n---\n# Story\n",
|
||||||
)
|
);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let name = read_story_name(tmp.path(), "2_current", "42_story_my_feature");
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let name = read_story_name(tmp.path(), "2_current", "9942_story_my_feature");
|
||||||
assert_eq!(name.as_deref(), Some("My Cool Feature"));
|
assert_eq!(name.as_deref(), Some("My Cool Feature"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn read_story_name_returns_none_for_missing_file() {
|
fn read_story_name_returns_none_for_missing_file() {
|
||||||
|
crate::db::ensure_content_store();
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let name = read_story_name(tmp.path(), "2_current", "99_story_missing");
|
let name = read_story_name(tmp.path(), "2_current", "99_story_missing_notif_test");
|
||||||
assert_eq!(name, None);
|
assert_eq!(name, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn read_story_name_returns_none_for_missing_name_field() {
|
fn read_story_name_returns_none_for_missing_name_field() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
crate::db::ensure_content_store();
|
||||||
let stage_dir = tmp.path().join(".huskies").join("work").join("2_current");
|
crate::db::write_item_with_content(
|
||||||
std::fs::create_dir_all(&stage_dir).unwrap();
|
"9943_story_no_name",
|
||||||
std::fs::write(
|
"2_current",
|
||||||
stage_dir.join("42_story_no_name.md"),
|
|
||||||
"---\ncoverage_baseline: 50%\n---\n# Story\n",
|
"---\ncoverage_baseline: 50%\n---\n# Story\n",
|
||||||
)
|
);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let name = read_story_name(tmp.path(), "2_current", "42_story_no_name");
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let name = read_story_name(tmp.path(), "2_current", "9943_story_no_name");
|
||||||
assert_eq!(name, None);
|
assert_eq!(name, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -786,13 +769,13 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn story_blocked_sends_notification_with_reason() {
|
async fn story_blocked_sends_notification_with_reason() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let stage_dir = tmp.path().join(".huskies").join("work").join("2_current");
|
// Seed story via CRDT (the only source of truth).
|
||||||
std::fs::create_dir_all(&stage_dir).unwrap();
|
crate::db::ensure_content_store();
|
||||||
std::fs::write(
|
crate::db::write_item_with_content(
|
||||||
stage_dir.join("425_story_blocking_test.md"),
|
"425_story_blocking_test",
|
||||||
|
"2_current",
|
||||||
"---\nname: Blocking Test Story\n---\n",
|
"---\nname: Blocking Test Story\n---\n",
|
||||||
)
|
);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let (watcher_tx, watcher_rx) = broadcast::channel::<WatcherEvent>(16);
|
let (watcher_tx, watcher_rx) = broadcast::channel::<WatcherEvent>(16);
|
||||||
let (transport, calls) = MockTransport::new();
|
let (transport, calls) = MockTransport::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user