huskies: merge 864

This commit is contained in:
dave
2026-04-30 22:23:21 +00:00
parent 3911c24c26
commit 61cf7684de
41 changed files with 540 additions and 71 deletions
+3
View File
@@ -432,6 +432,7 @@ mod tests {
"99950_story_lifecycle",
"1_backlog",
"---\nname: Lifecycle Test\n---\n# Story\n",
crate::db::ItemMeta::named("Lifecycle Test"),
);
move_story_to_current("99950_story_lifecycle").unwrap();
@@ -462,6 +463,7 @@ mod tests {
"99951_story_crdt_only",
"2_current",
"---\nname: CRDT Only Test\n---\n# Story\n",
crate::db::ItemMeta::named("CRDT Only Test"),
);
// No filesystem path is involved — lifecycle functions no longer
@@ -529,6 +531,7 @@ mod tests {
"99866_story_block_test",
"2_current",
"---\nname: Block Round Trip\n---\n# Story\n",
crate::db::ItemMeta::named("Block Round Trip"),
);
// Verify starting state is Coding.
@@ -57,7 +57,12 @@ mod tests {
.unwrap();
// Place the story in 2_current/ via CRDT (the only source of truth).
crate::db::ensure_content_store();
crate::db::write_item_with_content("story-3", "2_current", "---\nname: Story 3\n---\n");
crate::db::write_item_with_content(
"story-3",
"2_current",
"---\nname: Story 3\n---\n",
crate::db::ItemMeta::named("Story 3"),
);
let pool = AgentPool::new_test(3001);
// No agents are running — coder-1 is free.
@@ -139,6 +144,11 @@ mod tests {
"9930_story_qa1",
"3_qa",
"---\nname: QA Story\nagent: coder-1\n---\n",
crate::db::ItemMeta {
name: Some("QA Story".into()),
agent: Some("coder-1".into()),
..Default::default()
},
);
let pool = AgentPool::new_test(3001);
@@ -188,6 +198,11 @@ mod tests {
"story-pref",
"2_current",
"---\nname: Coder Story\nagent: coder-1\n---\n",
crate::db::ItemMeta {
name: Some("Coder Story".into()),
agent: Some("coder-1".into()),
..Default::default()
},
);
let pool = AgentPool::new_test(3001);
@@ -235,6 +250,11 @@ mod tests {
"9931_story_noqa",
"3_qa",
"---\nname: QA Story No Agent\nagent: coder-1\n---\n",
crate::db::ItemMeta {
name: Some("QA Story No Agent".into()),
agent: Some("coder-1".into()),
..Default::default()
},
);
let pool = AgentPool::new_test(3001);
@@ -274,6 +294,7 @@ mod tests {
"9932_story_waiting",
"2_current",
"---\nname: Waiting\ndepends_on: [9999]\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Waiting\ndepends_on: [9999]\n---\n"),
);
let pool = AgentPool::new_test(3001);
@@ -307,12 +328,18 @@ mod tests {
// Seed stories via CRDT (the only source of truth).
crate::db::ensure_content_store();
// Dep 999 is now done.
crate::db::write_item_with_content("999_story_dep", "5_done", "---\nname: Dep\n---\n");
crate::db::write_item_with_content(
"999_story_dep",
"5_done",
"---\nname: Dep\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Dep\n---\n"),
);
// Story 10 depends on 999 which is done.
crate::db::write_item_with_content(
"10_story_unblocked",
"2_current",
"---\nname: Unblocked\ndepends_on: [999]\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Unblocked\ndepends_on: [999]\n---\n"),
);
let pool = AgentPool::new_test(3001);
@@ -496,6 +523,9 @@ mod tests {
"9860_story_conflict",
"4_merge",
"---\nname: Conflict\nmerge_failure: \"CONFLICT (content): server/src/lib.rs\"\n---\n",
crate::db::ItemMeta::from_yaml(
"---\nname: Conflict\nmerge_failure: \"CONFLICT (content): server/src/lib.rs\"\n---\n",
),
);
let pool = AgentPool::new_test(3001);
@@ -530,6 +560,9 @@ mod tests {
"9861_story_nothing",
"4_merge",
"---\nname: Nothing\nmerge_failure: \"nothing to commit, working tree clean\"\n---\n",
crate::db::ItemMeta::from_yaml(
"---\nname: Nothing\nmerge_failure: \"nothing to commit, working tree clean\"\n---\n",
),
);
let pool = AgentPool::new_test(3001);
@@ -565,6 +598,9 @@ mod tests {
"9863_story_blocked_conflict",
"4_merge",
"---\nname: Blocked conflict\nmerge_failure: \"CONFLICT (content): foo.rs\"\nblocked: true\n---\n",
crate::db::ItemMeta::from_yaml(
"---\nname: Blocked conflict\nmerge_failure: \"CONFLICT (content): foo.rs\"\nblocked: true\n---\n",
),
);
let pool = AgentPool::new_test(3001);
@@ -599,6 +635,9 @@ mod tests {
"9862_story_attempted",
"4_merge",
"---\nname: Already tried\nmerge_failure: \"CONFLICT (content): foo.rs\"\nmergemaster_attempted: true\n---\n",
crate::db::ItemMeta::from_yaml(
"---\nname: Already tried\nmerge_failure: \"CONFLICT (content): foo.rs\"\nmergemaster_attempted: true\n---\n",
),
);
let pool = AgentPool::new_test(3001);
+6 -1
View File
@@ -83,7 +83,12 @@ impl AgentPool {
&contents,
);
crate::db::write_content(story_id, &updated);
crate::db::write_item_with_content(story_id, "4_merge", &updated);
crate::db::write_item_with_content(
story_id,
"4_merge",
&updated,
crate::db::ItemMeta::from_yaml(&updated),
);
}
crate::crdt_state::set_mergemaster_attempted(story_id, true);
if let Err(e) = self
+19 -3
View File
@@ -168,6 +168,7 @@ mod tests {
"9970_story_archived",
"6_archived",
"---\nname: Archived\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Archived\n---\n"),
);
// Also place a stale .md file in a temp 1_backlog/ dir.
@@ -200,9 +201,24 @@ mod tests {
fn scan_stage_items_returns_sorted_story_ids() {
// Write items via the CRDT store (the primary source of truth).
crate::db::ensure_content_store();
crate::db::write_item_with_content("9942_story_foo", "2_current", "---\nname: foo\n---");
crate::db::write_item_with_content("9940_story_bar", "2_current", "---\nname: bar\n---");
crate::db::write_item_with_content("9935_story_baz", "2_current", "---\nname: baz\n---");
crate::db::write_item_with_content(
"9942_story_foo",
"2_current",
"---\nname: foo\n---",
crate::db::ItemMeta::from_yaml("---\nname: foo\n---"),
);
crate::db::write_item_with_content(
"9940_story_bar",
"2_current",
"---\nname: bar\n---",
crate::db::ItemMeta::from_yaml("---\nname: bar\n---"),
);
crate::db::write_item_with_content(
"9935_story_baz",
"2_current",
"---\nname: baz\n---",
crate::db::ItemMeta::from_yaml("---\nname: baz\n---"),
);
let tmp = tempfile::tempdir().unwrap();
let items = scan_stage_items(tmp.path(), "2_current");
@@ -188,6 +188,9 @@ mod tests {
"10_spike_research",
"3_qa",
"---\nname: Research spike\nreview_hold: true\n---\n# Spike\n",
crate::db::ItemMeta::from_yaml(
"---\nname: Research spike\nreview_hold: true\n---\n# Spike\n",
),
);
assert!(has_review_hold(tmp.path(), "3_qa", "10_spike_research"));
}
@@ -88,7 +88,12 @@ pub(super) fn write_review_hold_to_store(story_id: &str) {
.flatten()
.map(|i| i.stage.dir_name().to_string())
.unwrap_or_else(|| "3_qa".to_string());
crate::db::write_item_with_content(story_id, &stage, &updated);
crate::db::write_item_with_content(
story_id,
&stage,
&updated,
crate::db::ItemMeta::from_yaml(&updated),
);
} else {
slog_error!("[pipeline] Cannot write review_hold for '{story_id}': no content in store");
}
@@ -172,7 +172,12 @@ async fn pipeline_advance_sends_agent_state_changed_to_watcher_tx() {
// Seed story via CRDT (the only source of truth).
crate::db::ensure_content_store();
crate::db::write_item_with_content("173_story_test", "2_current", "---\nname: test\n---\n");
crate::db::write_item_with_content(
"173_story_test",
"2_current",
"---\nname: test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: test\n---\n"),
);
// Write a project.toml with a qa agent so start_agent can resolve it.
fs::create_dir_all(root.join(".huskies")).unwrap();
@@ -51,6 +51,7 @@ async fn mergemaster_blocks_and_sends_story_blocked_when_no_commits_ahead() {
"9919_story_no_commits",
"2_current",
"---\nname: Test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Test\n---\n"),
);
let pool = AgentPool::new_test(3001);
@@ -145,11 +146,13 @@ stage = "qa"
"292_story_first",
"3_qa",
"---\nname: First\nqa: human\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: First\nqa: human\n---\n"),
);
crate::db::write_item_with_content(
"293_story_second",
"3_qa",
"---\nname: Second\nqa: human\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Second\nqa: human\n---\n"),
);
let pool = AgentPool::new_test(3001);
@@ -245,7 +248,12 @@ async fn stale_mergemaster_advance_for_done_story_is_noop() {
let story_id = "9929_story_zombie_merge";
let content = "---\nname: Zombie Merge Test\n---\n";
crate::db::write_content(story_id, content);
crate::db::write_item_with_content(story_id, "5_done", content);
crate::db::write_item_with_content(
story_id,
"5_done",
content,
crate::db::ItemMeta::from_yaml(content),
);
let pool = AgentPool::new_test(3001);
let mut rx = pool.watcher_tx.subscribe();
@@ -381,6 +389,7 @@ async fn work_survived_advances_to_qa_instead_of_blocking() {
"9945_story_survived",
"2_current",
"---\nname: Survived Test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Survived Test\n---\n"),
);
// Simulate a passing run_tests call during the agent's session (bug 668):
@@ -474,6 +483,7 @@ async fn no_committed_work_still_retries_and_blocks() {
"9946_story_nowork",
"2_current",
"---\nname: No Work Test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: No Work Test\n---\n"),
);
// Write a project.toml with max_retries = 1.
@@ -601,6 +611,7 @@ async fn gates_failed_no_test_evidence_does_not_advance() {
"9947_story_no_evidence",
"2_current",
"---\nname: No Evidence Test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: No Evidence Test\n---\n"),
);
// Explicitly ensure no test evidence exists for this story.
@@ -730,6 +741,7 @@ async fn gates_failed_with_test_evidence_and_committed_work_advances() {
"9948_story_with_evidence",
"2_current",
"---\nname: With Evidence Test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: With Evidence Test\n---\n"),
);
// Write the run_tests evidence — simulates the agent having called run_tests
@@ -813,6 +825,7 @@ stage = "coder"
"9950_story_warm_resume",
"2_current",
"---\nname: Warm Resume Test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Warm Resume Test\n---\n"),
);
let pool = AgentPool::new_test(3001);
@@ -651,6 +651,7 @@ async fn server_side_merge_happy_path_advances_to_done() {
"757a_happy",
"4_merge",
"---\nname: Happy path test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Happy path test\n---\n"),
);
let pool = Arc::new(AgentPool::new_test(3001));
@@ -787,6 +788,7 @@ async fn server_side_merge_conflict_sets_merge_failure() {
"757b_conflict",
"4_merge",
"---\nname: Conflict test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Conflict test\n---\n"),
);
let pool = Arc::new(AgentPool::new_test(3001));
@@ -898,6 +900,7 @@ async fn server_side_merge_gate_failure_sets_merge_failure() {
"757c_gates",
"4_merge",
"---\nname: Gate failure test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Gate failure test\n---\n"),
);
let pool = Arc::new(AgentPool::new_test(3001));
+18 -3
View File
@@ -597,7 +597,12 @@ mod tests {
crate::db::ensure_content_store();
let story_id = "9960_story_gate_injection_881";
crate::db::write_item_with_content(story_id, "2_current", "---\nname: Test\n---\n");
crate::db::write_item_with_content(
story_id,
"2_current",
"---\nname: Test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Test\n---\n"),
);
crate::crdt_state::set_retry_count(story_id, 1);
let gate_output =
@@ -630,7 +635,12 @@ mod tests {
crate::db::ensure_content_store();
let story_id = "9961_story_no_gate_injection_881";
crate::db::write_item_with_content(story_id, "2_current", "---\nname: Test\n---\n");
crate::db::write_item_with_content(
story_id,
"2_current",
"---\nname: Test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Test\n---\n"),
);
// retry_count is 0 (default — never bumped).
crate::db::write_content(&format!("{story_id}:gate_output"), "some previous output");
@@ -690,7 +700,12 @@ mod tests {
crate::db::ensure_content_store();
let story_id = "9962_story_abort_respawn_882";
crate::db::write_item_with_content(story_id, "2_current", "---\nname: Test\n---\n");
crate::db::write_item_with_content(
story_id,
"2_current",
"---\nname: Test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Test\n---\n"),
);
let db_key = format!("{story_id}:abort_respawn_count");
const CAP: u32 = 5;
@@ -429,7 +429,12 @@ async fn start_agent_rejects_mergemaster_on_coding_stage_story() {
)
.unwrap();
crate::db::ensure_content_store();
crate::db::write_item_with_content("310_story_foo", "2_current", "---\nname: Foo\n---\n");
crate::db::write_item_with_content(
"310_story_foo",
"2_current",
"---\nname: Foo\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Foo\n---\n"),
);
let pool = AgentPool::new_test(3099);
let result = pool
@@ -463,7 +468,12 @@ async fn start_agent_rejects_coder_on_qa_stage_story() {
)
.unwrap();
crate::db::ensure_content_store();
crate::db::write_item_with_content("8842_story_qa_guard", "3_qa", "---\nname: QA Guard\n---\n");
crate::db::write_item_with_content(
"8842_story_qa_guard",
"3_qa",
"---\nname: QA Guard\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: QA Guard\n---\n"),
);
let pool = AgentPool::new_test(3099);
let result = pool
@@ -497,7 +507,12 @@ async fn start_agent_rejects_qa_on_merge_stage_story() {
)
.unwrap();
crate::db::ensure_content_store();
crate::db::write_item_with_content("55_story_baz", "4_merge", "---\nname: Baz\n---\n");
crate::db::write_item_with_content(
"55_story_baz",
"4_merge",
"---\nname: Baz\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Baz\n---\n"),
);
let pool = AgentPool::new_test(3099);
let result = pool
+6 -1
View File
@@ -143,7 +143,12 @@ mod tests {
let story_content = "test";
fs::write(current.join("60_story_cleanup.md"), story_content).unwrap();
crate::db::ensure_content_store();
crate::db::write_item_with_content("60_story_cleanup", "2_current", story_content);
crate::db::write_item_with_content(
"60_story_cleanup",
"2_current",
story_content,
crate::db::ItemMeta::from_yaml(story_content),
);
let pool = AgentPool::new_test(3001);
pool.inject_test_agent("60_story_cleanup", "coder-1", AgentStatus::Completed);
+18 -3
View File
@@ -42,7 +42,12 @@ mod tests {
#[test]
fn find_active_story_stage_detects_current() {
crate::db::ensure_content_store();
crate::db::write_item_with_content("10_story_test", "2_current", "---\nname: Test\n---\n");
crate::db::write_item_with_content(
"10_story_test",
"2_current",
"---\nname: Test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Test\n---\n"),
);
let tmp = tempfile::tempdir().unwrap();
assert!(matches!(
find_active_story_stage(tmp.path(), "10_story_test"),
@@ -53,7 +58,12 @@ mod tests {
#[test]
fn find_active_story_stage_detects_qa() {
crate::db::ensure_content_store();
crate::db::write_item_with_content("11_story_test", "3_qa", "---\nname: Test\n---\n");
crate::db::write_item_with_content(
"11_story_test",
"3_qa",
"---\nname: Test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Test\n---\n"),
);
let tmp = tempfile::tempdir().unwrap();
assert!(matches!(
find_active_story_stage(tmp.path(), "11_story_test"),
@@ -64,7 +74,12 @@ mod tests {
#[test]
fn find_active_story_stage_detects_merge() {
crate::db::ensure_content_store();
crate::db::write_item_with_content("12_story_test", "4_merge", "---\nname: Test\n---\n");
crate::db::write_item_with_content(
"12_story_test",
"4_merge",
"---\nname: Test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Test\n---\n"),
);
let tmp = tempfile::tempdir().unwrap();
assert!(matches!(
find_active_story_stage(tmp.path(), "12_story_test"),
+24 -3
View File
@@ -527,6 +527,9 @@ fn merge_item_with_failure_shows_stop_sign_and_snippet() {
"903_story_merge_fail",
"4_merge",
"---\nname: Failed Story\nmerge_failure: \"conflicts in src/lib.rs\"\n---\n",
crate::db::ItemMeta::from_yaml(
"---\nname: Failed Story\nmerge_failure: \"conflicts in src/lib.rs\"\n---\n",
),
);
let items = vec![make_item(
"903_story_merge_fail",
@@ -552,7 +555,12 @@ fn merge_item_failure_snippet_truncated_at_120_chars() {
crate::db::ensure_content_store();
let long_reason = "x".repeat(200);
let content = format!("---\nname: Long Fail\nmerge_failure: \"{long_reason}\"\n---\n");
crate::db::write_item_with_content("904_story_long_fail", "4_merge", &content);
crate::db::write_item_with_content(
"904_story_long_fail",
"4_merge",
&content,
crate::db::ItemMeta::from_yaml(&content),
);
let items = vec![make_item("904_story_long_fail", "Long Fail", merge_stage())];
let agents = AgentPool::new_test(3000);
let output = build_status_from_items(tmp.path(), &agents, &items);
@@ -584,11 +592,21 @@ fn merge_item_failure_snippet_is_first_non_empty_line() {
"---\nname: Multi Line\nmerge_failure: \"{}\" \n---\n",
reason.replace('\n', "\\n")
);
crate::db::write_item_with_content("905_story_multiline", "4_merge", &content);
crate::db::write_item_with_content(
"905_story_multiline",
"4_merge",
&content,
crate::db::ItemMeta::from_yaml(&content),
);
// Write with literal \n as the content (simulating stored text with newlines).
let content2 =
"---\nname: Multi Line\nmerge_failure: |\n \n first line of error\n second line\n---\n";
crate::db::write_item_with_content("905_story_multiline", "4_merge", content2);
crate::db::write_item_with_content(
"905_story_multiline",
"4_merge",
content2,
crate::db::ItemMeta::from_yaml(content2),
);
let items = vec![make_item(
"905_story_multiline",
"Multi Line",
@@ -615,6 +633,9 @@ fn merge_item_det_merge_running_preferred_over_failure() {
"906_story_det_over_fail",
"4_merge",
"---\nname: Det Over Fail\nmerge_failure: \"old failure\"\n---\n",
crate::db::ItemMeta::from_yaml(
"---\nname: Det Over Fail\nmerge_failure: \"old failure\"\n---\n",
),
);
// Record a running deterministic merge in the CRDT.
crate::crdt_state::write_merge_job("906_story_det_over_fail", "running", 0.0, None, None);
+6 -1
View File
@@ -100,7 +100,12 @@ fn unblock_by_story_id(story_id: &str) -> String {
.flatten()
.map(|i| i.stage.dir_name().to_string())
.unwrap_or_else(|| "2_current".to_string());
crate::db::write_item_with_content(story_id, &stage, &updated);
crate::db::write_item_with_content(
story_id,
&stage,
&updated,
crate::db::ItemMeta::from_yaml(&updated),
);
crate::crdt_state::set_retry_count(story_id, 0);
}
}
+2 -1
View File
@@ -19,5 +19,6 @@ pub(crate) fn write_story_file(root: &Path, stage: &str, filename: &str, content
let story_id = filename.trim_end_matches(".md");
crate::db::ensure_content_store();
crate::db::write_item_with_content(story_id, stage, content);
let meta = crate::db::ItemMeta::from_yaml(content);
crate::db::write_item_with_content(story_id, stage, content, meta);
}
@@ -261,6 +261,9 @@ mod tests {
story_id,
"1_backlog",
"---\nname: CRDT Tombstone Check\n---\n\n# Story 9977\n",
crate::db::ItemMeta::from_yaml(
"---\nname: CRDT Tombstone Check\n---\n\n# Story 9977\n",
),
);
let tmp = tempfile::tempdir().unwrap();
@@ -291,6 +294,7 @@ mod tests {
"9975_story_some_feature",
"1_backlog",
"---\nname: Some Feature\n---\n\n# Story 9975\n",
crate::db::ItemMeta::from_yaml("---\nname: Some Feature\n---\n\n# Story 9975\n"),
);
let agents = std::sync::Arc::new(crate::agents::AgentPool::new_test(3000));
@@ -275,6 +275,7 @@ mod tests {
"9976_story_test",
"1_backlog",
"---\nname: Test Story\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Test Story\n---\n"),
);
let agents = Arc::new(AgentPool::new_test(3000));
+60 -1
View File
@@ -21,7 +21,7 @@ pub mod ops;
pub mod shadow_write;
pub use content_store::{all_content_ids, delete_content, read_content, write_content};
pub use ops::{delete_item, move_item_stage, next_item_number, write_item_with_content};
pub use ops::{ItemMeta, delete_item, move_item_stage, next_item_number, write_item_with_content};
pub use shadow_write::init;
#[cfg(test)]
@@ -320,6 +320,65 @@ mod tests {
);
}
/// Story 864: `write_item_with_content` no longer parses YAML front-matter
/// from `content`. The CRDT metadata reflects ONLY what the caller passes
/// via `ItemMeta`. This test writes a body without any front-matter at
/// all, sets metadata explicitly, and asserts the CRDT picks up the typed
/// values, not anything derived from `content`.
#[test]
fn write_item_typed_meta_takes_precedence_over_content() {
crate::crdt_state::init_for_test();
ensure_content_store();
let story_id = "9864_story_typed_meta";
// Body has NO YAML header — just plain markdown.
let content = "# Just a heading\n\nNo front matter here.\n";
let meta = ItemMeta {
name: Some("Typed Name".into()),
agent: Some("coder-1".into()),
retry_count: Some(2),
blocked: Some(true),
depends_on: Some(vec![100, 200]),
};
write_item_with_content(story_id, "2_current", content, meta);
let view = crate::crdt_state::read_item(story_id).expect("story exists in CRDT");
assert_eq!(view.stage, "2_current");
assert_eq!(view.name.as_deref(), Some("Typed Name"));
assert_eq!(view.agent.as_deref(), Some("coder-1"));
assert_eq!(view.retry_count, Some(2));
assert_eq!(view.blocked, Some(true));
assert_eq!(view.depends_on, Some(vec![100, 200]));
// Content is stored verbatim (no parsing, no rewrite).
assert_eq!(read_content(story_id).as_deref(), Some(content));
}
/// Story 864: passing `ItemMeta::default()` against a content blob that
/// LOOKS like front-matter must NOT silently extract metadata into the
/// CRDT. The whole point of removing the implicit YAML round-trip is
/// that metadata only flows in through the typed `ItemMeta` arg.
#[test]
fn write_item_default_meta_ignores_yaml_in_content() {
crate::crdt_state::init_for_test();
ensure_content_store();
let story_id = "9864_story_yaml_ignored";
let content = "---\nname: Should Not Appear\nagent: ghost\n---\n# Body\n";
write_item_with_content(story_id, "2_current", content, ItemMeta::default());
let view = crate::crdt_state::read_item(story_id).expect("story exists in CRDT");
assert_eq!(view.stage, "2_current");
assert_eq!(
view.name, None,
"name must come from typed meta, not parsed YAML"
);
assert_eq!(
view.agent, None,
"agent must come from typed meta, not parsed YAML"
);
}
/// Bug 780: stage transitions must reset retry_count to 0 in the CRDT.
/// Carryover from prior-stage retries was tripping the auto-assigner's
/// deterministic-merge skip logic.
+64 -22
View File
@@ -9,23 +9,65 @@ use super::content_store::{
use super::shadow_write::{PIPELINE_DB, PipelineWriteMsg};
use crate::io::story_metadata::parse_front_matter;
/// Typed metadata for a pipeline item write.
///
/// Replaces the prior YAML-parsing write path (story 864): callers now pass
/// metadata explicitly instead of round-tripping it through a serialized
/// front-matter blob. Every field is `Option`-typed; `None` means
/// "leave unchanged" on update, "use the default" on insert.
#[derive(Default, Clone, Debug)]
pub struct ItemMeta {
pub name: Option<String>,
pub agent: Option<String>,
pub retry_count: Option<i64>,
pub blocked: Option<bool>,
pub depends_on: Option<Vec<u32>>,
}
impl ItemMeta {
/// Convenience constructor for the common "just set a name" case.
#[cfg(test)]
pub fn named(name: impl Into<String>) -> Self {
Self {
name: Some(name.into()),
..Self::default()
}
}
/// Parse YAML front-matter from a content string into typed metadata.
///
/// This is an explicit caller-side conversion — the write path itself
/// no longer parses YAML. Use this when the caller has a raw content
/// string with front-matter and wants the metadata to flow into the
/// CRDT. Returns `Self::default()` if parsing fails or there is no
/// front-matter present.
pub fn from_yaml(content: &str) -> Self {
match parse_front_matter(content) {
Ok(m) => Self {
name: m.name,
agent: m.agent,
retry_count: m.retry_count.map(|r| r as i64),
blocked: m.blocked,
depends_on: m.depends_on,
},
Err(_) => Self::default(),
}
}
}
/// Write a pipeline item from in-memory content (no filesystem access).
///
/// This is the primary write path for the DB-backed pipeline. It updates
/// the CRDT, the in-memory content store, and the SQLite shadow table.
pub fn write_item_with_content(story_id: &str, stage: &str, content: &str) {
let (name, agent, retry_count, blocked, depends_on) = match parse_front_matter(content) {
Ok(meta) => (
meta.name,
meta.agent,
meta.retry_count.map(|r| r as i64),
meta.blocked,
meta.depends_on
///
/// The metadata in `meta` is authoritative: this function does NOT parse
/// `content` to extract front-matter fields. Callers must pass typed
/// metadata explicitly via `ItemMeta`.
pub fn write_item_with_content(story_id: &str, stage: &str, content: &str, meta: ItemMeta) {
let depends_on_json = meta
.depends_on
.as_ref()
.and_then(|d| serde_json::to_string(d).ok()),
),
Err(_) => (None, None, None, None, None),
};
.and_then(|d| serde_json::to_string(d).ok());
// Update in-memory content store.
ensure_content_store();
@@ -42,11 +84,11 @@ pub fn write_item_with_content(story_id: &str, stage: &str, content: &str) {
crate::crdt_state::write_item(
story_id,
stage,
name.as_deref(),
agent.as_deref(),
retry_count,
blocked,
depends_on.as_deref(),
meta.name.as_deref(),
meta.agent.as_deref(),
meta.retry_count,
meta.blocked,
depends_on_json.as_deref(),
None,
None,
merged_at_ts,
@@ -57,11 +99,11 @@ pub fn write_item_with_content(story_id: &str, stage: &str, content: &str) {
let msg = PipelineWriteMsg {
story_id: story_id.to_string(),
stage: stage.to_string(),
name,
agent,
retry_count,
blocked,
depends_on,
name: meta.name,
agent: meta.agent,
retry_count: meta.retry_count,
blocked: meta.blocked,
depends_on: depends_on_json,
content: Some(content.to_string()),
};
let _ = db.tx.send(msg);
+2
View File
@@ -326,6 +326,7 @@ async fn get_work_item_content_falls_back_to_crdt_when_no_file() {
"44_story_crdt_only",
"1_backlog",
"---\nname: \"CRDT Only\"\n---\n\nCRDT content.",
crate::db::ItemMeta::from_yaml("---\nname: \"CRDT Only\"\n---\n\nCRDT content."),
);
let ctx = AppContext::new_test(root);
let api = AgentsApi { ctx: Arc::new(ctx) };
@@ -348,6 +349,7 @@ async fn get_work_item_content_crdt_fallback_with_current_stage() {
"45_story_crdt_current",
"2_current",
"---\nname: \"Current CRDT\"\n---\n\nIn progress.",
crate::db::ItemMeta::from_yaml("---\nname: \"Current CRDT\"\n---\n\nIn progress."),
);
let ctx = AppContext::new_test(root);
let api = AgentsApi { ctx: Arc::new(ctx) };
+18 -3
View File
@@ -449,7 +449,12 @@ mod tests {
let content = "---\nname: Test\n---\n";
fs::write(backlog.join("5_story_test.md"), content).unwrap();
crate::db::ensure_content_store();
crate::db::write_item_with_content("5_story_test", "1_backlog", content);
crate::db::write_item_with_content(
"5_story_test",
"1_backlog",
content,
crate::db::ItemMeta::from_yaml(content),
);
let ctx = test_ctx(root);
let result = super::super::tool_move_story(
@@ -476,7 +481,12 @@ mod tests {
let content = "---\nname: Back\n---\n";
fs::write(current.join("6_story_back.md"), content).unwrap();
crate::db::ensure_content_store();
crate::db::write_item_with_content("6_story_back", "2_current", content);
crate::db::write_item_with_content(
"6_story_back",
"2_current",
content,
crate::db::ItemMeta::from_yaml(content),
);
let ctx = test_ctx(root);
let result = super::super::tool_move_story(
@@ -503,7 +513,12 @@ mod tests {
let content = "---\nname: Idem\n---\n";
fs::write(current.join("9907_story_idem.md"), content).unwrap();
crate::db::ensure_content_store();
crate::db::write_item_with_content("9907_story_idem", "2_current", content);
crate::db::write_item_with_content(
"9907_story_idem",
"2_current",
content,
crate::db::ItemMeta::from_yaml(content),
);
let ctx = test_ctx(root);
let result = super::super::tool_move_story(
+12 -2
View File
@@ -366,7 +366,12 @@ mod tests {
crate::crdt_state::init_for_test();
crate::db::ensure_content_store();
let story_content = "---\nname: Blocked Story\nblocked: true\nretry_count: 3\ndepends_on: [100, 200]\n---\n\n## Acceptance Criteria\n\n- [ ] Do the thing\n";
crate::db::write_item_with_content("9887_story_blocked_test", "2_current", story_content);
crate::db::write_item_with_content(
"9887_story_blocked_test",
"2_current",
story_content,
crate::db::ItemMeta::from_yaml(story_content),
);
let ctx = crate::http::context::AppContext::new_test(tmp.path().to_path_buf());
let result = tool_status(&json!({"story_id": "9887_story_blocked_test"}), &ctx)
@@ -388,7 +393,12 @@ mod tests {
crate::db::ensure_content_store();
let story_content = "---\nname: My Test Story\nagent: coder-1\n---\n\n## Acceptance Criteria\n\n- [ ] First criterion\n- [x] Second criterion\n\n## Out of Scope\n\n- nothing\n";
crate::db::write_item_with_content("9886_story_status_test", "2_current", story_content);
crate::db::write_item_with_content(
"9886_story_status_test",
"2_current",
story_content,
crate::db::ItemMeta::from_yaml(story_content),
);
let ctx = crate::http::context::AppContext::new_test(tmp.path().to_path_buf());
let result = tool_status(&json!({"story_id": "9886_story_status_test"}), &ctx)
+12 -1
View File
@@ -290,11 +290,17 @@ mod tests {
"9902_bug_crash",
"1_backlog",
"---\nname: \"App Crash\"\n---\n# Bug 9902: App Crash\n",
crate::db::ItemMeta::from_yaml(
"---\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",
crate::db::ItemMeta::from_yaml(
"---\nname: \"Typo in Header\"\n---\n# Bug 9903: Typo in Header\n",
),
);
let ctx = test_ctx(tmp.path());
@@ -438,7 +444,12 @@ mod tests {
let content = "# Bug 9901: Crash\n";
std::fs::write(&bug_file, content).unwrap();
crate::db::ensure_content_store();
crate::db::write_item_with_content("9901_bug_crash", "1_backlog", content);
crate::db::write_item_with_content(
"9901_bug_crash",
"1_backlog",
content,
crate::db::ItemMeta::from_yaml(content),
);
// Stage the file so it's tracked
std::process::Command::new("git")
.args(["add", "."])
+22 -1
View File
@@ -421,6 +421,9 @@ mod tests {
"9901_test",
"2_current",
"---\nname: Test\n---\n## AC\n- [ ] First\n- [x] Done\n- [ ] Second\n",
crate::db::ItemMeta::from_yaml(
"---\nname: Test\n---\n## AC\n- [ ] First\n- [x] Done\n- [ ] Second\n",
),
);
let ctx = test_ctx(tmp.path());
@@ -514,6 +517,7 @@ mod tests {
"9906_story_persist",
"2_current",
"---\nname: Persist\n---\n# Story\n",
crate::db::ItemMeta::from_yaml("---\nname: Persist\n---\n# Story\n"),
);
let ctx = test_ctx(tmp.path());
@@ -547,7 +551,12 @@ mod tests {
// 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";
crate::db::ensure_content_store();
crate::db::write_item_with_content("9905_story_file_only", "2_current", story_content);
crate::db::write_item_with_content(
"9905_story_file_only",
"2_current",
story_content,
crate::db::ItemMeta::from_yaml(story_content),
);
let ctx = test_ctx(tmp.path());
@@ -618,6 +627,9 @@ mod tests {
"9997_empty_branch",
"2_current",
"---\nname: Empty Branch Test\n---\n## AC\n- [ ] Implement the feature\n",
crate::db::ItemMeta::from_yaml(
"---\nname: Empty Branch Test\n---\n## AC\n- [ ] Implement the feature\n",
),
);
let ctx = test_ctx(tmp.path());
@@ -672,6 +684,9 @@ mod tests {
"9904_test",
"2_current",
"---\nname: Test\n---\n## AC\n- [ ] First criterion\n- [x] Already done\n",
crate::db::ItemMeta::from_yaml(
"---\nname: Test\n---\n## AC\n- [ ] First criterion\n- [x] Already done\n",
),
);
let ctx = test_ctx(tmp.path());
@@ -711,6 +726,9 @@ mod tests {
"9905_test",
"2_current",
"---\nname: Test\n---\n## Acceptance Criteria\n- [ ] Keep me\n- [ ] Remove me\n",
crate::db::ItemMeta::from_yaml(
"---\nname: Test\n---\n## Acceptance Criteria\n- [ ] Keep me\n- [ ] Remove me\n",
),
);
let ctx = test_ctx(tmp.path());
@@ -732,6 +750,9 @@ mod tests {
"9906_test",
"2_current",
"---\nname: Test\n---\n## Acceptance Criteria\n- [ ] Only one\n",
crate::db::ItemMeta::from_yaml(
"---\nname: Test\n---\n## Acceptance Criteria\n- [ ] Only one\n",
),
);
let ctx = test_ctx(tmp.path());
+9
View File
@@ -246,17 +246,26 @@ mod tests {
"9990_epic_rollup",
"1_backlog",
"---\ntype: epic\nname: \"Rollup Epic\"\n---\n\n## Goal\n\nTest\n",
crate::db::ItemMeta::from_yaml(
"---\ntype: epic\nname: \"Rollup Epic\"\n---\n\n## Goal\n\nTest\n",
),
);
// Write two member items: one done, one current.
crate::db::write_item_with_content(
"9991_story_member_done",
"5_done",
"---\ntype: story\nname: \"Done Member\"\nepic: \"9990_epic_rollup\"\n---\n",
crate::db::ItemMeta::from_yaml(
"---\ntype: story\nname: \"Done Member\"\nepic: \"9990_epic_rollup\"\n---\n",
),
);
crate::db::write_item_with_content(
"9992_story_member_current",
"2_current",
"---\ntype: story\nname: \"Current Member\"\nepic: \"9990_epic_rollup\"\n---\n",
crate::db::ItemMeta::from_yaml(
"---\ntype: story\nname: \"Current Member\"\nepic: \"9990_epic_rollup\"\n---\n",
),
);
let tmp = tempfile::tempdir().unwrap();
@@ -216,7 +216,12 @@ mod tests {
let content = "---\nname: No Branch\n---\n";
std::fs::write(current_dir.join("51_story_no_branch.md"), content).unwrap();
crate::db::ensure_content_store();
crate::db::write_item_with_content("51_story_no_branch", "2_current", content);
crate::db::write_item_with_content(
"51_story_no_branch",
"2_current",
content,
crate::db::ItemMeta::from_yaml(content),
);
let ctx = test_ctx(tmp.path());
let result = tool_accept_story(&json!({"story_id": "51_story_no_branch"}), &ctx);
@@ -60,6 +60,7 @@ mod tests {
story_id,
"2_current",
"---\nname: MCP Freeze Tool Test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: MCP Freeze Tool Test\n---\n"),
);
let tmp = tempfile::tempdir().unwrap();
@@ -88,6 +89,7 @@ mod tests {
story_id,
"2_current",
"---\nname: MCP Unfreeze Tool Test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: MCP Unfreeze Tool Test\n---\n"),
);
let tmp = tempfile::tempdir().unwrap();
+14 -2
View File
@@ -150,7 +150,12 @@ mod tests {
("4_merge", "9940_story_merge", "Merge Story"),
("5_done", "9950_story_done", "Done Story"),
] {
crate::db::write_item_with_content(id, stage, &format!("---\nname: \"{name}\"\n---\n"));
crate::db::write_item_with_content(
id,
stage,
&format!("---\nname: \"{name}\"\n---\n"),
crate::db::ItemMeta::from_yaml(&format!("---\nname: \"{name}\"\n---\n")),
);
}
let ctx = test_ctx(tmp.path());
@@ -187,6 +192,7 @@ mod tests {
"9921_story_active",
"2_current",
"---\nname: \"Active Story\"\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: \"Active Story\"\n---\n"),
);
let ctx = test_ctx(tmp.path());
@@ -219,6 +225,7 @@ mod tests {
"9907_test",
"2_current",
"---\nname: \"Valid Story\"\n---\n## AC\n- [ ] First\n",
crate::db::ItemMeta::from_yaml("---\nname: \"Valid Story\"\n---\n## AC\n- [ ] First\n"),
);
let ctx = test_ctx(tmp.path());
@@ -236,7 +243,12 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
crate::db::ensure_content_store();
crate::db::write_item_with_content("9908_test", "2_current", "## No front matter at all\n");
crate::db::write_item_with_content(
"9908_test",
"2_current",
"## No front matter at all\n",
crate::db::ItemMeta::from_yaml("## No front matter at all\n"),
);
let ctx = test_ctx(tmp.path());
let result = tool_validate_stories(&ctx).unwrap();
@@ -150,7 +150,12 @@ mod tests {
fs::create_dir_all(&current).unwrap();
fs::write(current.join(format!("{story_id}.md")), content).unwrap();
crate::db::ensure_content_store();
crate::db::write_item_with_content(story_id, "2_current", content);
crate::db::write_item_with_content(
story_id,
"2_current",
content,
crate::db::ItemMeta::from_yaml(content),
);
}
#[test]
+24 -3
View File
@@ -46,8 +46,18 @@ fn next_item_number_increments_from_existing_bugs() {
fs::write(backlog.join("1_bug_crash.md"), "").unwrap();
fs::write(backlog.join("3_bug_another.md"), "").unwrap();
// Also write to content store so next_item_number sees them.
crate::db::write_item_with_content("1_bug_crash", "1_backlog", "---\nname: Crash\n---\n");
crate::db::write_item_with_content("3_bug_another", "1_backlog", "---\nname: Another\n---\n");
crate::db::write_item_with_content(
"1_bug_crash",
"1_backlog",
"---\nname: Crash\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Crash\n---\n"),
);
crate::db::write_item_with_content(
"3_bug_another",
"1_backlog",
"---\nname: Another\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Another\n---\n"),
);
assert!(super::super::next_item_number(tmp.path()).unwrap() >= 4);
}
@@ -61,7 +71,12 @@ fn next_item_number_scans_archived_too() {
fs::create_dir_all(&archived).unwrap();
fs::write(archived.join("5_bug_old.md"), "").unwrap();
// Also write to content store so next_item_number sees it.
crate::db::write_item_with_content("5_bug_old", "5_done", "---\nname: Old Bug\n---\n");
crate::db::write_item_with_content(
"5_bug_old",
"5_done",
"---\nname: Old Bug\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Old Bug\n---\n"),
);
assert!(super::super::next_item_number(tmp.path()).unwrap() >= 6);
}
@@ -83,12 +98,14 @@ fn list_bug_files_excludes_archive_subdir() {
"7001_bug_open",
"1_backlog",
"---\nname: Open Bug\n---\n# Bug 7001: Open Bug\n",
crate::db::ItemMeta::from_yaml("---\nname: Open Bug\n---\n# Bug 7001: Open Bug\n"),
);
// Bug in done (should NOT appear — list_bug_files only returns Backlog).
crate::db::write_item_with_content(
"7002_bug_closed",
"5_done",
"---\nname: Closed Bug\n---\n# Bug 7002: Closed Bug\n",
crate::db::ItemMeta::from_yaml("---\nname: Closed Bug\n---\n# Bug 7002: Closed Bug\n"),
);
let result = list_bug_files(tmp.path()).unwrap();
@@ -108,16 +125,19 @@ fn list_bug_files_sorted_by_id() {
"7013_bug_third",
"1_backlog",
"---\nname: Third\n---\n# Bug 7013: Third\n",
crate::db::ItemMeta::from_yaml("---\nname: Third\n---\n# Bug 7013: Third\n"),
);
crate::db::write_item_with_content(
"7011_bug_first",
"1_backlog",
"---\nname: First\n---\n# Bug 7011: First\n",
crate::db::ItemMeta::from_yaml("---\nname: First\n---\n# Bug 7011: First\n"),
);
crate::db::write_item_with_content(
"7012_bug_second",
"1_backlog",
"---\nname: Second\n---\n# Bug 7012: Second\n",
crate::db::ItemMeta::from_yaml("---\nname: Second\n---\n# Bug 7012: Second\n"),
);
let result = list_bug_files(tmp.path()).unwrap();
@@ -349,6 +369,7 @@ fn create_spike_file_increments_from_existing_items() {
"7050_story_existing",
"1_backlog",
"---\nname: Existing\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Existing\n---\n"),
);
let spike_id = create_spike_file(tmp.path(), "My Spike", None, &[], None).unwrap();
+25 -2
View File
@@ -325,7 +325,12 @@ mod tests {
("4_merge", "9840_story_merge"),
("5_done", "9850_story_done"),
] {
crate::db::write_item_with_content(id, stage, &format!("---\nname: {id}\n---\n"));
crate::db::write_item_with_content(
id,
stage,
&format!("---\nname: {id}\n---\n"),
crate::db::ItemMeta::from_yaml(&format!("---\nname: {id}\n---\n")),
);
}
let ctx = crate::http::context::AppContext::new_test(root);
@@ -369,6 +374,7 @@ mod tests {
"9860_story_test",
"2_current",
"---\nname: Test Story\n---\n# Story\n",
crate::db::ItemMeta::from_yaml("---\nname: Test Story\n---\n# Story\n"),
);
let ctx = crate::http::context::AppContext::new_test(root);
@@ -404,6 +410,7 @@ mod tests {
"9861_story_done",
"2_current",
"---\nname: Done Story\n---\n# Story\n",
crate::db::ItemMeta::from_yaml("---\nname: Done Story\n---\n# Story\n"),
);
let ctx = crate::http::context::AppContext::new_test(root);
@@ -436,6 +443,7 @@ mod tests {
"9862_story_pending",
"2_current",
"---\nname: Pending Story\n---\n# Story\n",
crate::db::ItemMeta::from_yaml("---\nname: Pending Story\n---\n# Story\n"),
);
let ctx = crate::http::context::AppContext::new_test(root);
@@ -466,11 +474,15 @@ mod tests {
"9863_story_dependent",
"1_backlog",
"---\nname: Dependent Story\ndepends_on: [10, 11]\n---\n",
crate::db::ItemMeta::from_yaml(
"---\nname: Dependent Story\ndepends_on: [10, 11]\n---\n",
),
);
crate::db::write_item_with_content(
"9864_story_independent",
"1_backlog",
"---\nname: Independent Story\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Independent Story\n---\n"),
);
let tmp = tempfile::tempdir().unwrap();
@@ -499,11 +511,13 @@ mod tests {
"9870_story_view_upcoming",
"1_backlog",
"---\nname: View Upcoming\n---\n# Story\n",
crate::db::ItemMeta::from_yaml("---\nname: View Upcoming\n---\n# Story\n"),
);
crate::db::write_item_with_content(
"9871_story_worktree",
"1_backlog",
"---\nname: Worktree Orchestration\n---\n# Story\n",
crate::db::ItemMeta::from_yaml("---\nname: Worktree Orchestration\n---\n# Story\n"),
);
let tmp = tempfile::tempdir().unwrap();
@@ -530,6 +544,7 @@ mod tests {
"9872_story_example",
"1_backlog",
"---\nname: A Story\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: A Story\n---\n"),
);
let tmp = tempfile::tempdir().unwrap();
@@ -545,11 +560,13 @@ mod tests {
"9873_story_todos",
"2_current",
"---\nname: Show TODOs\n---\n# Story\n",
crate::db::ItemMeta::from_yaml("---\nname: Show TODOs\n---\n# Story\n"),
);
crate::db::write_item_with_content(
"9874_story_front_matter",
"1_backlog",
"---\nname: Enforce Front Matter\n---\n# Story\n",
crate::db::ItemMeta::from_yaml("---\nname: Enforce Front Matter\n---\n# Story\n"),
);
let tmp = tempfile::tempdir().unwrap();
@@ -569,7 +586,12 @@ mod tests {
#[test]
fn validate_story_dirs_missing_front_matter() {
crate::db::ensure_content_store();
crate::db::write_item_with_content("9875_story_no_fm", "2_current", "# No front matter\n");
crate::db::write_item_with_content(
"9875_story_no_fm",
"2_current",
"# No front matter\n",
crate::db::ItemMeta::from_yaml("# No front matter\n"),
);
let tmp = tempfile::tempdir().unwrap();
let results = validate_story_dirs(tmp.path()).unwrap();
@@ -588,6 +610,7 @@ mod tests {
"9876_story_no_name",
"2_current",
"---\n---\n# Story\n",
crate::db::ItemMeta::from_yaml("---\n---\n# Story\n"),
);
let tmp = tempfile::tempdir().unwrap();
@@ -143,6 +143,7 @@ mod tests {
"36_story_existing",
"1_backlog",
"---\nname: Existing\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Existing\n---\n"),
);
let number = super::super::super::next_item_number(tmp.path()).unwrap();
+10
View File
@@ -175,6 +175,7 @@ mod tests {
"8001_story_test",
"2_current",
"---\nname: Test\n---\n# Story\n",
crate::db::ItemMeta::from_yaml("---\nname: Test\n---\n# Story\n"),
);
let results = make_results();
@@ -200,6 +201,9 @@ mod tests {
"8002_story_check",
"2_current",
"---\nname: Check\n---\n# Story\n\n## Acceptance Criteria\n\n- [ ] AC1\n",
crate::db::ItemMeta::from_yaml(
"---\nname: Check\n---\n# Story\n\n## Acceptance Criteria\n\n- [ ] AC1\n",
),
);
let results = make_results();
@@ -222,6 +226,9 @@ mod tests {
"8003_story_overwrite",
"2_current",
"---\nname: Overwrite\n---\n# Story\n\n## Test Results\n\n<!-- huskies-test-results: {} -->\n\n### Unit Tests (0 passed, 0 failed)\n\n*No unit tests recorded.*\n",
crate::db::ItemMeta::from_yaml(
"---\nname: Overwrite\n---\n# Story\n\n## Test Results\n\n<!-- huskies-test-results: {} -->\n\n### Unit Tests (0 passed, 0 failed)\n\n*No unit tests recorded.*\n",
),
);
let results = make_results();
@@ -241,6 +248,7 @@ mod tests {
"8004_story_empty",
"2_current",
"---\nname: Empty\n---\n# Story\n",
crate::db::ItemMeta::from_yaml("---\nname: Empty\n---\n# Story\n"),
);
let result = read_test_results_from_story_file(tmp.path(), "8004_story_empty");
@@ -262,6 +270,7 @@ mod tests {
"8005_story_qa",
"3_qa",
"---\nname: QA Story\n---\n# Story\n",
crate::db::ItemMeta::from_yaml("---\nname: QA Story\n---\n# Story\n"),
);
let results = StoryTestResults {
@@ -286,6 +295,7 @@ mod tests {
"8006_story_cov",
"2_current",
"---\nname: Cov Story\n---\n# Story\n",
crate::db::ItemMeta::from_yaml("---\nname: Cov Story\n---\n# Story\n"),
);
write_coverage_baseline_to_story_file(tmp.path(), "8006_story_cov", 75.4).unwrap();
+12 -2
View File
@@ -17,7 +17,12 @@ pub(crate) fn write_story_content(
stage: &str,
content: &str,
) {
crate::db::write_item_with_content(story_id, stage, content);
crate::db::write_item_with_content(
story_id,
stage,
content,
crate::db::ItemMeta::from_yaml(content),
);
}
/// Determine what stage a story is in (from CRDT).
@@ -262,7 +267,12 @@ mod tests {
#[test]
fn next_item_number_increments_beyond_existing() {
crate::db::ensure_content_store();
crate::db::write_item_with_content("9877_story_foo", "1_backlog", "---\nname: Foo\n---\n");
crate::db::write_item_with_content(
"9877_story_foo",
"1_backlog",
"---\nname: Foo\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Foo\n---\n"),
);
let tmp = tempfile::tempdir().unwrap();
assert!(next_item_number(tmp.path()).unwrap() >= 9878);
}
+13 -2
View File
@@ -77,7 +77,12 @@ fn stage_metadata_returns_correct_actions() {
#[test]
fn sweep_moves_old_items_to_archived() {
crate::db::ensure_content_store();
crate::db::write_item_with_content("9880_story_sweep_old", "5_done", "---\nname: old\n---\n");
crate::db::write_item_with_content(
"9880_story_sweep_old",
"5_done",
"---\nname: old\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: old\n---\n"),
);
// With ZERO retention, any Done item should be swept.
sweep_done_to_archived(Duration::ZERO);
@@ -96,7 +101,12 @@ fn sweep_moves_old_items_to_archived() {
#[test]
fn sweep_keeps_recent_items_in_done() {
crate::db::ensure_content_store();
crate::db::write_item_with_content("9881_story_sweep_new", "5_done", "---\nname: new\n---\n");
crate::db::write_item_with_content(
"9881_story_sweep_new",
"5_done",
"---\nname: new\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: new\n---\n"),
);
// With a very long retention, the item (merged_at ≈ now) should stay.
sweep_done_to_archived(Duration::from_secs(999_999));
@@ -118,6 +128,7 @@ fn sweep_respects_custom_retention() {
"9882_story_sweep_custom",
"5_done",
"---\nname: custom\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: custom\n---\n"),
);
// With ZERO retention, sweep should promote.
+7 -1
View File
@@ -618,7 +618,12 @@ fn regression_freeze_unfreeze_restores_crdt_stage() {
let story_id = "9950_story_freeze_regression";
let content = "---\nname: Freeze Regression\n---\n# Story\n";
crate::db::write_item_with_content(story_id, "2_current", content);
crate::db::write_item_with_content(
story_id,
"2_current",
content,
crate::db::ItemMeta::from_yaml(content),
);
// Confirm starting stage.
let item = read_typed(story_id).unwrap().unwrap();
@@ -670,6 +675,7 @@ fn merge_failure_transition_emits_event_with_full_reason() {
story_id,
"4_merge",
"---\nname: Merge Failure Event Test\n---\n# Story\n",
crate::db::ItemMeta::from_yaml("---\nname: Merge Failure Event Test\n---\n# Story\n"),
);
let reason = "Conflict in server/src/main.rs: both modified";
@@ -18,6 +18,7 @@ async fn rate_limit_warning_sends_notification_with_agent_and_story() {
"365_story_rate_limit",
"2_current",
"---\nname: Rate Limit Test Story\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Rate Limit Test Story\n---\n"),
);
let (watcher_tx, watcher_rx) = broadcast::channel::<WatcherEvent>(16);
@@ -144,6 +145,7 @@ async fn story_blocked_sends_notification_with_reason() {
"425_story_blocking_test",
"2_current",
"---\nname: Blocking Test Story\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Blocking Test Story\n---\n"),
);
let (watcher_tx, watcher_rx) = broadcast::channel::<WatcherEvent>(16);
@@ -17,7 +17,12 @@ async fn stage_notification_uses_dynamic_room_ids() {
let tmp = tempfile::tempdir().unwrap();
// Seed story via CRDT (the only source of truth).
crate::db::ensure_content_store();
crate::db::write_item_with_content("10_story_foo", "3_qa", "---\nname: Foo Story\n---\n");
crate::db::write_item_with_content(
"10_story_foo",
"3_qa",
"---\nname: Foo Story\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Foo Story\n---\n"),
);
let (watcher_tx, watcher_rx) = broadcast::channel::<WatcherEvent>(16);
let (transport, calls) = MockTransport::new();
@@ -106,6 +111,7 @@ fn read_story_name_reads_from_front_matter() {
"9942_story_my_feature",
"2_current",
"---\nname: My Cool Feature\n---\n# Story\n",
crate::db::ItemMeta::from_yaml("---\nname: My Cool Feature\n---\n# Story\n"),
);
let tmp = tempfile::tempdir().unwrap();
@@ -128,6 +134,7 @@ fn read_story_name_returns_none_for_missing_name_field() {
"9943_story_no_name",
"2_current",
"---\ncoverage_baseline: 50%\n---\n# Story\n",
crate::db::ItemMeta::from_yaml("---\ncoverage_baseline: 50%\n---\n# Story\n"),
);
let tmp = tempfile::tempdir().unwrap();
+1
View File
@@ -214,6 +214,7 @@ mod tests {
story_id,
"1_backlog",
"---\nname: Service Delete Regression\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Service Delete Regression\n---\n"),
);
let tmp = tempfile::tempdir().unwrap();
+11 -1
View File
@@ -79,6 +79,7 @@ mod tests {
story_id,
"2_current",
"---\nname: Freeze Service Test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Freeze Service Test\n---\n"),
);
let result = freeze(story_id);
@@ -105,6 +106,7 @@ mod tests {
story_id,
"2_current",
"---\nname: Already Frozen\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Already Frozen\n---\n"),
);
freeze(story_id).expect("first freeze should succeed");
@@ -124,6 +126,7 @@ mod tests {
story_id,
"2_current",
"---\nname: Unfreeze Service Test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Unfreeze Service Test\n---\n"),
);
freeze(story_id).expect("freeze should succeed");
@@ -148,7 +151,12 @@ mod tests {
fn unfreeze_not_frozen_item_returns_not_frozen() {
crate::crdt_state::init_for_test();
let story_id = "8783_story_unfreeze_service_not_frozen";
crate::db::write_item_with_content(story_id, "2_current", "---\nname: Not Frozen\n---\n");
crate::db::write_item_with_content(
story_id,
"2_current",
"---\nname: Not Frozen\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Not Frozen\n---\n"),
);
let result = unfreeze(story_id);
assert!(
@@ -171,6 +179,7 @@ mod tests {
story_a,
"2_current",
"---\nname: Regression Chat Path\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Regression Chat Path\n---\n"),
);
// Story B simulates the MCP tool path.
@@ -179,6 +188,7 @@ mod tests {
story_b,
"2_current",
"---\nname: Regression MCP Path\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Regression MCP Path\n---\n"),
);
// Both paths call service::work_item::freeze().