ticket: remove action attention fields
This commit is contained in:
@@ -2109,18 +2109,6 @@ fn panel_close_blocker(ticket: &ticket::Ticket) -> Option<String> {
|
||||
ticket.meta.workflow_state.as_str()
|
||||
));
|
||||
}
|
||||
if let Some(reason) = non_empty_ticket_field(ticket.meta.attention_required.as_deref()) {
|
||||
return Some(format!(
|
||||
"Close blocked for Ticket {ticket_id}: attention_required is set ({}); no close was recorded.",
|
||||
bounded_panel_diagnostic(reason)
|
||||
));
|
||||
}
|
||||
if let Some(reason) = non_empty_ticket_field(ticket.meta.action_required.as_deref()) {
|
||||
return Some(format!(
|
||||
"Close blocked for Ticket {ticket_id}: action_required is set ({}); no close was recorded.",
|
||||
bounded_panel_diagnostic(reason)
|
||||
));
|
||||
}
|
||||
if ticket.resolution.is_some() {
|
||||
return Some(format!(
|
||||
"Close blocked for Ticket {ticket_id}: resolution.md already exists; no close was recorded."
|
||||
@@ -2129,10 +2117,6 @@ fn panel_close_blocker(ticket: &ticket::Ticket) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn non_empty_ticket_field(value: Option<&str>) -> Option<&str> {
|
||||
value.map(str::trim).filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn panel_close_resolution(
|
||||
ticket: &ticket::Ticket,
|
||||
record_language: Option<&str>,
|
||||
@@ -3295,48 +3279,6 @@ mod tests {
|
||||
}));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ticket_close_action_blocks_action_required_without_mutation() {
|
||||
let (temp, ticket_id, backend) = ticket_workspace(
|
||||
"panel-close-action-required",
|
||||
TicketWorkflowState::Done,
|
||||
|input| {
|
||||
input.action_required = Some("human decision needed".to_string());
|
||||
},
|
||||
);
|
||||
|
||||
let error =
|
||||
dispatch_ticket_action(request_for(&temp, ticket_id.clone(), NextUserAction::Close))
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(error.to_string().contains("action_required is set"));
|
||||
assert!(error.to_string().contains("no close was recorded"));
|
||||
let ticket = backend.show(TicketIdOrSlug::Id(ticket_id)).unwrap();
|
||||
assert_eq!(ticket.meta.workflow_state, TicketWorkflowState::Done);
|
||||
assert!(ticket.resolution.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ticket_close_action_blocks_attention_required_without_mutation() {
|
||||
let (temp, ticket_id, backend) = ticket_workspace(
|
||||
"panel-close-attention-required",
|
||||
TicketWorkflowState::Done,
|
||||
|input| {
|
||||
input.attention_required = Some("needs reply".to_string());
|
||||
},
|
||||
);
|
||||
|
||||
let error =
|
||||
dispatch_ticket_action(request_for(&temp, ticket_id.clone(), NextUserAction::Close))
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(error.to_string().contains("attention_required is set"));
|
||||
let ticket = backend.show(TicketIdOrSlug::Id(ticket_id)).unwrap();
|
||||
assert!(ticket.resolution.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ticket_close_action_blocks_existing_resolution_without_moving_ticket() {
|
||||
let (temp, ticket_id, backend) = done_ticket_workspace("panel-close-resolution");
|
||||
@@ -3489,8 +3431,8 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
||||
let mut ticket = NewTicket::new("Needs Human Reply");
|
||||
ticket.action_required = Some("answer intake question".to_string());
|
||||
let mut ticket = NewTicket::new("Ready Ticket");
|
||||
ticket.workflow_state = Some(TicketWorkflowState::Ready);
|
||||
backend.create(ticket).unwrap();
|
||||
let list = PodList::from_sources(
|
||||
PodVisibilitySource::ResumePicker,
|
||||
@@ -3502,7 +3444,7 @@ mod tests {
|
||||
let panel = build_workspace_panel(temp.path(), &list);
|
||||
let mut app = app_with_panel(list, panel);
|
||||
|
||||
assert_eq!(app.selected_panel_row().unwrap().title, "Needs Human Reply");
|
||||
assert_eq!(app.selected_panel_row().unwrap().title, "Ready Ticket");
|
||||
assert_eq!(app.selected_open_eligibility(), OpenEligibility::Disabled);
|
||||
let lines = list_lines(&app, 100, 6)
|
||||
.into_iter()
|
||||
@@ -3510,7 +3452,7 @@ mod tests {
|
||||
.collect::<Vec<_>>();
|
||||
let ticket_line = lines
|
||||
.iter()
|
||||
.position(|line| line.contains("Needs Human Reply"))
|
||||
.position(|line| line.contains("Ready Ticket"))
|
||||
.unwrap();
|
||||
let pod_line = lines.iter().position(|line| line.contains("idle")).unwrap();
|
||||
assert!(ticket_line < pod_line);
|
||||
@@ -4885,7 +4827,6 @@ mod tests {
|
||||
workflow_state: TicketWorkflowState::parse(state)
|
||||
.unwrap_or(TicketWorkflowState::Planning),
|
||||
workflow_state_explicit: true,
|
||||
attention_required: None,
|
||||
next_action: Some(next_action),
|
||||
updated_at: None,
|
||||
latest_event_kind: Some("implementation_report".to_string()),
|
||||
|
||||
@@ -233,7 +233,6 @@ pub(crate) struct TicketPanelEntry {
|
||||
pub(crate) priority: String,
|
||||
pub(crate) workflow_state: TicketWorkflowState,
|
||||
pub(crate) workflow_state_explicit: bool,
|
||||
pub(crate) attention_required: Option<String>,
|
||||
pub(crate) next_action: Option<NextUserAction>,
|
||||
pub(crate) updated_at: Option<String>,
|
||||
pub(crate) latest_event_kind: Option<String>,
|
||||
@@ -620,10 +619,8 @@ fn ticket_summary_from_meta(meta: &TicketMeta) -> TicketSummary {
|
||||
priority: meta.priority.clone(),
|
||||
labels: meta.labels.clone(),
|
||||
readiness: meta.readiness.clone(),
|
||||
action_required: meta.action_required.clone(),
|
||||
workflow_state: meta.workflow_state,
|
||||
workflow_state_explicit: meta.workflow_state_explicit,
|
||||
attention_required: meta.attention_required.clone(),
|
||||
queued_by: meta.queued_by.clone(),
|
||||
queued_at: meta.queued_at.clone(),
|
||||
updated_at: meta.updated_at.clone(),
|
||||
@@ -669,7 +666,6 @@ fn ticket_row(
|
||||
priority: summary.priority.clone(),
|
||||
workflow_state: summary.workflow_state,
|
||||
workflow_state_explicit: summary.workflow_state_explicit,
|
||||
attention_required: summary.attention_required.clone(),
|
||||
next_action: derived.action,
|
||||
updated_at: summary.updated_at.clone(),
|
||||
latest_event_kind: latest_event.map(|event| event.kind.as_str().to_string()),
|
||||
@@ -735,26 +731,6 @@ fn derive_ticket_state(
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(reason) = summary
|
||||
.attention_required
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
return DerivedTicketState {
|
||||
kind: PanelRowKind::Blocked,
|
||||
priority: ActionPriority::UserReply,
|
||||
action: Some(NextUserAction::Edit),
|
||||
disabled_reason: Some(
|
||||
"attention_required is set; resolve it before queueing or routing.".to_string(),
|
||||
),
|
||||
key_hint: Some(
|
||||
"Resolve attention_required in the Ticket frontmatter/thread".to_string(),
|
||||
),
|
||||
blocked_reason: Some(reason.to_string()),
|
||||
};
|
||||
}
|
||||
|
||||
match summary.workflow_state {
|
||||
TicketWorkflowState::Ready => DerivedTicketState {
|
||||
kind: PanelRowKind::Ticket,
|
||||
@@ -870,9 +846,6 @@ pub(crate) fn local_claim_status_for_pod(pod_name: &str, pods: &PodList) -> Tick
|
||||
|
||||
fn ticket_subtitle(entry: &TicketPanelEntry) -> Option<String> {
|
||||
let mut parts = vec![format!("{} · {}", entry.id, entry.workflow_state.as_str())];
|
||||
if let Some(reason) = entry.attention_required.as_deref() {
|
||||
parts.push(format!("attention: {reason}"));
|
||||
}
|
||||
if let Some(claim) = entry.local_claim.as_ref() {
|
||||
parts.push(format!(
|
||||
"claim: {} ({})",
|
||||
@@ -1044,9 +1017,7 @@ mod tests {
|
||||
fn workspace_panel_without_ticket_config_is_pod_only() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
||||
create_ticket(&backend, "Hidden Without Config", |input| {
|
||||
input.action_required = Some("answer me".to_string());
|
||||
});
|
||||
create_ticket(&backend, "Hidden Without Config", |_| {});
|
||||
|
||||
let model = build_workspace_panel(temp.path(), &live_pods(&["idle"]));
|
||||
|
||||
@@ -1068,37 +1039,22 @@ mod tests {
|
||||
create_ticket(&backend, "Ready Ticket", |input| {
|
||||
input.workflow_state = Some(TicketWorkflowState::Ready);
|
||||
});
|
||||
create_ticket(&backend, "Needs User", |input| {
|
||||
input.workflow_state = Some(TicketWorkflowState::Ready);
|
||||
input.attention_required = Some("answer clarification".to_string());
|
||||
});
|
||||
create_ticket(&backend, "Planning Ticket", |_| {});
|
||||
|
||||
let model = build_workspace_panel(temp.path(), &empty_pods());
|
||||
assert_eq!(
|
||||
model.composer.available_targets,
|
||||
vec![ComposerTarget::Companion, ComposerTarget::TicketIntake]
|
||||
);
|
||||
let rows = model
|
||||
let row = model
|
||||
.rows
|
||||
.iter()
|
||||
.map(|row| {
|
||||
(
|
||||
row.title.as_str(),
|
||||
row.status.as_str(),
|
||||
row.priority,
|
||||
row.next_action,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.find(|row| row.title == "Ready Ticket")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(rows[0].0, "Needs User");
|
||||
assert_eq!(rows[0].1, "ready");
|
||||
assert_eq!(rows[0].2, ActionPriority::UserReply);
|
||||
assert_eq!(rows[0].3, Some(NextUserAction::Edit));
|
||||
assert_eq!(rows[1].0, "Ready Ticket");
|
||||
assert_eq!(rows[1].1, "ready");
|
||||
assert_eq!(rows[1].2, ActionPriority::ReadyForQueue);
|
||||
assert_eq!(rows[1].3, Some(NextUserAction::Queue));
|
||||
assert_eq!(row.status, "ready");
|
||||
assert_eq!(row.priority, ActionPriority::ReadyForQueue);
|
||||
assert_eq!(row.next_action, Some(NextUserAction::Queue));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1198,45 +1154,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_panel_treats_yaml_null_attention_required_as_unblocked_planning() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
write_ticket_config(temp.path());
|
||||
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
||||
let ticket_ref = backend
|
||||
.create({
|
||||
let mut input = NewTicket::new("Null Attention Planning");
|
||||
input.workflow_state = Some(TicketWorkflowState::Planning);
|
||||
input
|
||||
})
|
||||
.unwrap();
|
||||
let item_path = temp
|
||||
.path()
|
||||
.join(".yoi/tickets")
|
||||
.join(&ticket_ref.id)
|
||||
.join("item.md");
|
||||
let item = fs::read_to_string(&item_path).unwrap();
|
||||
fs::write(
|
||||
&item_path,
|
||||
item.replace(
|
||||
"state: planning\ncreated_at:",
|
||||
"state: planning\nattention_required: null\ncreated_at:",
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let model = build_workspace_panel(temp.path(), &empty_pods());
|
||||
let row = model
|
||||
.rows
|
||||
.iter()
|
||||
.find(|row| row.title == "Null Attention Planning")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(row.status, "planning");
|
||||
assert_eq!(row.next_action, Some(NextUserAction::Clarify));
|
||||
assert_eq!(row.priority, ActionPriority::Background);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_panel_defaults_missing_open_state_to_planning_and_displays_done_state() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user