From a078d3df7cdccd0cf43f7ca08991890877337825 Mon Sep 17 00:00:00 2001 From: dave Date: Wed, 13 May 2026 16:47:56 +0000 Subject: [PATCH] huskies: merge 985 --- .../src/agents/pool/pipeline/advance/tests.rs | 67 +++++++++++++++++++ server/src/chat/commands/show.rs | 3 +- server/src/crdt_state/write/tests.rs | 38 +++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/server/src/agents/pool/pipeline/advance/tests.rs b/server/src/agents/pool/pipeline/advance/tests.rs index 92a68f6f..efbe4b2a 100644 --- a/server/src/agents/pool/pipeline/advance/tests.rs +++ b/server/src/agents/pool/pipeline/advance/tests.rs @@ -248,3 +248,70 @@ stage = "qa" (bug 173: lozenges must update when agents are assigned during pipeline advance)" ); } + +#[tokio::test] +async fn pipeline_advance_coder_gates_pass_human_qa_holds_for_review() { + use std::fs; + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path(); + + // Seed story via CRDT so qa_mode can be resolved from the store. + crate::db::ensure_content_store(); + crate::db::write_item_with_content( + "9910_story_human_qa", + "2_current", + "---\nname: Human Qa Test\n---\ntest", + crate::db::ItemMeta::named("Human Qa Test"), + ); + + // Set qa_mode = Human on the CRDT item directly. + crate::crdt_state::set_qa_mode( + "9910_story_human_qa", + Some(crate::io::story_metadata::QaMode::Human), + ); + + // Write a minimal project.toml so the pool can load config. + fs::create_dir_all(root.join(".huskies")).unwrap(); + fs::write( + root.join(".huskies/project.toml"), + r#" +[[agent]] +name = "coder-1" +role = "Coder" +command = "echo" +args = ["noop"] +prompt = "test" +stage = "coder" +"#, + ) + .unwrap(); + + let pool = AgentPool::new_test(3001); + pool.run_pipeline_advance( + "9910_story_human_qa", + "coder-1", + CompletionReport { + summary: "done".to_string(), + gates_passed: true, + gate_output: String::new(), + needs_commit_recovery: false, + }, + Some(root.to_path_buf()), + None, + false, + None, + ) + .await; + + // With qa: human the story should be held for human review (ReviewHold stage). + let item = crate::crdt_state::read_item("9910_story_human_qa") + .expect("story should exist in CRDT after advance"); + assert!( + matches!( + item.stage(), + crate::pipeline_state::Stage::ReviewHold { .. } + ), + "qa: human should leave story in ReviewHold, got: {:?}", + item.stage() + ); +} diff --git a/server/src/chat/commands/show.rs b/server/src/chat/commands/show.rs index e2f3790d..67f4f52d 100644 --- a/server/src/chat/commands/show.rs +++ b/server/src/chat/commands/show.rs @@ -1,6 +1,7 @@ //! Handler for the `show` command. use super::CommandContext; +use crate::io::story_metadata::QaMode; /// Strip YAML front matter and return a summary of useful fields + the remaining body. #[allow(clippy::string_slice)] // indices from find("\n---") on ASCII delimiter; "---" and "\n---" are ASCII-only @@ -41,7 +42,7 @@ fn strip_front_matter(text: &str) -> (String, String) { } } else if line.starts_with("qa:") { let val = line.trim_start_matches("qa:").trim().trim_matches('"'); - if val == "human" { + if let Some(QaMode::Human) = QaMode::from_str(val) { parts.push("**QA:** human review required".to_string()); } } else if line.starts_with("merge_failure:") { diff --git a/server/src/crdt_state/write/tests.rs b/server/src/crdt_state/write/tests.rs index 768341e2..4bfe713b 100644 --- a/server/src/crdt_state/write/tests.rs +++ b/server/src/crdt_state/write/tests.rs @@ -472,6 +472,44 @@ fn set_qa_mode_returns_false_for_unknown_story() { assert!(!ok, "set_qa_mode should return false for unknown story_id"); } +#[test] +fn set_qa_mode_round_trip_all_variants() { + use crate::io::story_metadata::QaMode; + init_for_test(); + + write_item_str( + "985_story_qa_all_variants", + "1_backlog", + Some("Qa All Variants"), + None, + None, + None, + None, + None, + None, + ); + + for mode in [QaMode::Server, QaMode::Agent, QaMode::Human] { + let ok = set_qa_mode("985_story_qa_all_variants", Some(mode)); + assert!(ok, "set_qa_mode({mode:?}) should succeed for known item"); + let view = read_item("985_story_qa_all_variants").unwrap(); + assert_eq!( + view.qa_mode, + Some(mode), + "CRDT register should round-trip {mode:?}" + ); + } + + // Clear → register reverts to unset (project default applies). + let ok = set_qa_mode("985_story_qa_all_variants", None); + assert!(ok, "set_qa_mode(None) should succeed"); + let view = read_item("985_story_qa_all_variants").unwrap(); + assert_eq!( + view.qa_mode, None, + "clearing qa_mode should leave register unset" + ); +} + // ── set_retry_count / bump_retry_count tests ───────────────────────────── #[test]