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.
@@ -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