huskies: merge 620_refactor_enforce_test_fixture_discipline_in_service_modules

This commit is contained in:
dave
2026-04-24 16:02:37 +00:00
parent 65c896f07f
commit 2dc2513fac
3 changed files with 96 additions and 35 deletions
+36
View File
@@ -86,6 +86,30 @@ HTTP handlers map service errors to **specific** HTTP status codes:
## 4. Test Pattern ## 4. Test Pattern
### Chosen default pattern: fixture helpers in `io::test_helpers`
All filesystem setup for tests lives in a `#[cfg(test)] pub mod test_helpers`
block inside `io.rs`. Test blocks in `mod.rs` and topic files call these
helpers instead of importing `std::fs` directly.
**Grep-enforceable check for test code:** The following must NOT appear inside
`#[cfg(test)]` blocks in any `service/<domain>/` file **other than `io.rs`**:
- `std::fs::` (any item)
- `tokio::fs`
- `std::process::` (any item)
- `Command::new`
Run to verify:
```sh
grep -rn --include='*.rs' \
'std::fs::\|tokio::fs\|std::process::\|Command::new' \
server/src/service/ | grep -v '/io\.rs'
```
This must return zero matches (including lines inside `#[cfg(test)]` blocks).
### Pure topic files (`<topic>.rs`) ### Pure topic files (`<topic>.rs`)
```rust ```rust
@@ -104,6 +128,17 @@ mod tests {
### `io.rs` ### `io.rs`
```rust ```rust
/// Fixture helpers — the ONLY place allowed to call std::fs in tests.
#[cfg(test)]
pub mod test_helpers {
use tempfile::TempDir;
pub fn make_work_dirs(tmp: &TempDir) { ... }
pub fn make_stage_dirs(tmp: &TempDir) { ... }
pub fn make_project_toml(tmp: &TempDir, content: &str) { ... }
pub fn write_story_file(tmp: &TempDir, relative_path: &str, content: &str) { ... }
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -122,6 +157,7 @@ mod tests {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use io::test_helpers::*; // ← fixture helpers; never import std::fs here
// Integration tests compose io + pure layers end-to-end. // Integration tests compose io + pure layers end-to-end.
// May use tempdirs. Keep the count small — they are integration-level. // May use tempdirs. Keep the count small — they are integration-level.
+50
View File
@@ -92,6 +92,56 @@ pub fn read_work_item_from_stage(
} }
} }
/// Test-fixture helpers that may call `std::fs` — kept here so that
/// `mod.rs` and topic-file `#[cfg(test)]` blocks never need to import
/// `std::fs`, `tokio::fs`, or `std::process` directly.
#[cfg(test)]
pub mod test_helpers {
use tempfile::TempDir;
/// Create the `.huskies/` directory.
pub fn make_huskies_dir(tmp: &TempDir) {
std::fs::create_dir_all(tmp.path().join(".huskies")).unwrap();
}
/// Create the `5_done` and `6_archived` work-stage directories.
pub fn make_work_dirs(tmp: &TempDir) {
for stage in &["5_done", "6_archived"] {
std::fs::create_dir_all(tmp.path().join(".huskies").join("work").join(stage)).unwrap();
}
}
/// Create all six pipeline stage directories under `.huskies/work/`.
pub fn make_stage_dirs(tmp: &TempDir) {
for stage in &[
"1_backlog",
"2_current",
"3_qa",
"4_merge",
"5_done",
"6_archived",
] {
std::fs::create_dir_all(tmp.path().join(".huskies").join("work").join(stage)).unwrap();
}
}
/// Write `.huskies/project.toml` with the given TOML content.
pub fn make_project_toml(tmp: &TempDir, content: &str) {
let sk_dir = tmp.path().join(".huskies");
std::fs::create_dir_all(&sk_dir).unwrap();
std::fs::write(sk_dir.join("project.toml"), content).unwrap();
}
/// Write a fixture file at `relative_path` (relative to the tmp root).
pub fn write_story_file(tmp: &TempDir, relative_path: &str, content: &str) {
let path = tmp.path().join(relative_path);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(path, content).unwrap();
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+10 -35
View File
@@ -290,6 +290,7 @@ fn config_to_entries(config: &ProjectConfig) -> Vec<AgentConfigEntry> {
mod tests { mod tests {
use super::*; use super::*;
use crate::agents::AgentStatus; use crate::agents::AgentStatus;
use io::test_helpers::*;
use std::sync::Arc; use std::sync::Arc;
use tempfile::TempDir; use tempfile::TempDir;
@@ -301,43 +302,17 @@ mod tests {
Arc::new(pool) Arc::new(pool)
} }
fn make_work_dirs(tmp: &TempDir) {
for stage in &["5_done", "6_archived"] {
std::fs::create_dir_all(tmp.path().join(".huskies").join("work").join(stage)).unwrap();
}
}
fn make_stage_dirs(tmp: &TempDir) {
for stage in &[
"1_backlog",
"2_current",
"3_qa",
"4_merge",
"5_done",
"6_archived",
] {
std::fs::create_dir_all(tmp.path().join(".huskies").join("work").join(stage)).unwrap();
}
}
fn make_project_toml(tmp: &TempDir, content: &str) {
let sk_dir = tmp.path().join(".huskies");
std::fs::create_dir_all(&sk_dir).unwrap();
std::fs::write(sk_dir.join("project.toml"), content).unwrap();
}
// ── list_agents ─────────────────────────────────────────────────────────── // ── list_agents ───────────────────────────────────────────────────────────
#[tokio::test] #[tokio::test]
async fn list_agents_excludes_archived_stories() { async fn list_agents_excludes_archived_stories() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
make_work_dirs(&tmp); make_work_dirs(&tmp);
std::fs::write( write_story_file(
tmp.path() &tmp,
.join(".huskies/work/6_archived/79_story_archived.md"), ".huskies/work/6_archived/79_story_archived.md",
"---\nname: archived\n---\n", "---\nname: archived\n---\n",
) );
.unwrap();
let pool = make_pool(&tmp); let pool = make_pool(&tmp);
pool.inject_test_agent("79_story_archived", "coder-1", AgentStatus::Completed); pool.inject_test_agent("79_story_archived", "coder-1", AgentStatus::Completed);
@@ -363,7 +338,7 @@ mod tests {
#[test] #[test]
fn get_agent_config_returns_default_when_no_toml() { fn get_agent_config_returns_default_when_no_toml() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
std::fs::create_dir_all(tmp.path().join(".huskies")).unwrap(); make_huskies_dir(&tmp);
let entries = get_agent_config(tmp.path()).unwrap(); let entries = get_agent_config(tmp.path()).unwrap();
assert_eq!(entries.len(), 1); assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "default"); assert_eq!(entries[0].name, "default");
@@ -405,11 +380,11 @@ max_budget_usd = 5.0
fn get_work_item_content_reads_from_backlog() { fn get_work_item_content_reads_from_backlog() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
make_stage_dirs(&tmp); make_stage_dirs(&tmp);
std::fs::write( write_story_file(
tmp.path().join(".huskies/work/1_backlog/42_story_foo.md"), &tmp,
".huskies/work/1_backlog/42_story_foo.md",
"---\nname: \"Foo Story\"\n---\n\nSome content.", "---\nname: \"Foo Story\"\n---\n\nSome content.",
) );
.unwrap();
let item = get_work_item_content(tmp.path(), "42_story_foo").unwrap(); let item = get_work_item_content(tmp.path(), "42_story_foo").unwrap();
assert!(item.content.contains("Some content.")); assert!(item.content.contains("Some content."));
assert_eq!(item.stage, "backlog"); assert_eq!(item.stage, "backlog");