huskies: merge 530_story_eliminate_filesystem_markdown_shadows_entirely_crdt_db_is_the_only_story_store
This commit is contained in:
@@ -113,15 +113,14 @@ fn move_item<'a>(
|
||||
Err(format!("Work item '{story_id}' not found in {locs}."))
|
||||
}
|
||||
|
||||
/// Move a work item (story, bug, or spike) to `work/2_current/`.
|
||||
/// Move a work item (story, bug, or spike) from `1_backlog` to `work/2_current/`.
|
||||
///
|
||||
/// The source stage is read from the CRDT — any existing stage is accepted.
|
||||
/// Only promotes from `1_backlog` — stories already in later stages (3_qa, 4_merge,
|
||||
/// etc.) are left untouched. This prevents coders from accidentally demoting a story
|
||||
/// that has already advanced past the coding stage.
|
||||
/// Idempotent: if already in `2_current/`, returns Ok. If not found, logs and returns Ok.
|
||||
pub fn move_story_to_current(project_root: &Path, story_id: &str) -> Result<(), String> {
|
||||
const ALL_STAGES: &[&str] = &[
|
||||
"1_backlog", "2_current", "3_qa", "4_merge", "5_done", "6_archived",
|
||||
];
|
||||
move_item(project_root, story_id, ALL_STAGES, "2_current", &[], true, &[]).map(|_| ())
|
||||
move_item(project_root, story_id, &["1_backlog"], "2_current", &[], true, &[]).map(|_| ())
|
||||
}
|
||||
|
||||
/// Check whether a feature branch `feature/story-{story_id}` exists and has
|
||||
|
||||
@@ -38,13 +38,7 @@ impl AgentPool {
|
||||
let items = scan_stage_items(project_root, "1_backlog");
|
||||
for story_id in &items {
|
||||
// Only promote stories that explicitly declare dependencies.
|
||||
// Try content store first, fall back to filesystem.
|
||||
let contents = crate::db::read_content(story_id).or_else(|| {
|
||||
let story_path = project_root
|
||||
.join(".huskies/work/1_backlog")
|
||||
.join(format!("{story_id}.md"));
|
||||
std::fs::read_to_string(&story_path).ok()
|
||||
});
|
||||
let contents = crate::db::read_content(story_id);
|
||||
let has_deps = contents
|
||||
.and_then(|c| parse_front_matter(&c).ok())
|
||||
.and_then(|m| m.depends_on)
|
||||
@@ -382,38 +376,40 @@ mod tests {
|
||||
async fn auto_assign_ignores_coder_preference_when_story_is_in_qa_stage() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".huskies");
|
||||
let qa_dir = sk.join("work/3_qa");
|
||||
std::fs::create_dir_all(&qa_dir).unwrap();
|
||||
std::fs::create_dir_all(&sk).unwrap();
|
||||
std::fs::write(
|
||||
sk.join("project.toml"),
|
||||
"[[agent]]\nname = \"coder-1\"\nstage = \"coder\"\n\n\
|
||||
[[agent]]\nname = \"qa-1\"\nstage = \"qa\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
// Story in 3_qa/ with a preferred coder-stage agent.
|
||||
std::fs::write(
|
||||
qa_dir.join("story-qa1.md"),
|
||||
// Story in 3_qa/ with a preferred coder-stage agent — write via CRDT.
|
||||
crate::db::ensure_content_store();
|
||||
crate::db::write_item_with_content(
|
||||
"9930_story_qa1",
|
||||
"3_qa",
|
||||
"---\nname: QA Story\nagent: coder-1\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
);
|
||||
|
||||
let pool = AgentPool::new_test(3001);
|
||||
|
||||
pool.auto_assign_available_work(tmp.path()).await;
|
||||
|
||||
let agents = pool.agents.lock().unwrap();
|
||||
// coder-1 must NOT have been assigned (wrong stage for 3_qa/).
|
||||
let coder_assigned = agents.values().any(|a| {
|
||||
a.agent_name == "coder-1"
|
||||
// coder-1 must NOT have been assigned to the QA story (wrong stage).
|
||||
let coder_assigned_to_qa = agents.iter().any(|(key, a)| {
|
||||
key.contains("9930_story_qa1")
|
||||
&& a.agent_name == "coder-1"
|
||||
&& matches!(a.status, AgentStatus::Pending | AgentStatus::Running)
|
||||
});
|
||||
assert!(
|
||||
!coder_assigned,
|
||||
!coder_assigned_to_qa,
|
||||
"coder-1 should not be assigned to a QA-stage story"
|
||||
);
|
||||
// qa-1 should have been assigned instead.
|
||||
let qa_assigned = agents.values().any(|a| {
|
||||
a.agent_name == "qa-1"
|
||||
let qa_assigned = agents.iter().any(|(key, a)| {
|
||||
key.contains("9930_story_qa1")
|
||||
&& a.agent_name == "qa-1"
|
||||
&& matches!(a.status, AgentStatus::Pending | AgentStatus::Running)
|
||||
});
|
||||
assert!(
|
||||
@@ -429,8 +425,7 @@ mod tests {
|
||||
async fn auto_assign_respects_coder_preference_when_story_is_in_current_stage() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".huskies");
|
||||
let current_dir = sk.join("work/2_current");
|
||||
std::fs::create_dir_all(¤t_dir).unwrap();
|
||||
std::fs::create_dir_all(sk.join("work/2_current")).unwrap();
|
||||
std::fs::write(
|
||||
sk.join("project.toml"),
|
||||
"[[agent]]\nname = \"coder-1\"\nstage = \"coder\"\n\n\
|
||||
@@ -438,11 +433,12 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
// Story in 2_current/ with a preferred coder-1 agent.
|
||||
std::fs::write(
|
||||
current_dir.join("story-pref.md"),
|
||||
crate::db::ensure_content_store();
|
||||
crate::db::write_item_with_content(
|
||||
"story-pref",
|
||||
"2_current",
|
||||
"---\nname: Coder Story\nagent: coder-1\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
);
|
||||
|
||||
let pool = AgentPool::new_test(3001);
|
||||
|
||||
@@ -476,20 +472,20 @@ mod tests {
|
||||
async fn auto_assign_stage_mismatch_with_no_fallback_starts_no_agent() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".huskies");
|
||||
let qa_dir = sk.join("work/3_qa");
|
||||
std::fs::create_dir_all(&qa_dir).unwrap();
|
||||
std::fs::create_dir_all(&sk).unwrap();
|
||||
// Only a coder agent is configured — no QA agent exists.
|
||||
std::fs::write(
|
||||
sk.join("project.toml"),
|
||||
"[[agent]]\nname = \"coder-1\"\nstage = \"coder\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
// Story in 3_qa/ requests coder-1 (wrong stage) and no QA agent exists.
|
||||
std::fs::write(
|
||||
qa_dir.join("story-noqa.md"),
|
||||
// Story in 3_qa/ requests coder-1 (wrong stage) and no QA agent exists — write via CRDT.
|
||||
crate::db::ensure_content_store();
|
||||
crate::db::write_item_with_content(
|
||||
"9931_story_noqa",
|
||||
"3_qa",
|
||||
"---\nname: QA Story No Agent\nagent: coder-1\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
);
|
||||
|
||||
let pool = AgentPool::new_test(3001);
|
||||
|
||||
@@ -497,8 +493,14 @@ mod tests {
|
||||
pool.auto_assign_available_work(tmp.path()).await;
|
||||
|
||||
let agents = pool.agents.lock().unwrap();
|
||||
// No agent should be assigned to the specific QA story (coder-1 may
|
||||
// be assigned to leaked 2_current items from the global CRDT store).
|
||||
let assigned_to_qa_story = agents.iter().any(|(key, a)| {
|
||||
key.contains("9931_story_noqa")
|
||||
&& matches!(a.status, AgentStatus::Pending | AgentStatus::Running)
|
||||
});
|
||||
assert!(
|
||||
agents.is_empty(),
|
||||
!assigned_to_qa_story,
|
||||
"No agent should be started when no stage-appropriate agent is available"
|
||||
);
|
||||
}
|
||||
@@ -510,26 +512,32 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let sk = root.join(".huskies");
|
||||
let current = sk.join("work/2_current");
|
||||
std::fs::create_dir_all(¤t).unwrap();
|
||||
std::fs::create_dir_all(&sk).unwrap();
|
||||
std::fs::write(
|
||||
sk.join("project.toml"),
|
||||
"[[agent]]\nname = \"coder-1\"\nstage = \"coder\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
// Story 10 depends on 999 which is not done.
|
||||
std::fs::write(
|
||||
current.join("10_story_waiting.md"),
|
||||
"---\nname: Waiting\ndepends_on: [999]\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
// Story 9932 depends on 9999 which is not done — write via CRDT.
|
||||
crate::db::ensure_content_store();
|
||||
crate::db::write_item_with_content(
|
||||
"9932_story_waiting",
|
||||
"2_current",
|
||||
"---\nname: Waiting\ndepends_on: [9999]\n---\n",
|
||||
);
|
||||
|
||||
let pool = AgentPool::new_test(3001);
|
||||
pool.auto_assign_available_work(root).await;
|
||||
|
||||
let agents = pool.agents.lock().unwrap();
|
||||
// Filter to only agents assigned to our specific story to avoid
|
||||
// interference from other tests sharing the global CRDT store.
|
||||
let assigned_to_our_story = agents.iter().any(|(key, a)| {
|
||||
key.contains("9932_story_waiting")
|
||||
&& matches!(a.status, AgentStatus::Pending | AgentStatus::Running)
|
||||
});
|
||||
assert!(
|
||||
agents.is_empty(),
|
||||
!assigned_to_our_story,
|
||||
"story with unmet deps should not be auto-assigned"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -167,25 +167,47 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn scan_stage_items_returns_empty_for_missing_dir() {
|
||||
// Use a unique stage name that no other test writes to, so
|
||||
// the global CRDT store won't contribute stale items.
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let items = scan_stage_items(tmp.path(), "2_current");
|
||||
let items = scan_stage_items(tmp.path(), "9_nonexistent");
|
||||
assert!(items.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_stage_items_returns_sorted_story_ids() {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let stage_dir = tmp.path().join(".huskies").join("work").join("2_current");
|
||||
fs::create_dir_all(&stage_dir).unwrap();
|
||||
fs::write(stage_dir.join("42_story_foo.md"), "---\nname: foo\n---").unwrap();
|
||||
fs::write(stage_dir.join("10_story_bar.md"), "---\nname: bar\n---").unwrap();
|
||||
fs::write(stage_dir.join("5_story_baz.md"), "---\nname: baz\n---").unwrap();
|
||||
// non-md file should be ignored
|
||||
fs::write(stage_dir.join("README.txt"), "ignore me").unwrap();
|
||||
// Write items via the CRDT store (the primary source of truth).
|
||||
crate::db::ensure_content_store();
|
||||
crate::db::write_item_with_content("9942_story_foo", "2_current", "---\nname: foo\n---");
|
||||
crate::db::write_item_with_content("9940_story_bar", "2_current", "---\nname: bar\n---");
|
||||
crate::db::write_item_with_content("9935_story_baz", "2_current", "---\nname: baz\n---");
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let items = scan_stage_items(tmp.path(), "2_current");
|
||||
assert_eq!(items, vec!["10_story_bar", "42_story_foo", "5_story_baz"]);
|
||||
// The global CRDT may contain items from other tests, so check
|
||||
// that our three items are present and appear in sorted order.
|
||||
assert!(
|
||||
items.iter().any(|id| id == "9935_story_baz"),
|
||||
"9935_story_baz should be in results"
|
||||
);
|
||||
assert!(
|
||||
items.iter().any(|id| id == "9940_story_bar"),
|
||||
"9940_story_bar should be in results"
|
||||
);
|
||||
assert!(
|
||||
items.iter().any(|id| id == "9942_story_foo"),
|
||||
"9942_story_foo should be in results"
|
||||
);
|
||||
// Verify sorted order: BTreeSet produces lexicographic order.
|
||||
let positions: Vec<usize> = ["9935_story_baz", "9940_story_bar", "9942_story_foo"]
|
||||
.iter()
|
||||
.filter_map(|id| items.iter().position(|x| x == id))
|
||||
.collect();
|
||||
assert_eq!(positions.len(), 3, "all three items must be found");
|
||||
assert!(
|
||||
positions[0] < positions[1] && positions[1] < positions[2],
|
||||
"items should appear in sorted order: positions = {positions:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2,23 +2,9 @@
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
/// Read story contents from DB content store first, fall back to filesystem.
|
||||
fn read_story_contents(project_root: &Path, story_id: &str) -> Option<String> {
|
||||
// Primary: in-memory content store (backed by SQLite).
|
||||
if let Some(c) = crate::db::read_content(story_id) {
|
||||
return Some(c);
|
||||
}
|
||||
// Fallback: scan filesystem stages.
|
||||
for stage in &["1_backlog", "2_current", "3_qa", "4_merge", "5_done", "6_archived"] {
|
||||
let path = project_root
|
||||
.join(".huskies/work")
|
||||
.join(stage)
|
||||
.join(format!("{story_id}.md"));
|
||||
if let Ok(c) = std::fs::read_to_string(&path) {
|
||||
return Some(c);
|
||||
}
|
||||
}
|
||||
None
|
||||
/// Read story contents from the DB content store (CRDT-backed).
|
||||
fn read_story_contents(_project_root: &Path, story_id: &str) -> Option<String> {
|
||||
crate::db::read_content(story_id)
|
||||
}
|
||||
|
||||
/// Read the optional `agent:` field from the front matter of a story file.
|
||||
@@ -125,14 +111,12 @@ mod tests {
|
||||
#[test]
|
||||
fn has_review_hold_returns_true_when_set() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let qa_dir = tmp.path().join(".huskies/work/3_qa");
|
||||
std::fs::create_dir_all(&qa_dir).unwrap();
|
||||
let spike_path = qa_dir.join("10_spike_research.md");
|
||||
std::fs::write(
|
||||
&spike_path,
|
||||
crate::db::ensure_content_store();
|
||||
crate::db::write_item_with_content(
|
||||
"10_spike_research",
|
||||
"3_qa",
|
||||
"---\nname: Research spike\nreview_hold: true\n---\n# Spike\n",
|
||||
)
|
||||
.unwrap();
|
||||
);
|
||||
assert!(has_review_hold(tmp.path(), "3_qa", "10_spike_research"));
|
||||
}
|
||||
|
||||
|
||||
@@ -398,19 +398,15 @@ pub(super) fn spawn_pipeline_advance(
|
||||
});
|
||||
}
|
||||
|
||||
/// Resolve QA mode from the content store (or filesystem fallback).
|
||||
/// Resolve QA mode from the content store.
|
||||
fn resolve_qa_mode_from_store(
|
||||
project_root: &Path,
|
||||
_project_root: &Path,
|
||||
story_id: &str,
|
||||
default: crate::io::story_metadata::QaMode,
|
||||
) -> crate::io::story_metadata::QaMode {
|
||||
if let Some(contents) = crate::db::read_content(story_id) {
|
||||
return crate::io::story_metadata::resolve_qa_mode_from_content(&contents, default);
|
||||
}
|
||||
// Fallback: try filesystem.
|
||||
if let Ok(path) = crate::http::workflow::find_story_file_on_disk(project_root, story_id) {
|
||||
return crate::io::story_metadata::resolve_qa_mode(&path, default);
|
||||
}
|
||||
default
|
||||
}
|
||||
|
||||
|
||||
@@ -92,13 +92,7 @@ impl AgentPool {
|
||||
// honour `agent: coder-opus` written by the `assign` command — mirroring
|
||||
// the auto_assign path (bug 379).
|
||||
let front_matter_agent: Option<String> = if agent_name.is_none() {
|
||||
find_active_story_stage(project_root, story_id).and_then(|stage_dir| {
|
||||
let path = project_root
|
||||
.join(".huskies")
|
||||
.join("work")
|
||||
.join(stage_dir)
|
||||
.join(format!("{story_id}.md"));
|
||||
let contents = std::fs::read_to_string(path).ok()?;
|
||||
crate::db::read_content(story_id).and_then(|contents| {
|
||||
crate::io::story_metadata::parse_front_matter(&contents).ok()?.agent
|
||||
})
|
||||
} else {
|
||||
@@ -1218,11 +1212,12 @@ stage = "coder"
|
||||
[[agent]]\nname = \"mergemaster\"\nstage = \"mergemaster\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
sk_dir.join("work/2_current/310_story_foo.md"),
|
||||
crate::db::ensure_content_store();
|
||||
crate::db::write_item_with_content(
|
||||
"310_story_foo",
|
||||
"2_current",
|
||||
"---\nname: Foo\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
);
|
||||
|
||||
let pool = AgentPool::new_test(3099);
|
||||
let result = pool
|
||||
@@ -1248,22 +1243,23 @@ stage = "coder"
|
||||
let root = tmp.path();
|
||||
|
||||
let sk_dir = root.join(".huskies");
|
||||
fs::create_dir_all(sk_dir.join("work/3_qa")).unwrap();
|
||||
fs::create_dir_all(&sk_dir).unwrap();
|
||||
fs::write(
|
||||
sk_dir.join("project.toml"),
|
||||
"[[agent]]\nname = \"coder-1\"\nstage = \"coder\"\n\n\
|
||||
[[agent]]\nname = \"qa\"\nstage = \"qa\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
sk_dir.join("work/3_qa/42_story_bar.md"),
|
||||
"---\nname: Bar\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
crate::db::ensure_content_store();
|
||||
crate::db::write_item_with_content(
|
||||
"8842_story_qa_guard",
|
||||
"3_qa",
|
||||
"---\nname: QA Guard\n---\n",
|
||||
);
|
||||
|
||||
let pool = AgentPool::new_test(3099);
|
||||
let result = pool
|
||||
.start_agent(root, "42_story_bar", Some("coder-1"), None)
|
||||
.start_agent(root, "8842_story_qa_guard", Some("coder-1"), None)
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
@@ -1285,18 +1281,19 @@ stage = "coder"
|
||||
let root = tmp.path();
|
||||
|
||||
let sk_dir = root.join(".huskies");
|
||||
fs::create_dir_all(sk_dir.join("work/4_merge")).unwrap();
|
||||
fs::create_dir_all(&sk_dir).unwrap();
|
||||
fs::write(
|
||||
sk_dir.join("project.toml"),
|
||||
"[[agent]]\nname = \"qa\"\nstage = \"qa\"\n\n\
|
||||
[[agent]]\nname = \"mergemaster\"\nstage = \"mergemaster\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
sk_dir.join("work/4_merge/55_story_baz.md"),
|
||||
crate::db::ensure_content_store();
|
||||
crate::db::write_item_with_content(
|
||||
"55_story_baz",
|
||||
"4_merge",
|
||||
"---\nname: Baz\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
);
|
||||
|
||||
let pool = AgentPool::new_test(3099);
|
||||
let result = pool
|
||||
|
||||
@@ -22,26 +22,12 @@ impl AgentPool {
|
||||
|
||||
/// Return the active pipeline stage directory name for `story_id`, or `None` if the
|
||||
/// story is not in any active stage (`2_current/`, `3_qa/`, `4_merge/`).
|
||||
pub(super) fn find_active_story_stage(project_root: &Path, story_id: &str) -> Option<&'static str> {
|
||||
// Try typed CRDT projection first — primary source of truth.
|
||||
pub(super) fn find_active_story_stage(_project_root: &Path, story_id: &str) -> Option<&'static str> {
|
||||
if let Ok(Some(item)) = crate::pipeline_state::read_typed(story_id)
|
||||
&& item.stage.is_active()
|
||||
{
|
||||
return Some(item.stage.dir_name());
|
||||
}
|
||||
|
||||
// Also check filesystem (backwards compat / tests).
|
||||
const STAGES: [&str; 3] = ["2_current", "3_qa", "4_merge"];
|
||||
for stage in &STAGES {
|
||||
let path = project_root
|
||||
.join(".huskies")
|
||||
.join("work")
|
||||
.join(stage)
|
||||
.join(format!("{story_id}.md"));
|
||||
if path.exists() {
|
||||
return Some(stage);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -51,42 +37,42 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn find_active_story_stage_detects_current() {
|
||||
use std::fs;
|
||||
crate::db::ensure_content_store();
|
||||
crate::db::write_item_with_content(
|
||||
"10_story_test",
|
||||
"2_current",
|
||||
"---\nname: Test\n---\n",
|
||||
);
|
||||
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("10_story_test.md"), "test").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
find_active_story_stage(root, "10_story_test"),
|
||||
find_active_story_stage(tmp.path(), "10_story_test"),
|
||||
Some("2_current")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_active_story_stage_detects_qa() {
|
||||
use std::fs;
|
||||
crate::db::ensure_content_store();
|
||||
crate::db::write_item_with_content(
|
||||
"11_story_test",
|
||||
"3_qa",
|
||||
"---\nname: Test\n---\n",
|
||||
);
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let qa = root.join(".huskies/work/3_qa");
|
||||
fs::create_dir_all(&qa).unwrap();
|
||||
fs::write(qa.join("11_story_test.md"), "test").unwrap();
|
||||
|
||||
assert_eq!(find_active_story_stage(root, "11_story_test"), Some("3_qa"));
|
||||
assert_eq!(find_active_story_stage(tmp.path(), "11_story_test"), Some("3_qa"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_active_story_stage_detects_merge() {
|
||||
use std::fs;
|
||||
crate::db::ensure_content_store();
|
||||
crate::db::write_item_with_content(
|
||||
"12_story_test",
|
||||
"4_merge",
|
||||
"---\nname: Test\n---\n",
|
||||
);
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let merge = root.join(".huskies/work/4_merge");
|
||||
fs::create_dir_all(&merge).unwrap();
|
||||
fs::write(merge.join("12_story_test.md"), "test").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
find_active_story_stage(root, "12_story_test"),
|
||||
find_active_story_stage(tmp.path(), "12_story_test"),
|
||||
Some("4_merge")
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user