huskies: merge 1042
This commit is contained in:
@@ -5,9 +5,6 @@ use crate::config::ProjectConfig;
|
|||||||
use crate::pipeline_state::{ArchiveReason, PipelineItem, Stage};
|
use crate::pipeline_state::{ArchiveReason, PipelineItem, Stage};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
/// Maximum number of dirty file paths shown inline per story before truncating.
|
|
||||||
const MAX_DIRTY_FILES_SHOWN: usize = 20;
|
|
||||||
|
|
||||||
/// Map a stage to its display section label, or `None` to skip it entirely.
|
/// Map a stage to its display section label, or `None` to skip it entirely.
|
||||||
///
|
///
|
||||||
/// This is the single source of truth for the "where does this item appear"
|
/// This is the single source of truth for the "where does this item appear"
|
||||||
@@ -218,9 +215,10 @@ pub(crate) fn build_status_from_items(
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render working tree summary lines for a story with uncommitted changes.
|
/// Render the one-line working tree summary for a story with uncommitted changes.
|
||||||
///
|
///
|
||||||
/// Returns an empty string when the working tree is clean.
|
/// Returns an empty string when the working tree is clean. File paths are not
|
||||||
|
/// listed here; use `status N` (triage) for the per-file breakdown.
|
||||||
fn render_working_tree_lines(info: &crate::service::git_ops::DirtyFiles) -> String {
|
fn render_working_tree_lines(info: &crate::service::git_ops::DirtyFiles) -> String {
|
||||||
if info.is_clean() {
|
if info.is_clean() {
|
||||||
return String::new();
|
return String::new();
|
||||||
@@ -230,16 +228,7 @@ fn render_working_tree_lines(info: &crate::service::git_ops::DirtyFiles) -> Stri
|
|||||||
(0, n) => format!("{n} new"),
|
(0, n) => format!("{n} new"),
|
||||||
(m, n) => format!("{m} modified, {n} new"),
|
(m, n) => format!("{m} modified, {n} new"),
|
||||||
};
|
};
|
||||||
let mut out = format!(" Working tree: {summary} (uncommitted)\n");
|
format!(" Working tree: {summary} (uncommitted)\n")
|
||||||
let shown = info.paths.len().min(MAX_DIRTY_FILES_SHOWN);
|
|
||||||
for path in &info.paths[..shown] {
|
|
||||||
out.push_str(&format!(" {path}\n"));
|
|
||||||
}
|
|
||||||
if info.paths.len() > MAX_DIRTY_FILES_SHOWN {
|
|
||||||
let remaining = info.paths.len() - MAX_DIRTY_FILES_SHOWN;
|
|
||||||
out.push_str(&format!(" ...and {remaining} more\n"));
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shared lookup tables passed to [`render_item_line`] to keep the argument count manageable.
|
/// Shared lookup tables passed to [`render_item_line`] to keep the argument count manageable.
|
||||||
|
|||||||
@@ -1232,10 +1232,10 @@ fn status_shows_working_tree_info_when_coder_has_uncommitted_changes() {
|
|||||||
output.contains("(uncommitted)"),
|
output.contains("(uncommitted)"),
|
||||||
"working tree line should say '(uncommitted)': {output}"
|
"working tree line should say '(uncommitted)': {output}"
|
||||||
);
|
);
|
||||||
// Should list the dirty files.
|
// Overview must NOT list individual file paths — those belong in `status N`.
|
||||||
assert!(
|
assert!(
|
||||||
output.contains("README.md") || output.contains("new_feature.rs"),
|
!output.contains("README.md") && !output.contains("new_feature.rs"),
|
||||||
"status should list at least one dirty file: {output}"
|
"overview must not list individual dirty file paths: {output}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1309,8 +1309,13 @@ fn status_dirty_files_capped_at_twenty_with_overflow_line() {
|
|||||||
let agents = AgentPool::new_test(3000);
|
let agents = AgentPool::new_test(3000);
|
||||||
let output = build_status_from_items(project_root, &agents, &items);
|
let output = build_status_from_items(project_root, &agents, &items);
|
||||||
|
|
||||||
|
// Overview shows only the summary line — no path listing, no overflow.
|
||||||
assert!(
|
assert!(
|
||||||
output.contains("...and 5 more"),
|
!output.contains("...and 5 more"),
|
||||||
"overflow line should appear when more than 20 files: {output}"
|
"overview must not show overflow line — paths belong in triage view: {output}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
output.contains("Working tree:") && output.contains("(uncommitted)"),
|
||||||
|
"overview should still show the summary line when dirty: {output}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ use super::CommandContext;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
/// Maximum number of dirty file paths listed per story before truncating.
|
||||||
|
const MAX_DIRTY_FILES_SHOWN: usize = 20;
|
||||||
|
|
||||||
/// Handle `{bot_name} status {number}`.
|
/// Handle `{bot_name} status {number}`.
|
||||||
pub(super) fn handle_triage(ctx: &CommandContext) -> Option<String> {
|
pub(super) fn handle_triage(ctx: &CommandContext) -> Option<String> {
|
||||||
let num_str = ctx.args.trim();
|
let num_str = ctx.args.trim();
|
||||||
@@ -163,6 +166,28 @@ fn build_triage_dump(
|
|||||||
} else {
|
} else {
|
||||||
out.push_str("**Recent commits (branch only):** *(none yet)*\n\n");
|
out.push_str("**Recent commits (branch only):** *(none yet)*\n\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Uncommitted working tree changes ----
|
||||||
|
let dirty = crate::service::git_ops::io::read_dirty_files_sync(&wt_path);
|
||||||
|
if dirty.is_clean() {
|
||||||
|
out.push_str("**Working tree:** working tree clean\n\n");
|
||||||
|
} else {
|
||||||
|
let summary = match (dirty.modified, dirty.new) {
|
||||||
|
(m, 0) => format!("{m} modified"),
|
||||||
|
(0, n) => format!("{n} new"),
|
||||||
|
(m, n) => format!("{m} modified, {n} new"),
|
||||||
|
};
|
||||||
|
out.push_str(&format!("**Working tree:** {summary} (uncommitted)\n"));
|
||||||
|
let shown = dirty.paths.len().min(MAX_DIRTY_FILES_SHOWN);
|
||||||
|
for path in &dirty.paths[..shown] {
|
||||||
|
out.push_str(&format!(" {path}\n"));
|
||||||
|
}
|
||||||
|
if dirty.paths.len() > MAX_DIRTY_FILES_SHOWN {
|
||||||
|
let remaining = dirty.paths.len() - MAX_DIRTY_FILES_SHOWN;
|
||||||
|
out.push_str(&format!(" ...and {remaining} more\n"));
|
||||||
|
}
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
out.push_str(&format!("**Branch:** `{branch}`\n"));
|
out.push_str(&format!("**Branch:** `{branch}`\n"));
|
||||||
out.push_str("**Worktree:** *(not yet created)*\n\n");
|
out.push_str("**Worktree:** *(not yet created)*\n\n");
|
||||||
@@ -500,4 +525,145 @@ mod tests {
|
|||||||
let result = parse_acceptance_criteria(input);
|
let result = parse_acceptance_criteria(input);
|
||||||
assert!(result.is_empty());
|
assert!(result.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- working tree state in triage output --------------------------------
|
||||||
|
|
||||||
|
/// Initialise a bare-minimum git repo in `dir` with one commit.
|
||||||
|
fn init_git_repo(dir: &std::path::Path) {
|
||||||
|
use std::process::Command as Cmd;
|
||||||
|
Cmd::new("git")
|
||||||
|
.args(["init", "-b", "main"])
|
||||||
|
.current_dir(dir)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
Cmd::new("git")
|
||||||
|
.args(["config", "user.email", "test@test.com"])
|
||||||
|
.current_dir(dir)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
Cmd::new("git")
|
||||||
|
.args(["config", "user.name", "Test"])
|
||||||
|
.current_dir(dir)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
std::fs::write(dir.join("README.md"), "# test").unwrap();
|
||||||
|
Cmd::new("git")
|
||||||
|
.args(["add", "README.md"])
|
||||||
|
.current_dir(dir)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
Cmd::new("git")
|
||||||
|
.args(["commit", "-m", "init"])
|
||||||
|
.current_dir(dir)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn triage_shows_dirty_files_when_worktree_is_dirty() {
|
||||||
|
use std::fs;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
crate::db::ensure_content_store();
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let project_root = tmp.path();
|
||||||
|
let story_id = "8801_story_triage_dirty";
|
||||||
|
|
||||||
|
write_story_file(
|
||||||
|
project_root,
|
||||||
|
"2_current",
|
||||||
|
&format!("{story_id}.md"),
|
||||||
|
"",
|
||||||
|
Some("Triage Dirty"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let wt_path = project_root
|
||||||
|
.join(".huskies")
|
||||||
|
.join("worktrees")
|
||||||
|
.join(story_id);
|
||||||
|
fs::create_dir_all(&wt_path).unwrap();
|
||||||
|
init_git_repo(&wt_path);
|
||||||
|
fs::write(wt_path.join("README.md"), "# changed").unwrap();
|
||||||
|
fs::write(wt_path.join("new_file.rs"), "fn foo() {}").unwrap();
|
||||||
|
|
||||||
|
let output = status_triage_cmd(project_root, "8801").unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
output.contains("Working tree:") && output.contains("(uncommitted)"),
|
||||||
|
"triage should show working tree summary when dirty: {output}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
output.contains("README.md") || output.contains("new_file.rs"),
|
||||||
|
"triage should list dirty file paths: {output}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn triage_shows_clean_when_worktree_is_clean() {
|
||||||
|
use std::fs;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
crate::db::ensure_content_store();
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let project_root = tmp.path();
|
||||||
|
let story_id = "8802_story_triage_clean";
|
||||||
|
|
||||||
|
write_story_file(
|
||||||
|
project_root,
|
||||||
|
"2_current",
|
||||||
|
&format!("{story_id}.md"),
|
||||||
|
"",
|
||||||
|
Some("Triage Clean"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let wt_path = project_root
|
||||||
|
.join(".huskies")
|
||||||
|
.join("worktrees")
|
||||||
|
.join(story_id);
|
||||||
|
fs::create_dir_all(&wt_path).unwrap();
|
||||||
|
init_git_repo(&wt_path);
|
||||||
|
|
||||||
|
let output = status_triage_cmd(project_root, "8802").unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
output.contains("working tree clean"),
|
||||||
|
"triage should show 'working tree clean' when no uncommitted changes: {output}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn triage_dirty_files_capped_at_twenty_with_overflow() {
|
||||||
|
use std::fs;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
crate::db::ensure_content_store();
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let project_root = tmp.path();
|
||||||
|
let story_id = "8803_story_triage_many";
|
||||||
|
|
||||||
|
write_story_file(
|
||||||
|
project_root,
|
||||||
|
"2_current",
|
||||||
|
&format!("{story_id}.md"),
|
||||||
|
"",
|
||||||
|
Some("Triage Many"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let wt_path = project_root
|
||||||
|
.join(".huskies")
|
||||||
|
.join("worktrees")
|
||||||
|
.join(story_id);
|
||||||
|
fs::create_dir_all(&wt_path).unwrap();
|
||||||
|
init_git_repo(&wt_path);
|
||||||
|
for i in 0..25 {
|
||||||
|
fs::write(wt_path.join(format!("file_{i:02}.rs")), "fn f() {}").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = status_triage_cmd(project_root, "8803").unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
output.contains("...and 5 more"),
|
||||||
|
"triage overflow line should appear when more than 20 dirty files: {output}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user