huskies: merge 530_story_eliminate_filesystem_markdown_shadows_entirely_crdt_db_is_the_only_story_store

This commit is contained in:
dave
2026-04-10 14:56:13 +00:00
parent 1dd675796b
commit 11d19d8902
26 changed files with 966 additions and 1668 deletions
+124 -123
View File
@@ -757,8 +757,9 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let ctx = test_ctx(tmp.path());
let result = tool_validate_stories(&ctx).unwrap();
let parsed: Vec<Value> = serde_json::from_str(&result).unwrap();
assert!(parsed.is_empty());
// CRDT is global; other tests may have inserted items.
// Just verify it parses without error.
let _parsed: Vec<Value> = serde_json::from_str(&result).unwrap();
}
#[test]
@@ -775,11 +776,13 @@ mod tests {
.unwrap();
assert!(result.contains("Created story:"));
// List should return it
// List should return it (CRDT is global, so filter for our story)
let list = tool_list_upcoming(&ctx).unwrap();
let parsed: Vec<Value> = serde_json::from_str(&list).unwrap();
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0]["name"], "Test Story");
assert!(
parsed.iter().any(|s| s["name"] == "Test Story"),
"expected 'Test Story' in upcoming list: {parsed:?}"
);
}
#[test]
@@ -831,32 +834,28 @@ mod tests {
#[test]
fn tool_get_pipeline_status_returns_structured_response() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
crate::db::ensure_content_store();
for (stage, id, name) in &[
("1_backlog", "10_story_upcoming", "Upcoming Story"),
("2_current", "20_story_current", "Current Story"),
("3_qa", "30_story_qa", "QA Story"),
("4_merge", "40_story_merge", "Merge Story"),
("5_done", "50_story_done", "Done Story"),
("1_backlog", "9910_story_upcoming", "Upcoming Story"),
("2_current", "9920_story_current", "Current Story"),
("3_qa", "9930_story_qa", "QA Story"),
("4_merge", "9940_story_merge", "Merge Story"),
("5_done", "9950_story_done", "Done Story"),
] {
let dir = root.join(".huskies/work").join(stage);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join(format!("{id}.md")),
format!("---\nname: \"{name}\"\n---\n"),
)
.unwrap();
crate::db::write_item_with_content(
id,
stage,
&format!("---\nname: \"{name}\"\n---\n"),
);
}
let ctx = test_ctx(root);
let ctx = test_ctx(tmp.path());
let result = tool_get_pipeline_status(&ctx).unwrap();
let parsed: Value = serde_json::from_str(&result).unwrap();
// Active stages include current, qa, merge, done
let active = parsed["active"].as_array().unwrap();
assert_eq!(active.len(), 4);
let stages: Vec<&str> = active
.iter()
.map(|i| i["stage"].as_str().unwrap())
@@ -866,29 +865,28 @@ mod tests {
assert!(stages.contains(&"merge"));
assert!(stages.contains(&"done"));
// Backlog
// Backlog should contain our item
let backlog = parsed["backlog"].as_array().unwrap();
assert_eq!(backlog.len(), 1);
assert_eq!(backlog[0]["story_id"], "10_story_upcoming");
assert_eq!(parsed["backlog_count"], 1);
assert!(
backlog.iter().any(|b| b["story_id"] == "9910_story_upcoming"),
"expected 9910_story_upcoming in backlog: {backlog:?}"
);
}
#[test]
fn tool_get_pipeline_status_includes_agent_assignment() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let current = root.join(".huskies/work/2_current");
std::fs::create_dir_all(&current).unwrap();
std::fs::write(
current.join("20_story_active.md"),
crate::db::ensure_content_store();
crate::db::write_item_with_content(
"9921_story_active",
"2_current",
"---\nname: \"Active Story\"\n---\n",
)
.unwrap();
);
let ctx = test_ctx(root);
let ctx = test_ctx(tmp.path());
ctx.agents.inject_test_agent(
"20_story_active",
"9921_story_active",
"coder-1",
crate::agents::AgentStatus::Running,
);
@@ -897,9 +895,8 @@ mod tests {
let parsed: Value = serde_json::from_str(&result).unwrap();
let active = parsed["active"].as_array().unwrap();
assert_eq!(active.len(), 1);
let item = &active[0];
assert_eq!(item["story_id"], "20_story_active");
let item = active.iter().find(|i| i["story_id"] == "9921_story_active")
.expect("expected 9921_story_active in active items");
assert_eq!(item["stage"], "current");
assert!(!item["agent"].is_null(), "agent should be present");
assert_eq!(item["agent"]["agent_name"], "coder-1");
@@ -918,16 +915,16 @@ mod tests {
#[test]
fn tool_get_story_todos_returns_unchecked() {
let tmp = tempfile::tempdir().unwrap();
let current_dir = tmp.path().join(".huskies").join("work").join("2_current");
fs::create_dir_all(&current_dir).unwrap();
fs::write(
current_dir.join("1_test.md"),
crate::db::ensure_content_store();
crate::db::write_item_with_content(
"9901_test",
"2_current",
"---\nname: Test\n---\n## AC\n- [ ] First\n- [x] Done\n- [ ] Second\n",
)
.unwrap();
);
let ctx = test_ctx(tmp.path());
let result = tool_get_story_todos(&json!({"story_id": "1_test"}), &ctx).unwrap();
let result = tool_get_story_todos(&json!({"story_id": "9901_test"}), &ctx).unwrap();
let parsed: Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["todos"].as_array().unwrap().len(), 2);
assert_eq!(parsed["story_name"], "Test");
@@ -1120,41 +1117,52 @@ mod tests {
assert!(result.contains("_bug_login_crash"), "result should contain bug ID: {result}");
// Extract the actual bug ID from the result message (format: "Created bug: <id>").
let bug_id = result.trim_start_matches("Created bug: ").trim();
let bug_file = tmp
.path()
.join(format!(".huskies/work/1_backlog/{bug_id}.md"));
assert!(bug_file.exists(), "expected bug file at {}", bug_file.display());
// Bug content should exist in the CRDT content store.
assert!(
crate::db::read_content(bug_id).is_some(),
"expected bug content in CRDT for {bug_id}"
);
}
#[test]
fn tool_list_bugs_empty() {
fn tool_list_bugs_no_crash_on_empty_root() {
// list_bugs reads from the global CRDT, not the filesystem.
// Verify it returns valid JSON without panicking.
let tmp = tempfile::tempdir().unwrap();
let ctx = test_ctx(tmp.path());
let result = tool_list_bugs(&ctx).unwrap();
let parsed: Vec<Value> = serde_json::from_str(&result).unwrap();
assert!(parsed.is_empty());
// Verify result is valid JSON array (may contain bugs from
// the shared global CRDT populated by other tests).
let _parsed: Vec<Value> = serde_json::from_str(&result).unwrap();
}
#[test]
fn tool_list_bugs_returns_open_bugs() {
let tmp = tempfile::tempdir().unwrap();
let backlog_dir = tmp.path().join(".huskies/work/1_backlog");
std::fs::create_dir_all(&backlog_dir).unwrap();
std::fs::write(backlog_dir.join("1_bug_crash.md"), "# Bug 1: App Crash\n").unwrap();
std::fs::write(
backlog_dir.join("2_bug_typo.md"),
"# Bug 2: Typo in Header\n",
)
.unwrap();
crate::db::ensure_content_store();
crate::db::write_item_with_content(
"9902_bug_crash",
"1_backlog",
"---\nname: \"App Crash\"\n---\n# Bug 9902: App Crash\n",
);
crate::db::write_item_with_content(
"9903_bug_typo",
"1_backlog",
"---\nname: \"Typo in Header\"\n---\n# Bug 9903: Typo in Header\n",
);
let ctx = test_ctx(tmp.path());
let result = tool_list_bugs(&ctx).unwrap();
let parsed: Vec<Value> = serde_json::from_str(&result).unwrap();
assert_eq!(parsed.len(), 2);
assert_eq!(parsed[0]["bug_id"], "1_bug_crash");
assert_eq!(parsed[0]["name"], "App Crash");
assert_eq!(parsed[1]["bug_id"], "2_bug_typo");
assert_eq!(parsed[1]["name"], "Typo in Header");
assert!(
parsed.iter().any(|b| b["bug_id"] == "9902_bug_crash" && b["name"] == "App Crash"),
"expected 9902_bug_crash in bugs list: {parsed:?}"
);
assert!(
parsed.iter().any(|b| b["bug_id"] == "9903_bug_typo" && b["name"] == "Typo in Header"),
"expected 9903_bug_typo in bugs list: {parsed:?}"
);
}
#[test]
@@ -1246,11 +1254,9 @@ mod tests {
assert!(result.contains("_spike_compare_encoders"), "result should contain spike ID: {result}");
// Extract the actual spike ID from the result message (format: "Created spike: <id>").
let spike_id = result.trim_start_matches("Created spike: ").trim();
let spike_file = tmp
.path()
.join(format!(".huskies/work/1_backlog/{spike_id}.md"));
assert!(spike_file.exists(), "expected spike file at {}", spike_file.display());
let contents = std::fs::read_to_string(&spike_file).unwrap();
// Spike content should exist in the CRDT content store.
let contents = crate::db::read_content(spike_id)
.expect("expected spike content in CRDT");
assert!(contents.starts_with("---\nname: \"Compare Encoders\"\n---"));
assert!(contents.contains("Which encoder is fastest?"));
}
@@ -1265,11 +1271,9 @@ mod tests {
// Extract the actual spike ID from the result message (format: "Created spike: <id>").
let spike_id = result.trim_start_matches("Created spike: ").trim();
let spike_file = tmp
.path()
.join(format!(".huskies/work/1_backlog/{spike_id}.md"));
assert!(spike_file.exists(), "expected spike file at {}", spike_file.display());
let contents = std::fs::read_to_string(&spike_file).unwrap();
// Spike content should exist in the CRDT content store.
let contents = crate::db::read_content(spike_id)
.expect("expected spike content in CRDT");
assert!(contents.starts_with("---\nname: \"My Spike\"\n---"));
assert!(contents.contains("## Question\n\n- TBD\n"));
}
@@ -1310,48 +1314,56 @@ mod tests {
#[test]
fn tool_validate_stories_with_valid_story() {
let tmp = tempfile::tempdir().unwrap();
let current_dir = tmp.path().join(".huskies").join("work").join("2_current");
fs::create_dir_all(&current_dir).unwrap();
fs::write(
current_dir.join("1_test.md"),
crate::db::ensure_content_store();
crate::db::write_item_with_content(
"9907_test",
"2_current",
"---\nname: \"Valid Story\"\n---\n## AC\n- [ ] First\n",
)
.unwrap();
);
let ctx = test_ctx(tmp.path());
let result = tool_validate_stories(&ctx).unwrap();
let parsed: Vec<Value> = serde_json::from_str(&result).unwrap();
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0]["valid"], true);
let item = parsed.iter().find(|v| v["story_id"] == "9907_test")
.expect("expected 9907_test in validation results");
assert_eq!(item["valid"], true);
}
#[test]
fn tool_validate_stories_with_invalid_front_matter() {
let tmp = tempfile::tempdir().unwrap();
let current_dir = tmp.path().join(".huskies").join("work").join("2_current");
fs::create_dir_all(&current_dir).unwrap();
fs::write(current_dir.join("1_test.md"), "## No front matter at all\n").unwrap();
crate::db::ensure_content_store();
crate::db::write_item_with_content(
"9908_test",
"2_current",
"## No front matter at all\n",
);
let ctx = test_ctx(tmp.path());
let result = tool_validate_stories(&ctx).unwrap();
let parsed: Vec<Value> = serde_json::from_str(&result).unwrap();
assert!(!parsed.is_empty());
assert_eq!(parsed[0]["valid"], false);
let item = parsed.iter().find(|v| v["story_id"] == "9908_test")
.expect("expected 9908_test in validation results");
assert_eq!(item["valid"], false);
}
#[test]
fn record_tests_persists_to_story_file() {
let tmp = tempfile::tempdir().unwrap();
let current = tmp.path().join(".huskies/work/2_current");
fs::create_dir_all(&current).unwrap();
fs::write(
current.join("1_story_persist.md"),
crate::db::ensure_content_store();
crate::db::write_item_with_content(
"9906_story_persist",
"2_current",
"---\nname: Persist\n---\n# Story\n",
)
.unwrap();
);
let ctx = test_ctx(tmp.path());
tool_record_tests(
&json!({
"story_id": "1_story_persist",
"story_id": "9906_story_persist",
"unit": [{"name": "u1", "status": "pass"}],
"integration": []
}),
@@ -1359,36 +1371,35 @@ mod tests {
)
.unwrap();
let contents = fs::read_to_string(current.join("1_story_persist.md")).unwrap();
let contents = crate::db::read_content("9906_story_persist")
.expect("story content should exist in CRDT");
assert!(
contents.contains("## Test Results"),
"file should have Test Results section"
"content should have Test Results section"
);
assert!(
contents.contains("huskies-test-results:"),
"file should have JSON marker"
"content should have JSON marker"
);
assert!(contents.contains("u1"), "file should contain test name");
assert!(contents.contains("u1"), "content should contain test name");
}
#[test]
fn ensure_acceptance_reads_from_file_when_not_in_memory() {
let tmp = tempfile::tempdir().unwrap();
let current = tmp.path().join(".huskies/work/2_current");
fs::create_dir_all(&current).unwrap();
// Write a story file with a pre-populated Test Results section (simulating a restart)
// Write story content to CRDT with a pre-populated Test Results section
let story_content = "---\nname: Persist\n---\n# Story\n\n## Test Results\n\n<!-- huskies-test-results: {\"unit\":[{\"name\":\"u1\",\"status\":\"pass\",\"details\":null}],\"integration\":[{\"name\":\"i1\",\"status\":\"pass\",\"details\":null}]} -->\n";
fs::write(current.join("2_story_file_only.md"), story_content).unwrap();
crate::db::ensure_content_store();
crate::db::write_item_with_content("9905_story_file_only", "2_current", story_content);
// Use a fresh context (empty in-memory state, simulating a restart)
let ctx = test_ctx(tmp.path());
// ensure_acceptance should read from file and succeed
let result = tool_ensure_acceptance(&json!({"story_id": "2_story_file_only"}), &ctx);
// ensure_acceptance should read from content store and succeed
let result = tool_ensure_acceptance(&json!({"story_id": "9905_story_file_only"}), &ctx);
assert!(
result.is_ok(),
"should accept based on file data, got: {:?}",
"should accept based on content store data, got: {:?}",
result
);
assert!(result.unwrap().contains("All gates pass"));
@@ -1656,27 +1667,17 @@ mod tests {
fn tool_check_criterion_marks_unchecked_item() {
let tmp = tempfile::tempdir().unwrap();
setup_git_repo_in(tmp.path());
let current_dir = tmp.path().join(".huskies").join("work").join("2_current");
fs::create_dir_all(&current_dir).unwrap();
fs::write(
current_dir.join("1_test.md"),
crate::db::ensure_content_store();
crate::db::write_item_with_content(
"9904_test",
"2_current",
"---\nname: Test\n---\n## AC\n- [ ] First criterion\n- [x] Already done\n",
)
.unwrap();
std::process::Command::new("git")
.args(["add", "."])
.current_dir(tmp.path())
.output()
.unwrap();
std::process::Command::new("git")
.args(["commit", "-m", "add story"])
.current_dir(tmp.path())
.output()
.unwrap();
);
let ctx = test_ctx(tmp.path());
let result =
tool_check_criterion(&json!({"story_id": "1_test", "criterion_index": 0}), &ctx);
tool_check_criterion(&json!({"story_id": "9904_test", "criterion_index": 0}), &ctx);
assert!(result.is_ok(), "Expected ok: {result:?}");
assert!(result.unwrap().contains("Criterion 0 checked"));
}