lex-extinction 0.2.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 +10 -0
- data/lex-extinction.gemspec +28 -0
- data/lib/legion/extensions/extinction/actors/protocol_monitor.rb +41 -0
- data/lib/legion/extensions/extinction/client.rb +23 -0
- data/lib/legion/extensions/extinction/helpers/levels.rb +39 -0
- data/lib/legion/extensions/extinction/helpers/protocol_state.rb +121 -0
- data/lib/legion/extensions/extinction/local_migrations/20260316000040_create_extinction_state.rb +13 -0
- data/lib/legion/extensions/extinction/runners/extinction.rb +126 -0
- data/lib/legion/extensions/extinction/version.rb +9 -0
- data/lib/legion/extensions/extinction.rb +21 -0
- data/spec/legion/extensions/extinction/actors/protocol_monitor_spec.rb +45 -0
- data/spec/legion/extensions/extinction/client_spec.rb +13 -0
- data/spec/legion/extensions/extinction/helpers/levels_spec.rb +180 -0
- data/spec/legion/extensions/extinction/helpers/protocol_state_spec.rb +291 -0
- data/spec/legion/extensions/extinction/runners/extinction_spec.rb +114 -0
- data/spec/local_persistence_spec.rb +188 -0
- data/spec/spec_helper.rb +20 -0
- metadata +64 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/extinction/helpers/levels'
|
|
4
|
+
require 'legion/extensions/extinction/helpers/protocol_state'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Legion::Extensions::Extinction::Helpers::ProtocolState do
|
|
7
|
+
subject(:state) { described_class.new }
|
|
8
|
+
|
|
9
|
+
describe '#initialize' do
|
|
10
|
+
it 'starts at level 0' do
|
|
11
|
+
expect(state.current_level).to eq(0)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'starts as inactive' do
|
|
15
|
+
expect(state.active).to be false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'starts with an empty history' do
|
|
19
|
+
expect(state.history).to eq([])
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe '#escalate' do
|
|
24
|
+
context 'with an invalid level' do
|
|
25
|
+
it 'returns :invalid_level for level 0' do
|
|
26
|
+
expect(state.escalate(0, authority: :governance_council, reason: 'test')).to eq(:invalid_level)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'returns :invalid_level for level 5' do
|
|
30
|
+
expect(state.escalate(5, authority: :governance_council, reason: 'test')).to eq(:invalid_level)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'does not change current_level on invalid level' do
|
|
34
|
+
state.escalate(99, authority: :governance_council, reason: 'test')
|
|
35
|
+
expect(state.current_level).to eq(0)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'does not add to history on invalid level' do
|
|
39
|
+
state.escalate(99, authority: :governance_council, reason: 'test')
|
|
40
|
+
expect(state.history).to be_empty
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
context 'when already at or above the requested level' do
|
|
45
|
+
before { state.escalate(2, authority: :governance_council, reason: 'setup') }
|
|
46
|
+
|
|
47
|
+
it 'returns :already_at_or_above for same level' do
|
|
48
|
+
expect(state.escalate(2, authority: :governance_council, reason: 'test')).to eq(:already_at_or_above)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'returns :already_at_or_above for lower level' do
|
|
52
|
+
expect(state.escalate(1, authority: :governance_council, reason: 'test')).to eq(:already_at_or_above)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
context 'with insufficient authority' do
|
|
57
|
+
it 'returns :insufficient_authority for level 1 with wrong authority' do
|
|
58
|
+
result = state.escalate(1, authority: :physical_keyholders, reason: 'test')
|
|
59
|
+
expect(result).to eq(:insufficient_authority)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'returns :insufficient_authority for level 3 with governance_council' do
|
|
63
|
+
state.escalate(1, authority: :governance_council, reason: 'step1')
|
|
64
|
+
state.escalate(2, authority: :governance_council, reason: 'step2')
|
|
65
|
+
result = state.escalate(3, authority: :governance_council, reason: 'test')
|
|
66
|
+
expect(result).to eq(:insufficient_authority)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'returns :insufficient_authority for level 4 with governance_council' do
|
|
70
|
+
result = state.escalate(4, authority: :governance_council, reason: 'test')
|
|
71
|
+
expect(result).to eq(:insufficient_authority)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'does not change level on insufficient authority' do
|
|
75
|
+
state.escalate(1, authority: :physical_keyholders, reason: 'test')
|
|
76
|
+
expect(state.current_level).to eq(0)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
context 'with valid escalation' do
|
|
81
|
+
it 'returns :escalated for level 1 with governance_council' do
|
|
82
|
+
expect(state.escalate(1, authority: :governance_council, reason: 'threat')).to eq(:escalated)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'updates current_level to 1' do
|
|
86
|
+
state.escalate(1, authority: :governance_council, reason: 'threat')
|
|
87
|
+
expect(state.current_level).to eq(1)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'sets active to true' do
|
|
91
|
+
state.escalate(1, authority: :governance_council, reason: 'threat')
|
|
92
|
+
expect(state.active).to be true
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it 'appends an entry to history' do
|
|
96
|
+
state.escalate(1, authority: :governance_council, reason: 'threat')
|
|
97
|
+
expect(state.history.size).to eq(1)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'records the correct action in history' do
|
|
101
|
+
state.escalate(1, authority: :governance_council, reason: 'threat')
|
|
102
|
+
expect(state.history.last[:action]).to eq(:escalate)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it 'records the level in history' do
|
|
106
|
+
state.escalate(1, authority: :governance_council, reason: 'threat')
|
|
107
|
+
expect(state.history.last[:level]).to eq(1)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'records the authority in history' do
|
|
111
|
+
state.escalate(1, authority: :governance_council, reason: 'threat')
|
|
112
|
+
expect(state.history.last[:authority]).to eq(:governance_council)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'records the reason in history' do
|
|
116
|
+
state.escalate(1, authority: :governance_council, reason: 'threat')
|
|
117
|
+
expect(state.history.last[:reason]).to eq('threat')
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'records a Time in history' do
|
|
121
|
+
state.escalate(1, authority: :governance_council, reason: 'threat')
|
|
122
|
+
expect(state.history.last[:at]).to be_a(Time)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'escalates to level 4 with physical_keyholders after reaching level 3' do
|
|
126
|
+
state.escalate(1, authority: :governance_council, reason: 's1')
|
|
127
|
+
state.escalate(2, authority: :governance_council, reason: 's2')
|
|
128
|
+
state.escalate(3, authority: :council_plus_executive, reason: 's3')
|
|
129
|
+
result = state.escalate(4, authority: :physical_keyholders, reason: 's4')
|
|
130
|
+
expect(result).to eq(:escalated)
|
|
131
|
+
expect(state.current_level).to eq(4)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it 'accumulates history across multiple escalations' do
|
|
135
|
+
state.escalate(1, authority: :governance_council, reason: 'r1')
|
|
136
|
+
state.escalate(2, authority: :governance_council, reason: 'r2')
|
|
137
|
+
expect(state.history.size).to eq(2)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it 'calls save_to_local after escalation' do
|
|
141
|
+
expect(state).to receive(:save_to_local)
|
|
142
|
+
state.escalate(1, authority: :governance_council, reason: 'test')
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it 'trims history beyond MAX_HISTORY' do
|
|
146
|
+
s = described_class.new
|
|
147
|
+
s.instance_variable_set(:@history, Array.new(510) { { action: :escalate, at: Time.now } })
|
|
148
|
+
s.send(:trim_history)
|
|
149
|
+
expect(s.history.size).to eq(500)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
describe '#deescalate' do
|
|
155
|
+
context 'when protocol is not active' do
|
|
156
|
+
it 'returns :not_active' do
|
|
157
|
+
expect(state.deescalate(0, authority: :governance_council, reason: 'test')).to eq(:not_active)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
context 'when target_level is invalid (not lower than current)' do
|
|
162
|
+
before { state.escalate(2, authority: :governance_council, reason: 'setup') }
|
|
163
|
+
|
|
164
|
+
it 'returns :invalid_target when target equals current' do
|
|
165
|
+
result = state.deescalate(2, authority: :governance_council, reason: 'test')
|
|
166
|
+
expect(result).to eq(:invalid_target)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it 'returns :invalid_target when target is above current' do
|
|
170
|
+
result = state.deescalate(3, authority: :governance_council, reason: 'test')
|
|
171
|
+
expect(result).to eq(:invalid_target)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
context 'when current level is irreversible (level 4)' do
|
|
176
|
+
before do
|
|
177
|
+
state.escalate(1, authority: :governance_council, reason: 's1')
|
|
178
|
+
state.escalate(2, authority: :governance_council, reason: 's2')
|
|
179
|
+
state.escalate(3, authority: :council_plus_executive, reason: 's3')
|
|
180
|
+
state.escalate(4, authority: :physical_keyholders, reason: 's4')
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it 'returns :irreversible' do
|
|
184
|
+
result = state.deescalate(0, authority: :physical_keyholders, reason: 'try')
|
|
185
|
+
expect(result).to eq(:irreversible)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it 'does not change current_level' do
|
|
189
|
+
state.deescalate(0, authority: :physical_keyholders, reason: 'try')
|
|
190
|
+
expect(state.current_level).to eq(4)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
context 'with insufficient authority for deescalation' do
|
|
195
|
+
before do
|
|
196
|
+
state.escalate(1, authority: :governance_council, reason: 's1')
|
|
197
|
+
state.escalate(2, authority: :governance_council, reason: 's2')
|
|
198
|
+
state.escalate(3, authority: :council_plus_executive, reason: 's3')
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it 'rejects wrong authority for level 3' do
|
|
202
|
+
result = state.deescalate(0, authority: :governance_council, reason: 'test')
|
|
203
|
+
expect(result).to eq(:insufficient_authority)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it 'accepts correct authority for level 3' do
|
|
207
|
+
result = state.deescalate(0, authority: :council_plus_executive, reason: 'test')
|
|
208
|
+
expect(result).to eq(:deescalated)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
context 'with valid de-escalation' do
|
|
213
|
+
before { state.escalate(2, authority: :governance_council, reason: 'setup') }
|
|
214
|
+
|
|
215
|
+
it 'returns :deescalated' do
|
|
216
|
+
result = state.deescalate(1, authority: :governance_council, reason: 'resolved')
|
|
217
|
+
expect(result).to eq(:deescalated)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it 'updates current_level to the target' do
|
|
221
|
+
state.deescalate(1, authority: :governance_council, reason: 'resolved')
|
|
222
|
+
expect(state.current_level).to eq(1)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
it 'remains active when target_level is positive' do
|
|
226
|
+
state.deescalate(1, authority: :governance_council, reason: 'resolved')
|
|
227
|
+
expect(state.active).to be true
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
it 'becomes inactive when target_level is 0' do
|
|
231
|
+
state.deescalate(0, authority: :governance_council, reason: 'fully resolved')
|
|
232
|
+
expect(state.active).to be false
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it 'appends a deescalate entry to history' do
|
|
236
|
+
state.deescalate(0, authority: :governance_council, reason: 'resolved')
|
|
237
|
+
last = state.history.last
|
|
238
|
+
expect(last[:action]).to eq(:deescalate)
|
|
239
|
+
expect(last[:level]).to eq(0)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
it 'records the reason in history' do
|
|
243
|
+
state.deescalate(0, authority: :governance_council, reason: 'resolved')
|
|
244
|
+
expect(state.history.last[:reason]).to eq('resolved')
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
it 'records a Time in the deescalate history entry' do
|
|
248
|
+
state.deescalate(0, authority: :governance_council, reason: 'resolved')
|
|
249
|
+
expect(state.history.last[:at]).to be_a(Time)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
it 'calls save_to_local after deescalation' do
|
|
253
|
+
expect(state).to receive(:save_to_local)
|
|
254
|
+
state.deescalate(0, authority: :governance_council, reason: 'test')
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
describe '#to_h' do
|
|
260
|
+
it 'returns a hash' do
|
|
261
|
+
expect(state.to_h).to be_a(Hash)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
it 'includes current_level' do
|
|
265
|
+
expect(state.to_h[:current_level]).to eq(0)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
it 'includes active' do
|
|
269
|
+
expect(state.to_h[:active]).to be false
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
it 'includes history_size' do
|
|
273
|
+
expect(state.to_h[:history_size]).to eq(0)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
it 'includes level_info as nil when at level 0' do
|
|
277
|
+
expect(state.to_h[:level_info]).to be_nil
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
it 'includes non-nil level_info when escalated' do
|
|
281
|
+
state.escalate(1, authority: :governance_council, reason: 'test')
|
|
282
|
+
expect(state.to_h[:level_info]).not_to be_nil
|
|
283
|
+
expect(state.to_h[:level_info][:name]).to eq(:mesh_isolation)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
it 'reflects updated history_size after escalation' do
|
|
287
|
+
state.escalate(1, authority: :governance_council, reason: 'test')
|
|
288
|
+
expect(state.to_h[:history_size]).to eq(1)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/extinction/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Extinction::Runners::Extinction do
|
|
6
|
+
let(:client) { Legion::Extensions::Extinction::Client.new }
|
|
7
|
+
|
|
8
|
+
describe '#escalate' do
|
|
9
|
+
it 'escalates to level 1' do
|
|
10
|
+
result = client.escalate(level: 1, authority: :governance_council, reason: 'threat detected')
|
|
11
|
+
expect(result[:escalated]).to be true
|
|
12
|
+
expect(result[:level]).to eq(1)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'rejects wrong authority' do
|
|
16
|
+
result = client.escalate(level: 4, authority: :governance_council, reason: 'test')
|
|
17
|
+
expect(result[:escalated]).to be false
|
|
18
|
+
expect(result[:reason]).to eq(:insufficient_authority)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'rejects escalation to same or lower level' do
|
|
22
|
+
client.escalate(level: 2, authority: :governance_council, reason: 'test')
|
|
23
|
+
result = client.escalate(level: 1, authority: :governance_council, reason: 'test')
|
|
24
|
+
expect(result[:escalated]).to be false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'escalates to level 4 with physical keyholders' do
|
|
28
|
+
client.escalate(level: 1, authority: :governance_council, reason: 'step 1')
|
|
29
|
+
client.escalate(level: 2, authority: :governance_council, reason: 'step 2')
|
|
30
|
+
client.escalate(level: 3, authority: :council_plus_executive, reason: 'step 3')
|
|
31
|
+
result = client.escalate(level: 4, authority: :physical_keyholders, reason: 'final')
|
|
32
|
+
expect(result[:escalated]).to be true
|
|
33
|
+
expect(result[:info][:reversible]).to be false
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe '#deescalate' do
|
|
38
|
+
it 'deescalates from reversible level' do
|
|
39
|
+
client.escalate(level: 2, authority: :governance_council, reason: 'test')
|
|
40
|
+
result = client.deescalate(target_level: 0, authority: :governance_council, reason: 'resolved')
|
|
41
|
+
expect(result[:deescalated]).to be true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'cannot deescalate level 4 (irreversible)' do
|
|
45
|
+
client.escalate(level: 1, authority: :governance_council, reason: 's1')
|
|
46
|
+
client.escalate(level: 2, authority: :governance_council, reason: 's2')
|
|
47
|
+
client.escalate(level: 3, authority: :council_plus_executive, reason: 's3')
|
|
48
|
+
client.escalate(level: 4, authority: :physical_keyholders, reason: 's4')
|
|
49
|
+
result = client.deescalate(target_level: 0, authority: :physical_keyholders, reason: 'try')
|
|
50
|
+
expect(result[:deescalated]).to be false
|
|
51
|
+
expect(result[:reason]).to eq(:irreversible)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe '#extinction_status' do
|
|
56
|
+
it 'returns current state' do
|
|
57
|
+
status = client.extinction_status
|
|
58
|
+
expect(status[:current_level]).to eq(0)
|
|
59
|
+
expect(status[:active]).to be false
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe '#escalate side effects' do
|
|
64
|
+
it 'emits escalation event when Legion::Events is defined' do
|
|
65
|
+
events = Module.new { def self.emit(*, **); end }
|
|
66
|
+
stub_const('Legion::Events', events)
|
|
67
|
+
expect(events).to receive(:emit).with('extinction.mesh_isolation', hash_including(level: 1))
|
|
68
|
+
client.escalate(level: 1, authority: :governance_council, reason: 'test')
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'triggers cryptographic erasure at level 4' do
|
|
72
|
+
events = Module.new { def self.emit(*, **); end }
|
|
73
|
+
stub_const('Legion::Events', events)
|
|
74
|
+
pc_mod = Module.new { def self.erase_all; end }
|
|
75
|
+
stub_const('Legion::Extensions::Privatecore::Runners::Privatecore', pc_mod)
|
|
76
|
+
|
|
77
|
+
client.escalate(level: 1, authority: :governance_council, reason: 's1')
|
|
78
|
+
client.escalate(level: 2, authority: :governance_council, reason: 's2')
|
|
79
|
+
client.escalate(level: 3, authority: :council_plus_executive, reason: 's3')
|
|
80
|
+
expect(pc_mod).to receive(:erase_all)
|
|
81
|
+
client.escalate(level: 4, authority: :physical_keyholders, reason: 'final')
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
describe '#monitor_protocol' do
|
|
86
|
+
it 'returns status hash at level 0' do
|
|
87
|
+
result = client.monitor_protocol
|
|
88
|
+
expect(result[:current_level]).to eq(0)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'detects stale escalation' do
|
|
92
|
+
client.escalate(level: 1, authority: :governance_council, reason: 'test')
|
|
93
|
+
state = client.send(:protocol_state)
|
|
94
|
+
state.history.last[:at] = Time.now.utc - 100_000
|
|
95
|
+
|
|
96
|
+
events = Module.new { def self.emit(*, **); end }
|
|
97
|
+
stub_const('Legion::Events', events)
|
|
98
|
+
expect(events).to receive(:emit).with('extinction.stale_escalation', anything)
|
|
99
|
+
client.monitor_protocol
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
describe '#check_reversibility' do
|
|
104
|
+
it 'reports levels 1-3 as reversible' do
|
|
105
|
+
[1, 2, 3].each do |level|
|
|
106
|
+
expect(client.check_reversibility(level: level)[:reversible]).to be true
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'reports level 4 as irreversible' do
|
|
111
|
+
expect(client.check_reversibility(level: 4)[:reversible]).to be false
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'sequel'
|
|
4
|
+
require 'sequel/extensions/migration'
|
|
5
|
+
|
|
6
|
+
require 'legion/extensions/extinction/helpers/levels'
|
|
7
|
+
require 'legion/extensions/extinction/helpers/protocol_state'
|
|
8
|
+
|
|
9
|
+
MIGRATIONS_PATH = File.expand_path(
|
|
10
|
+
'../lib/legion/extensions/extinction/local_migrations',
|
|
11
|
+
__dir__
|
|
12
|
+
).freeze
|
|
13
|
+
|
|
14
|
+
# Minimal stub for Legion::Data::Local used only within this spec file.
|
|
15
|
+
module Legion
|
|
16
|
+
module Data
|
|
17
|
+
module Local
|
|
18
|
+
class << self
|
|
19
|
+
attr_accessor :_connection, :_connected
|
|
20
|
+
|
|
21
|
+
def connection
|
|
22
|
+
_connection
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def connected?
|
|
26
|
+
_connected == true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def reset_test!
|
|
30
|
+
self._connection = nil
|
|
31
|
+
self._connected = false
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
RSpec.describe Legion::Extensions::Extinction::Helpers::ProtocolState do
|
|
39
|
+
let(:db) do
|
|
40
|
+
conn = Sequel.sqlite
|
|
41
|
+
Sequel::TimestampMigrator.new(conn, MIGRATIONS_PATH).run
|
|
42
|
+
conn
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
before do
|
|
46
|
+
Legion::Data::Local._connection = db
|
|
47
|
+
Legion::Data::Local._connected = true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
after do
|
|
51
|
+
Legion::Data::Local.reset_test!
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe 'save and load round-trip' do
|
|
55
|
+
it 'persists current_level via save_to_local and restores it on a new instance' do
|
|
56
|
+
state = described_class.new
|
|
57
|
+
state.escalate(1, authority: :governance_council, reason: 'test threat')
|
|
58
|
+
state.save_to_local
|
|
59
|
+
|
|
60
|
+
restored = described_class.new
|
|
61
|
+
expect(restored.current_level).to eq(1)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'persists active flag' do
|
|
65
|
+
state = described_class.new
|
|
66
|
+
state.escalate(1, authority: :governance_council, reason: 'threat')
|
|
67
|
+
state.save_to_local
|
|
68
|
+
|
|
69
|
+
restored = described_class.new
|
|
70
|
+
expect(restored.active).to be true
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'persists history entries' do
|
|
74
|
+
state = described_class.new
|
|
75
|
+
state.escalate(1, authority: :governance_council, reason: 'threat')
|
|
76
|
+
state.save_to_local
|
|
77
|
+
|
|
78
|
+
restored = described_class.new
|
|
79
|
+
expect(restored.history.size).to eq(1)
|
|
80
|
+
expect(restored.history.first[:action]).to eq(:escalate)
|
|
81
|
+
expect(restored.history.first[:level]).to eq(1)
|
|
82
|
+
expect(restored.history.first[:reason]).to eq('threat')
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'restores history :at as a Time object' do
|
|
86
|
+
state = described_class.new
|
|
87
|
+
state.escalate(1, authority: :governance_council, reason: 'threat')
|
|
88
|
+
state.save_to_local
|
|
89
|
+
|
|
90
|
+
restored = described_class.new
|
|
91
|
+
expect(restored.history.first[:at]).to be_a(Time)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'saves and loads an inactive protocol at level 0' do
|
|
95
|
+
state = described_class.new
|
|
96
|
+
# state is at level 0, no escalation
|
|
97
|
+
state.save_to_local
|
|
98
|
+
|
|
99
|
+
restored = described_class.new
|
|
100
|
+
expect(restored.current_level).to eq(0)
|
|
101
|
+
expect(restored.active).to be false
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'updates the existing row on a second save rather than inserting a duplicate' do
|
|
105
|
+
state = described_class.new
|
|
106
|
+
state.escalate(1, authority: :governance_council, reason: 'first')
|
|
107
|
+
state.save_to_local
|
|
108
|
+
|
|
109
|
+
state.escalate(2, authority: :governance_council, reason: 'second')
|
|
110
|
+
state.save_to_local
|
|
111
|
+
|
|
112
|
+
expect(db[:extinction_state].count).to eq(1)
|
|
113
|
+
|
|
114
|
+
restored = described_class.new
|
|
115
|
+
expect(restored.current_level).to eq(2)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
describe 'level-4 irreversibility across restarts' do
|
|
120
|
+
it 'starts at level 4 when DB contains level 4' do
|
|
121
|
+
# Directly insert a level-4 row to simulate a previous escalation
|
|
122
|
+
db[:extinction_state].insert(
|
|
123
|
+
id: 1,
|
|
124
|
+
current_level: 4,
|
|
125
|
+
active: true,
|
|
126
|
+
history: '[]',
|
|
127
|
+
updated_at: Time.now.utc
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
state = described_class.new
|
|
131
|
+
expect(state.current_level).to eq(4)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it 'cannot be de-escalated after reloading level 4 from DB' do
|
|
135
|
+
db[:extinction_state].insert(
|
|
136
|
+
id: 1,
|
|
137
|
+
current_level: 4,
|
|
138
|
+
active: true,
|
|
139
|
+
history: '[]',
|
|
140
|
+
updated_at: Time.now.utc
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
state = described_class.new
|
|
144
|
+
result = state.deescalate(0, authority: :physical_keyholders, reason: 'trying to escape')
|
|
145
|
+
expect(result).to eq(:irreversible)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'uses max(db_level, in-memory level) so a fresh instance never drops below DB level' do
|
|
149
|
+
db[:extinction_state].insert(
|
|
150
|
+
id: 1,
|
|
151
|
+
current_level: 3,
|
|
152
|
+
active: true,
|
|
153
|
+
history: '[]',
|
|
154
|
+
updated_at: Time.now.utc
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
state = described_class.new
|
|
158
|
+
# In-memory starts at 0, DB has 3, so max(3, 0) = 3
|
|
159
|
+
expect(state.current_level).to eq(3)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
describe 'graceful no-op when Local is not connected' do
|
|
164
|
+
before do
|
|
165
|
+
Legion::Data::Local._connected = false
|
|
166
|
+
Legion::Data::Local._connection = nil
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it 'initializes normally when Local is disconnected' do
|
|
170
|
+
state = described_class.new
|
|
171
|
+
expect(state.current_level).to eq(0)
|
|
172
|
+
expect(state.active).to be false
|
|
173
|
+
expect(state.history).to eq([])
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it 'save_to_local returns nil without raising when disconnected' do
|
|
177
|
+
state = described_class.new
|
|
178
|
+
expect { state.save_to_local }.not_to raise_error
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it 'does not mutate state during save_to_local when disconnected' do
|
|
182
|
+
state = described_class.new
|
|
183
|
+
state.escalate(1, authority: :governance_council, reason: 'test')
|
|
184
|
+
state.save_to_local
|
|
185
|
+
expect(state.current_level).to eq(1)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Logging
|
|
7
|
+
def self.debug(_msg); end
|
|
8
|
+
def self.info(_msg); end
|
|
9
|
+
def self.warn(_msg); end
|
|
10
|
+
def self.error(_msg); end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
require 'legion/extensions/extinction'
|
|
15
|
+
|
|
16
|
+
RSpec.configure do |config|
|
|
17
|
+
config.example_status_persistence_file_path = '.rspec_status'
|
|
18
|
+
config.disable_monkey_patching!
|
|
19
|
+
config.expect_with(:rspec) { |c| c.syntax = :expect }
|
|
20
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-extinction
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Esity
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Escalation and extinction protocol (4 levels) for brain-modeled agentic
|
|
13
|
+
AI
|
|
14
|
+
email:
|
|
15
|
+
- matthewdiverson@gmail.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- Gemfile
|
|
21
|
+
- lex-extinction.gemspec
|
|
22
|
+
- lib/legion/extensions/extinction.rb
|
|
23
|
+
- lib/legion/extensions/extinction/actors/protocol_monitor.rb
|
|
24
|
+
- lib/legion/extensions/extinction/client.rb
|
|
25
|
+
- lib/legion/extensions/extinction/helpers/levels.rb
|
|
26
|
+
- lib/legion/extensions/extinction/helpers/protocol_state.rb
|
|
27
|
+
- lib/legion/extensions/extinction/local_migrations/20260316000040_create_extinction_state.rb
|
|
28
|
+
- lib/legion/extensions/extinction/runners/extinction.rb
|
|
29
|
+
- lib/legion/extensions/extinction/version.rb
|
|
30
|
+
- spec/legion/extensions/extinction/actors/protocol_monitor_spec.rb
|
|
31
|
+
- spec/legion/extensions/extinction/client_spec.rb
|
|
32
|
+
- spec/legion/extensions/extinction/helpers/levels_spec.rb
|
|
33
|
+
- spec/legion/extensions/extinction/helpers/protocol_state_spec.rb
|
|
34
|
+
- spec/legion/extensions/extinction/runners/extinction_spec.rb
|
|
35
|
+
- spec/local_persistence_spec.rb
|
|
36
|
+
- spec/spec_helper.rb
|
|
37
|
+
homepage: https://github.com/LegionIO/lex-extinction
|
|
38
|
+
licenses:
|
|
39
|
+
- MIT
|
|
40
|
+
metadata:
|
|
41
|
+
homepage_uri: https://github.com/LegionIO/lex-extinction
|
|
42
|
+
source_code_uri: https://github.com/LegionIO/lex-extinction
|
|
43
|
+
documentation_uri: https://github.com/LegionIO/lex-extinction
|
|
44
|
+
changelog_uri: https://github.com/LegionIO/lex-extinction
|
|
45
|
+
bug_tracker_uri: https://github.com/LegionIO/lex-extinction/issues
|
|
46
|
+
rubygems_mfa_required: 'true'
|
|
47
|
+
rdoc_options: []
|
|
48
|
+
require_paths:
|
|
49
|
+
- lib
|
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.4'
|
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0'
|
|
60
|
+
requirements: []
|
|
61
|
+
rubygems_version: 3.6.9
|
|
62
|
+
specification_version: 4
|
|
63
|
+
summary: LEX Extinction
|
|
64
|
+
test_files: []
|