lex-apollo 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/CHANGELOG.md +25 -0
- data/README.md +135 -0
- data/lib/legion/extensions/apollo/actors/corroboration_checker.rb +22 -0
- data/lib/legion/extensions/apollo/actors/decay.rb +22 -0
- data/lib/legion/extensions/apollo/actors/expertise_aggregator.rb +22 -0
- data/lib/legion/extensions/apollo/actors/ingest.rb +25 -0
- data/lib/legion/extensions/apollo/actors/query_responder.rb +25 -0
- data/lib/legion/extensions/apollo/client.rb +30 -0
- data/lib/legion/extensions/apollo/helpers/confidence.rb +46 -0
- data/lib/legion/extensions/apollo/helpers/embedding.rb +22 -0
- data/lib/legion/extensions/apollo/helpers/graph_query.rb +77 -0
- data/lib/legion/extensions/apollo/helpers/similarity.rb +36 -0
- data/lib/legion/extensions/apollo/runners/expertise.rb +71 -0
- data/lib/legion/extensions/apollo/runners/knowledge.rb +213 -0
- data/lib/legion/extensions/apollo/runners/maintenance.rb +72 -0
- data/lib/legion/extensions/apollo/transport/exchanges/apollo.rb +19 -0
- data/lib/legion/extensions/apollo/transport/messages/ingest.rb +43 -0
- data/lib/legion/extensions/apollo/transport/messages/query.rb +43 -0
- data/lib/legion/extensions/apollo/transport/queues/ingest.rb +23 -0
- data/lib/legion/extensions/apollo/transport/queues/query.rb +23 -0
- data/lib/legion/extensions/apollo/version.rb +9 -0
- data/lib/legion/extensions/apollo.rb +25 -0
- data/spec/legion/extensions/apollo/actors/decay_spec.rb +45 -0
- data/spec/legion/extensions/apollo/actors/expertise_aggregator_spec.rb +41 -0
- data/spec/legion/extensions/apollo/actors/ingest_spec.rb +33 -0
- data/spec/legion/extensions/apollo/client_spec.rb +75 -0
- data/spec/legion/extensions/apollo/helpers/confidence_spec.rb +109 -0
- data/spec/legion/extensions/apollo/helpers/embedding_spec.rb +68 -0
- data/spec/legion/extensions/apollo/helpers/graph_query_spec.rb +69 -0
- data/spec/legion/extensions/apollo/helpers/similarity_spec.rb +58 -0
- data/spec/legion/extensions/apollo/runners/expertise_spec.rb +111 -0
- data/spec/legion/extensions/apollo/runners/knowledge_spec.rb +308 -0
- data/spec/legion/extensions/apollo/runners/maintenance_spec.rb +133 -0
- data/spec/legion/extensions/apollo/transport/messages/ingest_spec.rb +65 -0
- data/spec/legion/extensions/apollo/transport/messages/query_spec.rb +75 -0
- data/spec/spec_helper.rb +30 -0
- metadata +108 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/apollo/helpers/confidence'
|
|
5
|
+
require 'legion/extensions/apollo/helpers/similarity'
|
|
6
|
+
require 'legion/extensions/apollo/helpers/embedding'
|
|
7
|
+
require 'legion/extensions/apollo/helpers/graph_query'
|
|
8
|
+
require 'legion/extensions/apollo/runners/knowledge'
|
|
9
|
+
|
|
10
|
+
RSpec.describe Legion::Extensions::Apollo::Runners::Knowledge do
|
|
11
|
+
let(:runner) do
|
|
12
|
+
obj = Object.new
|
|
13
|
+
obj.extend(described_class)
|
|
14
|
+
obj
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe '#store_knowledge' do
|
|
18
|
+
it 'returns a message payload hash' do
|
|
19
|
+
result = runner.store_knowledge(
|
|
20
|
+
content: 'Vault namespace ash1234 uses PKI',
|
|
21
|
+
content_type: :fact,
|
|
22
|
+
tags: %w[vault pki]
|
|
23
|
+
)
|
|
24
|
+
expect(result).to be_a(Hash)
|
|
25
|
+
expect(result[:action]).to eq(:store)
|
|
26
|
+
expect(result[:content]).to eq('Vault namespace ash1234 uses PKI')
|
|
27
|
+
expect(result[:content_type]).to eq(:fact)
|
|
28
|
+
expect(result[:tags]).to eq(%w[vault pki])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'includes source_agent when provided' do
|
|
32
|
+
result = runner.store_knowledge(
|
|
33
|
+
content: 'test', content_type: :fact, source_agent: 'worker-1'
|
|
34
|
+
)
|
|
35
|
+
expect(result[:source_agent]).to eq('worker-1')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'rejects invalid content_type' do
|
|
39
|
+
expect do
|
|
40
|
+
runner.store_knowledge(content: 'test', content_type: :invalid)
|
|
41
|
+
end.to raise_error(ArgumentError, /content_type/)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe '#query_knowledge' do
|
|
46
|
+
it 'returns a query payload hash' do
|
|
47
|
+
result = runner.query_knowledge(query: 'PKI configuration')
|
|
48
|
+
expect(result[:action]).to eq(:query)
|
|
49
|
+
expect(result[:query]).to eq('PKI configuration')
|
|
50
|
+
expect(result[:limit]).to eq(10)
|
|
51
|
+
expect(result[:min_confidence]).to eq(0.3)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'accepts custom limit and filters' do
|
|
55
|
+
result = runner.query_knowledge(
|
|
56
|
+
query: 'vault', limit: 5, min_confidence: 0.5,
|
|
57
|
+
status: [:confirmed], tags: %w[vault]
|
|
58
|
+
)
|
|
59
|
+
expect(result[:limit]).to eq(5)
|
|
60
|
+
expect(result[:min_confidence]).to eq(0.5)
|
|
61
|
+
expect(result[:status]).to eq([:confirmed])
|
|
62
|
+
expect(result[:tags]).to eq(%w[vault])
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe '#related_entries' do
|
|
67
|
+
it 'returns a traversal payload hash' do
|
|
68
|
+
result = runner.related_entries(entry_id: 'uuid-123')
|
|
69
|
+
expect(result[:action]).to eq(:traverse)
|
|
70
|
+
expect(result[:entry_id]).to eq('uuid-123')
|
|
71
|
+
expect(result[:depth]).to eq(2)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'accepts relation_types filter' do
|
|
75
|
+
result = runner.related_entries(
|
|
76
|
+
entry_id: 'uuid-123', relation_types: %w[causes depends_on], depth: 3
|
|
77
|
+
)
|
|
78
|
+
expect(result[:relation_types]).to eq(%w[causes depends_on])
|
|
79
|
+
expect(result[:depth]).to eq(3)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
describe '#deprecate_entry' do
|
|
84
|
+
it 'returns a deprecate payload' do
|
|
85
|
+
result = runner.deprecate_entry(entry_id: 'uuid-123', reason: 'outdated')
|
|
86
|
+
expect(result[:action]).to eq(:deprecate)
|
|
87
|
+
expect(result[:entry_id]).to eq('uuid-123')
|
|
88
|
+
expect(result[:reason]).to eq('outdated')
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe '#handle_ingest' do
|
|
93
|
+
let(:host) { Object.new.extend(described_class) }
|
|
94
|
+
|
|
95
|
+
context 'when Apollo data is not available' do
|
|
96
|
+
before { hide_const('Legion::Data::Model::ApolloEntry') if defined?(Legion::Data::Model::ApolloEntry) }
|
|
97
|
+
|
|
98
|
+
it 'returns a structured error' do
|
|
99
|
+
result = host.handle_ingest(content: 'test', content_type: 'fact')
|
|
100
|
+
expect(result[:success]).to be false
|
|
101
|
+
expect(result[:error]).to eq('apollo_data_not_available')
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
context 'when Apollo data is available' do
|
|
106
|
+
let(:mock_entry_class) { double('ApolloEntry') }
|
|
107
|
+
let(:mock_relation_class) { double('ApolloRelation') }
|
|
108
|
+
let(:mock_expertise_class) { double('ApolloExpertise') }
|
|
109
|
+
let(:mock_access_log_class) { double('ApolloAccessLog') }
|
|
110
|
+
let(:mock_entry) { double('entry', id: 'uuid-123', embedding: nil) }
|
|
111
|
+
let(:empty_dataset) { double('dataset', each: nil) }
|
|
112
|
+
|
|
113
|
+
before do
|
|
114
|
+
stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
|
|
115
|
+
stub_const('Legion::Data::Model::ApolloRelation', mock_relation_class)
|
|
116
|
+
stub_const('Legion::Data::Model::ApolloExpertise', mock_expertise_class)
|
|
117
|
+
stub_const('Legion::Data::Model::ApolloAccessLog', mock_access_log_class)
|
|
118
|
+
allow(Legion::Extensions::Apollo::Helpers::Embedding).to receive(:generate)
|
|
119
|
+
.and_return(Array.new(1536, 0.0))
|
|
120
|
+
|
|
121
|
+
allow(mock_entry_class).to receive(:where).and_return(double(exclude: double(limit: empty_dataset)))
|
|
122
|
+
allow(mock_entry_class).to receive(:create).and_return(mock_entry)
|
|
123
|
+
allow(mock_expertise_class).to receive(:where).and_return(double(first: nil))
|
|
124
|
+
allow(mock_expertise_class).to receive(:create)
|
|
125
|
+
allow(mock_access_log_class).to receive(:create)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it 'creates a new candidate entry when novel' do
|
|
129
|
+
result = host.handle_ingest(content: 'Ruby is great', content_type: 'fact',
|
|
130
|
+
tags: ['ruby'], source_agent: 'agent-1')
|
|
131
|
+
expect(result[:success]).to be true
|
|
132
|
+
expect(result[:status]).to eq('candidate')
|
|
133
|
+
expect(result[:corroborated]).to be false
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it 'creates expertise record for source agent' do
|
|
137
|
+
expect(mock_expertise_class).to receive(:create).with(
|
|
138
|
+
hash_including(agent_id: 'agent-1', domain: 'ruby')
|
|
139
|
+
)
|
|
140
|
+
host.handle_ingest(content: 'Ruby is great', content_type: 'fact',
|
|
141
|
+
tags: ['ruby'], source_agent: 'agent-1')
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it 'logs access' do
|
|
145
|
+
expect(mock_access_log_class).to receive(:create).with(
|
|
146
|
+
hash_including(agent_id: 'agent-1', action: 'ingest')
|
|
147
|
+
)
|
|
148
|
+
host.handle_ingest(content: 'Ruby is great', content_type: 'fact',
|
|
149
|
+
tags: ['ruby'], source_agent: 'agent-1')
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it 'defaults domain to general when no tags' do
|
|
153
|
+
expect(mock_expertise_class).to receive(:create).with(
|
|
154
|
+
hash_including(domain: 'general')
|
|
155
|
+
)
|
|
156
|
+
host.handle_ingest(content: 'test', content_type: 'fact', source_agent: 'agent-1')
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
context 'when Sequel raises an error' do
|
|
161
|
+
before do
|
|
162
|
+
stub_const('Legion::Data::Model::ApolloEntry', Class.new)
|
|
163
|
+
allow(Legion::Extensions::Apollo::Helpers::Embedding).to receive(:generate)
|
|
164
|
+
.and_return(Array.new(1536, 0.0))
|
|
165
|
+
allow(Legion::Data::Model::ApolloEntry).to receive(:where)
|
|
166
|
+
.and_raise(Sequel::Error, 'connection lost')
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it 'returns a structured error' do
|
|
170
|
+
result = host.handle_ingest(content: 'test', content_type: 'fact', source_agent: 'a')
|
|
171
|
+
expect(result[:success]).to be false
|
|
172
|
+
expect(result[:error]).to eq('connection lost')
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
describe '#handle_query' do
|
|
178
|
+
let(:host) { Object.new.extend(described_class) }
|
|
179
|
+
|
|
180
|
+
context 'when Apollo data is not available' do
|
|
181
|
+
before { hide_const('Legion::Data::Model::ApolloEntry') if defined?(Legion::Data::Model::ApolloEntry) }
|
|
182
|
+
|
|
183
|
+
it 'returns a structured error' do
|
|
184
|
+
result = host.handle_query(query: 'test')
|
|
185
|
+
expect(result[:success]).to be false
|
|
186
|
+
expect(result[:error]).to eq('apollo_data_not_available')
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
context 'when Apollo data is available' do
|
|
191
|
+
let(:mock_entry_class) { double('ApolloEntry') }
|
|
192
|
+
let(:mock_access_log_class) { double('ApolloAccessLog') }
|
|
193
|
+
let(:mock_db) { double('db') }
|
|
194
|
+
let(:sample_entries) do
|
|
195
|
+
[{ id: 'uuid-1', content: 'Ruby is interpreted', content_type: 'fact',
|
|
196
|
+
confidence: 0.8, distance: 0.15, tags: ['ruby'], source_agent: 'agent-1',
|
|
197
|
+
access_count: 3 }]
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
before do
|
|
201
|
+
stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
|
|
202
|
+
stub_const('Legion::Data::Model::ApolloAccessLog', mock_access_log_class)
|
|
203
|
+
allow(Legion::Extensions::Apollo::Helpers::Embedding).to receive(:generate)
|
|
204
|
+
.and_return(Array.new(1536, 0.0))
|
|
205
|
+
allow(mock_entry_class).to receive(:db).and_return(mock_db)
|
|
206
|
+
allow(mock_db).to receive(:fetch).and_return(double(all: sample_entries))
|
|
207
|
+
allow(mock_entry_class).to receive(:where).and_return(double(update: true))
|
|
208
|
+
allow(mock_access_log_class).to receive(:create)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it 'returns matching entries' do
|
|
212
|
+
result = host.handle_query(query: 'Ruby', agent_id: 'agent-2')
|
|
213
|
+
expect(result[:success]).to be true
|
|
214
|
+
expect(result[:count]).to eq(1)
|
|
215
|
+
expect(result[:entries].first[:content]).to eq('Ruby is interpreted')
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
it 'boosts access count on matched entries' do
|
|
219
|
+
expect(mock_entry_class).to receive(:where).with(id: 'uuid-1')
|
|
220
|
+
.and_return(double(update: true))
|
|
221
|
+
host.handle_query(query: 'Ruby', agent_id: 'agent-2')
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
it 'logs query access' do
|
|
225
|
+
expect(mock_access_log_class).to receive(:create).with(
|
|
226
|
+
hash_including(agent_id: 'agent-2', action: 'query')
|
|
227
|
+
)
|
|
228
|
+
host.handle_query(query: 'Ruby', agent_id: 'agent-2')
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
context 'when no results found' do
|
|
233
|
+
let(:mock_entry_class) { double('ApolloEntry') }
|
|
234
|
+
let(:mock_db) { double('db') }
|
|
235
|
+
|
|
236
|
+
before do
|
|
237
|
+
stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
|
|
238
|
+
allow(Legion::Extensions::Apollo::Helpers::Embedding).to receive(:generate)
|
|
239
|
+
.and_return(Array.new(1536, 0.0))
|
|
240
|
+
allow(mock_entry_class).to receive(:db).and_return(mock_db)
|
|
241
|
+
allow(mock_db).to receive(:fetch).and_return(double(all: []))
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
it 'returns empty entries array' do
|
|
245
|
+
result = host.handle_query(query: 'nonexistent')
|
|
246
|
+
expect(result[:success]).to be true
|
|
247
|
+
expect(result[:entries]).to eq([])
|
|
248
|
+
expect(result[:count]).to eq(0)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
describe '#retrieve_relevant' do
|
|
254
|
+
let(:host) { Object.new.extend(described_class) }
|
|
255
|
+
|
|
256
|
+
it 'returns skipped when skip is true' do
|
|
257
|
+
result = host.retrieve_relevant(skip: true)
|
|
258
|
+
expect(result[:status]).to eq(:skipped)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
context 'when Apollo data is not available' do
|
|
262
|
+
before { hide_const('Legion::Data::Model::ApolloEntry') if defined?(Legion::Data::Model::ApolloEntry) }
|
|
263
|
+
|
|
264
|
+
it 'returns a structured error' do
|
|
265
|
+
result = host.retrieve_relevant(query: 'test')
|
|
266
|
+
expect(result[:success]).to be false
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
it 'returns empty when query is nil' do
|
|
271
|
+
stub_const('Legion::Data::Model::ApolloEntry', Class.new)
|
|
272
|
+
result = host.retrieve_relevant(query: nil)
|
|
273
|
+
expect(result[:success]).to be true
|
|
274
|
+
expect(result[:entries]).to eq([])
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
it 'returns empty when query is blank' do
|
|
278
|
+
stub_const('Legion::Data::Model::ApolloEntry', Class.new)
|
|
279
|
+
result = host.retrieve_relevant(query: ' ')
|
|
280
|
+
expect(result[:success]).to be true
|
|
281
|
+
expect(result[:entries]).to eq([])
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
context 'with valid query and data available' do
|
|
285
|
+
let(:mock_entry_class) { double('ApolloEntry') }
|
|
286
|
+
let(:mock_db) { double('db') }
|
|
287
|
+
let(:sample_entries) do
|
|
288
|
+
[{ id: 'uuid-1', content: 'fact', content_type: 'fact',
|
|
289
|
+
confidence: 0.7, distance: 0.2, tags: ['ruby'], source_agent: 'agent-1' }]
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
before do
|
|
293
|
+
stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
|
|
294
|
+
allow(Legion::Extensions::Apollo::Helpers::Embedding).to receive(:generate)
|
|
295
|
+
.and_return(Array.new(1536, 0.0))
|
|
296
|
+
allow(mock_entry_class).to receive(:db).and_return(mock_db)
|
|
297
|
+
allow(mock_db).to receive(:fetch).and_return(double(all: sample_entries))
|
|
298
|
+
allow(mock_entry_class).to receive(:where).and_return(double(update: true))
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
it 'returns entries without access logging' do
|
|
302
|
+
result = host.retrieve_relevant(query: 'Ruby facts')
|
|
303
|
+
expect(result[:success]).to be true
|
|
304
|
+
expect(result[:count]).to eq(1)
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/apollo/helpers/confidence'
|
|
5
|
+
require 'legion/extensions/apollo/helpers/similarity'
|
|
6
|
+
require 'legion/extensions/apollo/helpers/embedding'
|
|
7
|
+
require 'legion/extensions/apollo/runners/maintenance'
|
|
8
|
+
|
|
9
|
+
RSpec.describe Legion::Extensions::Apollo::Runners::Maintenance do
|
|
10
|
+
let(:runner) do
|
|
11
|
+
obj = Object.new
|
|
12
|
+
obj.extend(described_class)
|
|
13
|
+
obj
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe '#force_decay' do
|
|
17
|
+
it 'returns a force_decay payload' do
|
|
18
|
+
result = runner.force_decay(factor: 0.5)
|
|
19
|
+
expect(result[:action]).to eq(:force_decay)
|
|
20
|
+
expect(result[:factor]).to eq(0.5)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe '#archive_stale' do
|
|
25
|
+
it 'returns an archive payload with default days' do
|
|
26
|
+
result = runner.archive_stale
|
|
27
|
+
expect(result[:action]).to eq(:archive_stale)
|
|
28
|
+
expect(result[:days]).to eq(90)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe '#resolve_dispute' do
|
|
33
|
+
it 'returns a resolve payload' do
|
|
34
|
+
result = runner.resolve_dispute(entry_id: 'uuid-123', resolution: :keep)
|
|
35
|
+
expect(result[:action]).to eq(:resolve_dispute)
|
|
36
|
+
expect(result[:resolution]).to eq(:keep)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe '#check_corroboration' do
|
|
41
|
+
let(:host) { Object.new.extend(described_class) }
|
|
42
|
+
|
|
43
|
+
context 'when Apollo data is not available' do
|
|
44
|
+
before { hide_const('Legion::Data::Model::ApolloEntry') if defined?(Legion::Data::Model::ApolloEntry) }
|
|
45
|
+
|
|
46
|
+
it 'returns a structured error' do
|
|
47
|
+
result = host.check_corroboration
|
|
48
|
+
expect(result[:success]).to be false
|
|
49
|
+
expect(result[:error]).to eq('apollo_data_not_available')
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
context 'when no candidates exist' do
|
|
54
|
+
let(:mock_entry_class) { double('ApolloEntry') }
|
|
55
|
+
|
|
56
|
+
before do
|
|
57
|
+
stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
|
|
58
|
+
allow(mock_entry_class).to receive(:where).with(status: 'candidate')
|
|
59
|
+
.and_return(double(exclude: double(all: [])))
|
|
60
|
+
allow(mock_entry_class).to receive(:where).with(status: 'confirmed')
|
|
61
|
+
.and_return(double(exclude: double(all: [])))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'returns zero promoted and scanned' do
|
|
65
|
+
result = host.check_corroboration
|
|
66
|
+
expect(result[:success]).to be true
|
|
67
|
+
expect(result[:promoted]).to eq(0)
|
|
68
|
+
expect(result[:scanned]).to eq(0)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
context 'when a candidate matches a confirmed entry' do
|
|
73
|
+
let(:mock_entry_class) { double('ApolloEntry') }
|
|
74
|
+
let(:mock_relation_class) { double('ApolloRelation') }
|
|
75
|
+
let(:embedding) { Array.new(1536, 0.5) }
|
|
76
|
+
let(:candidate) do
|
|
77
|
+
double('candidate', id: 'c-1', content_type: 'fact', embedding: embedding,
|
|
78
|
+
confidence: 0.5, update: true)
|
|
79
|
+
end
|
|
80
|
+
let(:confirmed_entry) do
|
|
81
|
+
double('confirmed', id: 'f-1', content_type: 'fact', embedding: embedding)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
before do
|
|
85
|
+
stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
|
|
86
|
+
stub_const('Legion::Data::Model::ApolloRelation', mock_relation_class)
|
|
87
|
+
allow(mock_entry_class).to receive(:where).with(status: 'candidate')
|
|
88
|
+
.and_return(double(exclude: double(all: [candidate])))
|
|
89
|
+
allow(mock_entry_class).to receive(:where).with(status: 'confirmed')
|
|
90
|
+
.and_return(double(exclude: double(all: [confirmed_entry])))
|
|
91
|
+
allow(mock_relation_class).to receive(:create)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'promotes the candidate' do
|
|
95
|
+
expect(candidate).to receive(:update).with(hash_including(status: 'confirmed'))
|
|
96
|
+
result = host.check_corroboration
|
|
97
|
+
expect(result[:promoted]).to eq(1)
|
|
98
|
+
expect(result[:scanned]).to eq(1)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'creates a similar_to relation' do
|
|
102
|
+
expect(mock_relation_class).to receive(:create).with(
|
|
103
|
+
hash_including(from_entry_id: 'c-1', to_entry_id: 'f-1', relation_type: 'similar_to')
|
|
104
|
+
)
|
|
105
|
+
host.check_corroboration
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
context 'when candidate has different content_type than confirmed' do
|
|
110
|
+
let(:mock_entry_class) { double('ApolloEntry') }
|
|
111
|
+
let(:embedding) { Array.new(1536, 0.5) }
|
|
112
|
+
let(:candidate) do
|
|
113
|
+
double('candidate', id: 'c-1', content_type: 'fact', embedding: embedding, confidence: 0.5)
|
|
114
|
+
end
|
|
115
|
+
let(:confirmed_entry) do
|
|
116
|
+
double('confirmed', id: 'f-1', content_type: 'concept', embedding: embedding)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
before do
|
|
120
|
+
stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
|
|
121
|
+
allow(mock_entry_class).to receive(:where).with(status: 'candidate')
|
|
122
|
+
.and_return(double(exclude: double(all: [candidate])))
|
|
123
|
+
allow(mock_entry_class).to receive(:where).with(status: 'confirmed')
|
|
124
|
+
.and_return(double(exclude: double(all: [confirmed_entry])))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'does not promote' do
|
|
128
|
+
result = host.check_corroboration
|
|
129
|
+
expect(result[:promoted]).to eq(0)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
unless defined?(Legion::Transport::Message)
|
|
6
|
+
module Legion
|
|
7
|
+
module Transport
|
|
8
|
+
class Message
|
|
9
|
+
attr_reader :options
|
|
10
|
+
|
|
11
|
+
def initialize(**opts)
|
|
12
|
+
@options = opts
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def publish
|
|
16
|
+
{ published: true }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class Exchange
|
|
21
|
+
def exchange_name
|
|
22
|
+
'mock'
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
$LOADED_FEATURES << 'legion/transport/message' unless $LOADED_FEATURES.include?('legion/transport/message')
|
|
28
|
+
$LOADED_FEATURES << 'legion/transport/exchange' unless $LOADED_FEATURES.include?('legion/transport/exchange')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
require 'legion/extensions/apollo/transport/exchanges/apollo'
|
|
32
|
+
require 'legion/extensions/apollo/transport/messages/ingest'
|
|
33
|
+
|
|
34
|
+
RSpec.describe Legion::Extensions::Apollo::Transport::Messages::Ingest do
|
|
35
|
+
let(:message) do
|
|
36
|
+
described_class.new(
|
|
37
|
+
content: 'test fact',
|
|
38
|
+
content_type: :fact,
|
|
39
|
+
tags: %w[vault],
|
|
40
|
+
source_agent: 'worker-1'
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe '#routing_key' do
|
|
45
|
+
it 'returns apollo.ingest' do
|
|
46
|
+
expect(message.routing_key).to eq('apollo.ingest')
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe '#message' do
|
|
51
|
+
it 'includes content and metadata' do
|
|
52
|
+
msg = message.message
|
|
53
|
+
expect(msg[:content]).to eq('test fact')
|
|
54
|
+
expect(msg[:content_type]).to eq(:fact)
|
|
55
|
+
expect(msg[:tags]).to eq(%w[vault])
|
|
56
|
+
expect(msg[:source_agent]).to eq('worker-1')
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe '#type' do
|
|
61
|
+
it 'returns apollo_ingest' do
|
|
62
|
+
expect(message.type).to eq('apollo_ingest')
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
unless defined?(Legion::Transport::Message)
|
|
6
|
+
module Legion
|
|
7
|
+
module Transport
|
|
8
|
+
class Message
|
|
9
|
+
attr_reader :options
|
|
10
|
+
|
|
11
|
+
def initialize(**opts)
|
|
12
|
+
@options = opts
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def publish
|
|
16
|
+
{ published: true }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class Exchange
|
|
21
|
+
def exchange_name
|
|
22
|
+
'mock'
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
$LOADED_FEATURES << 'legion/transport/message' unless $LOADED_FEATURES.include?('legion/transport/message')
|
|
28
|
+
$LOADED_FEATURES << 'legion/transport/exchange' unless $LOADED_FEATURES.include?('legion/transport/exchange')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
require 'legion/extensions/apollo/transport/exchanges/apollo'
|
|
32
|
+
require 'legion/extensions/apollo/transport/messages/query'
|
|
33
|
+
|
|
34
|
+
RSpec.describe Legion::Extensions::Apollo::Transport::Messages::Query do
|
|
35
|
+
let(:message) do
|
|
36
|
+
described_class.new(
|
|
37
|
+
action: :query,
|
|
38
|
+
query: 'PKI configuration',
|
|
39
|
+
limit: 5,
|
|
40
|
+
min_confidence: 0.3,
|
|
41
|
+
reply_to: 'reply-queue',
|
|
42
|
+
correlation_id: 'corr-123'
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
describe '#routing_key' do
|
|
47
|
+
it 'returns apollo.query' do
|
|
48
|
+
expect(message.routing_key).to eq('apollo.query')
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe '#message' do
|
|
53
|
+
it 'includes query params' do
|
|
54
|
+
msg = message.message
|
|
55
|
+
expect(msg[:action]).to eq(:query)
|
|
56
|
+
expect(msg[:query]).to eq('PKI configuration')
|
|
57
|
+
expect(msg[:limit]).to eq(5)
|
|
58
|
+
expect(msg[:reply_to]).to eq('reply-queue')
|
|
59
|
+
expect(msg[:correlation_id]).to eq('corr-123')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'compacts nil values' do
|
|
63
|
+
simple = described_class.new(action: :query, query: 'test')
|
|
64
|
+
msg = simple.message
|
|
65
|
+
expect(msg).not_to have_key(:tags)
|
|
66
|
+
expect(msg).not_to have_key(:relation_types)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe '#type' do
|
|
71
|
+
it 'returns apollo_query' do
|
|
72
|
+
expect(message.type).to eq('apollo_query')
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
# Sequel is a runtime dependency via legion-data; stub for specs
|
|
15
|
+
unless defined?(Sequel)
|
|
16
|
+
module Sequel
|
|
17
|
+
class Error < StandardError; end
|
|
18
|
+
|
|
19
|
+
def self.pg_array(arr) = arr
|
|
20
|
+
def self.lit(str) = str
|
|
21
|
+
Expr = Struct.new(:value) { def +(other) = "#{value} + #{other}" }
|
|
22
|
+
def self.expr(sym) = Expr.new(sym)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
RSpec.configure do |config|
|
|
27
|
+
config.example_status_persistence_file_path = '.rspec_status'
|
|
28
|
+
config.disable_monkey_patching!
|
|
29
|
+
config.expect_with(:rspec) { |c| c.syntax = :expect }
|
|
30
|
+
end
|