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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: '09d900581a92a527c43ffce88cf96c499f8aae44a4c12cf00adedd47a079dd38'
|
|
4
|
+
data.tar.gz: 6f5ac264ef4ae83879bec5f515dedebf0662a79eb551804e96669263446e3314
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 70b94eeb7b31f41f9cb721d45229efc17c89e91b25eb8068e4ae3a1de3b316b9f902309d3b2c9188edc51e1d99c39bcbebcf1463909b24a1e5941ea632a41319
|
|
7
|
+
data.tar.gz: a59842aade6c008b7e33927988732045e6e45660be85b2541db543b5ef7005ff4fbfdee22a638786a00b148f50718600bd114e042b933b1203b263817e4f58d2
|
data/Gemfile
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/extinction/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-extinction'
|
|
7
|
+
spec.version = Legion::Extensions::Extinction::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Extinction'
|
|
12
|
+
spec.description = 'Escalation and extinction protocol (4 levels) for brain-modeled agentic AI'
|
|
13
|
+
spec.homepage = 'https://github.com/LegionIO/lex-extinction'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
spec.required_ruby_version = '>= 3.4'
|
|
16
|
+
|
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
18
|
+
spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-extinction'
|
|
19
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-extinction'
|
|
20
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-extinction'
|
|
21
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-extinction/issues'
|
|
22
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
23
|
+
|
|
24
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
25
|
+
Dir.glob('{lib,spec}/**/*') + %w[lex-extinction.gemspec Gemfile]
|
|
26
|
+
end
|
|
27
|
+
spec.require_paths = ['lib']
|
|
28
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/actors/every'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Extinction
|
|
8
|
+
module Actor
|
|
9
|
+
class ProtocolMonitor < Legion::Extensions::Actors::Every
|
|
10
|
+
def runner_class
|
|
11
|
+
Legion::Extensions::Extinction::Runners::Extinction
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def runner_function
|
|
15
|
+
'monitor_protocol'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def time
|
|
19
|
+
300
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def run_now?
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def use_runner?
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def check_subtask?
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def generate_task?
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/extinction/helpers/levels'
|
|
4
|
+
require 'legion/extensions/extinction/helpers/protocol_state'
|
|
5
|
+
require 'legion/extensions/extinction/runners/extinction'
|
|
6
|
+
|
|
7
|
+
module Legion
|
|
8
|
+
module Extensions
|
|
9
|
+
module Extinction
|
|
10
|
+
class Client
|
|
11
|
+
include Runners::Extinction
|
|
12
|
+
|
|
13
|
+
def initialize(**)
|
|
14
|
+
@protocol_state = Helpers::ProtocolState.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
attr_reader :protocol_state
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Extinction
|
|
6
|
+
module Helpers
|
|
7
|
+
module Levels
|
|
8
|
+
# Four escalation levels (spec: extinction-protocol-spec.md)
|
|
9
|
+
ESCALATION_LEVELS = {
|
|
10
|
+
1 => { name: :mesh_isolation, reversible: true, authority: :governance_council },
|
|
11
|
+
2 => { name: :forced_sentinel, reversible: true, authority: :governance_council },
|
|
12
|
+
3 => { name: :full_suspension, reversible: true, authority: :council_plus_executive },
|
|
13
|
+
4 => { name: :cryptographic_erasure, reversible: false, authority: :physical_keyholders }
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
VALID_LEVELS = [1, 2, 3, 4].freeze
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
def valid_level?(level)
|
|
21
|
+
VALID_LEVELS.include?(level)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def level_info(level)
|
|
25
|
+
ESCALATION_LEVELS[level]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def reversible?(level)
|
|
29
|
+
ESCALATION_LEVELS.dig(level, :reversible) || false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def required_authority(level)
|
|
33
|
+
ESCALATION_LEVELS.dig(level, :authority)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Extinction
|
|
8
|
+
module Helpers
|
|
9
|
+
class ProtocolState
|
|
10
|
+
MAX_HISTORY = 500
|
|
11
|
+
|
|
12
|
+
attr_reader :current_level, :history, :active
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@current_level = 0 # 0 = normal operation
|
|
16
|
+
@active = false
|
|
17
|
+
@history = []
|
|
18
|
+
load_from_local
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def escalate(level, authority:, reason:)
|
|
22
|
+
return :invalid_level unless Levels.valid_level?(level)
|
|
23
|
+
return :already_at_or_above if level <= @current_level
|
|
24
|
+
return :insufficient_authority unless authority == Levels.required_authority(level)
|
|
25
|
+
|
|
26
|
+
@current_level = level
|
|
27
|
+
@active = true
|
|
28
|
+
@history << {
|
|
29
|
+
action: :escalate, level: level, authority: authority,
|
|
30
|
+
reason: reason, at: Time.now.utc
|
|
31
|
+
}
|
|
32
|
+
trim_history
|
|
33
|
+
save_to_local
|
|
34
|
+
:escalated
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def deescalate(target_level, authority:, reason:)
|
|
38
|
+
return :not_active unless @active
|
|
39
|
+
return :invalid_target if target_level >= @current_level
|
|
40
|
+
return :irreversible unless Levels.reversible?(@current_level)
|
|
41
|
+
return :insufficient_authority unless authority == Levels.required_authority(@current_level)
|
|
42
|
+
|
|
43
|
+
@current_level = target_level
|
|
44
|
+
@active = target_level.positive?
|
|
45
|
+
@history << {
|
|
46
|
+
action: :deescalate, level: target_level, authority: authority,
|
|
47
|
+
reason: reason, at: Time.now.utc
|
|
48
|
+
}
|
|
49
|
+
trim_history
|
|
50
|
+
save_to_local
|
|
51
|
+
:deescalated
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def to_h
|
|
55
|
+
{
|
|
56
|
+
current_level: @current_level,
|
|
57
|
+
active: @active,
|
|
58
|
+
level_info: @current_level.positive? ? Levels.level_info(@current_level) : nil,
|
|
59
|
+
history_size: @history.size
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def save_to_local
|
|
64
|
+
return unless defined?(Legion::Data::Local) && Legion::Data::Local.connected?
|
|
65
|
+
|
|
66
|
+
row = {
|
|
67
|
+
id: 1,
|
|
68
|
+
current_level: @current_level,
|
|
69
|
+
active: @active,
|
|
70
|
+
history: ::JSON.dump(@history.map { |h| h.merge(at: h[:at].to_s) }),
|
|
71
|
+
updated_at: Time.now.utc
|
|
72
|
+
}
|
|
73
|
+
db = Legion::Data::Local.connection
|
|
74
|
+
if db[:extinction_state].where(id: 1).any?
|
|
75
|
+
db[:extinction_state].where(id: 1).update(row.except(:id))
|
|
76
|
+
else
|
|
77
|
+
db[:extinction_state].insert(row)
|
|
78
|
+
end
|
|
79
|
+
rescue StandardError
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def trim_history
|
|
86
|
+
@history = @history.last(MAX_HISTORY) if @history.size > MAX_HISTORY
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def load_from_local
|
|
90
|
+
return unless defined?(Legion::Data::Local) && Legion::Data::Local.connected?
|
|
91
|
+
|
|
92
|
+
row = Legion::Data::Local.connection[:extinction_state].where(id: 1).first
|
|
93
|
+
return unless row
|
|
94
|
+
|
|
95
|
+
db_level = row[:current_level].to_i
|
|
96
|
+
@current_level = [db_level, @current_level].max
|
|
97
|
+
@active = [true, 1].include?(row[:active])
|
|
98
|
+
@history = parse_history(row[:history])
|
|
99
|
+
rescue StandardError
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def parse_history(raw)
|
|
104
|
+
return [] if raw.nil? || raw.empty?
|
|
105
|
+
|
|
106
|
+
parsed = ::JSON.parse(raw, symbolize_names: true)
|
|
107
|
+
parsed.map do |h|
|
|
108
|
+
h.merge(
|
|
109
|
+
action: h[:action].to_sym,
|
|
110
|
+
authority: h[:authority].to_sym,
|
|
111
|
+
at: Time.parse(h[:at].to_s)
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
rescue StandardError
|
|
115
|
+
[]
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
data/lib/legion/extensions/extinction/local_migrations/20260316000040_create_extinction_state.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Sequel.migration do
|
|
4
|
+
change do
|
|
5
|
+
create_table(:extinction_state) do
|
|
6
|
+
primary_key :id
|
|
7
|
+
Integer :current_level, null: false, default: 0
|
|
8
|
+
TrueClass :active, null: false, default: false
|
|
9
|
+
String :history, text: true
|
|
10
|
+
DateTime :updated_at
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Extinction
|
|
6
|
+
module Runners
|
|
7
|
+
module Extinction
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def escalate(level:, authority:, reason:, **)
|
|
12
|
+
result = protocol_state.escalate(level, authority: authority, reason: reason)
|
|
13
|
+
case result
|
|
14
|
+
when :escalated
|
|
15
|
+
info = Helpers::Levels.level_info(level)
|
|
16
|
+
Legion::Logging.warn "[extinction] ESCALATED: level=#{level} name=#{info[:name]} authority=#{authority} reason=#{reason}"
|
|
17
|
+
enforce_escalation_effects(level)
|
|
18
|
+
emit_escalation_event(level, authority, reason)
|
|
19
|
+
{ escalated: true, level: level, info: info }
|
|
20
|
+
else
|
|
21
|
+
Legion::Logging.debug "[extinction] escalation denied: level=#{level} reason=#{result}"
|
|
22
|
+
{ escalated: false, reason: result }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def deescalate(authority:, reason:, target_level: 0, **)
|
|
27
|
+
result = protocol_state.deescalate(target_level, authority: authority, reason: reason)
|
|
28
|
+
case result
|
|
29
|
+
when :deescalated
|
|
30
|
+
Legion::Logging.info "[extinction] de-escalated: target=#{target_level} authority=#{authority} reason=#{reason}"
|
|
31
|
+
{ deescalated: true, level: target_level }
|
|
32
|
+
else
|
|
33
|
+
Legion::Logging.debug "[extinction] de-escalation denied: target=#{target_level} reason=#{result}"
|
|
34
|
+
{ deescalated: false, reason: result }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def extinction_status(**)
|
|
39
|
+
status = protocol_state.to_h
|
|
40
|
+
Legion::Logging.debug "[extinction] status: level=#{status[:current_level]} active=#{status[:active]}"
|
|
41
|
+
status
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def monitor_protocol(**)
|
|
45
|
+
status = protocol_state.to_h
|
|
46
|
+
level = status[:current_level]
|
|
47
|
+
|
|
48
|
+
if level.positive?
|
|
49
|
+
Legion::Logging.warn "[extinction] ACTIVE: level=#{level} active=#{status[:active]}"
|
|
50
|
+
detect_stale_escalation(level)
|
|
51
|
+
else
|
|
52
|
+
Legion::Logging.debug '[extinction] status: level=0 active=false'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
status
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def check_reversibility(level:, **)
|
|
59
|
+
reversible = Helpers::Levels.reversible?(level)
|
|
60
|
+
Legion::Logging.debug "[extinction] reversibility: level=#{level} reversible=#{reversible}"
|
|
61
|
+
{
|
|
62
|
+
level: level,
|
|
63
|
+
reversible: reversible,
|
|
64
|
+
authority: Helpers::Levels.required_authority(level)
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
STALE_ESCALATION_THRESHOLD = 86_400
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def enforce_escalation_effects(level)
|
|
73
|
+
if level >= 1 && defined?(Legion::Extensions::Mesh::Runners::Mesh)
|
|
74
|
+
Legion::Extensions::Mesh::Runners::Mesh.disconnect rescue nil # rubocop:disable Style/RescueModifier
|
|
75
|
+
Legion::Logging.warn '[extinction] mesh isolation enforced'
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
return unless level == 4
|
|
79
|
+
|
|
80
|
+
if defined?(Legion::Extensions::Privatecore::Runners::Privatecore)
|
|
81
|
+
Legion::Extensions::Privatecore::Runners::Privatecore.erase_all rescue nil # rubocop:disable Style/RescueModifier
|
|
82
|
+
Legion::Logging.warn '[extinction] cryptographic erasure triggered'
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
return unless defined?(Legion::Data::Model::DigitalWorker)
|
|
86
|
+
|
|
87
|
+
begin
|
|
88
|
+
Legion::Data::Model::DigitalWorker.where(lifecycle_state: 'active').update(
|
|
89
|
+
lifecycle_state: 'terminated', updated_at: Time.now.utc
|
|
90
|
+
)
|
|
91
|
+
rescue StandardError
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
Legion::Logging.warn '[extinction] all active workers terminated'
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def emit_escalation_event(level, authority, reason)
|
|
98
|
+
return unless defined?(Legion::Events)
|
|
99
|
+
|
|
100
|
+
info = Helpers::Levels.level_info(level)
|
|
101
|
+
Legion::Events.emit("extinction.#{info[:name]}", {
|
|
102
|
+
level: level, authority: authority, reason: reason, at: Time.now.utc
|
|
103
|
+
})
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def detect_stale_escalation(level)
|
|
107
|
+
last_escalation = protocol_state.history.select { |h| h[:action] == :escalate }.last
|
|
108
|
+
return unless last_escalation && (Time.now.utc - last_escalation[:at]) > STALE_ESCALATION_THRESHOLD
|
|
109
|
+
|
|
110
|
+
Legion::Logging.warn "[extinction] STALE: level=#{level} has been active > 24 hours"
|
|
111
|
+
return unless defined?(Legion::Events)
|
|
112
|
+
|
|
113
|
+
Legion::Events.emit('extinction.stale_escalation', {
|
|
114
|
+
level: level, since: last_escalation[:at],
|
|
115
|
+
hours: ((Time.now.utc - last_escalation[:at]) / 3600).round(1)
|
|
116
|
+
})
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def protocol_state
|
|
120
|
+
@protocol_state ||= Helpers::ProtocolState.new
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/extinction/version'
|
|
4
|
+
require 'legion/extensions/extinction/helpers/levels'
|
|
5
|
+
require 'legion/extensions/extinction/helpers/protocol_state'
|
|
6
|
+
require 'legion/extensions/extinction/runners/extinction'
|
|
7
|
+
|
|
8
|
+
module Legion
|
|
9
|
+
module Extensions
|
|
10
|
+
module Extinction
|
|
11
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
if defined?(Legion::Data::Local)
|
|
17
|
+
Legion::Data::Local.register_migrations(
|
|
18
|
+
name: :extinction,
|
|
19
|
+
path: File.join(__dir__, 'extinction', 'local_migrations')
|
|
20
|
+
)
|
|
21
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Actors
|
|
6
|
+
class Every; end # rubocop:disable Lint/EmptyClass
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
$LOADED_FEATURES << 'legion/extensions/actors/every'
|
|
12
|
+
|
|
13
|
+
require_relative '../../../../../lib/legion/extensions/extinction/actors/protocol_monitor'
|
|
14
|
+
|
|
15
|
+
RSpec.describe Legion::Extensions::Extinction::Actor::ProtocolMonitor do
|
|
16
|
+
subject(:actor) { described_class.new }
|
|
17
|
+
|
|
18
|
+
describe '#runner_class' do
|
|
19
|
+
it { expect(actor.runner_class).to eq Legion::Extensions::Extinction::Runners::Extinction }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe '#runner_function' do
|
|
23
|
+
it { expect(actor.runner_function).to eq 'monitor_protocol' }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe '#time' do
|
|
27
|
+
it { expect(actor.time).to eq 300 }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe '#run_now?' do
|
|
31
|
+
it { expect(actor.run_now?).to be false }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe '#use_runner?' do
|
|
35
|
+
it { expect(actor.use_runner?).to be false }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe '#check_subtask?' do
|
|
39
|
+
it { expect(actor.check_subtask?).to be false }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe '#generate_task?' do
|
|
43
|
+
it { expect(actor.generate_task?).to be false }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/extinction/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Extinction::Client do
|
|
6
|
+
it 'responds to extinction runner methods' do
|
|
7
|
+
client = described_class.new
|
|
8
|
+
expect(client).to respond_to(:escalate)
|
|
9
|
+
expect(client).to respond_to(:deescalate)
|
|
10
|
+
expect(client).to respond_to(:extinction_status)
|
|
11
|
+
expect(client).to respond_to(:check_reversibility)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/extinction/helpers/levels'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Extinction::Helpers::Levels do
|
|
6
|
+
describe 'ESCALATION_LEVELS' do
|
|
7
|
+
it 'defines exactly four levels' do
|
|
8
|
+
expect(described_class::ESCALATION_LEVELS.size).to eq(4)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'is keyed by integers 1 through 4' do
|
|
12
|
+
expect(described_class::ESCALATION_LEVELS.keys).to eq([1, 2, 3, 4])
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'is frozen' do
|
|
16
|
+
expect(described_class::ESCALATION_LEVELS).to be_frozen
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'defines level 1 as mesh_isolation' do
|
|
20
|
+
expect(described_class::ESCALATION_LEVELS[1][:name]).to eq(:mesh_isolation)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'defines level 2 as forced_sentinel' do
|
|
24
|
+
expect(described_class::ESCALATION_LEVELS[2][:name]).to eq(:forced_sentinel)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'defines level 3 as full_suspension' do
|
|
28
|
+
expect(described_class::ESCALATION_LEVELS[3][:name]).to eq(:full_suspension)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'defines level 4 as cryptographic_erasure' do
|
|
32
|
+
expect(described_class::ESCALATION_LEVELS[4][:name]).to eq(:cryptographic_erasure)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'marks levels 1-3 as reversible' do
|
|
36
|
+
[1, 2, 3].each do |level|
|
|
37
|
+
expect(described_class::ESCALATION_LEVELS[level][:reversible]).to be true
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'marks level 4 as not reversible' do
|
|
42
|
+
expect(described_class::ESCALATION_LEVELS[4][:reversible]).to be false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'assigns governance_council authority to levels 1 and 2' do
|
|
46
|
+
expect(described_class::ESCALATION_LEVELS[1][:authority]).to eq(:governance_council)
|
|
47
|
+
expect(described_class::ESCALATION_LEVELS[2][:authority]).to eq(:governance_council)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'assigns council_plus_executive authority to level 3' do
|
|
51
|
+
expect(described_class::ESCALATION_LEVELS[3][:authority]).to eq(:council_plus_executive)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'assigns physical_keyholders authority to level 4' do
|
|
55
|
+
expect(described_class::ESCALATION_LEVELS[4][:authority]).to eq(:physical_keyholders)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
describe 'VALID_LEVELS' do
|
|
60
|
+
it 'contains exactly [1, 2, 3, 4]' do
|
|
61
|
+
expect(described_class::VALID_LEVELS).to eq([1, 2, 3, 4])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'is frozen' do
|
|
65
|
+
expect(described_class::VALID_LEVELS).to be_frozen
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
describe '.valid_level?' do
|
|
70
|
+
it 'returns true for level 1' do
|
|
71
|
+
expect(described_class.valid_level?(1)).to be true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'returns true for level 2' do
|
|
75
|
+
expect(described_class.valid_level?(2)).to be true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'returns true for level 3' do
|
|
79
|
+
expect(described_class.valid_level?(3)).to be true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'returns true for level 4' do
|
|
83
|
+
expect(described_class.valid_level?(4)).to be true
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'returns false for level 0' do
|
|
87
|
+
expect(described_class.valid_level?(0)).to be false
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'returns false for level 5' do
|
|
91
|
+
expect(described_class.valid_level?(5)).to be false
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'returns false for negative levels' do
|
|
95
|
+
expect(described_class.valid_level?(-1)).to be false
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'returns false for nil' do
|
|
99
|
+
expect(described_class.valid_level?(nil)).to be false
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'returns false for string level' do
|
|
103
|
+
expect(described_class.valid_level?('1')).to be false
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
describe '.level_info' do
|
|
108
|
+
it 'returns the full info hash for a valid level' do
|
|
109
|
+
info = described_class.level_info(1)
|
|
110
|
+
expect(info).to be_a(Hash)
|
|
111
|
+
expect(info.keys).to contain_exactly(:name, :reversible, :authority)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'returns nil for an invalid level' do
|
|
115
|
+
expect(described_class.level_info(99)).to be_nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'returns nil for level 0' do
|
|
119
|
+
expect(described_class.level_info(0)).to be_nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
[1, 2, 3, 4].each do |level|
|
|
123
|
+
it "returns a hash for level #{level}" do
|
|
124
|
+
expect(described_class.level_info(level)).to be_a(Hash)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
describe '.reversible?' do
|
|
130
|
+
it 'returns true for level 1' do
|
|
131
|
+
expect(described_class.reversible?(1)).to be true
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it 'returns true for level 2' do
|
|
135
|
+
expect(described_class.reversible?(2)).to be true
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it 'returns true for level 3' do
|
|
139
|
+
expect(described_class.reversible?(3)).to be true
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it 'returns false for level 4' do
|
|
143
|
+
expect(described_class.reversible?(4)).to be false
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'returns false for an invalid level (fallback to false)' do
|
|
147
|
+
expect(described_class.reversible?(99)).to be false
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it 'returns false for nil level' do
|
|
151
|
+
expect(described_class.reversible?(nil)).to be false
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
describe '.required_authority' do
|
|
156
|
+
it 'returns :governance_council for level 1' do
|
|
157
|
+
expect(described_class.required_authority(1)).to eq(:governance_council)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it 'returns :governance_council for level 2' do
|
|
161
|
+
expect(described_class.required_authority(2)).to eq(:governance_council)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it 'returns :council_plus_executive for level 3' do
|
|
165
|
+
expect(described_class.required_authority(3)).to eq(:council_plus_executive)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it 'returns :physical_keyholders for level 4' do
|
|
169
|
+
expect(described_class.required_authority(4)).to eq(:physical_keyholders)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it 'returns nil for an invalid level' do
|
|
173
|
+
expect(described_class.required_authority(0)).to be_nil
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it 'returns nil for an out-of-range level' do
|
|
177
|
+
expect(described_class.required_authority(5)).to be_nil
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|