lex-agentic-learning 0.1.12 → 0.1.13
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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +1 -1
- data/lib/legion/extensions/agentic/learning/chrysalis/runners/cognitive_chrysalis.rb +1 -1
- data/lib/legion/extensions/agentic/learning/outcome_listener/client.rb +22 -9
- data/lib/legion/extensions/agentic/learning/outcome_listener/helpers/constants.rb +2 -0
- data/lib/legion/extensions/agentic/learning/outcome_listener/helpers/lesson_builder.rb +4 -3
- data/lib/legion/extensions/agentic/learning/outcome_listener/runners/outcome_listener.rb +41 -10
- data/lib/legion/extensions/agentic/learning/version.rb +1 -1
- data/spec/legion/extensions/agentic/learning/outcome_listener/helpers/lesson_builder_spec.rb +0 -10
- data/spec/legion/extensions/agentic/learning/outcome_listener/runners/outcome_listener_spec.rb +103 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cecc51a27c68a22d11bae4df4784f6b5eeba66d6621911809596a3f8bba5c03b
|
|
4
|
+
data.tar.gz: 4190bc8f28519fad9f5acb205c944c88a99f40459dfd1a23ff62a77c3ec39643
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c79885b2f17dc71be2c0e10f1eb31994e08e9d5b2a0b093fd3576f859a75c8d6c8c4970c2a066eddffac3617b2a0e04a9b9977e6c31a7ddb843a50f232de0c77
|
|
7
|
+
data.tar.gz: ef96ad14cd80eda16d36821f5a327bdd393fb8cf49ab307307edfd2c0e531ba3b481091998407b1e06c8abf9b5868e48d014b95defa8636f5bc35c95a5a86b73
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.13] - 2026-10-29
|
|
4
|
+
### Fixed
|
|
5
|
+
- Accept framework keyword/module outcome dispatch and reject unknown statuses or missing task attribution without updating learning models.
|
|
6
|
+
- Deduplicate explicit transport message identities across concurrent listener clients using bounded process-local history and a stable, eagerly initialized lock shared with state resets; repeated task IDs remain distinct executions.
|
|
7
|
+
- Report model update failures and identify task lessons as technical execution evidence, not demonstrated human acceptance.
|
|
8
|
+
- Fail closed when Apollo publication settings cannot be read. Event subscription activation and durable deduplication are unchanged.
|
|
9
|
+
|
|
3
10
|
## [0.1.12] - 2026-05-15
|
|
4
11
|
### Fixed
|
|
5
12
|
- Curiosity `store_insight_in_apollo` now calls `Legion::Apollo.ingest` directly (replacing `Legion::Extensions::Apollo.store` which was a non-existent method and silently failed on every call); adds `access_scope: 'private'` and `identity_principal_id: nil`.
|
data/README.md
CHANGED
|
@@ -14,7 +14,7 @@ module Legion
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
begin
|
|
17
|
-
include Legion::Extensions::Helpers::Lex
|
|
17
|
+
include Legion::Extensions::Helpers::Lex
|
|
18
18
|
rescue StandardError => e
|
|
19
19
|
log.error "[cognitive_chrysalis] failed to include Helpers::Lex: #{e.class}: #{e.message}"
|
|
20
20
|
end
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/extensions/agentic/learning/meta_learning/client'
|
|
4
|
+
require 'legion/extensions/agentic/learning/scaffolding/client'
|
|
5
|
+
require 'legion/extensions/agentic/learning/learning_rate/client'
|
|
3
6
|
require 'legion/extensions/agentic/learning/outcome_listener/helpers/constants'
|
|
4
7
|
require 'legion/extensions/agentic/learning/outcome_listener/helpers/domain_extractor'
|
|
5
8
|
require 'legion/extensions/agentic/learning/outcome_listener/helpers/lesson_builder'
|
|
@@ -13,32 +16,42 @@ module Legion
|
|
|
13
16
|
class Client
|
|
14
17
|
include Runners::OutcomeListener
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
@outcome_mutex = Mutex.new
|
|
20
|
+
|
|
21
|
+
# rubocop:disable-next ThreadSafety/ClassInstanceVariable
|
|
17
22
|
class << self
|
|
23
|
+
attr_reader :outcome_mutex
|
|
24
|
+
|
|
18
25
|
def meta_client
|
|
19
|
-
@meta_client ||= MetaLearning::Client.new
|
|
26
|
+
@meta_client ||= Legion::Extensions::Agentic::Learning::MetaLearning::Client.new
|
|
20
27
|
end
|
|
21
28
|
|
|
22
29
|
def scaffolding_client
|
|
23
|
-
@scaffolding_client ||= Scaffolding::Client.new
|
|
30
|
+
@scaffolding_client ||= Legion::Extensions::Agentic::Learning::Scaffolding::Client.new
|
|
24
31
|
end
|
|
25
32
|
|
|
26
33
|
def learning_rate_client
|
|
27
|
-
@learning_rate_client ||= LearningRate::Client.new
|
|
34
|
+
@learning_rate_client ||= Legion::Extensions::Agentic::Learning::LearningRate::Client.new
|
|
28
35
|
end
|
|
29
36
|
|
|
30
37
|
def domain_map
|
|
31
38
|
@domain_map ||= {}
|
|
32
39
|
end
|
|
33
40
|
|
|
41
|
+
def processed_outcomes
|
|
42
|
+
@processed_outcomes ||= {}
|
|
43
|
+
end
|
|
44
|
+
|
|
34
45
|
def reset!
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
outcome_mutex.synchronize do
|
|
47
|
+
@meta_client = nil
|
|
48
|
+
@scaffolding_client = nil
|
|
49
|
+
@learning_rate_client = nil
|
|
50
|
+
@domain_map = nil
|
|
51
|
+
@processed_outcomes = nil
|
|
52
|
+
end
|
|
39
53
|
end
|
|
40
54
|
end
|
|
41
|
-
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
42
55
|
end
|
|
43
56
|
end
|
|
44
57
|
end
|
|
@@ -10,6 +10,8 @@ module Legion
|
|
|
10
10
|
DEFAULT_MIN_LESSON_SEVERITY = 0.3
|
|
11
11
|
COMPLETED_STATUS = 'task.completed'
|
|
12
12
|
FAILED_STATUS = 'task.failed'
|
|
13
|
+
EXCEPTION_STATUS = 'task.exception'
|
|
14
|
+
MAX_OUTCOME_HISTORY = 1000
|
|
13
15
|
DEFAULT_DIFFICULTY = 0.5
|
|
14
16
|
DEFAULT_CONFIDENCE_SUCCESS = 0.7
|
|
15
17
|
DEFAULT_CONFIDENCE_FAILURE = 0.5
|
|
@@ -11,10 +11,11 @@ module Legion
|
|
|
11
11
|
|
|
12
12
|
module_function
|
|
13
13
|
|
|
14
|
-
def build(runner_class:, domain:, success:,
|
|
14
|
+
def build(runner_class:, domain:, success:, function: nil, source_agent: nil)
|
|
15
15
|
{
|
|
16
16
|
situation: build_situation(runner_class, function),
|
|
17
17
|
outcome: success ? :success : :failure,
|
|
18
|
+
evidence: :technical_execution,
|
|
18
19
|
lesson: build_lesson(domain, success),
|
|
19
20
|
domain: domain,
|
|
20
21
|
confidence: success ? DEFAULT_CONFIDENCE_SUCCESS : DEFAULT_CONFIDENCE_FAILURE,
|
|
@@ -31,9 +32,9 @@ module Legion
|
|
|
31
32
|
|
|
32
33
|
def build_lesson(domain, success)
|
|
33
34
|
if success
|
|
34
|
-
"Task in domain '#{domain}' completed
|
|
35
|
+
"Task execution in domain '#{domain}' completed without an exception"
|
|
35
36
|
else
|
|
36
|
-
"Task in domain '#{domain}' failed — review for corrective action"
|
|
37
|
+
"Task execution in domain '#{domain}' failed — review for corrective action"
|
|
37
38
|
end
|
|
38
39
|
end
|
|
39
40
|
end
|
|
@@ -10,7 +10,40 @@ module Legion
|
|
|
10
10
|
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
11
11
|
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
12
12
|
|
|
13
|
-
def process_outcome(payload = {}, **)
|
|
13
|
+
def process_outcome(payload = {}, **options)
|
|
14
|
+
payload = payload.merge(options)
|
|
15
|
+
status = payload[:status].to_s
|
|
16
|
+
unless [Helpers::Constants::COMPLETED_STATUS, Helpers::Constants::FAILED_STATUS,
|
|
17
|
+
Helpers::Constants::EXCEPTION_STATUS].include?(status)
|
|
18
|
+
return { success: false, skipped: true, reason: :unrecognized_status, outcome: nil }
|
|
19
|
+
end
|
|
20
|
+
if payload[:runner_class].to_s.strip.empty? || payload[:function].to_s.strip.empty?
|
|
21
|
+
return { success: false, skipped: true, reason: :missing_task_context, outcome: nil }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
owner = Legion::Extensions::Agentic::Learning::OutcomeListener::Client
|
|
25
|
+
owner.outcome_mutex.synchronize do
|
|
26
|
+
process_unique_outcome(payload, owner)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def process_unique_outcome(payload, owner)
|
|
33
|
+
message_id = payload[:message_id]
|
|
34
|
+
key = [payload[:source_agent] || payload[:agent_id], message_id]
|
|
35
|
+
tracked = !message_id.to_s.strip.empty?
|
|
36
|
+
return owner.processed_outcomes[key].merge(skipped: true, reason: :duplicate_outcome) if tracked && owner.processed_outcomes.key?(key)
|
|
37
|
+
|
|
38
|
+
result = process_task_outcome(payload)
|
|
39
|
+
if tracked
|
|
40
|
+
owner.processed_outcomes[key] = result
|
|
41
|
+
owner.processed_outcomes.shift while owner.processed_outcomes.size > Helpers::Constants::MAX_OUTCOME_HISTORY
|
|
42
|
+
end
|
|
43
|
+
result
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def process_task_outcome(payload)
|
|
14
47
|
runner_class_name = payload[:runner_class].to_s
|
|
15
48
|
status = payload[:status].to_s
|
|
16
49
|
domain = Helpers::DomainExtractor.extract(runner_class_name)
|
|
@@ -26,7 +59,6 @@ module Legion
|
|
|
26
59
|
lesson = Helpers::LessonBuilder.build(
|
|
27
60
|
runner_class: runner_class_name,
|
|
28
61
|
function: payload[:function],
|
|
29
|
-
status: status,
|
|
30
62
|
domain: domain,
|
|
31
63
|
success: success,
|
|
32
64
|
source_agent: source_agent
|
|
@@ -36,13 +68,12 @@ module Legion
|
|
|
36
68
|
end
|
|
37
69
|
|
|
38
70
|
log.debug "[outcome_listener] domain=#{domain} success=#{success} updates=#{updates.keys}"
|
|
39
|
-
{ success:
|
|
71
|
+
{ success: updates.values.none? { |update| update.key?(:error) },
|
|
72
|
+
domain: domain, outcome: success, evidence: :technical_execution, updates: updates }
|
|
40
73
|
end
|
|
41
74
|
|
|
42
|
-
private
|
|
43
|
-
|
|
44
75
|
def update_meta_learning_model(domain, success)
|
|
45
|
-
meta =
|
|
76
|
+
meta = Legion::Extensions::Agentic::Learning::OutcomeListener::Client.meta_client
|
|
46
77
|
domain_id = resolve_domain_id(meta, domain)
|
|
47
78
|
meta.record_learning_episode(domain_id: domain_id, success: success)
|
|
48
79
|
rescue StandardError => e
|
|
@@ -51,7 +82,7 @@ module Legion
|
|
|
51
82
|
end
|
|
52
83
|
|
|
53
84
|
def update_learning_rate_model(domain, success)
|
|
54
|
-
lr =
|
|
85
|
+
lr = Legion::Extensions::Agentic::Learning::OutcomeListener::Client.learning_rate_client
|
|
55
86
|
lr.record_prediction(correct: success, domain: domain.to_sym)
|
|
56
87
|
rescue StandardError => e
|
|
57
88
|
log.warn "[outcome_listener] learning_rate update failed: #{e.message}"
|
|
@@ -59,7 +90,7 @@ module Legion
|
|
|
59
90
|
end
|
|
60
91
|
|
|
61
92
|
def update_scaffolding_model(domain, success, payload)
|
|
62
|
-
sc =
|
|
93
|
+
sc = Legion::Extensions::Agentic::Learning::OutcomeListener::Client.scaffolding_client
|
|
63
94
|
scaffolds = sc.engine.by_domain(domain: domain)
|
|
64
95
|
return { skipped: true, reason: :no_scaffold } if scaffolds.empty?
|
|
65
96
|
|
|
@@ -72,7 +103,7 @@ module Legion
|
|
|
72
103
|
end
|
|
73
104
|
|
|
74
105
|
def resolve_domain_id(meta, domain)
|
|
75
|
-
map =
|
|
106
|
+
map = Legion::Extensions::Agentic::Learning::OutcomeListener::Client.domain_map
|
|
76
107
|
return map[domain] if map.key?(domain)
|
|
77
108
|
|
|
78
109
|
result = meta.create_learning_domain(name: domain)
|
|
@@ -102,7 +133,7 @@ module Legion
|
|
|
102
133
|
Legion::Settings.dig(:agentic, :learning, :write_to_apollo) != false
|
|
103
134
|
rescue StandardError => e
|
|
104
135
|
log.warn "[outcome_listener] write_to_apollo? check failed: #{e.message}"
|
|
105
|
-
|
|
136
|
+
false
|
|
106
137
|
end
|
|
107
138
|
|
|
108
139
|
def min_lesson_severity
|
data/spec/legion/extensions/agentic/learning/outcome_listener/helpers/lesson_builder_spec.rb
CHANGED
|
@@ -9,7 +9,6 @@ RSpec.describe Legion::Extensions::Agentic::Learning::OutcomeListener::Helpers::
|
|
|
9
9
|
described_class.build(
|
|
10
10
|
runner_class: 'Legion::Extensions::Http::Runners::Get',
|
|
11
11
|
function: 'fetch',
|
|
12
|
-
status: 'task.completed',
|
|
13
12
|
domain: 'http',
|
|
14
13
|
success: true,
|
|
15
14
|
source_agent: 'agent-1'
|
|
@@ -20,7 +19,6 @@ RSpec.describe Legion::Extensions::Agentic::Learning::OutcomeListener::Helpers::
|
|
|
20
19
|
described_class.build(
|
|
21
20
|
runner_class: 'Legion::Extensions::Consul::Runners::Kv',
|
|
22
21
|
function: 'put',
|
|
23
|
-
status: 'task.failed',
|
|
24
22
|
domain: 'consul',
|
|
25
23
|
success: false,
|
|
26
24
|
source_agent: 'agent-2'
|
|
@@ -54,14 +52,6 @@ RSpec.describe Legion::Extensions::Agentic::Learning::OutcomeListener::Helpers::
|
|
|
54
52
|
it 'includes recorded_at timestamp' do
|
|
55
53
|
expect(success_lesson[:recorded_at]).to be_a(Time)
|
|
56
54
|
end
|
|
57
|
-
|
|
58
|
-
it 'builds lesson text for success' do
|
|
59
|
-
expect(success_lesson[:lesson]).to include('completed successfully')
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
it 'builds lesson text for failure' do
|
|
63
|
-
expect(failure_lesson[:lesson]).to include('failed')
|
|
64
|
-
end
|
|
65
55
|
end
|
|
66
56
|
|
|
67
57
|
describe '.build_situation' do
|
data/spec/legion/extensions/agentic/learning/outcome_listener/runners/outcome_listener_spec.rb
CHANGED
|
@@ -109,10 +109,111 @@ RSpec.describe Legion::Extensions::Agentic::Learning::OutcomeListener::Runners::
|
|
|
109
109
|
expect(episodes.proficiency).to be > 0.0
|
|
110
110
|
end
|
|
111
111
|
|
|
112
|
-
it '
|
|
112
|
+
it 'ignores missing outcomes without creating learning state' do
|
|
113
113
|
result = client.process_outcome({})
|
|
114
|
+
expect(result).to include(success: false, skipped: true, outcome: nil)
|
|
115
|
+
expect(Legion::Extensions::Agentic::Learning::OutcomeListener::Client.meta_client.meta_learning_stats[:episode_count]).to eq(0)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'consumes keyword-dispatched technical failures from the current runner' do
|
|
119
|
+
result = client.process_outcome(**failed_payload, status: 'task.exception')
|
|
120
|
+
expect(result).to include(success: true, outcome: false, evidence: :technical_execution)
|
|
121
|
+
expect(result[:updates][:meta_learning]).to include(domain_name: 'consul', success: false)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'ignores nonterminal and unrecognized statuses without changing existing learning' do
|
|
125
|
+
client.process_outcome(completed_payload)
|
|
126
|
+
owner = Legion::Extensions::Agentic::Learning::OutcomeListener::Client
|
|
127
|
+
before = owner.meta_client.learning_domain_ranking
|
|
128
|
+
|
|
129
|
+
[nil, 'task.queued', 'transformer.succeeded', 'unexpected'].each do |status|
|
|
130
|
+
result = client.process_outcome(**completed_payload, status: status)
|
|
131
|
+
expect(result).to include(success: false, skipped: true, reason: :unrecognized_status, outcome: nil)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
expect(owner.meta_client.learning_domain_ranking).to eq(before)
|
|
135
|
+
expect(owner.meta_client.meta_learning_stats[:episode_count]).to eq(1)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it 'does not attribute context-free transport status updates to an unknown domain' do
|
|
139
|
+
result = client.process_outcome(task_id: 42, status: 'task.completed')
|
|
140
|
+
expect(result).to include(success: false, reason: :missing_task_context)
|
|
141
|
+
expect(Legion::Extensions::Agentic::Learning::OutcomeListener::Client.meta_client.meta_learning_stats[:domain_count]).to eq(0)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it 'does not reinforce a redelivered identified outcome twice across clients' do
|
|
145
|
+
payload = completed_payload.merge(task_id: 42, message_id: 'delivery-1')
|
|
146
|
+
client.process_outcome(**payload)
|
|
147
|
+
other = Legion::Extensions::Agentic::Learning::OutcomeListener::Client.new
|
|
148
|
+
result = other.process_outcome(**payload)
|
|
149
|
+
expect(result).to include(skipped: true, reason: :duplicate_outcome)
|
|
150
|
+
expect(Legion::Extensions::Agentic::Learning::OutcomeListener::Client.meta_client.meta_learning_stats[:episode_count]).to eq(1)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it 'reinforces concurrent redeliveries only once per shared state lifecycle' do
|
|
154
|
+
owner = Legion::Extensions::Agentic::Learning::OutcomeListener::Client
|
|
155
|
+
payload = completed_payload.merge(message_id: 'concurrent-delivery')
|
|
156
|
+
|
|
157
|
+
2.times do
|
|
158
|
+
ready = Queue.new
|
|
159
|
+
start = Queue.new
|
|
160
|
+
workers = Array.new(8) do
|
|
161
|
+
Thread.new do
|
|
162
|
+
listener = owner.new
|
|
163
|
+
ready << true
|
|
164
|
+
start.pop
|
|
165
|
+
listener.process_outcome(**payload)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
workers.size.times { ready.pop }
|
|
169
|
+
workers.size.times { start << true }
|
|
170
|
+
results = workers.map(&:value)
|
|
171
|
+
|
|
172
|
+
expect(results.count { |result| result[:reason] == :duplicate_outcome }).to eq(7)
|
|
173
|
+
expect(results).to all(include(success: true, outcome: true))
|
|
174
|
+
expect(owner.meta_client.meta_learning_stats[:episode_count]).to eq(1)
|
|
175
|
+
owner.reset!
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it 'keeps different agents and a later terminal status distinct' do
|
|
180
|
+
payload = completed_payload.merge(task_id: 42, message_id: 'delivery-1')
|
|
181
|
+
client.process_outcome(**payload)
|
|
182
|
+
client.process_outcome(**payload, source_agent: 'agent-2')
|
|
183
|
+
client.process_outcome(**payload, status: 'task.exception', message_id: 'delivery-2')
|
|
184
|
+
owner = Legion::Extensions::Agentic::Learning::OutcomeListener::Client
|
|
185
|
+
domain = owner.meta_client.learning_domain_ranking[:ranking].first
|
|
186
|
+
expect(domain).to include(successes: 2, failures: 1)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it 'does not treat repeated task IDs as delivery identities' do
|
|
190
|
+
2.times { client.process_outcome(**completed_payload, task_id: 42) }
|
|
191
|
+
owner = Legion::Extensions::Agentic::Learning::OutcomeListener::Client
|
|
192
|
+
expect(owner.meta_client.meta_learning_stats[:episode_count]).to eq(2)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it 'updates the same scaffold decision when invoked as a framework runner module' do
|
|
196
|
+
owner = Legion::Extensions::Agentic::Learning::OutcomeListener::Client
|
|
197
|
+
scaffold = owner.scaffolding_client.create_scaffold(skill_name: 'http_requests', domain: 'http')
|
|
198
|
+
id = scaffold[:scaffold][:id]
|
|
199
|
+
before = owner.scaffolding_client.recommend_scaffolded_task(scaffold_id: id)
|
|
200
|
+
runner = Module.new
|
|
201
|
+
runner.extend(described_class)
|
|
202
|
+
result = runner.process_outcome(**completed_payload)
|
|
203
|
+
|
|
114
204
|
expect(result[:success]).to be true
|
|
115
|
-
|
|
205
|
+
after = owner.scaffolding_client.recommend_scaffolded_task(scaffold_id: id)
|
|
206
|
+
expect(after[:recommendation][:difficulty]).to be > before[:recommendation][:difficulty]
|
|
207
|
+
expect(owner.meta_client.meta_learning_stats[:episode_count]).to eq(1)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it 'fails closed when the lesson publication setting cannot be read' do
|
|
211
|
+
allow(Legion::Settings).to receive(:dig).with(:agentic, :learning, :write_to_apollo).and_raise(IOError, 'unavailable')
|
|
212
|
+
apollo = Module.new
|
|
213
|
+
stub_const('Legion::Apollo', apollo)
|
|
214
|
+
expect(apollo).not_to receive(:ingest)
|
|
215
|
+
|
|
216
|
+
expect(client.process_outcome(**completed_payload)[:success]).to be true
|
|
116
217
|
end
|
|
117
218
|
end
|
|
118
219
|
|