lex-mental-simulation 0.1.0
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 +11 -0
- data/lex-mental-simulation.gemspec +29 -0
- data/lib/legion/extensions/mental_simulation/helpers/client.rb +19 -0
- data/lib/legion/extensions/mental_simulation/helpers/constants.rb +39 -0
- data/lib/legion/extensions/mental_simulation/helpers/simulation.rb +112 -0
- data/lib/legion/extensions/mental_simulation/helpers/simulation_engine.rb +137 -0
- data/lib/legion/extensions/mental_simulation/helpers/simulation_step.rb +49 -0
- data/lib/legion/extensions/mental_simulation/runners/mental_simulation.rb +88 -0
- data/lib/legion/extensions/mental_simulation/version.rb +9 -0
- data/lib/legion/extensions/mental_simulation.rb +17 -0
- data/spec/legion/extensions/mental_simulation/helpers/constants_spec.rb +107 -0
- data/spec/legion/extensions/mental_simulation/helpers/simulation_engine_spec.rb +217 -0
- data/spec/legion/extensions/mental_simulation/helpers/simulation_spec.rb +223 -0
- data/spec/legion/extensions/mental_simulation/helpers/simulation_step_spec.rb +111 -0
- data/spec/legion/extensions/mental_simulation/runners/mental_simulation_spec.rb +184 -0
- data/spec/spec_helper.rb +20 -0
- metadata +77 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::MentalSimulation::Helpers::SimulationEngine do
|
|
4
|
+
subject(:engine) { described_class.new }
|
|
5
|
+
|
|
6
|
+
let(:label) { 'deploy pipeline' }
|
|
7
|
+
let(:domain) { :infrastructure }
|
|
8
|
+
|
|
9
|
+
def build_favorable_simulation
|
|
10
|
+
sim = engine.create_simulation(label: label, domain: domain)
|
|
11
|
+
engine.add_simulation_step(simulation_id: sim.id, action: 'check', confidence: 0.9, risk: 0.05)
|
|
12
|
+
engine.add_simulation_step(simulation_id: sim.id, action: 'deploy', confidence: 0.85, risk: 0.05)
|
|
13
|
+
sim
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def build_risky_simulation
|
|
17
|
+
sim = engine.create_simulation(label: 'risky plan', domain: :security)
|
|
18
|
+
engine.add_simulation_step(simulation_id: sim.id, action: 'dangerous op', confidence: 0.9, risk: 0.8)
|
|
19
|
+
sim
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe '#create_simulation' do
|
|
23
|
+
it 'creates a simulation and returns it' do
|
|
24
|
+
sim = engine.create_simulation(label: label, domain: domain)
|
|
25
|
+
expect(sim).to be_a(Legion::Extensions::MentalSimulation::Helpers::Simulation)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'stores the simulation internally' do
|
|
29
|
+
sim = engine.create_simulation(label: label, domain: domain)
|
|
30
|
+
expect(engine.assess_simulation(simulation_id: sim.id)[:label]).to eq(label)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe '#add_simulation_step' do
|
|
35
|
+
it 'returns added: true on success' do
|
|
36
|
+
sim = engine.create_simulation(label: label, domain: domain)
|
|
37
|
+
result = engine.add_simulation_step(simulation_id: sim.id, action: 'check prereqs')
|
|
38
|
+
expect(result[:added]).to be true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'returns step_id' do
|
|
42
|
+
sim = engine.create_simulation(label: label, domain: domain)
|
|
43
|
+
result = engine.add_simulation_step(simulation_id: sim.id, action: 'check')
|
|
44
|
+
expect(result[:step_id]).to match(/\A[0-9a-f-]{36}\z/)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'returns error for unknown simulation_id' do
|
|
48
|
+
result = engine.add_simulation_step(simulation_id: 'no-such-id', action: 'go')
|
|
49
|
+
expect(result[:error]).to eq(:simulation_not_found)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'returns error when max steps reached' do
|
|
53
|
+
sim = engine.create_simulation(label: label, domain: domain)
|
|
54
|
+
Legion::Extensions::MentalSimulation::Helpers::Constants::MAX_STEPS_PER_SIM.times do |i|
|
|
55
|
+
engine.add_simulation_step(simulation_id: sim.id, action: "step #{i}")
|
|
56
|
+
end
|
|
57
|
+
result = engine.add_simulation_step(simulation_id: sim.id, action: 'one too many')
|
|
58
|
+
expect(result[:error]).to eq(:max_steps_reached)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe '#run_simulation' do
|
|
63
|
+
it 'returns state after running' do
|
|
64
|
+
sim = build_favorable_simulation
|
|
65
|
+
result = engine.run_simulation(simulation_id: sim.id)
|
|
66
|
+
expect(result[:state]).to be_a(Symbol)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'returns favorable: true for a good plan' do
|
|
70
|
+
sim = build_favorable_simulation
|
|
71
|
+
result = engine.run_simulation(simulation_id: sim.id)
|
|
72
|
+
expect(result[:favorable]).to be true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'returns error for unknown simulation' do
|
|
76
|
+
result = engine.run_simulation(simulation_id: 'ghost')
|
|
77
|
+
expect(result[:error]).to eq(:simulation_not_found)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'returns overall_confidence in result' do
|
|
81
|
+
sim = build_favorable_simulation
|
|
82
|
+
result = engine.run_simulation(simulation_id: sim.id)
|
|
83
|
+
expect(result[:overall_confidence]).to be > 0
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'returns cumulative_risk in result' do
|
|
87
|
+
sim = build_favorable_simulation
|
|
88
|
+
result = engine.run_simulation(simulation_id: sim.id)
|
|
89
|
+
expect(result[:cumulative_risk]).to be >= 0
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
describe '#abort_simulation' do
|
|
94
|
+
it 'aborts a simulation and returns aborted: true' do
|
|
95
|
+
sim = engine.create_simulation(label: label, domain: domain)
|
|
96
|
+
result = engine.abort_simulation(simulation_id: sim.id)
|
|
97
|
+
expect(result[:aborted]).to be true
|
|
98
|
+
expect(result[:state]).to eq(:aborted)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'returns error for unknown simulation' do
|
|
102
|
+
result = engine.abort_simulation(simulation_id: 'ghost')
|
|
103
|
+
expect(result[:error]).to eq(:simulation_not_found)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
describe '#assess_simulation' do
|
|
108
|
+
it 'returns full assessment without running' do
|
|
109
|
+
sim = engine.create_simulation(label: label, domain: domain)
|
|
110
|
+
engine.add_simulation_step(simulation_id: sim.id, action: 'assess me')
|
|
111
|
+
result = engine.assess_simulation(simulation_id: sim.id)
|
|
112
|
+
expect(result[:state]).to eq(:pending)
|
|
113
|
+
expect(result[:steps].size).to eq(1)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'returns error for unknown simulation' do
|
|
117
|
+
result = engine.assess_simulation(simulation_id: 'ghost')
|
|
118
|
+
expect(result[:error]).to eq(:simulation_not_found)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it 'includes confidence and risk in assessment' do
|
|
122
|
+
sim = engine.create_simulation(label: label, domain: domain)
|
|
123
|
+
engine.add_simulation_step(simulation_id: sim.id, action: 'step', confidence: 0.7, risk: 0.2)
|
|
124
|
+
result = engine.assess_simulation(simulation_id: sim.id)
|
|
125
|
+
expect(result[:overall_confidence]).to eq(0.7)
|
|
126
|
+
expect(result[:cumulative_risk]).to be_within(0.001).of(0.2)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
describe '#favorable_simulations' do
|
|
131
|
+
it 'returns only favorable simulations' do
|
|
132
|
+
sim = build_favorable_simulation
|
|
133
|
+
engine.run_simulation(simulation_id: sim.id)
|
|
134
|
+
favs = engine.favorable_simulations
|
|
135
|
+
expect(favs).not_to be_empty
|
|
136
|
+
expect(favs.all?(&:favorable?)).to be true
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it 'excludes non-favorable simulations' do
|
|
140
|
+
sim = engine.create_simulation(label: label, domain: domain)
|
|
141
|
+
engine.add_simulation_step(simulation_id: sim.id, action: 'low conf', confidence: 0.2, risk: 0.1)
|
|
142
|
+
engine.run_simulation(simulation_id: sim.id)
|
|
143
|
+
favs = engine.favorable_simulations
|
|
144
|
+
expect(favs.map(&:id)).not_to include(sim.id)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
describe '#failed_simulations' do
|
|
149
|
+
it 'returns simulations in :failed state' do
|
|
150
|
+
sim = engine.create_simulation(label: label, domain: domain)
|
|
151
|
+
engine.add_simulation_step(simulation_id: sim.id, action: 'low conf', confidence: 0.2, risk: 0.1)
|
|
152
|
+
engine.run_simulation(simulation_id: sim.id)
|
|
153
|
+
failed = engine.failed_simulations
|
|
154
|
+
expect(failed.map(&:id)).to include(sim.id)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
describe '#simulations_by_domain' do
|
|
159
|
+
it 'returns simulations for the specified domain' do
|
|
160
|
+
sim_a = engine.create_simulation(label: 'a', domain: :networking)
|
|
161
|
+
sim_b = engine.create_simulation(label: 'b', domain: :security)
|
|
162
|
+
result = engine.simulations_by_domain(domain: :networking)
|
|
163
|
+
expect(result.map(&:id)).to include(sim_a.id)
|
|
164
|
+
expect(result.map(&:id)).not_to include(sim_b.id)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it 'handles domain as string by converting to symbol' do
|
|
168
|
+
sim = engine.create_simulation(label: 'str domain', domain: :cloud)
|
|
169
|
+
result = engine.simulations_by_domain(domain: 'cloud')
|
|
170
|
+
expect(result.map(&:id)).to include(sim.id)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
describe '#riskiest_simulations' do
|
|
175
|
+
it 'returns simulations sorted by descending cumulative_risk' do
|
|
176
|
+
low_risk_sim = engine.create_simulation(label: 'safe', domain: :app)
|
|
177
|
+
engine.add_simulation_step(simulation_id: low_risk_sim.id, action: 'safe', risk: 0.1)
|
|
178
|
+
|
|
179
|
+
high_risk_sim = build_risky_simulation
|
|
180
|
+
|
|
181
|
+
result = engine.riskiest_simulations(limit: 2)
|
|
182
|
+
expect(result.first.id).to eq(high_risk_sim.id)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it 'respects the limit parameter' do
|
|
186
|
+
3.times { |i| engine.create_simulation(label: "sim #{i}", domain: :test) }
|
|
187
|
+
expect(engine.riskiest_simulations(limit: 2).size).to be <= 2
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
describe '#most_confident' do
|
|
192
|
+
it 'returns simulations sorted by descending overall_confidence' do
|
|
193
|
+
low_conf_sim = engine.create_simulation(label: 'uncertain', domain: :app)
|
|
194
|
+
engine.add_simulation_step(simulation_id: low_conf_sim.id, action: 'maybe', confidence: 0.2)
|
|
195
|
+
|
|
196
|
+
high_conf_sim = engine.create_simulation(label: 'sure', domain: :app)
|
|
197
|
+
engine.add_simulation_step(simulation_id: high_conf_sim.id, action: 'definitely', confidence: 0.95)
|
|
198
|
+
|
|
199
|
+
result = engine.most_confident(limit: 2)
|
|
200
|
+
expect(result.first.id).to eq(high_conf_sim.id)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it 'respects the limit parameter' do
|
|
204
|
+
4.times { |i| engine.create_simulation(label: "sim #{i}", domain: :test) }
|
|
205
|
+
expect(engine.most_confident(limit: 2).size).to be <= 2
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
describe '#to_h' do
|
|
210
|
+
it 'returns engine summary hash' do
|
|
211
|
+
engine.create_simulation(label: label, domain: domain)
|
|
212
|
+
h = engine.to_h
|
|
213
|
+
expect(h).to include(:total_simulations, :history_size, :favorable_count, :failed_count, :simulations)
|
|
214
|
+
expect(h[:total_simulations]).to eq(1)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::MentalSimulation::Helpers::Simulation do
|
|
4
|
+
subject(:sim) { described_class.new(label: 'test plan', domain: :infrastructure) }
|
|
5
|
+
|
|
6
|
+
describe '#initialize' do
|
|
7
|
+
it 'generates a UUID id' do
|
|
8
|
+
expect(sim.id).to match(/\A[0-9a-f-]{36}\z/)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'stores the label' do
|
|
12
|
+
expect(sim.label).to eq('test plan')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'stores the domain as a symbol' do
|
|
16
|
+
expect(sim.domain).to eq(:infrastructure)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'starts with no steps' do
|
|
20
|
+
expect(sim.steps).to be_empty
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'starts in :pending state' do
|
|
24
|
+
expect(sim.state).to eq(:pending)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'records created_at' do
|
|
28
|
+
expect(sim.created_at).to be_a(Time)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'has nil completed_at initially' do
|
|
32
|
+
expect(sim.completed_at).to be_nil
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe '#add_step' do
|
|
37
|
+
it 'adds a SimulationStep and returns it' do
|
|
38
|
+
step = sim.add_step(action: 'check health')
|
|
39
|
+
expect(step).to be_a(Legion::Extensions::MentalSimulation::Helpers::SimulationStep)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'increments step_count' do
|
|
43
|
+
sim.add_step(action: 'step one')
|
|
44
|
+
sim.add_step(action: 'step two')
|
|
45
|
+
expect(sim.step_count).to eq(2)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'passes parameters through to the step' do
|
|
49
|
+
step = sim.add_step(action: 'deploy', predicted_outcome: :failure, confidence: 0.3, risk: 0.8)
|
|
50
|
+
expect(step.predicted_outcome).to eq(:failure)
|
|
51
|
+
expect(step.confidence).to eq(0.3)
|
|
52
|
+
expect(step.risk).to eq(0.8)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe '#overall_confidence' do
|
|
57
|
+
it 'returns 0.0 when no steps' do
|
|
58
|
+
expect(sim.overall_confidence).to eq(0.0)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'returns product of step confidences' do
|
|
62
|
+
sim.add_step(action: 'a', confidence: 0.8)
|
|
63
|
+
sim.add_step(action: 'b', confidence: 0.5)
|
|
64
|
+
expect(sim.overall_confidence).to be_within(0.001).of(0.4)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'returns single step confidence when only one step' do
|
|
68
|
+
sim.add_step(action: 'a', confidence: 0.75)
|
|
69
|
+
expect(sim.overall_confidence).to eq(0.75)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe '#cumulative_risk' do
|
|
74
|
+
it 'returns 0.0 when no steps' do
|
|
75
|
+
expect(sim.cumulative_risk).to eq(0.0)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'computes combined risk correctly' do
|
|
79
|
+
sim.add_step(action: 'a', risk: 0.2)
|
|
80
|
+
sim.add_step(action: 'b', risk: 0.3)
|
|
81
|
+
# 1 - (0.8 * 0.7) = 1 - 0.56 = 0.44
|
|
82
|
+
expect(sim.cumulative_risk).to be_within(0.001).of(0.44)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'returns high risk when steps have high individual risk' do
|
|
86
|
+
sim.add_step(action: 'a', risk: 0.9)
|
|
87
|
+
sim.add_step(action: 'b', risk: 0.9)
|
|
88
|
+
expect(sim.cumulative_risk).to be > 0.9
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe '#run!' do
|
|
93
|
+
context 'with favorable steps' do
|
|
94
|
+
before do
|
|
95
|
+
sim.add_step(action: 'check prereqs', confidence: 0.9, risk: 0.1)
|
|
96
|
+
sim.add_step(action: 'deploy', confidence: 0.8, risk: 0.1)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 'sets state to :completed' do
|
|
100
|
+
sim.run!
|
|
101
|
+
expect(sim.state).to eq(:completed)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'sets completed_at' do
|
|
105
|
+
sim.run!
|
|
106
|
+
expect(sim.completed_at).to be_a(Time)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
context 'when a step predicts failure with high confidence' do
|
|
111
|
+
before do
|
|
112
|
+
sim.add_step(action: 'safe step', confidence: 0.9, risk: 0.1)
|
|
113
|
+
sim.add_step(action: 'dangerous step', predicted_outcome: :failure, confidence: 0.8, risk: 0.5)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'aborts the simulation' do
|
|
117
|
+
sim.run!
|
|
118
|
+
expect(sim.state).to eq(:aborted)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
context 'with low overall confidence' do
|
|
123
|
+
before do
|
|
124
|
+
sim.add_step(action: 'a', confidence: 0.3, risk: 0.1)
|
|
125
|
+
sim.add_step(action: 'b', confidence: 0.3, risk: 0.1)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it 'sets state to :failed' do
|
|
129
|
+
sim.run!
|
|
130
|
+
expect(sim.state).to eq(:failed)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
context 'with high cumulative risk' do
|
|
135
|
+
before do
|
|
136
|
+
sim.add_step(action: 'a', confidence: 0.9, risk: 0.7)
|
|
137
|
+
sim.add_step(action: 'b', confidence: 0.9, risk: 0.7)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it 'sets state to :failed when cumulative risk >= 0.6' do
|
|
141
|
+
sim.run!
|
|
142
|
+
expect(sim.state).to eq(:failed)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'sets state to :running briefly and transitions' do
|
|
147
|
+
sim.add_step(action: 'a', confidence: 0.9, risk: 0.1)
|
|
148
|
+
sim.run!
|
|
149
|
+
expect(%i[completed failed aborted]).to include(sim.state)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
describe '#abort!' do
|
|
154
|
+
it 'sets state to :aborted' do
|
|
155
|
+
sim.abort!
|
|
156
|
+
expect(sim.state).to eq(:aborted)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it 'sets completed_at' do
|
|
160
|
+
sim.abort!
|
|
161
|
+
expect(sim.completed_at).to be_a(Time)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
describe '#favorable?' do
|
|
166
|
+
it 'returns false for pending simulation' do
|
|
167
|
+
sim.add_step(action: 'a', confidence: 0.9, risk: 0.1)
|
|
168
|
+
expect(sim.favorable?).to be false
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it 'returns true for completed simulation with good confidence and low risk' do
|
|
172
|
+
sim.add_step(action: 'a', confidence: 0.9, risk: 0.1)
|
|
173
|
+
sim.add_step(action: 'b', confidence: 0.9, risk: 0.1)
|
|
174
|
+
sim.run!
|
|
175
|
+
expect(sim.favorable?).to be true
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it 'returns false for aborted simulation' do
|
|
179
|
+
sim.add_step(action: 'a', predicted_outcome: :failure, confidence: 0.9, risk: 0.5)
|
|
180
|
+
sim.run!
|
|
181
|
+
expect(sim.favorable?).to be false
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
describe '#confidence_label' do
|
|
186
|
+
it 'returns :very_confident for high overall confidence' do
|
|
187
|
+
sim.add_step(action: 'a', confidence: 0.95)
|
|
188
|
+
expect(sim.confidence_label).to eq(:very_confident)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it 'returns :very_doubtful when no steps' do
|
|
192
|
+
expect(sim.confidence_label).to eq(:very_doubtful)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
describe '#risk_label' do
|
|
197
|
+
it 'returns :negligible for low risk' do
|
|
198
|
+
sim.add_step(action: 'a', risk: 0.1)
|
|
199
|
+
expect(sim.risk_label).to eq(:negligible)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
it 'returns :critical for very high risk' do
|
|
203
|
+
sim.add_step(action: 'a', risk: 0.95)
|
|
204
|
+
sim.add_step(action: 'b', risk: 0.95)
|
|
205
|
+
expect(sim.risk_label).to eq(:critical)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
describe '#to_h' do
|
|
210
|
+
it 'returns a complete hash' do
|
|
211
|
+
sim.add_step(action: 'test step')
|
|
212
|
+
h = sim.to_h
|
|
213
|
+
expect(h).to include(:id, :label, :domain, :state, :step_count, :overall_confidence,
|
|
214
|
+
:cumulative_risk, :confidence_label, :risk_label, :favorable, :steps)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it 'includes step hashes' do
|
|
218
|
+
sim.add_step(action: 'step one')
|
|
219
|
+
h = sim.to_h
|
|
220
|
+
expect(h[:steps].first[:action]).to eq('step one')
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::MentalSimulation::Helpers::SimulationStep do
|
|
4
|
+
subject(:step) { described_class.new(action: 'deploy service') }
|
|
5
|
+
|
|
6
|
+
describe '#initialize' do
|
|
7
|
+
it 'generates a UUID id' do
|
|
8
|
+
expect(step.id).to match(/\A[0-9a-f-]{36}\z/)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'stores the action' do
|
|
12
|
+
expect(step.action).to eq('deploy service')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'defaults predicted_outcome to :success' do
|
|
16
|
+
expect(step.predicted_outcome).to eq(:success)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'defaults confidence to 0.5' do
|
|
20
|
+
expect(step.confidence).to eq(0.5)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'defaults risk to 0.1' do
|
|
24
|
+
expect(step.risk).to eq(0.1)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'defaults preconditions to empty array' do
|
|
28
|
+
expect(step.preconditions).to eq([])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'defaults postconditions to empty array' do
|
|
32
|
+
expect(step.postconditions).to eq([])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'records created_at' do
|
|
36
|
+
expect(step.created_at).to be_a(Time)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'clamps confidence above 1.0 to 1.0' do
|
|
40
|
+
s = described_class.new(action: 'test', confidence: 1.5)
|
|
41
|
+
expect(s.confidence).to eq(1.0)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'clamps confidence below 0.0 to 0.0' do
|
|
45
|
+
s = described_class.new(action: 'test', confidence: -0.5)
|
|
46
|
+
expect(s.confidence).to eq(0.0)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'clamps risk above 1.0 to 1.0' do
|
|
50
|
+
s = described_class.new(action: 'test', risk: 2.0)
|
|
51
|
+
expect(s.risk).to eq(1.0)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe '#favorable?' do
|
|
56
|
+
it 'returns true for success with sufficient confidence' do
|
|
57
|
+
s = described_class.new(action: 'go', predicted_outcome: :success, confidence: 0.8)
|
|
58
|
+
expect(s.favorable?).to be true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'returns true for partial_success with sufficient confidence' do
|
|
62
|
+
s = described_class.new(action: 'go', predicted_outcome: :partial_success, confidence: 0.6)
|
|
63
|
+
expect(s.favorable?).to be true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'returns false for success with low confidence' do
|
|
67
|
+
s = described_class.new(action: 'go', predicted_outcome: :success, confidence: 0.4)
|
|
68
|
+
expect(s.favorable?).to be false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'returns false for failure regardless of confidence' do
|
|
72
|
+
s = described_class.new(action: 'go', predicted_outcome: :failure, confidence: 0.9)
|
|
73
|
+
expect(s.favorable?).to be false
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'returns false for unknown outcome' do
|
|
77
|
+
s = described_class.new(action: 'go', predicted_outcome: :unknown, confidence: 0.9)
|
|
78
|
+
expect(s.favorable?).to be false
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe '#risky?' do
|
|
83
|
+
it 'returns true when risk >= 0.6' do
|
|
84
|
+
s = described_class.new(action: 'go', risk: 0.7)
|
|
85
|
+
expect(s.risky?).to be true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'returns false when risk < 0.6' do
|
|
89
|
+
s = described_class.new(action: 'go', risk: 0.4)
|
|
90
|
+
expect(s.risky?).to be false
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'returns true at exactly 0.6' do
|
|
94
|
+
s = described_class.new(action: 'go', risk: 0.6)
|
|
95
|
+
expect(s.risky?).to be true
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
describe '#to_h' do
|
|
100
|
+
it 'returns a hash with all fields' do
|
|
101
|
+
h = step.to_h
|
|
102
|
+
expect(h).to include(:id, :action, :predicted_outcome, :confidence, :risk,
|
|
103
|
+
:preconditions, :postconditions, :created_at)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it 'includes stored preconditions' do
|
|
107
|
+
s = described_class.new(action: 'go', preconditions: ['service_up'])
|
|
108
|
+
expect(s.to_h[:preconditions]).to eq(['service_up'])
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|