huskies: merge 1007
This commit is contained in:
@@ -94,17 +94,39 @@ pub(crate) fn tool_create_story(args: &Value, ctx: &AppContext) -> Result<String
|
||||
/// - Is persisted to `crdt_ops` so the eviction survives a server restart
|
||||
/// - Drops the in-memory `CONTENT_STORE` entry for the story
|
||||
///
|
||||
/// Half-written items (content store has a row but the CRDT has no entry —
|
||||
/// the bug 1001 split-brain scenario) are also handled: if `evict_item` fails
|
||||
/// because the CRDT entry is absent, the tool checks the content store and
|
||||
/// removes the orphaned row, returning success instead of an error.
|
||||
///
|
||||
/// This tool does NOT touch: running agents, worktrees, the `pipeline_items`
|
||||
/// shadow table, `timers.json`, or filesystem shadows. Compose with
|
||||
/// `stop_agent`, `remove_worktree`, etc. as needed for a full purge — or
|
||||
/// see story 514 (delete_story full cleanup) for a future "do it all" tool.
|
||||
/// use `delete_story` for a complete cleanup sequence.
|
||||
pub(crate) fn tool_purge_story(args: &Value, _ctx: &AppContext) -> Result<String, String> {
|
||||
let story_id = args
|
||||
.get("story_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("Missing required argument: story_id")?;
|
||||
|
||||
crate::crdt_state::evict_item(story_id)?;
|
||||
if let Err(evict_err) = crate::crdt_state::evict_item(story_id) {
|
||||
// evict_item failed — either the CRDT is not initialised or the item
|
||||
// has no live CRDT entry. Handle the half-written-item case: the
|
||||
// content store has the row but the CRDT doesn't (bug 1001 remnant).
|
||||
let in_content_store =
|
||||
crate::db::read_content(crate::db::ContentKey::Story(story_id)).is_some();
|
||||
if in_content_store {
|
||||
crate::db::delete_item(story_id);
|
||||
return Ok(format!(
|
||||
"'{story_id}' had no CRDT entry but existed in the content store \
|
||||
(half-written item — likely a pre-fix bug 1001 artifact). \
|
||||
Removed the orphaned content-store row."
|
||||
));
|
||||
}
|
||||
return Err(format!(
|
||||
"'{story_id}' not found in CRDT or content store: {evict_err}"
|
||||
));
|
||||
}
|
||||
|
||||
Ok(format!(
|
||||
"Evicted '{story_id}' from in-memory CRDT (tombstone op persisted to crdt_ops; CONTENT_STORE entry dropped)."
|
||||
@@ -117,6 +139,60 @@ mod tests {
|
||||
use crate::http::test_helpers::test_ctx;
|
||||
use serde_json::json;
|
||||
|
||||
/// Regression for bug 1001: `purge_story` must succeed on a half-written
|
||||
/// item (content store has a row but the CRDT has no entry).
|
||||
#[test]
|
||||
fn tool_purge_story_cleans_half_written_item() {
|
||||
crate::crdt_state::init_for_test();
|
||||
crate::db::ensure_content_store();
|
||||
|
||||
// Seed a content-store row without a corresponding CRDT entry
|
||||
// by writing then tombstoning, then re-writing the content store
|
||||
// directly — this mimics the pre-fix half-write scenario.
|
||||
let story_id = "9960_story_purge_halfwrite";
|
||||
crate::db::write_item_with_content(
|
||||
story_id,
|
||||
"1_backlog",
|
||||
"---\nname: Half Written\n---\n",
|
||||
crate::db::ItemMeta::named("Half Written"),
|
||||
);
|
||||
crate::crdt_state::evict_item(story_id).expect("evict must succeed for setup");
|
||||
// Re-inject only the content row (simulating the bug 1001 half-write).
|
||||
crate::db::write_content(
|
||||
crate::db::ContentKey::Story(story_id),
|
||||
"---\nname: Half Written\n---\n",
|
||||
);
|
||||
// CRDT must have no entry at this point.
|
||||
assert!(
|
||||
crate::crdt_state::read_item(story_id).is_none(),
|
||||
"setup: CRDT must not have the item"
|
||||
);
|
||||
// But content store must have it.
|
||||
assert!(
|
||||
crate::db::read_content(crate::db::ContentKey::Story(story_id)).is_some(),
|
||||
"setup: content store must have the item"
|
||||
);
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = test_ctx(tmp.path());
|
||||
let result = tool_purge_story(&json!({"story_id": story_id}), &ctx);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"purge_story must succeed on a half-written item: {result:?}"
|
||||
);
|
||||
let msg = result.unwrap();
|
||||
assert!(
|
||||
msg.contains("half-written") || msg.contains("orphaned"),
|
||||
"result should mention the half-write: {msg}"
|
||||
);
|
||||
|
||||
// Content store must be clean after purge.
|
||||
assert!(
|
||||
crate::db::read_content(crate::db::ContentKey::Story(story_id)).is_none(),
|
||||
"content store must be empty after purge of half-written item"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_create_story_missing_name() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user