huskies: merge 1201 story Chat notification when a new work item is filed

This commit is contained in:
Huskies Agent
2026-07-17 19:43:27 +00:00
parent b9af302baf
commit 80efb7fcfc
14 changed files with 1390 additions and 941 deletions
+2 -1
View File
@@ -28,7 +28,7 @@ pub(crate) fn tool_create_bug(args: &Value, ctx: &AppContext) -> Result<String,
// Bug 1102: resolve and validate origin BEFORE creating the bug file so a
// missing-attribution call leaves no half-state behind.
let origin = super::build_origin(args)?;
let (origin, origin_label) = super::build_origin(args)?;
let root = ctx.state.get_project_root()?;
let bug_id = create_bug_file(
@@ -50,6 +50,7 @@ pub(crate) fn tool_create_bug(args: &Value, ctx: &AppContext) -> Result<String,
item_id: bug_id.clone(),
item_type: "bug".to_string(),
name: req.name.as_ref().to_string(),
origin: origin_label,
});
Ok(format!("Created bug: {bug_id}"))
+10 -1
View File
@@ -15,7 +15,7 @@ pub(crate) fn tool_create_epic(args: &Value, ctx: &AppContext) -> Result<String,
// Bug 1102: resolve and validate origin BEFORE creating the epic so a
// missing-attribution call leaves no half-state behind.
let origin = super::build_origin(args)?;
let (origin, origin_label) = super::build_origin(args)?;
let root = ctx.state.get_project_root()?;
let success_criteria = req.success_criteria_strings();
@@ -35,6 +35,15 @@ pub(crate) fn tool_create_epic(args: &Value, ctx: &AppContext) -> Result<String,
crate::crdt_state::set_origin(&epic_id, &origin);
let _ = ctx
.watcher_tx
.send(crate::io::watcher::WatcherEvent::NewItemCreated {
item_id: epic_id.clone(),
item_type: "epic".to_string(),
name: req.name.as_ref().to_string(),
origin: origin_label,
});
Ok(format!("Created epic: {epic_id}"))
}
+10 -6
View File
@@ -22,11 +22,14 @@ mod story;
/// that every work item carries a usable provenance trail (bug 1102 — we lost
/// 1102's attribution because the default was `id=""`).
///
/// Returns the canonical origin JSON string on success. Returns `Err` with a
/// human-readable explanation when the caller failed to identify itself; the
/// caller (`tool_create_*` handlers) must propagate the error without creating
/// the work item, so a missing-attribution call leaves no half-state behind.
pub(super) fn build_origin(args: &serde_json::Value) -> Result<String, String> {
/// Returns `(origin_json, origin_label)` on success: the canonical origin
/// JSON string (for `crdt_state::set_origin`) alongside a human-readable
/// `"{kind} {id}"` label (for creation-notification display, story 1201).
/// Returns `Err` with a human-readable explanation when the caller failed to
/// identify itself; the caller (`tool_create_*` handlers) must propagate the
/// error without creating the work item, so a missing-attribution call
/// leaves no half-state behind.
pub(super) fn build_origin(args: &serde_json::Value) -> Result<(String, String), String> {
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
@@ -57,7 +60,8 @@ pub(super) fn build_origin(args: &serde_json::Value) -> Result<String, String> {
.unwrap_or("user");
let ts_val = origin_obj.get("ts").and_then(|v| v.as_f64()).unwrap_or(ts);
Ok(serde_json::json!({"kind": kind, "id": id, "ts": ts_val}).to_string())
let origin_json = serde_json::json!({"kind": kind, "id": id, "ts": ts_val}).to_string();
Ok((origin_json, format!("{kind} {id}")))
}
pub(crate) use bug::{tool_close_bug, tool_create_bug, tool_list_bugs};
+2 -1
View File
@@ -29,7 +29,7 @@ pub(crate) fn tool_create_refactor(args: &Value, ctx: &AppContext) -> Result<Str
// Bug 1102: resolve and validate origin BEFORE creating the refactor file
// so a missing-attribution call leaves no half-state behind.
let origin = super::build_origin(args)?;
let (origin, origin_label) = super::build_origin(args)?;
let root = ctx.state.get_project_root()?;
let refactor_id = create_refactor_file(
@@ -48,6 +48,7 @@ pub(crate) fn tool_create_refactor(args: &Value, ctx: &AppContext) -> Result<Str
item_id: refactor_id.clone(),
item_type: "refactor".to_string(),
name: req.name.as_ref().to_string(),
origin: origin_label,
});
Ok(format!("Created refactor: {refactor_id}"))
+2 -1
View File
@@ -29,7 +29,7 @@ pub(crate) fn tool_create_spike(args: &Value, ctx: &AppContext) -> Result<String
// Bug 1102: resolve and validate origin BEFORE creating the spike file so
// a missing-attribution call leaves no half-state behind.
let origin = super::build_origin(args)?;
let (origin, origin_label) = super::build_origin(args)?;
let root = ctx.state.get_project_root()?;
let spike_id = create_spike_file(
@@ -48,6 +48,7 @@ pub(crate) fn tool_create_spike(args: &Value, ctx: &AppContext) -> Result<String
item_id: spike_id.clone(),
item_type: "spike".to_string(),
name: req.name.as_ref().to_string(),
origin: origin_label,
});
Ok(format!("Created spike: {spike_id}"))
@@ -17,7 +17,7 @@ pub(crate) fn tool_create_story(args: &Value, ctx: &AppContext) -> Result<String
// Bug 1102: resolve and validate origin BEFORE creating the story file so
// a missing-attribution call leaves no half-state behind.
let origin = super::super::build_origin(args)?;
let (origin, origin_label) = super::super::build_origin(args)?;
let root = ctx.state.get_project_root()?;
let depends_on_ids = req.depends_on_ids();
@@ -43,6 +43,7 @@ pub(crate) fn tool_create_story(args: &Value, ctx: &AppContext) -> Result<String
item_id: story_id.clone(),
item_type: "story".to_string(),
name: req.name.as_ref().to_string(),
origin: origin_label,
});
// Bug 503: warn at creation time if any depends_on points at an already-archived story.
@@ -188,6 +189,29 @@ mod tests {
);
}
/// AC4 (story 1201): a create call must succeed even when the
/// notification pipeline can't deliver — `test_ctx`'s `watcher_tx` has
/// zero receivers, so the `send` inside `tool_create_story` returns
/// `Err`, which must be swallowed rather than failing the create call.
#[test]
fn tool_create_story_succeeds_when_no_notification_listener_present() {
crate::db::ensure_content_store();
let tmp = tempfile::tempdir().unwrap();
let ctx = test_ctx(tmp.path());
let result = tool_create_story(
&json!({
"name": "No Listener Story",
"acceptance_criteria": ["It works"],
"origin": {"kind": "test", "id": "test-suite"}
}),
&ctx,
);
assert!(
result.is_ok(),
"create must succeed even when notification delivery fails: {result:?}"
);
}
#[test]
fn tool_create_story_missing_name() {
let tmp = tempfile::tempdir().unwrap();