huskies: merge 871
This commit is contained in:
@@ -120,6 +120,10 @@ pub async fn handle_assign(
|
|||||||
return format!("Failed to assign model to **{story_name}**: {e}");
|
return format!("Failed to assign model to **{story_name}**: {e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mirror the assignment into the CRDT register so the in-memory pipeline
|
||||||
|
// state stays consistent with the front-matter.
|
||||||
|
crate::crdt_state::set_agent(&story_id, Some(&agent_name));
|
||||||
|
|
||||||
// Check whether a coder is already running on this story.
|
// Check whether a coder is already running on this story.
|
||||||
let running_coders: Vec<_> = agents
|
let running_coders: Vec<_> = agents
|
||||||
.list_agents()
|
.list_agents()
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ pub use types::{
|
|||||||
};
|
};
|
||||||
pub use write::{
|
pub use write::{
|
||||||
bump_retry_count, migrate_names_from_slugs, migrate_story_ids_to_numeric, name_from_story_id,
|
bump_retry_count, migrate_names_from_slugs, migrate_story_ids_to_numeric, name_from_story_id,
|
||||||
set_qa_mode, set_retry_count, write_item,
|
set_agent, set_qa_mode, set_retry_count, write_item,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -186,6 +186,36 @@ pub fn migrate_names_from_slugs() {
|
|||||||
slog!("[crdt] Migrated names for {count} items from story ID slugs");
|
slog!("[crdt] Migrated names for {count} items from story ID slugs");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the `agent` field for a pipeline item by its story ID.
|
||||||
|
///
|
||||||
|
/// `Some(name)` writes the agent name into the CRDT register.
|
||||||
|
/// `None` clears the register by writing an empty string — use this
|
||||||
|
/// to unpin an agent without touching the surrounding item.
|
||||||
|
///
|
||||||
|
/// This is the typed setter counterpart to [`write_item`]'s `agent` parameter.
|
||||||
|
/// Callers that only need to update the agent (e.g. the `update_story` MCP tool
|
||||||
|
/// and the Matrix `!assign` command) should prefer this function over
|
||||||
|
/// passing `agent` through the full [`write_item`] call, which requires all
|
||||||
|
/// other fields to be known.
|
||||||
|
///
|
||||||
|
/// Returns `true` if the item was found and the write was performed.
|
||||||
|
pub fn set_agent(story_id: &str, agent: Option<&str>) -> bool {
|
||||||
|
let Some(state_mutex) = get_crdt() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Ok(mut state) = state_mutex.lock() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(&idx) = state.index.get(story_id) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let value = agent.unwrap_or("").to_string();
|
||||||
|
apply_and_persist(&mut state, |s| {
|
||||||
|
s.crdt.doc.items[idx].agent.set(value.clone())
|
||||||
|
});
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the typed `qa_mode` CRDT register for a pipeline item.
|
/// Set the typed `qa_mode` CRDT register for a pipeline item.
|
||||||
///
|
///
|
||||||
/// Passing `Some(mode)` writes the mode string (e.g. `"server"`, `"agent"`, `"human"`)
|
/// Passing `Some(mode)` writes the mode string (e.g. `"server"`, `"agent"`, `"human"`)
|
||||||
@@ -685,6 +715,79 @@ mod tests {
|
|||||||
migrate_names_from_slugs();
|
migrate_names_from_slugs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── set_agent tests ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_agent_some_writes_name() {
|
||||||
|
init_for_test();
|
||||||
|
|
||||||
|
write_item(
|
||||||
|
"871_story_set_agent_write",
|
||||||
|
"2_current",
|
||||||
|
Some("Set Agent Write"),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let found = set_agent("871_story_set_agent_write", Some("coder-1"));
|
||||||
|
assert!(found, "set_agent should return true for an existing item");
|
||||||
|
|
||||||
|
let item = read_item("871_story_set_agent_write").expect("item must exist");
|
||||||
|
assert_eq!(
|
||||||
|
item.agent.as_deref(),
|
||||||
|
Some("coder-1"),
|
||||||
|
"agent should be written to CRDT register"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_agent_none_clears_register() {
|
||||||
|
init_for_test();
|
||||||
|
|
||||||
|
write_item(
|
||||||
|
"871_story_set_agent_clear",
|
||||||
|
"2_current",
|
||||||
|
Some("Set Agent Clear"),
|
||||||
|
Some("coder-2"),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Confirm agent is set.
|
||||||
|
let before = read_item("871_story_set_agent_clear").expect("item must exist");
|
||||||
|
assert_eq!(before.agent.as_deref(), Some("coder-2"));
|
||||||
|
|
||||||
|
// Clear it.
|
||||||
|
let found = set_agent("871_story_set_agent_clear", None);
|
||||||
|
assert!(found, "set_agent should return true for an existing item");
|
||||||
|
|
||||||
|
let after = read_item("871_story_set_agent_clear").expect("item must exist");
|
||||||
|
assert!(
|
||||||
|
after.agent.as_deref().unwrap_or("").is_empty(),
|
||||||
|
"agent should be cleared (empty string) after set_agent(None)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_agent_returns_false_for_unknown_story() {
|
||||||
|
init_for_test();
|
||||||
|
|
||||||
|
let found = set_agent("999_story_nonexistent", Some("coder-1"));
|
||||||
|
assert!(
|
||||||
|
!found,
|
||||||
|
"set_agent should return false when story is not in the CRDT"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── set_qa_mode regression tests ─────────────────────────────────────────
|
// ── set_qa_mode regression tests ─────────────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -44,6 +44,19 @@ pub(crate) fn tool_update_story(args: &Value, ctx: &AppContext) -> Result<String
|
|||||||
Some(&front_matter)
|
Some(&front_matter)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Capture the agent value before moving front_matter into the file writer,
|
||||||
|
// so we can mirror it into the CRDT register below.
|
||||||
|
let agent_for_crdt = args
|
||||||
|
.get("agent")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.or_else(|| {
|
||||||
|
args.get("front_matter")
|
||||||
|
.and_then(|v| v.as_object())
|
||||||
|
.and_then(|o| o.get("agent"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
})
|
||||||
|
.map(str::to_string);
|
||||||
|
|
||||||
let root = ctx.state.get_project_root()?;
|
let root = ctx.state.get_project_root()?;
|
||||||
|
|
||||||
// Only call update_story_in_file when there is something left to write.
|
// Only call update_story_in_file when there is something left to write.
|
||||||
@@ -51,6 +64,12 @@ pub(crate) fn tool_update_story(args: &Value, ctx: &AppContext) -> Result<String
|
|||||||
update_story_in_file(&root, story_id, user_story, description, front_matter_opt)?;
|
update_story_in_file(&root, story_id, user_story, description, front_matter_opt)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mirror the agent assignment into the CRDT register so the in-memory
|
||||||
|
// pipeline state stays consistent with the front-matter.
|
||||||
|
if let Some(ref a) = agent_for_crdt {
|
||||||
|
crate::crdt_state::set_agent(story_id, Some(a));
|
||||||
|
}
|
||||||
|
|
||||||
// Bug 503: warn if any depends_on in the (now updated) story points at an archived story.
|
// Bug 503: warn if any depends_on in the (now updated) story points at an archived story.
|
||||||
let stage = crate::pipeline_state::read_typed(story_id)
|
let stage = crate::pipeline_state::read_typed(story_id)
|
||||||
.ok()
|
.ok()
|
||||||
|
|||||||
Reference in New Issue
Block a user