huskies: merge 945

This commit is contained in:
dave
2026-05-13 06:05:01 +00:00
parent 3a8894ea8f
commit 9ce5a8df0c
53 changed files with 497 additions and 654 deletions
+11 -21
View File
@@ -57,21 +57,20 @@ mod tests {
write_story(
&stage_dir,
"10_story_shadow_test.md",
"---\nname: Shadow Test\nagent: coder-opus\nretry_count: 2\nblocked: false\n---\n# Story\n",
"---\nname: Shadow Test\nagent: coder-opus\nretry_count: 2\n---\n# Story\n",
);
// Perform the upsert directly (bypass the global singleton).
let now = chrono::Utc::now().to_rfc3339();
sqlx::query(
"INSERT INTO pipeline_items \
(id, name, stage, agent, retry_count, blocked, depends_on, content, created_at, updated_at) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?9) \
(id, name, stage, agent, retry_count, depends_on, content, created_at, updated_at) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?8) \
ON CONFLICT(id) DO UPDATE SET \
name = excluded.name, \
stage = excluded.stage, \
agent = excluded.agent, \
retry_count = excluded.retry_count, \
blocked = excluded.blocked, \
depends_on = excluded.depends_on, \
content = COALESCE(excluded.content, pipeline_items.content), \
updated_at = excluded.updated_at",
@@ -81,7 +80,6 @@ mod tests {
.bind("2_current")
.bind("coder-opus")
.bind(2_i64)
.bind(0_i64)
.bind(Option::<String>::None)
.bind("---\nname: Shadow Test\n---\n# Story\n")
.bind(&now)
@@ -122,15 +120,14 @@ mod tests {
let content = "---\nname: Test\n---\n# Story\n";
sqlx::query(
"INSERT INTO pipeline_items \
(id, name, stage, agent, retry_count, blocked, depends_on, content, created_at, updated_at) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?9)",
(id, name, stage, agent, retry_count, depends_on, content, created_at, updated_at) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?8)",
)
.bind("99_story_col_test")
.bind(Option::<String>::None)
.bind("1_backlog")
.bind(Option::<String>::None)
.bind(Option::<i64>::None)
.bind(Option::<i64>::None)
.bind(Option::<String>::None)
.bind(content)
.bind(&now)
@@ -162,15 +159,14 @@ mod tests {
// Insert initial row in backlog.
sqlx::query(
"INSERT INTO pipeline_items \
(id, name, stage, agent, retry_count, blocked, depends_on, content, created_at, updated_at) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?9)",
(id, name, stage, agent, retry_count, depends_on, content, created_at, updated_at) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?8)",
)
.bind("5_story_move")
.bind("Move Me")
.bind("1_backlog")
.bind(Option::<String>::None)
.bind(Option::<i64>::None)
.bind(Option::<i64>::None)
.bind(Option::<String>::None)
.bind("---\nname: Move Me\n---\n")
.bind(&now)
@@ -181,14 +177,13 @@ mod tests {
// Upsert with new stage (simulating move to current).
sqlx::query(
"INSERT INTO pipeline_items \
(id, name, stage, agent, retry_count, blocked, depends_on, content, created_at, updated_at) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?9) \
(id, name, stage, agent, retry_count, depends_on, content, created_at, updated_at) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?8) \
ON CONFLICT(id) DO UPDATE SET \
name = excluded.name, \
stage = excluded.stage, \
agent = excluded.agent, \
retry_count = excluded.retry_count, \
blocked = excluded.blocked, \
depends_on = excluded.depends_on, \
content = COALESCE(excluded.content, pipeline_items.content), \
updated_at = excluded.updated_at",
@@ -198,7 +193,6 @@ mod tests {
.bind("2_current")
.bind(Option::<String>::None)
.bind(Option::<i64>::None)
.bind(Option::<i64>::None)
.bind(Option::<String>::None)
.bind(Option::<String>::None) // content NULL → COALESCE preserves existing
.bind(&now)
@@ -266,15 +260,14 @@ mod tests {
// Insert a live row.
sqlx::query(
"INSERT INTO pipeline_items \
(id, name, stage, agent, retry_count, blocked, depends_on, content, created_at, updated_at) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?9)",
(id, name, stage, agent, retry_count, depends_on, content, created_at, updated_at) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?8)",
)
.bind("42_story_to_delete")
.bind("Delete Me")
.bind("2_current")
.bind(Option::<String>::None)
.bind(Option::<i64>::None)
.bind(Option::<i64>::None)
.bind(Option::<String>::None)
.bind("---\nname: Delete Me\n---\n")
.bind(&now)
@@ -322,7 +315,6 @@ mod tests {
name: Some("Typed Name".into()),
agent: Some("coder-1".into()),
retry_count: Some(2),
blocked: Some(true),
depends_on: Some(vec![100, 200]),
};
write_item_with_content(story_id, "2_current", content, meta);
@@ -332,7 +324,6 @@ mod tests {
assert_eq!(view.name(), Some("Typed Name"));
assert_eq!(view.agent(), Some("coder-1"));
assert_eq!(view.retry_count(), 2);
assert!(view.blocked());
assert_eq!(view.depends_on(), &[100, 200]);
// Content is stored verbatim (no parsing, no rewrite).
@@ -416,7 +407,6 @@ mod tests {
None,
None,
None,
None,
);
write_content(
story_id,
+7 -15
View File
@@ -18,7 +18,6 @@ pub struct ItemMeta {
pub name: Option<String>,
pub agent: Option<String>,
pub retry_count: Option<i64>,
pub blocked: Option<bool>,
pub depends_on: Option<Vec<u32>>,
}
@@ -50,12 +49,11 @@ fn normalise_stage_str(stage: &str) -> &str {
"3_qa" => "qa",
"4_merge" => "merge",
"4_merge_failure" => "merge_failure",
"4_merge_failure_final" => "merge_failure_final",
"5_done" => "done",
"6_archived" => "archived",
// `7_frozen` has no direct clean equivalent (the variant was
// removed in story 934 stage 4). Returning the unmapped string
// makes `Stage::from_dir` return None, so the write is logged and
// skipped — frozen items should be seeded via the `frozen` flag.
"7_frozen" => "frozen",
"7_review_hold" => "review_hold",
other => other,
}
}
@@ -94,7 +92,6 @@ pub fn write_item_with_content(story_id: &str, stage: &str, content: &str, meta:
meta.name.as_deref(),
meta.agent.as_deref(),
meta.retry_count,
meta.blocked,
depends_on_json.as_deref(),
None,
None,
@@ -109,7 +106,6 @@ pub fn write_item_with_content(story_id: &str, stage: &str, content: &str, meta:
name: meta.name,
agent: meta.agent,
retry_count: meta.retry_count,
blocked: meta.blocked,
depends_on: depends_on_json,
content: Some(content.to_string()),
};
@@ -139,10 +135,10 @@ pub fn move_item_stage(
_ => None,
};
// Story 929: metadata (name/agent/blocked/depends_on) is owned by the
// CRDT typed registers — no need to re-derive it from the content body's
// YAML front matter on every stage transition. Pass `None` for those
// fields so write_item leaves the existing registers untouched.
// Story 929: metadata (name/agent/depends_on) is owned by the CRDT typed
// registers — no need to re-derive it from the content body's YAML front
// matter on every stage transition. Pass `None` for those fields so
// write_item leaves the existing registers untouched.
let new_stage = normalise_stage_str(new_stage);
let Some(typed_stage) = crate::pipeline_state::Stage::from_dir(new_stage) else {
crate::slog!(
@@ -161,7 +157,6 @@ pub fn move_item_stage(
None,
None,
None,
None,
merged_at_ts,
);
// Bug 780: stage transitions reset retry_count to 0. retry_count tracks
@@ -177,7 +172,6 @@ pub fn move_item_stage(
let view = crate::crdt_state::read_item(story_id);
let name = view.as_ref().and_then(|v| v.name().map(str::to_string));
let agent = view.as_ref().and_then(|v| v.agent().map(str::to_string));
let blocked = view.as_ref().map(|v| v.blocked());
let depends_on = view
.as_ref()
.map(|v| v.depends_on())
@@ -189,7 +183,6 @@ pub fn move_item_stage(
name,
agent,
retry_count: Some(0),
blocked,
depends_on,
content,
};
@@ -212,7 +205,6 @@ pub fn delete_item(story_id: &str) {
name: None,
agent: None,
retry_count: None,
blocked: None,
depends_on: None,
content: None,
};
+2 -5
View File
@@ -18,7 +18,6 @@ pub(super) struct PipelineWriteMsg {
pub(super) name: Option<String>,
pub(super) agent: Option<String>,
pub(super) retry_count: Option<i64>,
pub(super) blocked: Option<bool>,
pub(super) depends_on: Option<String>,
pub(super) content: Option<String>,
}
@@ -84,14 +83,13 @@ pub async fn init(db_path: &Path) -> Result<(), sqlx::Error> {
let now = chrono::Utc::now().to_rfc3339();
let result = sqlx::query(
"INSERT INTO pipeline_items \
(id, name, stage, agent, retry_count, blocked, depends_on, content, created_at, updated_at) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?9) \
(id, name, stage, agent, retry_count, depends_on, content, created_at, updated_at) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?8) \
ON CONFLICT(id) DO UPDATE SET \
name = excluded.name, \
stage = excluded.stage, \
agent = excluded.agent, \
retry_count = excluded.retry_count, \
blocked = excluded.blocked, \
depends_on = excluded.depends_on, \
content = COALESCE(excluded.content, pipeline_items.content), \
updated_at = excluded.updated_at",
@@ -101,7 +99,6 @@ pub async fn init(db_path: &Path) -> Result<(), sqlx::Error> {
.bind(&msg.stage)
.bind(&msg.agent)
.bind(msg.retry_count)
.bind(msg.blocked.map(|b| b as i64))
.bind(&msg.depends_on)
.bind(&msg.content)
.bind(&now)