huskies: merge 1201 story Chat notification when a new work item is filed
This commit is contained in:
@@ -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}"))
|
||||
|
||||
@@ -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}"))
|
||||
}
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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}"))
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user