huskies: merge 503_bug_depends_on_pointing_at_an_archived_story_is_silently_treated_as_deps_met_surprising_users
This commit is contained in:
@@ -14,8 +14,8 @@ use super::scan::{
|
||||
is_story_assigned_for_stage, scan_stage_items,
|
||||
};
|
||||
use super::story_checks::{
|
||||
has_merge_failure, has_review_hold, has_unmet_dependencies, is_story_blocked,
|
||||
read_story_front_matter_agent,
|
||||
check_archived_dependencies, has_merge_failure, has_review_hold, has_unmet_dependencies,
|
||||
is_story_blocked, read_story_front_matter_agent,
|
||||
};
|
||||
|
||||
impl AgentPool {
|
||||
@@ -24,6 +24,14 @@ impl AgentPool {
|
||||
/// A story is only promoted if it explicitly lists `depends_on` AND every
|
||||
/// listed dependency has reached `5_done` or `6_archived`. Stories with no
|
||||
/// `depends_on` are left in the backlog for human scheduling.
|
||||
///
|
||||
/// **Archived dep semantics:** a dep in `6_archived` counts as satisfied (since
|
||||
/// stories auto-sweep from `5_done` to `6_archived` after 4 hours, and the
|
||||
/// dependent story would normally already be promoted by then). However, if a
|
||||
/// dep was already in `6_archived` when the dependent story was created (e.g. it
|
||||
/// was abandoned/superseded before the dependent existed), a prominent warning is
|
||||
/// logged so the user can see the promotion was triggered by an archived dep, not
|
||||
/// a clean completion.
|
||||
fn promote_ready_backlog_stories(&self, project_root: &Path) {
|
||||
use crate::io::story_metadata::parse_front_matter;
|
||||
|
||||
@@ -49,6 +57,17 @@ impl AgentPool {
|
||||
if has_unmet_dependencies(project_root, "1_backlog", story_id) {
|
||||
continue;
|
||||
}
|
||||
// Warn if any deps were satisfied via archive rather than via clean done.
|
||||
let archived_deps = check_archived_dependencies(project_root, "1_backlog", story_id);
|
||||
if !archived_deps.is_empty() {
|
||||
slog_warn!(
|
||||
"[auto-assign] Story '{story_id}' is being promoted because deps \
|
||||
{archived_deps:?} are in 6_archived (not cleanly completed via 5_done). \
|
||||
These deps may have been abandoned or superseded. If this promotion is \
|
||||
unintentional, remove the depends_on or manually move the story back to \
|
||||
1_backlog."
|
||||
);
|
||||
}
|
||||
// All deps met — promote from backlog to current.
|
||||
slog!("[auto-assign] Story '{story_id}' deps met; promoting from backlog to current.");
|
||||
if let Err(e) =
|
||||
@@ -623,6 +642,53 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Bug 503: archived-dep promotion visibility ─────────────────────────────
|
||||
|
||||
/// A backlog story whose dep is in 6_archived must still be promoted
|
||||
/// (archived = satisfied), but the promotion must not silently skip the warning
|
||||
/// path. This test verifies the promotion itself fires; the warning is a
|
||||
/// slog_warn! side-effect that we can't easily assert on in unit tests.
|
||||
#[tokio::test]
|
||||
async fn auto_assign_promotes_backlog_story_when_dep_is_archived() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let sk = root.join(".huskies");
|
||||
let backlog = sk.join("work/1_backlog");
|
||||
let current = sk.join("work/2_current");
|
||||
let archived = sk.join("work/6_archived");
|
||||
std::fs::create_dir_all(&backlog).unwrap();
|
||||
std::fs::create_dir_all(¤t).unwrap();
|
||||
std::fs::create_dir_all(&archived).unwrap();
|
||||
std::fs::write(
|
||||
sk.join("project.toml"),
|
||||
"[[agent]]\nname = \"coder-1\"\nstage = \"coder\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
// Dep 490 is in 6_archived (e.g. a CRDT spike that was archived/superseded).
|
||||
std::fs::write(archived.join("490_spike_crdt.md"), "---\nname: CRDT Spike\n---\n")
|
||||
.unwrap();
|
||||
// Story 478 depends on 490 (the archived spike).
|
||||
std::fs::write(
|
||||
backlog.join("478_story_dependent.md"),
|
||||
"---\nname: Dependent\ndepends_on: [490]\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let pool = AgentPool::new_test(3001);
|
||||
pool.auto_assign_available_work(root).await;
|
||||
|
||||
// Story 478 must be promoted to 2_current/ even though dep 490 is only in
|
||||
// 6_archived (not in 5_done), because archived = satisfied.
|
||||
assert!(
|
||||
current.join("478_story_dependent.md").exists(),
|
||||
"story 478 should be promoted to 2_current/ when dep 490 is in 6_archived"
|
||||
);
|
||||
assert!(
|
||||
!backlog.join("478_story_dependent.md").exists(),
|
||||
"story 478 must be removed from 1_backlog/ after promotion"
|
||||
);
|
||||
}
|
||||
|
||||
/// Stories in backlog with NO depends_on must NOT be auto-promoted.
|
||||
#[tokio::test]
|
||||
async fn auto_assign_does_not_promote_backlog_story_without_deps() {
|
||||
|
||||
@@ -84,6 +84,25 @@ pub(super) fn has_unmet_dependencies(
|
||||
!crate::io::story_metadata::check_unmet_deps(project_root, stage_dir, story_id).is_empty()
|
||||
}
|
||||
|
||||
/// Return the list of dependency story numbers that are in `6_archived` (satisfied
|
||||
/// via archive rather than via a clean `5_done` completion).
|
||||
///
|
||||
/// Used to emit a warning when backlog promotion fires because one or more deps were
|
||||
/// archived. Returns an empty `Vec` when no deps are archived. Reads from CRDT
|
||||
/// first; falls back to filesystem when CRDT is not initialised.
|
||||
pub(super) fn check_archived_dependencies(
|
||||
project_root: &Path,
|
||||
stage_dir: &str,
|
||||
story_id: &str,
|
||||
) -> Vec<u32> {
|
||||
// Prefer CRDT-based check when the item is known to CRDT.
|
||||
if crate::crdt_state::read_item(story_id).is_some() {
|
||||
return crate::crdt_state::check_archived_deps_crdt(story_id);
|
||||
}
|
||||
// Fallback: filesystem.
|
||||
crate::io::story_metadata::check_archived_deps(project_root, stage_dir, story_id)
|
||||
}
|
||||
|
||||
/// Return `true` if the story file has a `merge_failure` field in its front matter.
|
||||
pub(super) fn has_merge_failure(project_root: &Path, _stage_dir: &str, story_id: &str) -> bool {
|
||||
use crate::io::story_metadata::parse_front_matter;
|
||||
@@ -170,4 +189,44 @@ mod tests {
|
||||
std::fs::write(current.join("5_story_free.md"), "---\nname: Free\n---\n").unwrap();
|
||||
assert!(!has_unmet_dependencies(tmp.path(), "2_current", "5_story_free"));
|
||||
}
|
||||
|
||||
// ── Bug 503: archived-dep visibility ─────────────────────────────────────
|
||||
|
||||
/// check_archived_dependencies returns dep IDs that are in 6_archived.
|
||||
#[test]
|
||||
fn check_archived_dependencies_returns_archived_ids() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let backlog = tmp.path().join(".huskies/work/1_backlog");
|
||||
let archived = tmp.path().join(".huskies/work/6_archived");
|
||||
std::fs::create_dir_all(&backlog).unwrap();
|
||||
std::fs::create_dir_all(&archived).unwrap();
|
||||
std::fs::write(archived.join("500_spike_crdt.md"), "---\nname: CRDT Spike\n---\n").unwrap();
|
||||
std::fs::write(
|
||||
backlog.join("503_story_dependent.md"),
|
||||
"---\nname: Dependent\ndepends_on: [500]\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
let archived_deps =
|
||||
check_archived_dependencies(tmp.path(), "1_backlog", "503_story_dependent");
|
||||
assert_eq!(archived_deps, vec![500]);
|
||||
}
|
||||
|
||||
/// check_archived_dependencies returns empty when dep is in 5_done (not archived).
|
||||
#[test]
|
||||
fn check_archived_dependencies_empty_when_dep_in_done() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let backlog = tmp.path().join(".huskies/work/1_backlog");
|
||||
let done = tmp.path().join(".huskies/work/5_done");
|
||||
std::fs::create_dir_all(&backlog).unwrap();
|
||||
std::fs::create_dir_all(&done).unwrap();
|
||||
std::fs::write(done.join("490_story_done.md"), "---\nname: Done\n---\n").unwrap();
|
||||
std::fs::write(
|
||||
backlog.join("503_story_waiting.md"),
|
||||
"---\nname: Waiting\ndepends_on: [490]\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
let archived_deps =
|
||||
check_archived_dependencies(tmp.path(), "1_backlog", "503_story_waiting");
|
||||
assert!(archived_deps.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user