238 lines
6.9 KiB
Rust
238 lines
6.9 KiB
Rust
|
|
//! Basic pipeline advance tests.
|
||
|
|
use super::super::super::AgentPool;
|
||
|
|
use crate::agents::CompletionReport;
|
||
|
|
use crate::io::watcher::WatcherEvent;
|
||
|
|
|
||
|
|
// ── pipeline advance tests ────────────────────────────────────────────────
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn pipeline_advance_coder_gates_pass_server_qa_moves_to_merge() {
|
||
|
|
use std::fs;
|
||
|
|
let tmp = tempfile::tempdir().unwrap();
|
||
|
|
let root = tmp.path();
|
||
|
|
|
||
|
|
// Set up story in 2_current/ (no qa frontmatter → uses project default "server").
|
||
|
|
// Use a unique high-numbered ID to avoid collision with the agent_qa test.
|
||
|
|
let current = root.join(".huskies/work/2_current");
|
||
|
|
fs::create_dir_all(¤t).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");
|
||
|
|
|
||
|
|
let pool = AgentPool::new_test(3001);
|
||
|
|
pool.run_pipeline_advance(
|
||
|
|
"9908_story_server_qa",
|
||
|
|
"coder-1",
|
||
|
|
CompletionReport {
|
||
|
|
summary: "done".to_string(),
|
||
|
|
gates_passed: true,
|
||
|
|
gate_output: String::new(),
|
||
|
|
},
|
||
|
|
Some(root.to_path_buf()),
|
||
|
|
None,
|
||
|
|
false,
|
||
|
|
None,
|
||
|
|
)
|
||
|
|
.await;
|
||
|
|
|
||
|
|
// 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(),
|
||
|
|
"story should still exist in content store after move to merge"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn pipeline_advance_coder_gates_pass_agent_qa_moves_to_qa() {
|
||
|
|
use std::fs;
|
||
|
|
let tmp = tempfile::tempdir().unwrap();
|
||
|
|
let root = tmp.path();
|
||
|
|
|
||
|
|
// Set up story in 2_current/ with qa: agent frontmatter.
|
||
|
|
// Use a unique high-numbered ID to avoid collision with the server_qa test.
|
||
|
|
let current = root.join(".huskies/work/2_current");
|
||
|
|
fs::create_dir_all(¤t).unwrap();
|
||
|
|
fs::write(
|
||
|
|
current.join("9909_story_agent_qa.md"),
|
||
|
|
"---\nname: Test\nqa: agent\n---\ntest",
|
||
|
|
)
|
||
|
|
.unwrap();
|
||
|
|
crate::db::ensure_content_store();
|
||
|
|
crate::db::write_content(
|
||
|
|
"9909_story_agent_qa",
|
||
|
|
"---\nname: Test\nqa: agent\n---\ntest",
|
||
|
|
);
|
||
|
|
|
||
|
|
let pool = AgentPool::new_test(3001);
|
||
|
|
pool.run_pipeline_advance(
|
||
|
|
"9909_story_agent_qa",
|
||
|
|
"coder-1",
|
||
|
|
CompletionReport {
|
||
|
|
summary: "done".to_string(),
|
||
|
|
gates_passed: true,
|
||
|
|
gate_output: String::new(),
|
||
|
|
},
|
||
|
|
Some(root.to_path_buf()),
|
||
|
|
None,
|
||
|
|
false,
|
||
|
|
None,
|
||
|
|
)
|
||
|
|
.await;
|
||
|
|
|
||
|
|
// 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(),
|
||
|
|
"story should still exist in content store after move to qa"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn pipeline_advance_qa_gates_pass_moves_story_to_merge() {
|
||
|
|
use std::fs;
|
||
|
|
let tmp = tempfile::tempdir().unwrap();
|
||
|
|
let root = tmp.path();
|
||
|
|
|
||
|
|
// Set up story in 3_qa/
|
||
|
|
let qa_dir = root.join(".huskies/work/3_qa");
|
||
|
|
fs::create_dir_all(&qa_dir).unwrap();
|
||
|
|
// qa: server so the story skips human review and goes straight to merge.
|
||
|
|
fs::write(
|
||
|
|
qa_dir.join("51_story_test.md"),
|
||
|
|
"---\nname: Test\nqa: server\n---\ntest",
|
||
|
|
)
|
||
|
|
.unwrap();
|
||
|
|
crate::db::ensure_content_store();
|
||
|
|
crate::db::write_content("51_story_test", "---\nname: Test\nqa: server\n---\ntest");
|
||
|
|
|
||
|
|
let pool = AgentPool::new_test(3001);
|
||
|
|
pool.run_pipeline_advance(
|
||
|
|
"51_story_test",
|
||
|
|
"qa",
|
||
|
|
CompletionReport {
|
||
|
|
summary: "QA done".to_string(),
|
||
|
|
gates_passed: true,
|
||
|
|
gate_output: String::new(),
|
||
|
|
},
|
||
|
|
Some(root.to_path_buf()),
|
||
|
|
None,
|
||
|
|
false,
|
||
|
|
None,
|
||
|
|
)
|
||
|
|
.await;
|
||
|
|
|
||
|
|
// 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(),
|
||
|
|
"story should still exist in content store after move to merge"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn pipeline_advance_supervisor_does_not_advance() {
|
||
|
|
use std::fs;
|
||
|
|
let tmp = tempfile::tempdir().unwrap();
|
||
|
|
let root = tmp.path();
|
||
|
|
|
||
|
|
let current = root.join(".huskies/work/2_current");
|
||
|
|
fs::create_dir_all(¤t).unwrap();
|
||
|
|
fs::write(current.join("52_story_test.md"), "test").unwrap();
|
||
|
|
|
||
|
|
let pool = AgentPool::new_test(3001);
|
||
|
|
pool.run_pipeline_advance(
|
||
|
|
"52_story_test",
|
||
|
|
"supervisor",
|
||
|
|
CompletionReport {
|
||
|
|
summary: "supervised".to_string(),
|
||
|
|
gates_passed: true,
|
||
|
|
gate_output: String::new(),
|
||
|
|
},
|
||
|
|
Some(root.to_path_buf()),
|
||
|
|
None,
|
||
|
|
false,
|
||
|
|
None,
|
||
|
|
)
|
||
|
|
.await;
|
||
|
|
|
||
|
|
// Story should NOT have moved (supervisors don't advance pipeline)
|
||
|
|
assert!(
|
||
|
|
current.join("52_story_test.md").exists(),
|
||
|
|
"story should still be in 2_current/ for supervisor"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn pipeline_advance_sends_agent_state_changed_to_watcher_tx() {
|
||
|
|
use std::fs;
|
||
|
|
|
||
|
|
let tmp = tempfile::tempdir().unwrap();
|
||
|
|
let root = tmp.path();
|
||
|
|
|
||
|
|
// Seed story via CRDT (the only source of truth).
|
||
|
|
crate::db::ensure_content_store();
|
||
|
|
crate::db::write_item_with_content("173_story_test", "2_current", "---\nname: test\n---\n");
|
||
|
|
|
||
|
|
// Write a project.toml with a qa agent so start_agent can resolve it.
|
||
|
|
fs::create_dir_all(root.join(".huskies")).unwrap();
|
||
|
|
fs::write(
|
||
|
|
root.join(".huskies/project.toml"),
|
||
|
|
r#"
|
||
|
|
default_qa = "agent"
|
||
|
|
|
||
|
|
[[agent]]
|
||
|
|
name = "coder-1"
|
||
|
|
role = "Coder"
|
||
|
|
command = "echo"
|
||
|
|
args = ["noop"]
|
||
|
|
prompt = "test"
|
||
|
|
stage = "coder"
|
||
|
|
|
||
|
|
[[agent]]
|
||
|
|
name = "qa"
|
||
|
|
role = "QA"
|
||
|
|
command = "echo"
|
||
|
|
args = ["noop"]
|
||
|
|
prompt = "test"
|
||
|
|
stage = "qa"
|
||
|
|
"#,
|
||
|
|
)
|
||
|
|
.unwrap();
|
||
|
|
|
||
|
|
let pool = AgentPool::new_test(3001);
|
||
|
|
// Subscribe to the watcher channel BEFORE the pipeline advance.
|
||
|
|
let mut rx = pool.watcher_tx.subscribe();
|
||
|
|
|
||
|
|
pool.run_pipeline_advance(
|
||
|
|
"173_story_test",
|
||
|
|
"coder-1",
|
||
|
|
CompletionReport {
|
||
|
|
summary: "done".to_string(),
|
||
|
|
gates_passed: true,
|
||
|
|
gate_output: String::new(),
|
||
|
|
},
|
||
|
|
Some(root.to_path_buf()),
|
||
|
|
None,
|
||
|
|
false,
|
||
|
|
None,
|
||
|
|
)
|
||
|
|
.await;
|
||
|
|
|
||
|
|
// The pipeline advance should have sent AgentStateChanged events via
|
||
|
|
// the pool's watcher_tx (not a dummy channel). Collect all events.
|
||
|
|
let mut got_agent_state_changed = false;
|
||
|
|
while let Ok(evt) = rx.try_recv() {
|
||
|
|
if matches!(evt, WatcherEvent::AgentStateChanged) {
|
||
|
|
got_agent_state_changed = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
assert!(
|
||
|
|
got_agent_state_changed,
|
||
|
|
"pipeline advance should send AgentStateChanged through the real watcher_tx \
|
||
|
|
(bug 173: lozenges must update when agents are assigned during pipeline advance)"
|
||
|
|
);
|
||
|
|
}
|