diff --git a/spec/jobs/automation_rules/process_pending_execution_job_spec.rb b/spec/jobs/automation_rules/process_pending_execution_job_spec.rb index a7c88e9e0..72f755a8a 100644 --- a/spec/jobs/automation_rules/process_pending_execution_job_spec.rb +++ b/spec/jobs/automation_rules/process_pending_execution_job_spec.rb @@ -55,15 +55,22 @@ RSpec.describe AutomationRules::ProcessPendingExecutionJob do expect(conversation.reload.label_list).to be_empty end - it 'skips with conditions_changed when the re-check no longer matches the edited rule' do - pending_execution - # Rule edited while pending: the re-check enforces the current conditions (by design). - rule.update!(conditions: [{ 'values' => ['open'], 'attribute_key' => 'status', 'query_operator' => nil, - 'filter_operator' => 'equal_to' }]) - job.perform(pending_execution.reload) + it 'skips with conditions_changed when the conversation drifts but the episode is intact' do + # A message_created (reply-chase) rule whose extra condition is the conversation status. + message_rule = create(:automation_rule, account: account, event_name: 'message_created', execution_delay: 60, + conditions: [{ 'values' => ['pending'], 'attribute_key' => 'status', + 'query_operator' => nil, 'filter_operator' => 'equal_to' }], + actions: [{ 'action_name' => 'add_label', 'action_params' => ['stale'] }]) + agent_reply = create(:message, conversation: conversation, account: account, message_type: :outgoing) + AutomationRulePendingExecution.schedule(rule: message_rule, conversation: conversation, message: agent_reply) + row = AutomationRulePendingExecution.last - expect(pending_execution.reload).to be_skipped - expect(pending_execution.skip_reason).to eq('conditions_changed') + # Status change fails the condition but leaves the reply_chase episode (max incoming id) intact. + conversation.update!(status: :open) + job.perform(row) + + expect(row.reload).to be_skipped + expect(row.skip_reason).to eq('conditions_changed') end it 'skips with expired when the row is past the due window' do diff --git a/spec/models/automation_rule_pending_execution_spec.rb b/spec/models/automation_rule_pending_execution_spec.rb index 46b16b3d4..5f9fb153f 100644 --- a/spec/models/automation_rule_pending_execution_spec.rb +++ b/spec/models/automation_rule_pending_execution_spec.rb @@ -23,6 +23,18 @@ RSpec.describe AutomationRulePendingExecution do expect(described_class.episode_key_for(conversation.reload, message)).to eq("awaiting_agent:#{conversation.waiting_since.to_i}") end + it 'arms an awaiting_agent episode from the message created_at when waiting_since is not yet written' do + message = create(:message, conversation: conversation, account: account, message_type: :incoming) + # Simulate the race where the listener arms before update_waiting_since commits. + conversation.update!(waiting_since: nil) + armed_key = described_class.arm_episode_key_for(conversation.reload, message) + + # Once waiting_since settles to the message's created_at, the strict fire-time key matches. + conversation.update!(waiting_since: message.created_at) + expect(armed_key).to eq("awaiting_agent:#{message.created_at.to_i}") + expect(armed_key).to eq(described_class.episode_key_for(conversation.reload, message)) + end + it 'derives reply_chase episodes from the max incoming message id for outgoing messages' do incoming = create(:message, conversation: conversation, account: account, message_type: :incoming) outgoing = create(:message, conversation: conversation, account: account, message_type: :outgoing) diff --git a/spec/models/automation_rule_spec.rb b/spec/models/automation_rule_spec.rb index 86d611a0f..91bf91df8 100644 --- a/spec/models/automation_rule_spec.rb +++ b/spec/models/automation_rule_spec.rb @@ -174,5 +174,60 @@ RSpec.describe AutomationRule do expect(rule).not_to be_valid expect(rule.errors[:execution_delay]).to include('cannot be used with attribute_changed conditions.') end + + it 'rejects a delayed conversation-level rule with a non-status condition' do + rule.event_name = 'conversation_updated' + rule.execution_delay = 60 + rule.conditions = [{ 'attribute_key' => 'priority', 'filter_operator' => 'equal_to', 'values' => ['urgent'], 'query_operator' => nil }] + expect(rule).not_to be_valid + expect(rule.errors[:execution_delay]).to include('only supports status conditions for conversation-level events.') + end + + it 'allows a delayed conversation-level rule with only status conditions' do + rule.event_name = 'conversation_updated' + rule.execution_delay = 60 + rule.conditions = [{ 'attribute_key' => 'status', 'filter_operator' => 'equal_to', 'values' => ['pending'], 'query_operator' => nil }] + expect(rule).to be_valid + end + + it 'allows a delayed message_created rule with a non-status condition' do + rule.event_name = 'message_created' + rule.execution_delay = 60 + rule.conditions = [{ 'attribute_key' => 'message_type', 'filter_operator' => 'equal_to', 'values' => ['outgoing'], 'query_operator' => nil }] + expect(rule).to be_valid + end + end + + describe 'discarding stale pending executions on edit' do + let(:account) { create(:account) } + let(:conversation) { create(:conversation, account: account, status: :pending) } + let(:status_condition) { { 'attribute_key' => 'status', 'filter_operator' => 'equal_to', 'values' => ['pending'], 'query_operator' => nil } } + let(:rule) do + create(:automation_rule, account: account, event_name: 'conversation_updated', execution_delay: 60, + conditions: [status_condition], actions: [{ 'action_name' => 'add_label', 'action_params' => ['stale'] }]) + end + + before { AutomationRulePendingExecution.schedule(rule: rule, conversation: conversation) } + + it 'discards armed rows when the actions change' do + rule.update!(actions: [{ 'action_name' => 'add_label', 'action_params' => ['urgent'] }]) + expect(rule.pending_executions.pending).to be_empty + end + + it 'discards armed rows when the delay changes' do + rule.update!(execution_delay: 120) + expect(rule.pending_executions.pending).to be_empty + end + + it 'frees the episode slot so the new definition re-arms for the same episode' do + rule.update!(actions: [{ 'action_name' => 'add_label', 'action_params' => ['urgent'] }]) + AutomationRulePendingExecution.schedule(rule: rule, conversation: conversation) + expect(rule.pending_executions.pending.count).to eq(1) + end + + it 'leaves armed rows untouched on a name-only edit' do + rule.update!(name: 'Renamed rule') + expect(rule.pending_executions.pending.count).to eq(1) + end end end