test(automations): cover event restriction, stale-edit discard, and awaiting-agent arm race

This commit is contained in:
Tanmay Deep Sharma
2026-07-15 13:08:29 +05:30
parent cc23867774
commit 92bcd2f214
3 changed files with 82 additions and 8 deletions
@@ -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
@@ -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)
+55
View File
@@ -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