huskies: merge 961

This commit is contained in:
dave
2026-05-13 11:22:57 +00:00
parent 78b1ecdc3c
commit 8b53e20ca9
38 changed files with 327 additions and 146 deletions
@@ -384,18 +384,18 @@ mod tests {
crate::db::ensure_content_store();
let dep_content = "---\nname: Dep\n---\n";
std::fs::write(done.join("1_story_dep.md"), dep_content).unwrap();
crate::db::write_content("1_story_dep", dep_content);
crate::db::write_content(crate::db::ContentKey::Story("1_story_dep"), dep_content);
// Story B depends on story 1.
let story_b_content = "---\nname: B\ndepends_on: [1]\n---\n";
std::fs::write(backlog.join("2_story_b.md"), story_b_content).unwrap();
crate::db::write_content("2_story_b", story_b_content);
crate::db::write_content(crate::db::ContentKey::Story("2_story_b"), story_b_content);
let pool = AgentPool::new_test(3001);
pool.auto_assign_available_work(root).await;
// The lifecycle function updates the content store (not the filesystem),
// so verify the move via the DB.
let content = crate::db::read_content("2_story_b")
let content = crate::db::read_content(crate::db::ContentKey::Story("2_story_b"))
.expect("story B should be in content store after promotion");
assert!(
content.contains("name: B"),
@@ -458,11 +458,14 @@ mod tests {
crate::db::ensure_content_store();
let dep_content = "---\nname: CRDT Spike\n---\n";
std::fs::write(archived.join("490_spike_crdt.md"), dep_content).unwrap();
crate::db::write_content("490_spike_crdt", dep_content);
crate::db::write_content(crate::db::ContentKey::Story("490_spike_crdt"), dep_content);
// Story 478 depends on 490 (the archived spike).
let story_content = "---\nname: Dependent\ndepends_on: [490]\n---\n";
std::fs::write(backlog.join("478_story_dependent.md"), story_content).unwrap();
crate::db::write_content("478_story_dependent", story_content);
crate::db::write_content(
crate::db::ContentKey::Story("478_story_dependent"),
story_content,
);
let pool = AgentPool::new_test(3001);
pool.auto_assign_available_work(root).await;
@@ -470,7 +473,7 @@ mod tests {
// Story 478 must be promoted even though dep 490 is only in 6_archived
// (not in 5_done), because archived = satisfied. The lifecycle function
// updates the content store, so verify via the DB.
let content = crate::db::read_content("478_story_dependent")
let content = crate::db::read_content(crate::db::ContentKey::Story("478_story_dependent"))
.expect("story 478 should be in content store after promotion");
assert!(
content.contains("name: Dependent"),
@@ -531,7 +534,7 @@ mod tests {
// After master c228ae16, has_content_conflict_failure reads from
// {story_id}:gate_output (not the story description), so seed it there.
crate::db::write_content(
"9860_story_conflict:gate_output",
crate::db::ContentKey::GateOutput("9860_story_conflict"),
"CONFLICT (content): server/src/lib.rs",
);
@@ -690,11 +693,14 @@ mod tests {
// After master c228ae16, has_content_conflict_failure reads from
// {story_id}:gate_output (not the story description), so seed it there.
crate::db::write_content(
"920_story_transient:gate_output",
crate::db::ContentKey::GateOutput("920_story_transient"),
"CONFLICT (content): foo.rs",
);
// Simulate two previous transient exits (below cap of 3) recorded in DB.
crate::db::write_content("920_story_transient:mergemaster_spawn_count", "2");
crate::db::write_content(
crate::db::ContentKey::MergeMasterSpawnCount("920_story_transient"),
"2",
);
// mergemaster_attempted must still be false (transient exits don't set it).
let pool = AgentPool::new_test(3001);
@@ -74,9 +74,8 @@ pub(super) fn has_content_conflict_failure(
}
// The projection does not carry the reason string; read the gate output
// (where the merge runner persists the failure message) and scan for
// conflict markers. NB: the key is `{story_id}:gate_output`, not `{story_id}`
// — the latter is the story's *description* text and would never match.
crate::db::read_content(&format!("{story_id}:gate_output"))
// conflict markers.
crate::db::read_content(crate::db::ContentKey::GateOutput(story_id))
.map(|content| {
content.contains("Merge conflict") || content.contains("CONFLICT (content):")
})
@@ -242,7 +242,7 @@ max_turns = 10
// watchdog's retry/block path has something to read+update.
let story_id = "42_story_runaway";
let initial = "---\nname: Runaway Story\n---\n# Runaway Story\n";
crate::db::write_content(story_id, initial);
crate::db::write_content(crate::db::ContentKey::Story(story_id), initial);
crate::crdt_state::write_item_str(
story_id,
"2_current",
@@ -369,7 +369,10 @@ max_turns = 10
);
let story_id = "story_e_per_session";
crate::db::write_content(story_id, "---\nname: Per-Session Test\n---\n");
crate::db::write_content(
crate::db::ContentKey::Story(story_id),
"---\nname: Per-Session Test\n---\n",
);
crate::crdt_state::write_item_str(
story_id,
"2_current",
@@ -448,7 +451,7 @@ max_turns = 10
let story_id = "88_story_retry_watchdog";
let initial = "---\nname: Retry Test\n---\n";
crate::crdt_state::init_for_test();
crate::db::write_content(story_id, initial);
crate::db::write_content(crate::db::ContentKey::Story(story_id), initial);
crate::crdt_state::write_item_str(
story_id,
"2_current",
+23 -10
View File
@@ -78,10 +78,15 @@ impl AgentPool {
// The coder exited with uncommitted content but no commits.
// Check if this is already a second recovery attempt (the
// first recovery respawn also produced no commits).
let recovery_key = format!("{story_id}:commit_recovery_pending");
if crate::db::read_content(&recovery_key).is_some() {
if crate::db::read_content(crate::db::ContentKey::CommitRecoveryPending(
story_id,
))
.is_some()
{
// Second attempt still produced no commits → block.
crate::db::delete_content(&recovery_key);
crate::db::delete_content(crate::db::ContentKey::CommitRecoveryPending(
story_id,
));
slog!(
"[pipeline] Coder '{agent_name}' (commit-recovery respawn) \
still produced no commits for '{story_id}'. Blocking story."
@@ -99,7 +104,10 @@ impl AgentPool {
} else {
// First occurrence: issue a commit-only recovery respawn.
// This does NOT consume a retry_count slot.
crate::db::write_content(&recovery_key, "1");
crate::db::write_content(
crate::db::ContentKey::CommitRecoveryPending(story_id),
"1",
);
slog!(
"[pipeline] Coder '{agent_name}' exited with uncommitted work \
for '{story_id}'. Issuing commit-only recovery respawn."
@@ -125,7 +133,9 @@ impl AgentPool {
}
} else if completion.gates_passed {
// Clear any stale recovery key when the coder succeeds normally.
crate::db::delete_content(&format!("{story_id}:commit_recovery_pending"));
crate::db::delete_content(crate::db::ContentKey::CommitRecoveryPending(
story_id,
));
// Determine effective QA mode for this story.
let qa_mode = {
let item_type = crate::agents::lifecycle::item_type_from_id(story_id);
@@ -183,7 +193,9 @@ impl AgentPool {
} else {
// Clear any stale recovery key when gates fail normally (agent committed
// but the build is broken — treat as a standard retry, not a recovery).
crate::db::delete_content(&format!("{story_id}:commit_recovery_pending"));
crate::db::delete_content(crate::db::ContentKey::CommitRecoveryPending(
story_id,
));
// Bug 645 / 668: Before retry/block, check if the agent left committed
// work AND the agent had a passing run_tests result captured during its
// session. An agent may crash mid-output (e.g. Claude Code CLI PTY write
@@ -195,8 +207,9 @@ impl AgentPool {
// whenever script/test exits 0 inside a story worktree. Consume the
// evidence here so it does not persist to the next agent session.
let has_test_evidence =
crate::db::read_content(&format!("{story_id}:run_tests_ok")).is_some();
crate::db::delete_content(&format!("{story_id}:run_tests_ok"));
crate::db::read_content(crate::db::ContentKey::RunTestsOk(story_id))
.is_some();
crate::db::delete_content(crate::db::ContentKey::RunTestsOk(story_id));
let work_survived = has_test_evidence
&& worktree_path.as_ref().is_some_and(|wt_path| {
crate::agents::gates::worktree_has_committed_work(wt_path)
@@ -258,7 +271,7 @@ impl AgentPool {
// Persist gate_output so the retry spawn can inject it into
// --append-system-prompt (story 881).
crate::db::write_content(
&format!("{story_id}:gate_output"),
crate::db::ContentKey::GateOutput(story_id),
&completion.gate_output,
);
// Increment retry count and check if blocked.
@@ -384,7 +397,7 @@ impl AgentPool {
// Persist gate_output so the retry spawn can inject it into
// --append-system-prompt (story 881).
crate::db::write_content(
&format!("{story_id}:gate_output"),
crate::db::ContentKey::GateOutput(story_id),
&completion.gate_output,
);
if let Some(reason) = should_block_story(story_id, config.max_retries, "qa") {
@@ -17,7 +17,7 @@ async fn pipeline_advance_coder_gates_pass_server_qa_moves_to_merge() {
fs::create_dir_all(&current).unwrap();
fs::write(current.join("9908_story_server_qa.md"), "test").unwrap();
crate::db::ensure_content_store();
crate::db::write_content("9908_story_server_qa", "test");
crate::db::write_content(crate::db::ContentKey::Story("9908_story_server_qa"), "test");
let pool = AgentPool::new_test(3001);
pool.run_pipeline_advance(
@@ -39,7 +39,7 @@ async fn pipeline_advance_coder_gates_pass_server_qa_moves_to_merge() {
// With default qa: server, story skips QA and goes straight to 4_merge/
// Lifecycle moves now update the content store, not the filesystem.
assert!(
crate::db::read_content("9908_story_server_qa").is_some(),
crate::db::read_content(crate::db::ContentKey::Story("9908_story_server_qa")).is_some(),
"story should still exist in content store after move to merge"
);
}
@@ -61,7 +61,7 @@ async fn pipeline_advance_coder_gates_pass_agent_qa_moves_to_qa() {
.unwrap();
crate::db::ensure_content_store();
crate::db::write_content(
"9909_story_agent_qa",
crate::db::ContentKey::Story("9909_story_agent_qa"),
"---\nname: Test\nqa: agent\n---\ntest",
);
@@ -85,7 +85,7 @@ async fn pipeline_advance_coder_gates_pass_agent_qa_moves_to_qa() {
// With qa: agent, story should move to 3_qa/
// Lifecycle moves now update the content store, not the filesystem.
assert!(
crate::db::read_content("9909_story_agent_qa").is_some(),
crate::db::read_content(crate::db::ContentKey::Story("9909_story_agent_qa")).is_some(),
"story should still exist in content store after move to qa"
);
}
@@ -106,7 +106,10 @@ async fn pipeline_advance_qa_gates_pass_moves_story_to_merge() {
)
.unwrap();
crate::db::ensure_content_store();
crate::db::write_content("51_story_test", "---\nname: Test\nqa: server\n---\ntest");
crate::db::write_content(
crate::db::ContentKey::Story("51_story_test"),
"---\nname: Test\nqa: server\n---\ntest",
);
let pool = AgentPool::new_test(3001);
pool.run_pipeline_advance(
@@ -128,7 +131,7 @@ async fn pipeline_advance_qa_gates_pass_moves_story_to_merge() {
// Story should have moved to 4_merge/
// Lifecycle moves now update the content store, not the filesystem.
assert!(
crate::db::read_content("51_story_test").is_some(),
crate::db::read_content(crate::db::ContentKey::Story("51_story_test")).is_some(),
"story should still exist in content store after move to merge"
);
}
@@ -76,7 +76,7 @@ async fn mergemaster_blocks_and_sends_story_blocked_when_no_commits_ahead() {
// Story should still exist in the content store after moving to merge.
assert!(
crate::db::read_content("9919_story_no_commits").is_some(),
crate::db::read_content(crate::db::ContentKey::Story("9919_story_no_commits")).is_some(),
"story should remain in content store — not removed"
);
@@ -249,7 +249,7 @@ async fn stale_mergemaster_advance_for_done_story_is_noop() {
// Seed the story in 5_done via the DB, which also writes to the CRDT.
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_content(crate::db::ContentKey::Story(story_id), content);
crate::db::write_item_with_content(
story_id,
"5_done",
@@ -387,7 +387,10 @@ async fn work_survived_advances_to_qa_instead_of_blocking() {
// Set up the story in the content store.
crate::db::ensure_content_store();
crate::db::write_content("9945_story_survived", "---\nname: Survived Test\n---\n");
crate::db::write_content(
crate::db::ContentKey::Story("9945_story_survived"),
"---\nname: Survived Test\n---\n",
);
crate::db::write_item_with_content(
"9945_story_survived",
"2_current",
@@ -397,7 +400,10 @@ async fn work_survived_advances_to_qa_instead_of_blocking() {
// Simulate a passing run_tests call during the agent's session (bug 668):
// the agent ran script/test, it passed, and the server captured the evidence.
crate::db::write_content("9945_story_survived:run_tests_ok", "1");
crate::db::write_content(
crate::db::ContentKey::RunTestsOk("9945_story_survived"),
"1",
);
let pool = AgentPool::new_test(3001);
@@ -421,7 +427,7 @@ async fn work_survived_advances_to_qa_instead_of_blocking() {
// Story should have advanced — content store should reflect the move.
// The work-survived check should have moved it to QA (or merge for
// server qa mode), NOT incremented retry_count.
let content = crate::db::read_content("9945_story_survived")
let content = crate::db::read_content(crate::db::ContentKey::Story("9945_story_survived"))
.expect("story should exist in content store");
assert!(
!content.contains("blocked"),
@@ -482,7 +488,10 @@ async fn no_committed_work_still_retries_and_blocks() {
// Set up the story with max_retries=1 so it blocks immediately.
crate::crdt_state::init_for_test();
crate::db::ensure_content_store();
crate::db::write_content("9946_story_nowork", "---\nname: No Work Test\n---\n");
crate::db::write_content(
crate::db::ContentKey::Story("9946_story_nowork"),
"---\nname: No Work Test\n---\n",
);
crate::db::write_item_with_content(
"9946_story_nowork",
"2_current",
@@ -609,7 +618,7 @@ async fn gates_failed_no_test_evidence_does_not_advance() {
// Set up the story with max_retries=1 so we can observe the retry/block.
crate::db::ensure_content_store();
crate::db::write_content(
"9947_story_no_evidence",
crate::db::ContentKey::Story("9947_story_no_evidence"),
"---\nname: No Evidence Test\n---\n",
);
crate::db::write_item_with_content(
@@ -620,7 +629,7 @@ async fn gates_failed_no_test_evidence_does_not_advance() {
);
// Explicitly ensure no test evidence exists for this story.
crate::db::delete_content("9947_story_no_evidence:run_tests_ok");
crate::db::delete_content(crate::db::ContentKey::RunTestsOk("9947_story_no_evidence"));
fs::create_dir_all(root.join(".huskies")).unwrap();
fs::write(
@@ -740,7 +749,7 @@ async fn gates_failed_with_test_evidence_and_committed_work_advances() {
crate::db::ensure_content_store();
crate::db::write_content(
"9948_story_with_evidence",
crate::db::ContentKey::Story("9948_story_with_evidence"),
"---\nname: With Evidence Test\n---\n",
);
crate::db::write_item_with_content(
@@ -752,7 +761,10 @@ async fn gates_failed_with_test_evidence_and_committed_work_advances() {
// Write the run_tests evidence — simulates the agent having called run_tests
// MCP and getting a passing result before it crashed.
crate::db::write_content("9948_story_with_evidence:run_tests_ok", "1");
crate::db::write_content(
crate::db::ContentKey::RunTestsOk("9948_story_with_evidence"),
"1",
);
let pool = AgentPool::new_test(3001);
@@ -774,7 +786,7 @@ async fn gates_failed_with_test_evidence_and_committed_work_advances() {
.await;
// Story should advance (not blocked, no retry_count).
let content = crate::db::read_content("9948_story_with_evidence")
let content = crate::db::read_content(crate::db::ContentKey::Story("9948_story_with_evidence"))
.expect("story must exist in content store");
assert!(
!content.contains("blocked"),
@@ -786,7 +798,10 @@ async fn gates_failed_with_test_evidence_and_committed_work_advances() {
);
// Evidence must be consumed (cleared) after use.
assert!(
crate::db::read_content("9948_story_with_evidence:run_tests_ok").is_none(),
crate::db::read_content(crate::db::ContentKey::RunTestsOk(
"9948_story_with_evidence"
))
.is_none(),
"run_tests evidence must be cleared after pipeline advance consumes it"
);
}
@@ -919,7 +934,9 @@ stage = "coder"
crate::db::ItemMeta::named("Recovery Test"),
);
// Ensure no stale recovery key exists.
crate::db::delete_content("9954_story_recovery:commit_recovery_pending");
crate::db::delete_content(crate::db::ContentKey::CommitRecoveryPending(
"9954_story_recovery",
));
let pool = AgentPool::new_test(3001);
@@ -966,7 +983,10 @@ stage = "coder"
// The recovery key must be set so a second failure triggers a block.
assert!(
crate::db::read_content("9954_story_recovery:commit_recovery_pending").is_some(),
crate::db::read_content(crate::db::ContentKey::CommitRecoveryPending(
"9954_story_recovery"
))
.is_some(),
"commit_recovery_pending key must be set after issuing recovery respawn"
);
}
@@ -1008,7 +1028,10 @@ stage = "coder"
// Simulate the recovery key already being set (first recovery respawn was
// issued previously).
crate::db::write_content("9955_story_recovery2:commit_recovery_pending", "1");
crate::db::write_content(
crate::db::ContentKey::CommitRecoveryPending("9955_story_recovery2"),
"1",
);
let pool = AgentPool::new_test(3001);
let mut rx = pool.watcher_tx.subscribe();
@@ -1052,7 +1075,10 @@ stage = "coder"
// The recovery key must be cleared after blocking.
assert!(
crate::db::read_content("9955_story_recovery2:commit_recovery_pending").is_none(),
crate::db::read_content(crate::db::ContentKey::CommitRecoveryPending(
"9955_story_recovery2"
))
.is_none(),
"commit_recovery_pending key must be cleared after blocking the story"
);
@@ -1128,7 +1154,10 @@ async fn coder_completion_with_test_evidence_and_zero_commits_does_not_advance()
// Simulate the agent having called run_tests with a passing result (bug-645
// evidence) — but the feature branch still has zero commits ahead of master.
crate::db::write_content("9953_story_zero_commits:run_tests_ok", "1");
crate::db::write_content(
crate::db::ContentKey::RunTestsOk("9953_story_zero_commits"),
"1",
);
// Write a project.toml with max_retries=1 so the story blocks immediately,
// giving us a clean assertion target (StoryBlocked event).
@@ -1193,7 +1222,8 @@ async fn coder_completion_with_test_evidence_and_zero_commits_does_not_advance()
// Test evidence must have been consumed (cleared) by the advance handler.
assert!(
crate::db::read_content("9953_story_zero_commits:run_tests_ok").is_none(),
crate::db::read_content(crate::db::ContentKey::RunTestsOk("9953_story_zero_commits"))
.is_none(),
"run_tests evidence must be cleared after pipeline advance consumes it"
);
}
@@ -703,7 +703,7 @@ async fn server_side_merge_happy_path_advances_to_done() {
if report.success {
// story_archived may or may not be true depending on gate env,
// but merge_failure must NOT be in the content store.
let content = crate::db::read_content("757a_happy");
let content = crate::db::read_content(crate::db::ContentKey::Story("757a_happy"));
if let Some(c) = content {
assert!(
!c.contains("merge_failure"),
@@ -713,7 +713,7 @@ async fn server_side_merge_happy_path_advances_to_done() {
} else {
// Gate failure (no script/test) is acceptable in test env —
// but merge_failure should be written.
let content = crate::db::read_content("757a_happy");
let content = crate::db::read_content(crate::db::ContentKey::Story("757a_happy"));
if let Some(c) = content {
// merge_failure should be written for gate failures
assert!(
+35 -20
View File
@@ -72,7 +72,8 @@ pub(super) fn maybe_inject_gate_failure(args: &mut Vec<String>, story_id: &str)
.map(|item| item.retry_count())
.unwrap_or(0);
if retry_count > 0
&& let Some(gate_output) = crate::db::read_content(&format!("{story_id}:gate_output"))
&& let Some(gate_output) =
crate::db::read_content(crate::db::ContentKey::GateOutput(story_id))
{
inject_gate_failure_section(args, &gate_output);
}
@@ -230,7 +231,10 @@ pub(super) async fn run_agent_spawn(
// Story 933: epic linkage is now a typed CRDT register on PipelineItemCrdt.
if let Some(view) = crate::crdt_state::read_item(&sid)
&& let Some(epic_id) = view.epic()
&& let Some(epic_content) = crate::db::read_content(&epic_id.to_string())
&& let Some(epic_content) = {
let epic_id_str = epic_id.to_string();
crate::db::read_content(crate::db::ContentKey::Story(&epic_id_str))
}
{
let block = format!(
"# Epic Context\n\nThis work item belongs to epic `{epic_id}`.\
@@ -434,12 +438,14 @@ pub(super) async fn run_agent_spawn(
// infinite loops; after the cap, block the story with a clear reason.
if result.aborted_signal && stage != PipelineStage::Mergemaster {
const ABORT_RESPAWN_CAP: u32 = 5;
let db_key = format!("{sid}:abort_respawn_count");
let count = crate::db::read_content(&db_key)
let count = crate::db::read_content(crate::db::ContentKey::AbortRespawnCount(&sid))
.and_then(|s| s.trim().parse::<u32>().ok())
.unwrap_or(0)
+ 1;
crate::db::write_content(&db_key, &count.to_string());
crate::db::write_content(
crate::db::ContentKey::AbortRespawnCount(&sid),
&count.to_string(),
);
// Remove the agent entry from the pool and emit Done so that
// any caller blocked on wait_for_agent is unblocked.
@@ -523,7 +529,7 @@ pub(super) async fn run_agent_spawn(
// Reset the abort-respawn counter on any non-aborted exit so that
// a single successful run clears the consecutive-crash history.
crate::db::delete_content(&format!("{sid}:abort_respawn_count"));
crate::db::delete_content(crate::db::ContentKey::AbortRespawnCount(&sid));
if stage == PipelineStage::Mergemaster {
let (tx_done, done_session_id, merge_failure_reported) = {
@@ -555,7 +561,6 @@ pub(super) async fn run_agent_spawn(
// Only mark mergemaster_attempted on a genuine give-up so that
// transient exits can be re-spawned up to the cap (story 920).
const MERGEMASTER_RESPAWN_CAP: u32 = 3;
let spawn_count_key = format!("{sid}:mergemaster_spawn_count");
let is_genuine = if merge_failure_reported {
slog!(
"[agents] Mergemaster '{aname}' for '{sid}' gave up genuinely \
@@ -563,11 +568,15 @@ pub(super) async fn run_agent_spawn(
);
true
} else {
let count = crate::db::read_content(&spawn_count_key)
.and_then(|s| s.trim().parse::<u32>().ok())
.unwrap_or(0)
+ 1;
crate::db::write_content(&spawn_count_key, &count.to_string());
let count =
crate::db::read_content(crate::db::ContentKey::MergeMasterSpawnCount(&sid))
.and_then(|s| s.trim().parse::<u32>().ok())
.unwrap_or(0)
+ 1;
crate::db::write_content(
crate::db::ContentKey::MergeMasterSpawnCount(&sid),
&count.to_string(),
);
if count >= MERGEMASTER_RESPAWN_CAP {
slog!(
"[agents] Mergemaster '{aname}' for '{sid}' exhausted \
@@ -667,7 +676,7 @@ mod tests {
let gate_output =
"error[E0308]: mismatched types\n --> src/lib.rs:5:10\n = expected i32, found &str";
crate::db::write_content(&format!("{story_id}:gate_output"), gate_output);
crate::db::write_content(crate::db::ContentKey::GateOutput(story_id), gate_output);
let mut args: Vec<String> = vec!["--verbose".to_string()];
maybe_inject_gate_failure(&mut args, story_id);
@@ -703,7 +712,10 @@ mod tests {
);
// retry_count is 0 (default — never bumped).
crate::db::write_content(&format!("{story_id}:gate_output"), "some previous output");
crate::db::write_content(
crate::db::ContentKey::GateOutput(story_id),
"some previous output",
);
let mut args: Vec<String> = vec!["--verbose".to_string()];
maybe_inject_gate_failure(&mut args, story_id);
@@ -767,17 +779,19 @@ mod tests {
crate::db::ItemMeta::named("Test"),
);
let db_key = format!("{story_id}:abort_respawn_count");
const CAP: u32 = 5;
// Simulate CAP consecutive abort-before-session exits.
for expected_count in 1u32..=CAP {
// This is exactly the counter logic in run_agent_spawn's abort path.
let count = crate::db::read_content(&db_key)
let count = crate::db::read_content(crate::db::ContentKey::AbortRespawnCount(story_id))
.and_then(|s| s.trim().parse::<u32>().ok())
.unwrap_or(0)
+ 1;
crate::db::write_content(&db_key, &count.to_string());
crate::db::write_content(
crate::db::ContentKey::AbortRespawnCount(story_id),
&count.to_string(),
);
assert_eq!(
count, expected_count,
"abort counter must increment by 1 each time"
@@ -795,9 +809,10 @@ mod tests {
}
// After CAP cycles the counter equals the cap — the story would be blocked.
let final_count: u32 = crate::db::read_content(&db_key)
.and_then(|s| s.trim().parse().ok())
.unwrap_or(0);
let final_count: u32 =
crate::db::read_content(crate::db::ContentKey::AbortRespawnCount(story_id))
.and_then(|s| s.trim().parse().ok())
.unwrap_or(0);
assert_eq!(
final_count, CAP,
"counter must equal {CAP} after {CAP} abort cycles"
@@ -102,7 +102,7 @@ stage = "coder"
let story_content = "---\nname: Story 3\n---\n";
std::fs::write(backlog.join("story-3.md"), story_content).unwrap();
crate::db::ensure_content_store();
crate::db::write_content("story-3", story_content);
crate::db::write_content(crate::db::ContentKey::Story("story-3"), story_content);
let pool = AgentPool::new_test(3001);
pool.inject_test_agent("story-1", "coder-1", AgentStatus::Running);
@@ -124,7 +124,7 @@ stage = "coder"
// The lifecycle function updates the content store (not the filesystem),
// so verify the move via the DB.
let content = crate::db::read_content("story-3")
let content = crate::db::read_content(crate::db::ContentKey::Story("story-3"))
.expect("story-3 should be in content store after move to current");
assert!(
content.contains("name: Story 3"),
@@ -280,7 +280,10 @@ stage = "coder"
// left a stale entry for "368_story_test" in the global CRDT.
std::fs::write(current.join("368_story_test.md"), story_content).unwrap();
crate::db::ensure_content_store();
crate::db::write_content("368_story_test", story_content);
crate::db::write_content(
crate::db::ContentKey::Story("368_story_test"),
story_content,
);
// Story 929: agent pin comes from the CRDT register, not YAML. Seed it.
crate::crdt_state::init_for_test();
crate::crdt_state::write_item_str(
+1 -1
View File
@@ -170,7 +170,7 @@ mod tests {
// The lifecycle function updates the content store (not the filesystem),
// so verify the move via the DB.
let content = crate::db::read_content("60_story_cleanup")
let content = crate::db::read_content(crate::db::ContentKey::Story("60_story_cleanup"))
.expect("60_story_cleanup should be in content store after move to done");
assert_eq!(content, "test", "content should be preserved after move");
}