fix: defer intake claim until launch acceptance

This commit is contained in:
Keisuke Hirata 2026-06-08 12:23:06 +09:00
parent d2a040d77a
commit 6797be30f0
No known key found for this signature in database

View File

@ -294,7 +294,12 @@ pub(crate) enum IntakeRegistryUpdate {
origin: RoleSessionOrigin, origin: RoleSessionOrigin,
related_tickets: Vec<RelatedTicketRef>, related_tickets: Vec<RelatedTicketRef>,
}, },
ClaimedTicket, ClaimTicket {
registry_root: PathBuf,
ticket_id: String,
ticket_slug: Option<String>,
pod_name: String,
},
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@ -447,28 +452,7 @@ async fn launch_intake_with_handoff(request: IntakeLaunchRequest) -> IntakeLaunc
options, options,
) )
.await?; .await?;
let registry_warning = match request.registry_update { let registry_warning = commit_intake_registry_update(request.registry_update);
IntakeRegistryUpdate::RecordSession {
registry_root,
pod_name,
origin,
related_tickets,
} => PanelRegistryStore::from_root(registry_root)
.record_session(
pod_name,
TicketRole::Intake.as_str().to_string(),
origin,
None,
related_tickets,
)
.err()
.map(|error| {
bounded_panel_diagnostic(format!(
"local role session registry could not be updated after Intake launch: {error}"
))
}),
IntakeRegistryUpdate::ClaimedTicket => None,
};
let peer_registration = match (orchestrator_pod, skip_warning) { let peer_registration = match (orchestrator_pod, skip_warning) {
(_, Some(warning)) => warning, (_, Some(warning)) => warning,
(Some(orchestrator_pod), None) if launch.pre_run_warnings.is_empty() => { (Some(orchestrator_pod), None) if launch.pre_run_warnings.is_empty() => {
@ -493,6 +477,46 @@ async fn launch_intake_with_handoff(request: IntakeLaunchRequest) -> IntakeLaunc
}) })
} }
fn commit_intake_registry_update(update: IntakeRegistryUpdate) -> Option<String> {
match update {
IntakeRegistryUpdate::RecordSession {
registry_root,
pod_name,
origin,
related_tickets,
} => PanelRegistryStore::from_root(registry_root)
.record_session(
pod_name,
TicketRole::Intake.as_str().to_string(),
origin,
None,
related_tickets,
)
.err()
.map(|error| {
bounded_panel_diagnostic(format!(
"local role session registry could not be updated after Intake launch: {error}"
))
}),
IntakeRegistryUpdate::ClaimTicket {
registry_root,
ticket_id,
ticket_slug,
pod_name,
} => match PanelRegistryStore::from_root(registry_root).claim_ticket(
&ticket_id,
ticket_slug.as_deref(),
&pod_name,
TicketRole::Intake.as_str(),
) {
Ok(TicketClaimResult::Claimed) | Ok(TicketClaimResult::AlreadyOwned(_)) => None,
Err(error) => Some(bounded_panel_diagnostic(format!(
"local Ticket Intake claim could not be committed after launch acceptance: {error}"
))),
},
}
}
pub(crate) struct MultiPodApp { pub(crate) struct MultiPodApp {
pub(crate) list: PodList, pub(crate) list: PodList,
pub(crate) panel: WorkspacePanelViewModel, pub(crate) panel: WorkspacePanelViewModel,
@ -1069,6 +1093,13 @@ impl MultiPodApp {
} }
pub(crate) fn prepare_existing_ticket_intake_launch(&mut self) -> Option<IntakeLaunchRequest> { pub(crate) fn prepare_existing_ticket_intake_launch(&mut self) -> Option<IntakeLaunchRequest> {
if self.sending {
self.notice = Some(
"Ticket Intake launch is already in progress; wait for it to finish before retrying."
.to_string(),
);
return None;
}
let row = match self.selected_panel_row() { let row = match self.selected_panel_row() {
Some(row) if row.is_ticket_action() => row, Some(row) if row.is_ticket_action() => row,
Some(row) if row.ticket.is_some() => { Some(row) if row.ticket.is_some() => {
@ -1140,18 +1171,7 @@ impl MultiPodApp {
} }
}; };
context.pod_name = Some(planned.pod_name.clone()); context.pod_name = Some(planned.pod_name.clone());
match store.claim_ticket( let pod_name = planned.pod_name.clone();
&ticket_id,
Some(&ticket_slug),
&planned.pod_name,
TicketRole::Intake.as_str(),
) {
Ok(TicketClaimResult::Claimed) | Ok(TicketClaimResult::AlreadyOwned(_)) => {}
Err(error) => {
self.notice = Some(format!("Ticket claim diagnostic required: {error}"));
return None;
}
}
let peer_registration = self.prepare_intake_peer_registration(&mut context); let peer_registration = self.prepare_intake_peer_registration(&mut context);
self.sending = true; self.sending = true;
self.notice = Some(format!( self.notice = Some(format!(
@ -1162,7 +1182,12 @@ impl MultiPodApp {
context, context,
runtime_command: self.runtime_command.clone(), runtime_command: self.runtime_command.clone(),
peer_registration, peer_registration,
registry_update: IntakeRegistryUpdate::ClaimedTicket, registry_update: IntakeRegistryUpdate::ClaimTicket {
registry_root: store.root().to_path_buf(),
ticket_id,
ticket_slug: Some(ticket_slug),
pod_name,
},
}) })
} }
@ -4305,6 +4330,82 @@ mod tests {
assert!(app.notice.as_deref().unwrap().contains("composer kept")); assert!(app.notice.as_deref().unwrap().contains("composer kept"));
} }
#[test]
fn intake_registry_update_claim_is_durable_only_after_commit() {
let temp = TempDir::new().unwrap();
let root = temp.path().join("registry");
let store = PanelRegistryStore::from_root(root.clone());
let update = IntakeRegistryUpdate::ClaimTicket {
registry_root: root,
ticket_id: "20260608-000000-existing".to_string(),
ticket_slug: Some("existing".to_string()),
pod_name: "existing-intake".to_string(),
};
assert!(
store
.claim_for_ticket("20260608-000000-existing")
.unwrap()
.is_none(),
"holding a pending Intake registry update must not persist a Ticket claim"
);
assert!(commit_intake_registry_update(update.clone()).is_none());
assert!(
store
.claim_for_ticket("20260608-000000-existing")
.unwrap()
.is_some(),
"the claim is persisted only by the post-acceptance commit step"
);
assert!(commit_intake_registry_update(update).is_none());
let snapshot = store.snapshot().unwrap();
assert_eq!(snapshot.claims.len(), 1);
assert_eq!(snapshot.sessions.len(), 1);
assert_eq!(snapshot.sessions[0].pod_name, "existing-intake");
assert_eq!(snapshot.sessions[0].origin, RoleSessionOrigin::TicketClaim);
assert_eq!(snapshot.sessions[0].related_tickets.len(), 1);
assert_eq!(
snapshot.sessions[0].related_tickets[0].id,
"20260608-000000-existing"
);
}
#[test]
fn intake_registry_update_claim_conflict_is_diagnostic_not_overwrite() {
let temp = TempDir::new().unwrap();
let root = temp.path().join("registry");
let store = PanelRegistryStore::from_root(root.clone());
store
.claim_ticket(
"20260608-000001-existing",
Some("existing"),
"first-intake",
TicketRole::Intake.as_str(),
)
.unwrap();
let warning = commit_intake_registry_update(IntakeRegistryUpdate::ClaimTicket {
registry_root: root,
ticket_id: "20260608-000001-existing".to_string(),
ticket_slug: Some("existing".to_string()),
pod_name: "second-intake".to_string(),
})
.expect("conflicting post-success claim should be reported");
assert!(warning.contains("could not be committed"));
let claim = store
.claim_for_ticket("20260608-000001-existing")
.unwrap()
.unwrap();
assert_eq!(claim.pod_name, "first-intake");
let snapshot = store.snapshot().unwrap();
assert_eq!(snapshot.claims.len(), 1);
assert_eq!(snapshot.sessions.len(), 1);
assert_eq!(snapshot.sessions[0].pod_name, "first-intake");
}
#[test] #[test]
fn multi_empty_enter_on_non_openable_row_reports_open_diagnostic() { fn multi_empty_enter_on_non_openable_row_reports_open_diagnostic() {
let mut app = test_app(vec![unreachable_live_info("unreachable")]); let mut app = test_app(vec![unreachable_live_info("unreachable")]);