huskies: merge 1052
This commit is contained in:
@@ -55,8 +55,8 @@ pub use types::{
|
|||||||
pub use write::{
|
pub use write::{
|
||||||
bump_retry_count, migrate_legacy_stage_strings, migrate_merge_job, migrate_names_from_slugs,
|
bump_retry_count, migrate_legacy_stage_strings, migrate_merge_job, migrate_names_from_slugs,
|
||||||
migrate_node_claims_to_agent_claims, migrate_story_ids_to_numeric, name_from_story_id,
|
migrate_node_claims_to_agent_claims, migrate_story_ids_to_numeric, name_from_story_id,
|
||||||
set_agent, set_depends_on, set_epic, set_item_type, set_name, set_plan_state, set_qa_mode,
|
purge_done_stage_merge_jobs, set_agent, set_depends_on, set_epic, set_item_type, set_name,
|
||||||
set_resume_to, set_resume_to_raw, set_retry_count, write_item,
|
set_plan_state, set_qa_mode, set_resume_to, set_resume_to_raw, set_retry_count, write_item,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -650,6 +650,61 @@ pub fn migrate_merge_job(db_path: &std::path::Path) {
|
|||||||
slog!("[crdt] Migrated {count} MergeJob entries to typed MergeResult format");
|
slog!("[crdt] Migrated {count} MergeJob entries to typed MergeResult format");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete MergeJob CRDT entries for stories that have reached a terminal stage
|
||||||
|
/// (Done, Archived, Abandoned, Superseded, Rejected).
|
||||||
|
///
|
||||||
|
/// Pre-1036 code left stale MergeJob entries after a story recovered from a
|
||||||
|
/// merge failure and transitioned to Done. Those entries caused the gateway
|
||||||
|
/// UI to mislabel recovered stories as "FAILED" because `load_pipeline_state`
|
||||||
|
/// unconditionally read the MergeJob error field for every story. This
|
||||||
|
/// migration removes the orphaned entries so the state is clean on the next
|
||||||
|
/// server start.
|
||||||
|
///
|
||||||
|
/// Running this migration repeatedly is safe — tombstoned entries are filtered
|
||||||
|
/// out by the read path, so subsequent calls are no-ops.
|
||||||
|
pub fn purge_done_stage_merge_jobs() {
|
||||||
|
use crate::pipeline_state::Stage;
|
||||||
|
|
||||||
|
let Some(jobs) = crate::crdt_state::read_all_merge_jobs() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if jobs.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect IDs of every story currently in a terminal stage.
|
||||||
|
let terminal_ids: std::collections::HashSet<String> = crate::pipeline_state::read_all_typed()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|item| {
|
||||||
|
matches!(
|
||||||
|
item.stage,
|
||||||
|
Stage::Done { .. }
|
||||||
|
| Stage::Archived { .. }
|
||||||
|
| Stage::Abandoned { .. }
|
||||||
|
| Stage::Superseded { .. }
|
||||||
|
| Stage::Rejected { .. }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(|item| item.story_id.0.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let to_delete: Vec<String> = jobs
|
||||||
|
.into_iter()
|
||||||
|
.filter(|j| terminal_ids.contains(&j.story_id))
|
||||||
|
.map(|j| j.story_id)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if to_delete.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = to_delete.len();
|
||||||
|
for story_id in &to_delete {
|
||||||
|
crate::crdt_state::delete_merge_job(story_id);
|
||||||
|
}
|
||||||
|
slog!("[crdt] Purged {count} stale MergeJob entries for terminal-stage stories");
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod merge_job_migration_tests {
|
mod merge_job_migration_tests {
|
||||||
use super::super::super::state::init_for_test;
|
use super::super::super::state::init_for_test;
|
||||||
|
|||||||
@@ -19,4 +19,5 @@ pub use item::write_item_str;
|
|||||||
pub use migrations::{
|
pub use migrations::{
|
||||||
migrate_legacy_stage_strings, migrate_merge_job, migrate_names_from_slugs,
|
migrate_legacy_stage_strings, migrate_merge_job, migrate_names_from_slugs,
|
||||||
migrate_node_claims_to_agent_claims, migrate_story_ids_to_numeric, name_from_story_id,
|
migrate_node_claims_to_agent_claims, migrate_story_ids_to_numeric, name_from_story_id,
|
||||||
|
purge_done_stage_merge_jobs,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -116,7 +116,9 @@ pub fn load_pipeline_state(ctx: &AppContext) -> Result<PipelineState, String> {
|
|||||||
|
|
||||||
// Story 945: review_hold is `Stage::ReviewHold`; qa_mode and epic_id
|
// Story 945: review_hold is `Stage::ReviewHold`; qa_mode and epic_id
|
||||||
// come from typed CRDT registers. merge_failure detail lives on the
|
// come from typed CRDT registers. merge_failure detail lives on the
|
||||||
// MergeJob CRDT entry (same as status_tools.rs).
|
// MergeJob CRDT entry, but only surfaces for items currently in a
|
||||||
|
// failure stage — Done/terminal items must never inherit stale errors
|
||||||
|
// from a previously-failed merge attempt (story 1052).
|
||||||
let view = crate::crdt_state::read_item(sid);
|
let view = crate::crdt_state::read_item(sid);
|
||||||
let review_hold = if matches!(item.stage, Stage::ReviewHold { .. }) {
|
let review_hold = if matches!(item.stage, Stage::ReviewHold { .. }) {
|
||||||
Some(true)
|
Some(true)
|
||||||
@@ -125,7 +127,14 @@ pub fn load_pipeline_state(ctx: &AppContext) -> Result<PipelineState, String> {
|
|||||||
};
|
};
|
||||||
let qa = view.as_ref().and_then(|v| v.qa_mode());
|
let qa = view.as_ref().and_then(|v| v.qa_mode());
|
||||||
let epic_id = view.as_ref().and_then(|v| v.epic());
|
let epic_id = view.as_ref().and_then(|v| v.epic());
|
||||||
let merge_failure = crate::crdt_state::read_merge_job(sid).and_then(|j| j.error);
|
let merge_failure = if matches!(
|
||||||
|
item.stage,
|
||||||
|
Stage::MergeFailure { .. } | Stage::MergeFailureFinal { .. }
|
||||||
|
) {
|
||||||
|
crate::crdt_state::read_merge_job(sid).and_then(|j| j.error)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let story = UpcomingStory {
|
let story = UpcomingStory {
|
||||||
story_id: sid.clone(),
|
story_id: sid.clone(),
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ pub fn aggregate_pipeline_counts(pipeline: &Value) -> Value {
|
|||||||
.map(|f| !f.is_null() && f != "")
|
.map(|f| !f.is_null() && f != "")
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
if is_blocked || has_merge_failure {
|
if (is_blocked || has_merge_failure) && stage != "done" {
|
||||||
let story_id = item
|
let story_id = item
|
||||||
.get("story_id")
|
.get("story_id")
|
||||||
.and_then(|s| s.as_str())
|
.and_then(|s| s.as_str())
|
||||||
|
|||||||
@@ -167,6 +167,9 @@ pub(crate) async fn init_subsystems(app_state: &Arc<SessionState>, cwd: &Path) {
|
|||||||
crdt_state::migrate_merge_job(db_path);
|
crdt_state::migrate_merge_job(db_path);
|
||||||
// Story 1009: drop legacy node-hex claims that can't be converted to AgentName.
|
// Story 1009: drop legacy node-hex claims that can't be converted to AgentName.
|
||||||
crdt_state::migrate_node_claims_to_agent_claims();
|
crdt_state::migrate_node_claims_to_agent_claims();
|
||||||
|
// Story 1052: remove stale MergeJob entries for terminal-stage
|
||||||
|
// stories so they can never cause "FAILED" labels in the UI.
|
||||||
|
crdt_state::purge_done_stage_merge_jobs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user