lex-conflict 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +10 -0
- data/lex-conflict.gemspec +29 -0
- data/lib/legion/extensions/conflict/actors/stale_check.rb +41 -0
- data/lib/legion/extensions/conflict/client.rb +23 -0
- data/lib/legion/extensions/conflict/helpers/conflict_log.rb +66 -0
- data/lib/legion/extensions/conflict/helpers/llm_enhancer.rb +126 -0
- data/lib/legion/extensions/conflict/helpers/severity.rb +43 -0
- data/lib/legion/extensions/conflict/runners/conflict.rb +108 -0
- data/lib/legion/extensions/conflict/version.rb +9 -0
- data/lib/legion/extensions/conflict.rb +15 -0
- data/spec/legion/extensions/conflict/actors/stale_check_spec.rb +45 -0
- data/spec/legion/extensions/conflict/client_spec.rb +15 -0
- data/spec/legion/extensions/conflict/helpers/conflict_log_spec.rb +232 -0
- data/spec/legion/extensions/conflict/helpers/llm_enhancer_spec.rb +189 -0
- data/spec/legion/extensions/conflict/helpers/severity_spec.rb +215 -0
- data/spec/legion/extensions/conflict/runners/conflict_spec.rb +151 -0
- data/spec/spec_helper.rb +20 -0
- metadata +78 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Conflict::Helpers::ConflictLog do
|
|
6
|
+
subject(:log) { described_class.new }
|
|
7
|
+
|
|
8
|
+
let(:parties) { %w[agent human] }
|
|
9
|
+
let(:severity) { :high }
|
|
10
|
+
let(:description) { 'disagreement over shutdown procedure' }
|
|
11
|
+
|
|
12
|
+
describe '#initialize' do
|
|
13
|
+
it 'starts with an empty conflicts hash' do
|
|
14
|
+
expect(log.conflicts).to eq({})
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe '#record' do
|
|
19
|
+
it 'returns a UUID string' do
|
|
20
|
+
id = log.record(parties: parties, severity: severity, description: description)
|
|
21
|
+
expect(id).to match(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'stores the conflict under the returned id' do
|
|
25
|
+
id = log.record(parties: parties, severity: severity, description: description)
|
|
26
|
+
expect(log.conflicts[id]).not_to be_nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'sets conflict_id on the record' do
|
|
30
|
+
id = log.record(parties: parties, severity: severity, description: description)
|
|
31
|
+
expect(log.conflicts[id][:conflict_id]).to eq(id)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'stores the parties array' do
|
|
35
|
+
id = log.record(parties: parties, severity: severity, description: description)
|
|
36
|
+
expect(log.conflicts[id][:parties]).to eq(parties)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'stores the severity' do
|
|
40
|
+
id = log.record(parties: parties, severity: severity, description: description)
|
|
41
|
+
expect(log.conflicts[id][:severity]).to eq(:high)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'stores the description' do
|
|
45
|
+
id = log.record(parties: parties, severity: severity, description: description)
|
|
46
|
+
expect(log.conflicts[id][:description]).to eq(description)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'sets status to :active' do
|
|
50
|
+
id = log.record(parties: parties, severity: severity, description: description)
|
|
51
|
+
expect(log.conflicts[id][:status]).to eq(:active)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'sets outcome to nil' do
|
|
55
|
+
id = log.record(parties: parties, severity: severity, description: description)
|
|
56
|
+
expect(log.conflicts[id][:outcome]).to be_nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'sets resolved_at to nil' do
|
|
60
|
+
id = log.record(parties: parties, severity: severity, description: description)
|
|
61
|
+
expect(log.conflicts[id][:resolved_at]).to be_nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'initializes exchanges to an empty array' do
|
|
65
|
+
id = log.record(parties: parties, severity: severity, description: description)
|
|
66
|
+
expect(log.conflicts[id][:exchanges]).to eq([])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'sets created_at to a recent UTC time' do
|
|
70
|
+
before = Time.now.utc
|
|
71
|
+
id = log.record(parties: parties, severity: severity, description: description)
|
|
72
|
+
expect(log.conflicts[id][:created_at]).to be >= before
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'auto-assigns posture via recommended_posture when no posture given' do
|
|
76
|
+
id = log.record(parties: parties, severity: :critical, description: description)
|
|
77
|
+
expect(log.conflicts[id][:posture]).to eq(:stubborn_presence)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'uses the provided posture when explicitly passed' do
|
|
81
|
+
id = log.record(parties: parties, severity: :low, description: description, posture: :persistent_engagement)
|
|
82
|
+
expect(log.conflicts[id][:posture]).to eq(:persistent_engagement)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'each call returns a unique id' do
|
|
86
|
+
id1 = log.record(parties: parties, severity: :low, description: 'a')
|
|
87
|
+
id2 = log.record(parties: parties, severity: :low, description: 'b')
|
|
88
|
+
expect(id1).not_to eq(id2)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe '#add_exchange' do
|
|
93
|
+
let!(:conflict_id) { log.record(parties: parties, severity: severity, description: description) }
|
|
94
|
+
|
|
95
|
+
it 'appends to the exchanges array' do
|
|
96
|
+
log.add_exchange(conflict_id, speaker: 'agent', message: 'I object')
|
|
97
|
+
expect(log.conflicts[conflict_id][:exchanges].size).to eq(1)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'stores the speaker' do
|
|
101
|
+
log.add_exchange(conflict_id, speaker: 'human', message: 'proceed anyway')
|
|
102
|
+
exchange = log.conflicts[conflict_id][:exchanges].last
|
|
103
|
+
expect(exchange[:speaker]).to eq('human')
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it 'stores the message' do
|
|
107
|
+
log.add_exchange(conflict_id, speaker: 'agent', message: 'I strongly disagree')
|
|
108
|
+
exchange = log.conflicts[conflict_id][:exchanges].last
|
|
109
|
+
expect(exchange[:message]).to eq('I strongly disagree')
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'sets :at to a recent UTC time' do
|
|
113
|
+
before = Time.now.utc
|
|
114
|
+
log.add_exchange(conflict_id, speaker: 'agent', message: 'noted')
|
|
115
|
+
exchange = log.conflicts[conflict_id][:exchanges].last
|
|
116
|
+
expect(exchange[:at]).to be >= before
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'supports multiple sequential exchanges' do
|
|
120
|
+
log.add_exchange(conflict_id, speaker: 'agent', message: 'message 1')
|
|
121
|
+
log.add_exchange(conflict_id, speaker: 'human', message: 'message 2')
|
|
122
|
+
log.add_exchange(conflict_id, speaker: 'agent', message: 'message 3')
|
|
123
|
+
expect(log.conflicts[conflict_id][:exchanges].size).to eq(3)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'returns nil for a non-existent conflict_id' do
|
|
127
|
+
result = log.add_exchange('no-such-id', speaker: 'agent', message: 'hello')
|
|
128
|
+
expect(result).to be_nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'does not modify exchanges when conflict_id is missing' do
|
|
132
|
+
log.add_exchange('ghost-id', speaker: 'x', message: 'y')
|
|
133
|
+
expect(log.conflicts.values.flat_map { |c| c[:exchanges] }).to be_empty
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
describe '#resolve' do
|
|
138
|
+
let!(:conflict_id) { log.record(parties: parties, severity: severity, description: description) }
|
|
139
|
+
|
|
140
|
+
it 'returns the updated conflict hash' do
|
|
141
|
+
result = log.resolve(conflict_id, outcome: :compromise)
|
|
142
|
+
expect(result).to be_a(Hash)
|
|
143
|
+
expect(result[:conflict_id]).to eq(conflict_id)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'sets status to :resolved' do
|
|
147
|
+
log.resolve(conflict_id, outcome: :compromise)
|
|
148
|
+
expect(log.conflicts[conflict_id][:status]).to eq(:resolved)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it 'stores the outcome' do
|
|
152
|
+
log.resolve(conflict_id, outcome: :agreement)
|
|
153
|
+
expect(log.conflicts[conflict_id][:outcome]).to eq(:agreement)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it 'sets resolved_at to a recent UTC time' do
|
|
157
|
+
before = Time.now.utc
|
|
158
|
+
log.resolve(conflict_id, outcome: :withdrawn)
|
|
159
|
+
expect(log.conflicts[conflict_id][:resolved_at]).to be >= before
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it 'stores resolution_notes when provided' do
|
|
163
|
+
log.resolve(conflict_id, outcome: :compromise, resolution_notes: 'both parties agreed to defer')
|
|
164
|
+
expect(log.conflicts[conflict_id][:resolution_notes]).to eq('both parties agreed to defer')
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it 'stores nil resolution_notes when not provided' do
|
|
168
|
+
log.resolve(conflict_id, outcome: :compromise)
|
|
169
|
+
expect(log.conflicts[conflict_id][:resolution_notes]).to be_nil
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it 'returns nil for a non-existent conflict_id' do
|
|
173
|
+
result = log.resolve('ghost-id', outcome: :compromise)
|
|
174
|
+
expect(result).to be_nil
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
describe '#active_conflicts' do
|
|
179
|
+
it 'returns empty array when no conflicts exist' do
|
|
180
|
+
expect(log.active_conflicts).to be_empty
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it 'returns all unresolved conflicts' do
|
|
184
|
+
3.times { log.record(parties: parties, severity: :low, description: 'test') }
|
|
185
|
+
expect(log.active_conflicts.size).to eq(3)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it 'excludes resolved conflicts' do
|
|
189
|
+
id = log.record(parties: parties, severity: :low, description: 'resolved one')
|
|
190
|
+
log.record(parties: parties, severity: :high, description: 'still active')
|
|
191
|
+
log.resolve(id, outcome: :withdrawn)
|
|
192
|
+
expect(log.active_conflicts.size).to eq(1)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it 'returns conflict hashes with status :active' do
|
|
196
|
+
log.record(parties: parties, severity: :medium, description: 'check status')
|
|
197
|
+
log.active_conflicts.each do |conflict|
|
|
198
|
+
expect(conflict[:status]).to eq(:active)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
describe '#get' do
|
|
204
|
+
it 'returns the conflict hash for a known id' do
|
|
205
|
+
id = log.record(parties: parties, severity: severity, description: description)
|
|
206
|
+
result = log.get(id)
|
|
207
|
+
expect(result[:conflict_id]).to eq(id)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it 'returns nil for an unknown id' do
|
|
211
|
+
expect(log.get('missing-id')).to be_nil
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
describe '#count' do
|
|
216
|
+
it 'returns 0 for a new log' do
|
|
217
|
+
expect(log.count).to eq(0)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it 'returns the total number of recorded conflicts' do
|
|
221
|
+
3.times { log.record(parties: parties, severity: :low, description: 'x') }
|
|
222
|
+
expect(log.count).to eq(3)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
it 'counts resolved conflicts as well as active ones' do
|
|
226
|
+
id = log.record(parties: parties, severity: :low, description: 'resolve me')
|
|
227
|
+
log.resolve(id, outcome: :agreement)
|
|
228
|
+
log.record(parties: parties, severity: :high, description: 'still active')
|
|
229
|
+
expect(log.count).to eq(2)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Conflict::Helpers::LlmEnhancer do
|
|
4
|
+
describe '.available?' do
|
|
5
|
+
context 'when Legion::LLM is not defined' do
|
|
6
|
+
it 'returns a falsy value' do
|
|
7
|
+
# Legion::LLM is not defined in the test environment
|
|
8
|
+
expect(described_class.available?).to be_falsy
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
context 'when Legion::LLM is defined but not started' do
|
|
13
|
+
before do
|
|
14
|
+
stub_const('Legion::LLM', double(respond_to?: true, started?: false))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'returns false' do
|
|
18
|
+
expect(described_class.available?).to be false
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
context 'when Legion::LLM is started' do
|
|
23
|
+
before do
|
|
24
|
+
stub_const('Legion::LLM', double(respond_to?: true, started?: true))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'returns true' do
|
|
28
|
+
expect(described_class.available?).to be true
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
context 'when an error is raised' do
|
|
33
|
+
before do
|
|
34
|
+
stub_const('Legion::LLM', double)
|
|
35
|
+
allow(Legion::LLM).to receive(:respond_to?).and_raise(StandardError)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'returns false' do
|
|
39
|
+
expect(described_class.available?).to be false
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe '.suggest_resolution' do
|
|
45
|
+
let(:fake_response) do
|
|
46
|
+
double(content: <<~TEXT)
|
|
47
|
+
OUTCOME: resolved
|
|
48
|
+
NOTES: Both parties agreed to table the discussion for 48 hours. The agent will document its reasoning and the human partner will review before resuming.
|
|
49
|
+
TEXT
|
|
50
|
+
end
|
|
51
|
+
let(:fake_chat) { double }
|
|
52
|
+
|
|
53
|
+
before do
|
|
54
|
+
stub_const('Legion::LLM', double)
|
|
55
|
+
allow(Legion::LLM).to receive(:chat).and_return(fake_chat)
|
|
56
|
+
allow(fake_chat).to receive(:with_instructions)
|
|
57
|
+
allow(fake_chat).to receive(:ask).and_return(fake_response)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'returns resolution_notes and suggested_outcome' do
|
|
61
|
+
result = described_class.suggest_resolution(
|
|
62
|
+
description: 'Agent and human disagree on approach',
|
|
63
|
+
severity: :high,
|
|
64
|
+
exchanges: [{ speaker: :agent, message: 'I think we should proceed' },
|
|
65
|
+
{ speaker: :human, message: 'I disagree with that approach' }]
|
|
66
|
+
)
|
|
67
|
+
expect(result).to be_a(Hash)
|
|
68
|
+
expect(result[:resolution_notes]).to be_a(String)
|
|
69
|
+
expect(result[:resolution_notes]).not_to be_empty
|
|
70
|
+
expect(result[:suggested_outcome]).to eq(:resolved)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'handles deferred outcome' do
|
|
74
|
+
allow(fake_chat).to receive(:ask).and_return(
|
|
75
|
+
double(content: "OUTCOME: deferred\nNOTES: Issue requires more context before resolution.")
|
|
76
|
+
)
|
|
77
|
+
result = described_class.suggest_resolution(
|
|
78
|
+
description: 'Ongoing concern', severity: :medium, exchanges: []
|
|
79
|
+
)
|
|
80
|
+
expect(result[:suggested_outcome]).to eq(:deferred)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'handles escalated outcome' do
|
|
84
|
+
allow(fake_chat).to receive(:ask).and_return(
|
|
85
|
+
double(content: "OUTCOME: escalated\nNOTES: Critical disagreement requires governance council review.")
|
|
86
|
+
)
|
|
87
|
+
result = described_class.suggest_resolution(
|
|
88
|
+
description: 'Safety concern', severity: :critical, exchanges: []
|
|
89
|
+
)
|
|
90
|
+
expect(result[:suggested_outcome]).to eq(:escalated)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
context 'when LLM returns nil content' do
|
|
94
|
+
before { allow(fake_chat).to receive(:ask).and_return(double(content: nil)) }
|
|
95
|
+
|
|
96
|
+
it 'returns nil' do
|
|
97
|
+
result = described_class.suggest_resolution(
|
|
98
|
+
description: 'test', severity: :low, exchanges: []
|
|
99
|
+
)
|
|
100
|
+
expect(result).to be_nil
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
context 'when LLM raises an error' do
|
|
105
|
+
before { allow(fake_chat).to receive(:ask).and_raise(StandardError, 'LLM unavailable') }
|
|
106
|
+
|
|
107
|
+
it 'returns nil and logs a warning' do
|
|
108
|
+
expect(Legion::Logging).to receive(:warn).with(/suggest_resolution failed/)
|
|
109
|
+
result = described_class.suggest_resolution(
|
|
110
|
+
description: 'test', severity: :low, exchanges: []
|
|
111
|
+
)
|
|
112
|
+
expect(result).to be_nil
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
describe '.analyze_stale_conflict' do
|
|
118
|
+
let(:fake_response) do
|
|
119
|
+
double(content: <<~TEXT)
|
|
120
|
+
RECOMMENDATION: escalate
|
|
121
|
+
ANALYSIS: This conflict has persisted for 36 hours without progress. The critical severity and lack of exchanges suggests governance council intervention.
|
|
122
|
+
TEXT
|
|
123
|
+
end
|
|
124
|
+
let(:fake_chat) { double }
|
|
125
|
+
|
|
126
|
+
before do
|
|
127
|
+
stub_const('Legion::LLM', double)
|
|
128
|
+
allow(Legion::LLM).to receive(:chat).and_return(fake_chat)
|
|
129
|
+
allow(fake_chat).to receive(:with_instructions)
|
|
130
|
+
allow(fake_chat).to receive(:ask).and_return(fake_response)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'returns analysis and recommendation' do
|
|
134
|
+
result = described_class.analyze_stale_conflict(
|
|
135
|
+
description: 'Critical safety disagreement',
|
|
136
|
+
severity: :critical,
|
|
137
|
+
age_hours: 36.5,
|
|
138
|
+
exchange_count: 2
|
|
139
|
+
)
|
|
140
|
+
expect(result).to be_a(Hash)
|
|
141
|
+
expect(result[:analysis]).to be_a(String)
|
|
142
|
+
expect(result[:analysis]).not_to be_empty
|
|
143
|
+
expect(result[:recommendation]).to eq(:escalate)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'handles retry recommendation' do
|
|
147
|
+
allow(fake_chat).to receive(:ask).and_return(
|
|
148
|
+
double(content: "RECOMMENDATION: retry\nANALYSIS: The conflict may be resolvable with a fresh attempt after cooling off.")
|
|
149
|
+
)
|
|
150
|
+
result = described_class.analyze_stale_conflict(
|
|
151
|
+
description: 'Minor dispute', severity: :low, age_hours: 25.0, exchange_count: 1
|
|
152
|
+
)
|
|
153
|
+
expect(result[:recommendation]).to eq(:retry)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it 'handles close recommendation' do
|
|
157
|
+
allow(fake_chat).to receive(:ask).and_return(
|
|
158
|
+
double(content: "RECOMMENDATION: close\nANALYSIS: The issue is no longer relevant and can be safely closed.")
|
|
159
|
+
)
|
|
160
|
+
result = described_class.analyze_stale_conflict(
|
|
161
|
+
description: 'Old dispute', severity: :low, age_hours: 48.0, exchange_count: 0
|
|
162
|
+
)
|
|
163
|
+
expect(result[:recommendation]).to eq(:close)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
context 'when LLM returns nil content' do
|
|
167
|
+
before { allow(fake_chat).to receive(:ask).and_return(double(content: nil)) }
|
|
168
|
+
|
|
169
|
+
it 'returns nil' do
|
|
170
|
+
result = described_class.analyze_stale_conflict(
|
|
171
|
+
description: 'test', severity: :low, age_hours: 25.0, exchange_count: 0
|
|
172
|
+
)
|
|
173
|
+
expect(result).to be_nil
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
context 'when LLM raises an error' do
|
|
178
|
+
before { allow(fake_chat).to receive(:ask).and_raise(StandardError, 'connection refused') }
|
|
179
|
+
|
|
180
|
+
it 'returns nil and logs a warning' do
|
|
181
|
+
expect(Legion::Logging).to receive(:warn).with(/analyze_stale_conflict failed/)
|
|
182
|
+
result = described_class.analyze_stale_conflict(
|
|
183
|
+
description: 'test', severity: :medium, age_hours: 30.0, exchange_count: 3
|
|
184
|
+
)
|
|
185
|
+
expect(result).to be_nil
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Conflict::Helpers::Severity do
|
|
6
|
+
describe 'LEVELS' do
|
|
7
|
+
it 'is a frozen array of symbols' do
|
|
8
|
+
expect(described_class::LEVELS).to be_a(Array)
|
|
9
|
+
expect(described_class::LEVELS).to be_frozen
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'contains exactly four levels' do
|
|
13
|
+
expect(described_class::LEVELS.size).to eq(4)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'includes :low' do
|
|
17
|
+
expect(described_class::LEVELS).to include(:low)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'includes :medium' do
|
|
21
|
+
expect(described_class::LEVELS).to include(:medium)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'includes :high' do
|
|
25
|
+
expect(described_class::LEVELS).to include(:high)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'includes :critical' do
|
|
29
|
+
expect(described_class::LEVELS).to include(:critical)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'is ordered from least to most severe' do
|
|
33
|
+
expect(described_class::LEVELS).to eq(%i[low medium high critical])
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe 'POSTURES' do
|
|
38
|
+
it 'is a frozen array of symbols' do
|
|
39
|
+
expect(described_class::POSTURES).to be_a(Array)
|
|
40
|
+
expect(described_class::POSTURES).to be_frozen
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'contains exactly three postures' do
|
|
44
|
+
expect(described_class::POSTURES.size).to eq(3)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'includes :speak_once' do
|
|
48
|
+
expect(described_class::POSTURES).to include(:speak_once)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'includes :persistent_engagement' do
|
|
52
|
+
expect(described_class::POSTURES).to include(:persistent_engagement)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'includes :stubborn_presence' do
|
|
56
|
+
expect(described_class::POSTURES).to include(:stubborn_presence)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe 'LEVEL_ORDER' do
|
|
61
|
+
it 'is a frozen hash' do
|
|
62
|
+
expect(described_class::LEVEL_ORDER).to be_a(Hash)
|
|
63
|
+
expect(described_class::LEVEL_ORDER).to be_frozen
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'assigns :low the lowest numeric value' do
|
|
67
|
+
expect(described_class::LEVEL_ORDER[:low]).to eq(0)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'assigns :medium a higher value than :low' do
|
|
71
|
+
expect(described_class::LEVEL_ORDER[:medium]).to be > described_class::LEVEL_ORDER[:low]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'assigns :high a higher value than :medium' do
|
|
75
|
+
expect(described_class::LEVEL_ORDER[:high]).to be > described_class::LEVEL_ORDER[:medium]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'assigns :critical the highest numeric value' do
|
|
79
|
+
expect(described_class::LEVEL_ORDER[:critical]).to be > described_class::LEVEL_ORDER[:high]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'covers all LEVELS' do
|
|
83
|
+
described_class::LEVELS.each do |level|
|
|
84
|
+
expect(described_class::LEVEL_ORDER).to have_key(level)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
describe '.valid_level?' do
|
|
90
|
+
it 'returns true for :low' do
|
|
91
|
+
expect(described_class.valid_level?(:low)).to be true
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'returns true for :medium' do
|
|
95
|
+
expect(described_class.valid_level?(:medium)).to be true
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'returns true for :high' do
|
|
99
|
+
expect(described_class.valid_level?(:high)).to be true
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'returns true for :critical' do
|
|
103
|
+
expect(described_class.valid_level?(:critical)).to be true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it 'returns false for an unknown symbol' do
|
|
107
|
+
expect(described_class.valid_level?(:catastrophic)).to be false
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'returns false for a string form of a valid level' do
|
|
111
|
+
expect(described_class.valid_level?('high')).to be false
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'returns false for nil' do
|
|
115
|
+
expect(described_class.valid_level?(nil)).to be false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'returns true for every member of LEVELS' do
|
|
119
|
+
described_class::LEVELS.each do |level|
|
|
120
|
+
expect(described_class.valid_level?(level)).to be true
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
describe '.valid_posture?' do
|
|
126
|
+
it 'returns true for :speak_once' do
|
|
127
|
+
expect(described_class.valid_posture?(:speak_once)).to be true
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'returns true for :persistent_engagement' do
|
|
131
|
+
expect(described_class.valid_posture?(:persistent_engagement)).to be true
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it 'returns true for :stubborn_presence' do
|
|
135
|
+
expect(described_class.valid_posture?(:stubborn_presence)).to be true
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it 'returns false for an unknown posture symbol' do
|
|
139
|
+
expect(described_class.valid_posture?(:passive)).to be false
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it 'returns false for nil' do
|
|
143
|
+
expect(described_class.valid_posture?(nil)).to be false
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'returns true for every member of POSTURES' do
|
|
147
|
+
described_class::POSTURES.each do |posture|
|
|
148
|
+
expect(described_class.valid_posture?(posture)).to be true
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
describe '.recommended_posture' do
|
|
154
|
+
it 'returns :stubborn_presence for :critical' do
|
|
155
|
+
expect(described_class.recommended_posture(:critical)).to eq(:stubborn_presence)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it 'returns :persistent_engagement for :high' do
|
|
159
|
+
expect(described_class.recommended_posture(:high)).to eq(:persistent_engagement)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it 'returns :speak_once for :medium' do
|
|
163
|
+
expect(described_class.recommended_posture(:medium)).to eq(:speak_once)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it 'returns :speak_once for :low' do
|
|
167
|
+
expect(described_class.recommended_posture(:low)).to eq(:speak_once)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it 'returns :speak_once for an unrecognized severity' do
|
|
171
|
+
expect(described_class.recommended_posture(:unknown)).to eq(:speak_once)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it 'returns a posture that is a member of POSTURES for every valid level' do
|
|
175
|
+
described_class::LEVELS.each do |level|
|
|
176
|
+
posture = described_class.recommended_posture(level)
|
|
177
|
+
expect(described_class::POSTURES).to include(posture)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
describe '.severity_gte?' do
|
|
183
|
+
it 'returns true when left equals right' do
|
|
184
|
+
expect(described_class.severity_gte?(:medium, :medium)).to be true
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it 'returns true when left is strictly greater' do
|
|
188
|
+
expect(described_class.severity_gte?(:high, :medium)).to be true
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it 'returns false when left is strictly less' do
|
|
192
|
+
expect(described_class.severity_gte?(:low, :medium)).to be false
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it ':critical >= :high is true' do
|
|
196
|
+
expect(described_class.severity_gte?(:critical, :high)).to be true
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it ':critical >= :critical is true' do
|
|
200
|
+
expect(described_class.severity_gte?(:critical, :critical)).to be true
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it ':low >= :critical is false' do
|
|
204
|
+
expect(described_class.severity_gte?(:low, :critical)).to be false
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it 'treats an unknown left level as order 0 (same as :low)' do
|
|
208
|
+
expect(described_class.severity_gte?(:unknown, :medium)).to be false
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it 'treats an unknown right level as order 0, so any valid level >= it' do
|
|
212
|
+
expect(described_class.severity_gte?(:low, :unknown)).to be true
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|