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", "99950_story_lifecycle",
"1_backlog", "1_backlog",
"---\nname: Lifecycle Test\n---\n# Story\n", "---\nname: Lifecycle Test\n---\n# Story\n",
crate::db::ItemMeta::named("Lifecycle Test"),
); );
move_story_to_current("99950_story_lifecycle").unwrap(); move_story_to_current("99950_story_lifecycle").unwrap();
@@ -462,6 +463,7 @@ mod tests {
"99951_story_crdt_only", "99951_story_crdt_only",
"2_current", "2_current",
"---\nname: CRDT Only Test\n---\n# Story\n", "---\nname: CRDT Only Test\n---\n# Story\n",
crate::db::ItemMeta::named("CRDT Only Test"),
); );
// No filesystem path is involved — lifecycle functions no longer // No filesystem path is involved — lifecycle functions no longer
@@ -529,6 +531,7 @@ mod tests {
"99866_story_block_test", "99866_story_block_test",
"2_current", "2_current",
"---\nname: Block Round Trip\n---\n# Story\n", "---\nname: Block Round Trip\n---\n# Story\n",
crate::db::ItemMeta::named("Block Round Trip"),
); );
// Verify starting state is Coding. // Verify starting state is Coding.
@@ -57,7 +57,12 @@ mod tests {
.unwrap(); .unwrap();
// Place the story in 2_current/ via CRDT (the only source of truth). // Place the story in 2_current/ via CRDT (the only source of truth).
crate::db::ensure_content_store(); 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); let pool = AgentPool::new_test(3001);
// No agents are running — coder-1 is free. // No agents are running — coder-1 is free.
@@ -139,6 +144,11 @@ mod tests {
"9930_story_qa1", "9930_story_qa1",
"3_qa", "3_qa",
"---\nname: QA Story\nagent: coder-1\n---\n", "---\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); let pool = AgentPool::new_test(3001);
@@ -188,6 +198,11 @@ mod tests {
"story-pref", "story-pref",
"2_current", "2_current",
"---\nname: Coder Story\nagent: coder-1\n---\n", "---\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); let pool = AgentPool::new_test(3001);
@@ -235,6 +250,11 @@ mod tests {
"9931_story_noqa", "9931_story_noqa",
"3_qa", "3_qa",
"---\nname: QA Story No Agent\nagent: coder-1\n---\n", "---\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); let pool = AgentPool::new_test(3001);
@@ -274,6 +294,7 @@ mod tests {
"9932_story_waiting", "9932_story_waiting",
"2_current", "2_current",
"---\nname: Waiting\ndepends_on: [9999]\n---\n", "---\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); let pool = AgentPool::new_test(3001);
@@ -307,12 +328,18 @@ mod tests {
// Seed stories via CRDT (the only source of truth). // Seed stories via CRDT (the only source of truth).
crate::db::ensure_content_store(); crate::db::ensure_content_store();
// Dep 999 is now done. // 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. // Story 10 depends on 999 which is done.
crate::db::write_item_with_content( crate::db::write_item_with_content(
"10_story_unblocked", "10_story_unblocked",
"2_current", "2_current",
"---\nname: Unblocked\ndepends_on: [999]\n---\n", "---\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); let pool = AgentPool::new_test(3001);
@@ -496,6 +523,9 @@ mod tests {
"9860_story_conflict", "9860_story_conflict",
"4_merge", "4_merge",
"---\nname: Conflict\nmerge_failure: \"CONFLICT (content): server/src/lib.rs\"\n---\n", "---\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); let pool = AgentPool::new_test(3001);
@@ -530,6 +560,9 @@ mod tests {
"9861_story_nothing", "9861_story_nothing",
"4_merge", "4_merge",
"---\nname: Nothing\nmerge_failure: \"nothing to commit, working tree clean\"\n---\n", "---\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); let pool = AgentPool::new_test(3001);
@@ -565,6 +598,9 @@ mod tests {
"9863_story_blocked_conflict", "9863_story_blocked_conflict",
"4_merge", "4_merge",
"---\nname: Blocked conflict\nmerge_failure: \"CONFLICT (content): foo.rs\"\nblocked: true\n---\n", "---\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); let pool = AgentPool::new_test(3001);
@@ -599,6 +635,9 @@ mod tests {
"9862_story_attempted", "9862_story_attempted",
"4_merge", "4_merge",
"---\nname: Already tried\nmerge_failure: \"CONFLICT (content): foo.rs\"\nmergemaster_attempted: true\n---\n", "---\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); let pool = AgentPool::new_test(3001);
+6 -1
View File
@@ -83,7 +83,12 @@ impl AgentPool {
&contents, &contents,
); );
crate::db::write_content(story_id, &updated); 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); crate::crdt_state::set_mergemaster_attempted(story_id, true);
if let Err(e) = self if let Err(e) = self
+19 -3
View File
@@ -168,6 +168,7 @@ mod tests {
"9970_story_archived", "9970_story_archived",
"6_archived", "6_archived",
"---\nname: Archived\n---\n", "---\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. // 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() { fn scan_stage_items_returns_sorted_story_ids() {
// Write items via the CRDT store (the primary source of truth). // Write items via the CRDT store (the primary source of truth).
crate::db::ensure_content_store(); 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(
crate::db::write_item_with_content("9940_story_bar", "2_current", "---\nname: bar\n---"); "9942_story_foo",
crate::db::write_item_with_content("9935_story_baz", "2_current", "---\nname: baz\n---"); "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 tmp = tempfile::tempdir().unwrap();
let items = scan_stage_items(tmp.path(), "2_current"); let items = scan_stage_items(tmp.path(), "2_current");
@@ -188,6 +188,9 @@ mod tests {
"10_spike_research", "10_spike_research",
"3_qa", "3_qa",
"---\nname: Research spike\nreview_hold: true\n---\n# Spike\n", "---\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")); 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() .flatten()
.map(|i| i.stage.dir_name().to_string()) .map(|i| i.stage.dir_name().to_string())
.unwrap_or_else(|| "3_qa".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 { } else {
slog_error!("[pipeline] Cannot write review_hold for '{story_id}': no content in store"); 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). // Seed story via CRDT (the only source of truth).
crate::db::ensure_content_store(); 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. // 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();
@@ -51,6 +51,7 @@ async fn mergemaster_blocks_and_sends_story_blocked_when_no_commits_ahead() {
"9919_story_no_commits", "9919_story_no_commits",
"2_current", "2_current",
"---\nname: Test\n---\n", "---\nname: Test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Test\n---\n"),
); );
let pool = AgentPool::new_test(3001); let pool = AgentPool::new_test(3001);
@@ -145,11 +146,13 @@ stage = "qa"
"292_story_first", "292_story_first",
"3_qa", "3_qa",
"---\nname: First\nqa: human\n---\n", "---\nname: First\nqa: human\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: First\nqa: human\n---\n"),
); );
crate::db::write_item_with_content( crate::db::write_item_with_content(
"293_story_second", "293_story_second",
"3_qa", "3_qa",
"---\nname: Second\nqa: human\n---\n", "---\nname: Second\nqa: human\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Second\nqa: human\n---\n"),
); );
let pool = AgentPool::new_test(3001); 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 story_id = "9929_story_zombie_merge";
let content = "---\nname: Zombie Merge Test\n---\n"; let content = "---\nname: Zombie Merge Test\n---\n";
crate::db::write_content(story_id, content); 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 pool = AgentPool::new_test(3001);
let mut rx = pool.watcher_tx.subscribe(); let mut rx = pool.watcher_tx.subscribe();
@@ -381,6 +389,7 @@ async fn work_survived_advances_to_qa_instead_of_blocking() {
"9945_story_survived", "9945_story_survived",
"2_current", "2_current",
"---\nname: Survived Test\n---\n", "---\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): // 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", "9946_story_nowork",
"2_current", "2_current",
"---\nname: No Work Test\n---\n", "---\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. // 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", "9947_story_no_evidence",
"2_current", "2_current",
"---\nname: No Evidence Test\n---\n", "---\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. // 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", "9948_story_with_evidence",
"2_current", "2_current",
"---\nname: With Evidence Test\n---\n", "---\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 // Write the run_tests evidence — simulates the agent having called run_tests
@@ -813,6 +825,7 @@ stage = "coder"
"9950_story_warm_resume", "9950_story_warm_resume",
"2_current", "2_current",
"---\nname: Warm Resume Test\n---\n", "---\nname: Warm Resume Test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Warm Resume Test\n---\n"),
); );
let pool = AgentPool::new_test(3001); let pool = AgentPool::new_test(3001);
@@ -651,6 +651,7 @@ async fn server_side_merge_happy_path_advances_to_done() {
"757a_happy", "757a_happy",
"4_merge", "4_merge",
"---\nname: Happy path test\n---\n", "---\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)); let pool = Arc::new(AgentPool::new_test(3001));
@@ -787,6 +788,7 @@ async fn server_side_merge_conflict_sets_merge_failure() {
"757b_conflict", "757b_conflict",
"4_merge", "4_merge",
"---\nname: Conflict test\n---\n", "---\nname: Conflict test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Conflict test\n---\n"),
); );
let pool = Arc::new(AgentPool::new_test(3001)); let pool = Arc::new(AgentPool::new_test(3001));
@@ -898,6 +900,7 @@ async fn server_side_merge_gate_failure_sets_merge_failure() {
"757c_gates", "757c_gates",
"4_merge", "4_merge",
"---\nname: Gate failure test\n---\n", "---\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)); let pool = Arc::new(AgentPool::new_test(3001));
+18 -3
View File
@@ -597,7 +597,12 @@ mod tests {
crate::db::ensure_content_store(); crate::db::ensure_content_store();
let story_id = "9960_story_gate_injection_881"; 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); crate::crdt_state::set_retry_count(story_id, 1);
let gate_output = let gate_output =
@@ -630,7 +635,12 @@ mod tests {
crate::db::ensure_content_store(); crate::db::ensure_content_store();
let story_id = "9961_story_no_gate_injection_881"; 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). // retry_count is 0 (default — never bumped).
crate::db::write_content(&format!("{story_id}:gate_output"), "some previous output"); crate::db::write_content(&format!("{story_id}:gate_output"), "some previous output");
@@ -690,7 +700,12 @@ mod tests {
crate::db::ensure_content_store(); crate::db::ensure_content_store();
let story_id = "9962_story_abort_respawn_882"; 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"); let db_key = format!("{story_id}:abort_respawn_count");
const CAP: u32 = 5; const CAP: u32 = 5;
@@ -429,7 +429,12 @@ async fn start_agent_rejects_mergemaster_on_coding_stage_story() {
) )
.unwrap(); .unwrap();
crate::db::ensure_content_store(); 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 pool = AgentPool::new_test(3099);
let result = pool let result = pool
@@ -463,7 +468,12 @@ async fn start_agent_rejects_coder_on_qa_stage_story() {
) )
.unwrap(); .unwrap();
crate::db::ensure_content_store(); 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 pool = AgentPool::new_test(3099);
let result = pool let result = pool
@@ -497,7 +507,12 @@ async fn start_agent_rejects_qa_on_merge_stage_story() {
) )
.unwrap(); .unwrap();
crate::db::ensure_content_store(); 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 pool = AgentPool::new_test(3099);
let result = pool let result = pool
+6 -1
View File
@@ -143,7 +143,12 @@ mod tests {
let story_content = "test"; let story_content = "test";
fs::write(current.join("60_story_cleanup.md"), story_content).unwrap(); fs::write(current.join("60_story_cleanup.md"), story_content).unwrap();
crate::db::ensure_content_store(); 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); let pool = AgentPool::new_test(3001);
pool.inject_test_agent("60_story_cleanup", "coder-1", AgentStatus::Completed); pool.inject_test_agent("60_story_cleanup", "coder-1", AgentStatus::Completed);
+18 -3
View File
@@ -42,7 +42,12 @@ mod tests {
#[test] #[test]
fn find_active_story_stage_detects_current() { fn find_active_story_stage_detects_current() {
crate::db::ensure_content_store(); 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(); let tmp = tempfile::tempdir().unwrap();
assert!(matches!( assert!(matches!(
find_active_story_stage(tmp.path(), "10_story_test"), find_active_story_stage(tmp.path(), "10_story_test"),
@@ -53,7 +58,12 @@ mod tests {
#[test] #[test]
fn find_active_story_stage_detects_qa() { fn find_active_story_stage_detects_qa() {
crate::db::ensure_content_store(); 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(); let tmp = tempfile::tempdir().unwrap();
assert!(matches!( assert!(matches!(
find_active_story_stage(tmp.path(), "11_story_test"), find_active_story_stage(tmp.path(), "11_story_test"),
@@ -64,7 +74,12 @@ mod tests {
#[test] #[test]
fn find_active_story_stage_detects_merge() { fn find_active_story_stage_detects_merge() {
crate::db::ensure_content_store(); 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(); let tmp = tempfile::tempdir().unwrap();
assert!(matches!( assert!(matches!(
find_active_story_stage(tmp.path(), "12_story_test"), 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", "903_story_merge_fail",
"4_merge", "4_merge",
"---\nname: Failed Story\nmerge_failure: \"conflicts in src/lib.rs\"\n---\n", "---\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( let items = vec![make_item(
"903_story_merge_fail", "903_story_merge_fail",
@@ -552,7 +555,12 @@ fn merge_item_failure_snippet_truncated_at_120_chars() {
crate::db::ensure_content_store(); crate::db::ensure_content_store();
let long_reason = "x".repeat(200); let long_reason = "x".repeat(200);
let content = format!("---\nname: Long Fail\nmerge_failure: \"{long_reason}\"\n---\n"); 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 items = vec![make_item("904_story_long_fail", "Long Fail", merge_stage())];
let agents = AgentPool::new_test(3000); let agents = AgentPool::new_test(3000);
let output = build_status_from_items(tmp.path(), &agents, &items); 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", "---\nname: Multi Line\nmerge_failure: \"{}\" \n---\n",
reason.replace('\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). // Write with literal \n as the content (simulating stored text with newlines).
let content2 = let content2 =
"---\nname: Multi Line\nmerge_failure: |\n \n first line of error\n second line\n---\n"; "---\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( let items = vec![make_item(
"905_story_multiline", "905_story_multiline",
"Multi Line", "Multi Line",
@@ -615,6 +633,9 @@ fn merge_item_det_merge_running_preferred_over_failure() {
"906_story_det_over_fail", "906_story_det_over_fail",
"4_merge", "4_merge",
"---\nname: Det Over Fail\nmerge_failure: \"old failure\"\n---\n", "---\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. // Record a running deterministic merge in the CRDT.
crate::crdt_state::write_merge_job("906_story_det_over_fail", "running", 0.0, None, None); 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() .flatten()
.map(|i| i.stage.dir_name().to_string()) .map(|i| i.stage.dir_name().to_string())
.unwrap_or_else(|| "2_current".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); 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"); let story_id = filename.trim_end_matches(".md");
crate::db::ensure_content_store(); 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, story_id,
"1_backlog", "1_backlog",
"---\nname: CRDT Tombstone Check\n---\n\n# Story 9977\n", "---\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(); let tmp = tempfile::tempdir().unwrap();
@@ -291,6 +294,7 @@ mod tests {
"9975_story_some_feature", "9975_story_some_feature",
"1_backlog", "1_backlog",
"---\nname: Some Feature\n---\n\n# Story 9975\n", "---\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)); let agents = std::sync::Arc::new(crate::agents::AgentPool::new_test(3000));
@@ -275,6 +275,7 @@ mod tests {
"9976_story_test", "9976_story_test",
"1_backlog", "1_backlog",
"---\nname: Test Story\n---\n", "---\nname: Test Story\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Test Story\n---\n"),
); );
let agents = Arc::new(AgentPool::new_test(3000)); let agents = Arc::new(AgentPool::new_test(3000));
+60 -1
View File
@@ -21,7 +21,7 @@ pub mod ops;
pub mod shadow_write; pub mod shadow_write;
pub use content_store::{all_content_ids, delete_content, read_content, write_content}; 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; pub use shadow_write::init;
#[cfg(test)] #[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. /// Bug 780: stage transitions must reset retry_count to 0 in the CRDT.
/// Carryover from prior-stage retries was tripping the auto-assigner's /// Carryover from prior-stage retries was tripping the auto-assigner's
/// deterministic-merge skip logic. /// deterministic-merge skip logic.
+65 -23
View File
@@ -9,23 +9,65 @@ use super::content_store::{
use super::shadow_write::{PIPELINE_DB, PipelineWriteMsg}; use super::shadow_write::{PIPELINE_DB, PipelineWriteMsg};
use crate::io::story_metadata::parse_front_matter; 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). /// Write a pipeline item from in-memory content (no filesystem access).
/// ///
/// This is the primary write path for the DB-backed pipeline. It updates /// 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. /// 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) { /// The metadata in `meta` is authoritative: this function does NOT parse
Ok(meta) => ( /// `content` to extract front-matter fields. Callers must pass typed
meta.name, /// metadata explicitly via `ItemMeta`.
meta.agent, pub fn write_item_with_content(story_id: &str, stage: &str, content: &str, meta: ItemMeta) {
meta.retry_count.map(|r| r as i64), let depends_on_json = meta
meta.blocked, .depends_on
meta.depends_on .as_ref()
.as_ref() .and_then(|d| serde_json::to_string(d).ok());
.and_then(|d| serde_json::to_string(d).ok()),
),
Err(_) => (None, None, None, None, None),
};
// Update in-memory content store. // Update in-memory content store.
ensure_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( crate::crdt_state::write_item(
story_id, story_id,
stage, stage,
name.as_deref(), meta.name.as_deref(),
agent.as_deref(), meta.agent.as_deref(),
retry_count, meta.retry_count,
blocked, meta.blocked,
depends_on.as_deref(), depends_on_json.as_deref(),
None, None,
None, None,
merged_at_ts, merged_at_ts,
@@ -57,11 +99,11 @@ pub fn write_item_with_content(story_id: &str, stage: &str, content: &str) {
let msg = PipelineWriteMsg { let msg = PipelineWriteMsg {
story_id: story_id.to_string(), story_id: story_id.to_string(),
stage: stage.to_string(), stage: stage.to_string(),
name, name: meta.name,
agent, agent: meta.agent,
retry_count, retry_count: meta.retry_count,
blocked, blocked: meta.blocked,
depends_on, depends_on: depends_on_json,
content: Some(content.to_string()), content: Some(content.to_string()),
}; };
let _ = db.tx.send(msg); 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", "44_story_crdt_only",
"1_backlog", "1_backlog",
"---\nname: \"CRDT Only\"\n---\n\nCRDT content.", "---\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 ctx = AppContext::new_test(root);
let api = AgentsApi { ctx: Arc::new(ctx) }; 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", "45_story_crdt_current",
"2_current", "2_current",
"---\nname: \"Current CRDT\"\n---\n\nIn progress.", "---\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 ctx = AppContext::new_test(root);
let api = AgentsApi { ctx: Arc::new(ctx) }; let api = AgentsApi { ctx: Arc::new(ctx) };
+18 -3
View File
@@ -449,7 +449,12 @@ mod tests {
let content = "---\nname: Test\n---\n"; let content = "---\nname: Test\n---\n";
fs::write(backlog.join("5_story_test.md"), content).unwrap(); fs::write(backlog.join("5_story_test.md"), content).unwrap();
crate::db::ensure_content_store(); 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 ctx = test_ctx(root);
let result = super::super::tool_move_story( let result = super::super::tool_move_story(
@@ -476,7 +481,12 @@ mod tests {
let content = "---\nname: Back\n---\n"; let content = "---\nname: Back\n---\n";
fs::write(current.join("6_story_back.md"), content).unwrap(); fs::write(current.join("6_story_back.md"), content).unwrap();
crate::db::ensure_content_store(); 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 ctx = test_ctx(root);
let result = super::super::tool_move_story( let result = super::super::tool_move_story(
@@ -503,7 +513,12 @@ mod tests {
let content = "---\nname: Idem\n---\n"; let content = "---\nname: Idem\n---\n";
fs::write(current.join("9907_story_idem.md"), content).unwrap(); fs::write(current.join("9907_story_idem.md"), content).unwrap();
crate::db::ensure_content_store(); 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 ctx = test_ctx(root);
let result = super::super::tool_move_story( let result = super::super::tool_move_story(
+12 -2
View File
@@ -366,7 +366,12 @@ mod tests {
crate::crdt_state::init_for_test(); crate::crdt_state::init_for_test();
crate::db::ensure_content_store(); 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"; 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 ctx = crate::http::context::AppContext::new_test(tmp.path().to_path_buf());
let result = tool_status(&json!({"story_id": "9887_story_blocked_test"}), &ctx) let result = tool_status(&json!({"story_id": "9887_story_blocked_test"}), &ctx)
@@ -388,7 +393,12 @@ mod tests {
crate::db::ensure_content_store(); 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"; 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 ctx = crate::http::context::AppContext::new_test(tmp.path().to_path_buf());
let result = tool_status(&json!({"story_id": "9886_story_status_test"}), &ctx) 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", "9902_bug_crash",
"1_backlog", "1_backlog",
"---\nname: \"App Crash\"\n---\n# Bug 9902: App Crash\n", "---\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( crate::db::write_item_with_content(
"9903_bug_typo", "9903_bug_typo",
"1_backlog", "1_backlog",
"---\nname: \"Typo in Header\"\n---\n# Bug 9903: Typo in Header\n", "---\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()); let ctx = test_ctx(tmp.path());
@@ -438,7 +444,12 @@ mod tests {
let content = "# Bug 9901: Crash\n"; let content = "# Bug 9901: Crash\n";
std::fs::write(&bug_file, content).unwrap(); std::fs::write(&bug_file, content).unwrap();
crate::db::ensure_content_store(); 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 // Stage the file so it's tracked
std::process::Command::new("git") std::process::Command::new("git")
.args(["add", "."]) .args(["add", "."])
+22 -1
View File
@@ -421,6 +421,9 @@ mod tests {
"9901_test", "9901_test",
"2_current", "2_current",
"---\nname: Test\n---\n## AC\n- [ ] First\n- [x] Done\n- [ ] Second\n", "---\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()); let ctx = test_ctx(tmp.path());
@@ -514,6 +517,7 @@ mod tests {
"9906_story_persist", "9906_story_persist",
"2_current", "2_current",
"---\nname: Persist\n---\n# Story\n", "---\nname: Persist\n---\n# Story\n",
crate::db::ItemMeta::from_yaml("---\nname: Persist\n---\n# Story\n"),
); );
let ctx = test_ctx(tmp.path()); let ctx = test_ctx(tmp.path());
@@ -547,7 +551,12 @@ mod tests {
// Write story content to CRDT with a pre-populated Test Results section // 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"; 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::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()); let ctx = test_ctx(tmp.path());
@@ -618,6 +627,9 @@ mod tests {
"9997_empty_branch", "9997_empty_branch",
"2_current", "2_current",
"---\nname: Empty Branch Test\n---\n## AC\n- [ ] Implement the feature\n", "---\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()); let ctx = test_ctx(tmp.path());
@@ -672,6 +684,9 @@ mod tests {
"9904_test", "9904_test",
"2_current", "2_current",
"---\nname: Test\n---\n## AC\n- [ ] First criterion\n- [x] Already done\n", "---\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()); let ctx = test_ctx(tmp.path());
@@ -711,6 +726,9 @@ mod tests {
"9905_test", "9905_test",
"2_current", "2_current",
"---\nname: Test\n---\n## Acceptance Criteria\n- [ ] Keep me\n- [ ] Remove me\n", "---\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()); let ctx = test_ctx(tmp.path());
@@ -732,6 +750,9 @@ mod tests {
"9906_test", "9906_test",
"2_current", "2_current",
"---\nname: Test\n---\n## Acceptance Criteria\n- [ ] Only one\n", "---\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()); let ctx = test_ctx(tmp.path());
+9
View File
@@ -246,17 +246,26 @@ mod tests {
"9990_epic_rollup", "9990_epic_rollup",
"1_backlog", "1_backlog",
"---\ntype: epic\nname: \"Rollup Epic\"\n---\n\n## Goal\n\nTest\n", "---\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. // Write two member items: one done, one current.
crate::db::write_item_with_content( crate::db::write_item_with_content(
"9991_story_member_done", "9991_story_member_done",
"5_done", "5_done",
"---\ntype: story\nname: \"Done Member\"\nepic: \"9990_epic_rollup\"\n---\n", "---\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( crate::db::write_item_with_content(
"9992_story_member_current", "9992_story_member_current",
"2_current", "2_current",
"---\ntype: story\nname: \"Current Member\"\nepic: \"9990_epic_rollup\"\n---\n", "---\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(); let tmp = tempfile::tempdir().unwrap();
@@ -216,7 +216,12 @@ mod tests {
let content = "---\nname: No Branch\n---\n"; let content = "---\nname: No Branch\n---\n";
std::fs::write(current_dir.join("51_story_no_branch.md"), content).unwrap(); std::fs::write(current_dir.join("51_story_no_branch.md"), content).unwrap();
crate::db::ensure_content_store(); 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 ctx = test_ctx(tmp.path());
let result = tool_accept_story(&json!({"story_id": "51_story_no_branch"}), &ctx); let result = tool_accept_story(&json!({"story_id": "51_story_no_branch"}), &ctx);
@@ -60,6 +60,7 @@ mod tests {
story_id, story_id,
"2_current", "2_current",
"---\nname: MCP Freeze Tool Test\n---\n", "---\nname: MCP Freeze Tool Test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: MCP Freeze Tool Test\n---\n"),
); );
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
@@ -88,6 +89,7 @@ mod tests {
story_id, story_id,
"2_current", "2_current",
"---\nname: MCP Unfreeze Tool Test\n---\n", "---\nname: MCP Unfreeze Tool Test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: MCP Unfreeze Tool Test\n---\n"),
); );
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
+14 -2
View File
@@ -150,7 +150,12 @@ mod tests {
("4_merge", "9940_story_merge", "Merge Story"), ("4_merge", "9940_story_merge", "Merge Story"),
("5_done", "9950_story_done", "Done 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()); let ctx = test_ctx(tmp.path());
@@ -187,6 +192,7 @@ mod tests {
"9921_story_active", "9921_story_active",
"2_current", "2_current",
"---\nname: \"Active Story\"\n---\n", "---\nname: \"Active Story\"\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: \"Active Story\"\n---\n"),
); );
let ctx = test_ctx(tmp.path()); let ctx = test_ctx(tmp.path());
@@ -219,6 +225,7 @@ mod tests {
"9907_test", "9907_test",
"2_current", "2_current",
"---\nname: \"Valid Story\"\n---\n## AC\n- [ ] First\n", "---\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()); let ctx = test_ctx(tmp.path());
@@ -236,7 +243,12 @@ mod tests {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
crate::db::ensure_content_store(); 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 ctx = test_ctx(tmp.path());
let result = tool_validate_stories(&ctx).unwrap(); let result = tool_validate_stories(&ctx).unwrap();
@@ -150,7 +150,12 @@ mod tests {
fs::create_dir_all(&current).unwrap(); fs::create_dir_all(&current).unwrap();
fs::write(current.join(format!("{story_id}.md")), content).unwrap(); fs::write(current.join(format!("{story_id}.md")), content).unwrap();
crate::db::ensure_content_store(); 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] #[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("1_bug_crash.md"), "").unwrap();
fs::write(backlog.join("3_bug_another.md"), "").unwrap(); fs::write(backlog.join("3_bug_another.md"), "").unwrap();
// Also write to content store so next_item_number sees them. // 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(
crate::db::write_item_with_content("3_bug_another", "1_backlog", "---\nname: Another\n---\n"); "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); 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::create_dir_all(&archived).unwrap();
fs::write(archived.join("5_bug_old.md"), "").unwrap(); fs::write(archived.join("5_bug_old.md"), "").unwrap();
// Also write to content store so next_item_number sees it. // 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); assert!(super::super::next_item_number(tmp.path()).unwrap() >= 6);
} }
@@ -83,12 +98,14 @@ fn list_bug_files_excludes_archive_subdir() {
"7001_bug_open", "7001_bug_open",
"1_backlog", "1_backlog",
"---\nname: Open Bug\n---\n# Bug 7001: Open Bug\n", "---\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). // Bug in done (should NOT appear — list_bug_files only returns Backlog).
crate::db::write_item_with_content( crate::db::write_item_with_content(
"7002_bug_closed", "7002_bug_closed",
"5_done", "5_done",
"---\nname: Closed Bug\n---\n# Bug 7002: Closed Bug\n", "---\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(); let result = list_bug_files(tmp.path()).unwrap();
@@ -108,16 +125,19 @@ fn list_bug_files_sorted_by_id() {
"7013_bug_third", "7013_bug_third",
"1_backlog", "1_backlog",
"---\nname: Third\n---\n# Bug 7013: Third\n", "---\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( crate::db::write_item_with_content(
"7011_bug_first", "7011_bug_first",
"1_backlog", "1_backlog",
"---\nname: First\n---\n# Bug 7011: First\n", "---\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( crate::db::write_item_with_content(
"7012_bug_second", "7012_bug_second",
"1_backlog", "1_backlog",
"---\nname: Second\n---\n# Bug 7012: Second\n", "---\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(); let result = list_bug_files(tmp.path()).unwrap();
@@ -349,6 +369,7 @@ fn create_spike_file_increments_from_existing_items() {
"7050_story_existing", "7050_story_existing",
"1_backlog", "1_backlog",
"---\nname: Existing\n---\n", "---\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(); 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"), ("4_merge", "9840_story_merge"),
("5_done", "9850_story_done"), ("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); let ctx = crate::http::context::AppContext::new_test(root);
@@ -369,6 +374,7 @@ mod tests {
"9860_story_test", "9860_story_test",
"2_current", "2_current",
"---\nname: Test Story\n---\n# Story\n", "---\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); let ctx = crate::http::context::AppContext::new_test(root);
@@ -404,6 +410,7 @@ mod tests {
"9861_story_done", "9861_story_done",
"2_current", "2_current",
"---\nname: Done Story\n---\n# Story\n", "---\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); let ctx = crate::http::context::AppContext::new_test(root);
@@ -436,6 +443,7 @@ mod tests {
"9862_story_pending", "9862_story_pending",
"2_current", "2_current",
"---\nname: Pending Story\n---\n# Story\n", "---\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); let ctx = crate::http::context::AppContext::new_test(root);
@@ -466,11 +474,15 @@ mod tests {
"9863_story_dependent", "9863_story_dependent",
"1_backlog", "1_backlog",
"---\nname: Dependent Story\ndepends_on: [10, 11]\n---\n", "---\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( crate::db::write_item_with_content(
"9864_story_independent", "9864_story_independent",
"1_backlog", "1_backlog",
"---\nname: Independent Story\n---\n", "---\nname: Independent Story\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Independent Story\n---\n"),
); );
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
@@ -499,11 +511,13 @@ mod tests {
"9870_story_view_upcoming", "9870_story_view_upcoming",
"1_backlog", "1_backlog",
"---\nname: View Upcoming\n---\n# Story\n", "---\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( crate::db::write_item_with_content(
"9871_story_worktree", "9871_story_worktree",
"1_backlog", "1_backlog",
"---\nname: Worktree Orchestration\n---\n# Story\n", "---\nname: Worktree Orchestration\n---\n# Story\n",
crate::db::ItemMeta::from_yaml("---\nname: Worktree Orchestration\n---\n# Story\n"),
); );
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
@@ -530,6 +544,7 @@ mod tests {
"9872_story_example", "9872_story_example",
"1_backlog", "1_backlog",
"---\nname: A Story\n---\n", "---\nname: A Story\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: A Story\n---\n"),
); );
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
@@ -545,11 +560,13 @@ mod tests {
"9873_story_todos", "9873_story_todos",
"2_current", "2_current",
"---\nname: Show TODOs\n---\n# Story\n", "---\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( crate::db::write_item_with_content(
"9874_story_front_matter", "9874_story_front_matter",
"1_backlog", "1_backlog",
"---\nname: Enforce Front Matter\n---\n# Story\n", "---\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(); let tmp = tempfile::tempdir().unwrap();
@@ -569,7 +586,12 @@ mod tests {
#[test] #[test]
fn validate_story_dirs_missing_front_matter() { fn validate_story_dirs_missing_front_matter() {
crate::db::ensure_content_store(); 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 tmp = tempfile::tempdir().unwrap();
let results = validate_story_dirs(tmp.path()).unwrap(); let results = validate_story_dirs(tmp.path()).unwrap();
@@ -588,6 +610,7 @@ mod tests {
"9876_story_no_name", "9876_story_no_name",
"2_current", "2_current",
"---\n---\n# Story\n", "---\n---\n# Story\n",
crate::db::ItemMeta::from_yaml("---\n---\n# Story\n"),
); );
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
@@ -143,6 +143,7 @@ mod tests {
"36_story_existing", "36_story_existing",
"1_backlog", "1_backlog",
"---\nname: Existing\n---\n", "---\nname: Existing\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Existing\n---\n"),
); );
let number = super::super::super::next_item_number(tmp.path()).unwrap(); let number = super::super::super::next_item_number(tmp.path()).unwrap();
+10
View File
@@ -175,6 +175,7 @@ mod tests {
"8001_story_test", "8001_story_test",
"2_current", "2_current",
"---\nname: Test\n---\n# Story\n", "---\nname: Test\n---\n# Story\n",
crate::db::ItemMeta::from_yaml("---\nname: Test\n---\n# Story\n"),
); );
let results = make_results(); let results = make_results();
@@ -200,6 +201,9 @@ mod tests {
"8002_story_check", "8002_story_check",
"2_current", "2_current",
"---\nname: Check\n---\n# Story\n\n## Acceptance Criteria\n\n- [ ] AC1\n", "---\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(); let results = make_results();
@@ -222,6 +226,9 @@ mod tests {
"8003_story_overwrite", "8003_story_overwrite",
"2_current", "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", "---\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(); let results = make_results();
@@ -241,6 +248,7 @@ mod tests {
"8004_story_empty", "8004_story_empty",
"2_current", "2_current",
"---\nname: Empty\n---\n# Story\n", "---\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"); let result = read_test_results_from_story_file(tmp.path(), "8004_story_empty");
@@ -262,6 +270,7 @@ mod tests {
"8005_story_qa", "8005_story_qa",
"3_qa", "3_qa",
"---\nname: QA Story\n---\n# Story\n", "---\nname: QA Story\n---\n# Story\n",
crate::db::ItemMeta::from_yaml("---\nname: QA Story\n---\n# Story\n"),
); );
let results = StoryTestResults { let results = StoryTestResults {
@@ -286,6 +295,7 @@ mod tests {
"8006_story_cov", "8006_story_cov",
"2_current", "2_current",
"---\nname: Cov Story\n---\n# Story\n", "---\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(); 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, stage: &str,
content: &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). /// Determine what stage a story is in (from CRDT).
@@ -262,7 +267,12 @@ mod tests {
#[test] #[test]
fn next_item_number_increments_beyond_existing() { fn next_item_number_increments_beyond_existing() {
crate::db::ensure_content_store(); 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(); let tmp = tempfile::tempdir().unwrap();
assert!(next_item_number(tmp.path()).unwrap() >= 9878); assert!(next_item_number(tmp.path()).unwrap() >= 9878);
} }
+13 -2
View File
@@ -77,7 +77,12 @@ fn stage_metadata_returns_correct_actions() {
#[test] #[test]
fn sweep_moves_old_items_to_archived() { fn sweep_moves_old_items_to_archived() {
crate::db::ensure_content_store(); 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. // With ZERO retention, any Done item should be swept.
sweep_done_to_archived(Duration::ZERO); sweep_done_to_archived(Duration::ZERO);
@@ -96,7 +101,12 @@ fn sweep_moves_old_items_to_archived() {
#[test] #[test]
fn sweep_keeps_recent_items_in_done() { fn sweep_keeps_recent_items_in_done() {
crate::db::ensure_content_store(); 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. // With a very long retention, the item (merged_at ≈ now) should stay.
sweep_done_to_archived(Duration::from_secs(999_999)); sweep_done_to_archived(Duration::from_secs(999_999));
@@ -118,6 +128,7 @@ fn sweep_respects_custom_retention() {
"9882_story_sweep_custom", "9882_story_sweep_custom",
"5_done", "5_done",
"---\nname: custom\n---\n", "---\nname: custom\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: custom\n---\n"),
); );
// With ZERO retention, sweep should promote. // 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 story_id = "9950_story_freeze_regression";
let content = "---\nname: Freeze Regression\n---\n# Story\n"; 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. // Confirm starting stage.
let item = read_typed(story_id).unwrap().unwrap(); let item = read_typed(story_id).unwrap().unwrap();
@@ -670,6 +675,7 @@ fn merge_failure_transition_emits_event_with_full_reason() {
story_id, story_id,
"4_merge", "4_merge",
"---\nname: Merge Failure Event Test\n---\n# Story\n", "---\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"; 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", "365_story_rate_limit",
"2_current", "2_current",
"---\nname: Rate Limit Test Story\n---\n", "---\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); 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", "425_story_blocking_test",
"2_current", "2_current",
"---\nname: Blocking Test Story\n---\n", "---\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); 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(); let tmp = tempfile::tempdir().unwrap();
// Seed story via CRDT (the only source of truth). // Seed story via CRDT (the only source of truth).
crate::db::ensure_content_store(); 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 (watcher_tx, watcher_rx) = broadcast::channel::<WatcherEvent>(16);
let (transport, calls) = MockTransport::new(); let (transport, calls) = MockTransport::new();
@@ -106,6 +111,7 @@ fn read_story_name_reads_from_front_matter() {
"9942_story_my_feature", "9942_story_my_feature",
"2_current", "2_current",
"---\nname: My Cool Feature\n---\n# Story\n", "---\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(); let tmp = tempfile::tempdir().unwrap();
@@ -128,6 +134,7 @@ fn read_story_name_returns_none_for_missing_name_field() {
"9943_story_no_name", "9943_story_no_name",
"2_current", "2_current",
"---\ncoverage_baseline: 50%\n---\n# Story\n", "---\ncoverage_baseline: 50%\n---\n# Story\n",
crate::db::ItemMeta::from_yaml("---\ncoverage_baseline: 50%\n---\n# Story\n"),
); );
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
+1
View File
@@ -214,6 +214,7 @@ mod tests {
story_id, story_id,
"1_backlog", "1_backlog",
"---\nname: Service Delete Regression\n---\n", "---\nname: Service Delete Regression\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Service Delete Regression\n---\n"),
); );
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
+11 -1
View File
@@ -79,6 +79,7 @@ mod tests {
story_id, story_id,
"2_current", "2_current",
"---\nname: Freeze Service Test\n---\n", "---\nname: Freeze Service Test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Freeze Service Test\n---\n"),
); );
let result = freeze(story_id); let result = freeze(story_id);
@@ -105,6 +106,7 @@ mod tests {
story_id, story_id,
"2_current", "2_current",
"---\nname: Already Frozen\n---\n", "---\nname: Already Frozen\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Already Frozen\n---\n"),
); );
freeze(story_id).expect("first freeze should succeed"); freeze(story_id).expect("first freeze should succeed");
@@ -124,6 +126,7 @@ mod tests {
story_id, story_id,
"2_current", "2_current",
"---\nname: Unfreeze Service Test\n---\n", "---\nname: Unfreeze Service Test\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Unfreeze Service Test\n---\n"),
); );
freeze(story_id).expect("freeze should succeed"); freeze(story_id).expect("freeze should succeed");
@@ -148,7 +151,12 @@ mod tests {
fn unfreeze_not_frozen_item_returns_not_frozen() { fn unfreeze_not_frozen_item_returns_not_frozen() {
crate::crdt_state::init_for_test(); crate::crdt_state::init_for_test();
let story_id = "8783_story_unfreeze_service_not_frozen"; 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); let result = unfreeze(story_id);
assert!( assert!(
@@ -171,6 +179,7 @@ mod tests {
story_a, story_a,
"2_current", "2_current",
"---\nname: Regression Chat Path\n---\n", "---\nname: Regression Chat Path\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Regression Chat Path\n---\n"),
); );
// Story B simulates the MCP tool path. // Story B simulates the MCP tool path.
@@ -179,6 +188,7 @@ mod tests {
story_b, story_b,
"2_current", "2_current",
"---\nname: Regression MCP Path\n---\n", "---\nname: Regression MCP Path\n---\n",
crate::db::ItemMeta::from_yaml("---\nname: Regression MCP Path\n---\n"),
); );
// Both paths call service::work_item::freeze(). // Both paths call service::work_item::freeze().