huskies: merge 879
This commit is contained in:
@@ -7,9 +7,7 @@
|
||||
//! Passing no dependency numbers clears the field entirely.
|
||||
|
||||
use super::CommandContext;
|
||||
use crate::io::story_metadata::{
|
||||
parse_front_matter, write_depends_on, write_depends_on_in_content,
|
||||
};
|
||||
use crate::io::story_metadata::parse_front_matter;
|
||||
|
||||
/// Handle the `depends` command.
|
||||
///
|
||||
@@ -53,7 +51,7 @@ pub(super) fn handle_depends(ctx: &CommandContext) -> Option<String> {
|
||||
}
|
||||
|
||||
// Find the story by numeric prefix: CRDT → content store → filesystem.
|
||||
let (story_id, stage_dir, path, content) =
|
||||
let (story_id, _stage_dir, _path, content) =
|
||||
match crate::chat::lookup::find_story_by_number(ctx.effective_root(), num_str) {
|
||||
Some(found) => found,
|
||||
None => {
|
||||
@@ -69,49 +67,20 @@ pub(super) fn handle_depends(ctx: &CommandContext) -> Option<String> {
|
||||
.and_then(|m| m.name)
|
||||
.unwrap_or_else(|| story_id.clone());
|
||||
|
||||
// Prefer the CRDT content store; fall back to filesystem only when the
|
||||
// story has not been loaded into the DB (e.g. very early startup or tests
|
||||
// that haven't called write_item_with_content).
|
||||
if let Some(existing) = crate::db::read_content(&story_id) {
|
||||
let updated = write_depends_on_in_content(&existing, &deps);
|
||||
crate::db::write_content(&story_id, &updated);
|
||||
let stage = crate::pipeline_state::read_typed(&story_id)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|i| i.stage.dir_name().to_string())
|
||||
.unwrap_or_else(|| stage_dir.clone());
|
||||
crate::db::write_item_with_content(&story_id, &stage, &updated);
|
||||
// Sync depends_on to the typed CRDT register.
|
||||
crate::crdt_state::set_depends_on(&story_id, &deps);
|
||||
if deps.is_empty() {
|
||||
Some(format!(
|
||||
"Cleared all dependencies for **{story_name}** ({story_id})."
|
||||
))
|
||||
} else {
|
||||
let nums: Vec<String> = deps.iter().map(|n| n.to_string()).collect();
|
||||
Some(format!(
|
||||
"Set depends_on: [{}] for **{story_name}** ({story_id}).",
|
||||
nums.join(", ")
|
||||
))
|
||||
}
|
||||
// Write depends_on to the typed CRDT register — single source of truth.
|
||||
// No YAML mutation: the CRDT register is the canonical location for deps.
|
||||
crate::crdt_state::set_depends_on(&story_id, &deps);
|
||||
|
||||
if deps.is_empty() {
|
||||
Some(format!(
|
||||
"Cleared all dependencies for **{story_name}** ({story_id})."
|
||||
))
|
||||
} else {
|
||||
match write_depends_on(&path, &deps) {
|
||||
Ok(()) if deps.is_empty() => {
|
||||
crate::crdt_state::set_depends_on(&story_id, &[]);
|
||||
Some(format!(
|
||||
"Cleared all dependencies for **{story_name}** ({story_id})."
|
||||
))
|
||||
}
|
||||
Ok(()) => {
|
||||
crate::crdt_state::set_depends_on(&story_id, &deps);
|
||||
let nums: Vec<String> = deps.iter().map(|n| n.to_string()).collect();
|
||||
Some(format!(
|
||||
"Set depends_on: [{}] for **{story_name}** ({story_id}).",
|
||||
nums.join(", ")
|
||||
))
|
||||
}
|
||||
Err(e) => Some(format!("Failed to update dependencies for {story_id}: {e}")),
|
||||
}
|
||||
let nums: Vec<String> = deps.iter().map(|n| n.to_string()).collect();
|
||||
Some(format!(
|
||||
"Set depends_on: [{}] for **{story_name}** ({story_id}).",
|
||||
nums.join(", ")
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +176,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn depends_sets_deps_and_writes_to_content_store() {
|
||||
fn depends_sets_deps_in_crdt_not_yaml() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
write_story_file(
|
||||
tmp.path(),
|
||||
@@ -220,33 +189,86 @@ mod tests {
|
||||
output.contains("477") && output.contains("478"),
|
||||
"response should mention dep numbers: {output}"
|
||||
);
|
||||
let contents = crate::db::read_content("9910_story_foo")
|
||||
.expect("content store should have updated story");
|
||||
// CRDT register must hold the deps.
|
||||
let view = crate::crdt_state::read_item("9910_story_foo").expect("CRDT should have story");
|
||||
assert_eq!(
|
||||
view.depends_on,
|
||||
Some(vec![477, 478]),
|
||||
"CRDT register should hold [477, 478]: {view:?}"
|
||||
);
|
||||
// Content store YAML must NOT be mutated with depends_on.
|
||||
let contents =
|
||||
crate::db::read_content("9910_story_foo").expect("content store should have story");
|
||||
assert!(
|
||||
contents.contains("depends_on: [477, 478]"),
|
||||
"content store should have depends_on set: {contents}"
|
||||
!contents.contains("depends_on"),
|
||||
"content store YAML must not contain depends_on after chat command: {contents}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn depends_clears_deps_when_no_deps_given() {
|
||||
fn depends_clears_deps_in_crdt_not_yaml() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
write_story_file(
|
||||
tmp.path(),
|
||||
"2_current",
|
||||
"9911_story_bar.md",
|
||||
"---\nname: Bar\ndepends_on: [477]\n---\n",
|
||||
"---\nname: Bar\n---\n",
|
||||
);
|
||||
// Pre-seed CRDT with deps so we can verify clearing.
|
||||
crate::crdt_state::set_depends_on("9911_story_bar", &[477]);
|
||||
let output = depends_cmd_with_root(tmp.path(), "9911").unwrap();
|
||||
assert!(
|
||||
output.contains("Cleared"),
|
||||
"should confirm clearing deps: {output}"
|
||||
);
|
||||
let contents = crate::db::read_content("9911_story_bar")
|
||||
.expect("content store should have updated story");
|
||||
// CRDT register must be empty after clear.
|
||||
let view = crate::crdt_state::read_item("9911_story_bar").expect("CRDT should have story");
|
||||
assert_eq!(
|
||||
view.depends_on, None,
|
||||
"CRDT register should be empty after clearing: {view:?}"
|
||||
);
|
||||
// Content store YAML must not be mutated.
|
||||
let contents =
|
||||
crate::db::read_content("9911_story_bar").expect("content store should have story");
|
||||
assert!(
|
||||
!contents.contains("depends_on"),
|
||||
"content store should have depends_on cleared: {contents}"
|
||||
"content store YAML must not contain depends_on after clear: {contents}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression (AC3, chat path): chat `depends` must write deps to CRDT only —
|
||||
/// no `depends_on` in the YAML content store.
|
||||
/// The MCP counterpart is `tool_update_story_depends_on_routes_to_crdt_not_yaml`
|
||||
/// in `http/mcp/story_tools/story/update.rs`. Both assert `Some(vec![500, 501])`
|
||||
/// proving identical CRDT state across transports.
|
||||
#[test]
|
||||
fn chat_depends_sets_crdt_no_yaml_regression() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
write_story_file(
|
||||
tmp.path(),
|
||||
"1_backlog",
|
||||
"8790_story_chat_dep.md",
|
||||
"---\nname: Chat Dep\n---\n",
|
||||
);
|
||||
|
||||
let out = depends_cmd_with_root(tmp.path(), "8790 500 501").unwrap();
|
||||
assert!(
|
||||
out.contains("500"),
|
||||
"chat response should mention dep: {out}"
|
||||
);
|
||||
|
||||
let view =
|
||||
crate::crdt_state::read_item("8790_story_chat_dep").expect("CRDT must have chat story");
|
||||
assert_eq!(
|
||||
view.depends_on,
|
||||
Some(vec![500, 501]),
|
||||
"CRDT must hold [500, 501]: {view:?}"
|
||||
);
|
||||
|
||||
let content = crate::db::read_content("8790_story_chat_dep").unwrap();
|
||||
assert!(
|
||||
!content.contains("depends_on"),
|
||||
"chat must not write depends_on to YAML: {content}"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user