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,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/apollo/helpers/confidence'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Legion::Extensions::Apollo::Helpers::Confidence do
|
|
7
|
+
describe 'constants' do
|
|
8
|
+
it 'defines INITIAL_CONFIDENCE' do
|
|
9
|
+
expect(described_class::INITIAL_CONFIDENCE).to eq(0.5)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'defines CORROBORATION_BOOST' do
|
|
13
|
+
expect(described_class::CORROBORATION_BOOST).to eq(0.3)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'defines RETRIEVAL_BOOST' do
|
|
17
|
+
expect(described_class::RETRIEVAL_BOOST).to eq(0.02)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'defines HOURLY_DECAY_FACTOR' do
|
|
21
|
+
expect(described_class::HOURLY_DECAY_FACTOR).to eq(0.998)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'defines DECAY_THRESHOLD' do
|
|
25
|
+
expect(described_class::DECAY_THRESHOLD).to eq(0.1)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'defines CORROBORATION_SIMILARITY_THRESHOLD' do
|
|
29
|
+
expect(described_class::CORROBORATION_SIMILARITY_THRESHOLD).to eq(0.9)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'defines WRITE_CONFIDENCE_GATE' do
|
|
33
|
+
expect(described_class::WRITE_CONFIDENCE_GATE).to eq(0.6)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'defines WRITE_NOVELTY_GATE' do
|
|
37
|
+
expect(described_class::WRITE_NOVELTY_GATE).to eq(0.3)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'defines STALE_DAYS' do
|
|
41
|
+
expect(described_class::STALE_DAYS).to eq(90)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe '.apply_decay' do
|
|
46
|
+
it 'multiplies confidence by HOURLY_DECAY_FACTOR' do
|
|
47
|
+
result = described_class.apply_decay(confidence: 1.0)
|
|
48
|
+
expect(result).to eq(0.998)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'accepts a custom factor' do
|
|
52
|
+
result = described_class.apply_decay(confidence: 1.0, factor: 0.5)
|
|
53
|
+
expect(result).to eq(0.5)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'clamps to 0.0 minimum' do
|
|
57
|
+
result = described_class.apply_decay(confidence: 0.001, factor: 0.001)
|
|
58
|
+
expect(result).to be >= 0.0
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe '.apply_retrieval_boost' do
|
|
63
|
+
it 'adds RETRIEVAL_BOOST to confidence' do
|
|
64
|
+
result = described_class.apply_retrieval_boost(confidence: 0.5)
|
|
65
|
+
expect(result).to eq(0.52)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'clamps to 1.0 maximum' do
|
|
69
|
+
result = described_class.apply_retrieval_boost(confidence: 0.99)
|
|
70
|
+
expect(result).to eq(1.0)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
describe '.apply_corroboration_boost' do
|
|
75
|
+
it 'adds CORROBORATION_BOOST to confidence' do
|
|
76
|
+
result = described_class.apply_corroboration_boost(confidence: 0.5)
|
|
77
|
+
expect(result).to eq(0.8)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'clamps to 1.0 maximum' do
|
|
81
|
+
result = described_class.apply_corroboration_boost(confidence: 0.9)
|
|
82
|
+
expect(result).to eq(1.0)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
describe '.decayed?' do
|
|
87
|
+
it 'returns true when confidence below threshold' do
|
|
88
|
+
expect(described_class.decayed?(confidence: 0.05)).to be true
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'returns false when confidence at or above threshold' do
|
|
92
|
+
expect(described_class.decayed?(confidence: 0.1)).to be false
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
describe '.meets_write_gate?' do
|
|
97
|
+
it 'returns true when both gates met' do
|
|
98
|
+
expect(described_class.meets_write_gate?(confidence: 0.7, novelty: 0.4)).to be true
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'returns false when confidence below gate' do
|
|
102
|
+
expect(described_class.meets_write_gate?(confidence: 0.5, novelty: 0.4)).to be false
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it 'returns false when novelty below gate' do
|
|
106
|
+
expect(described_class.meets_write_gate?(confidence: 0.7, novelty: 0.2)).to be false
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/apollo/helpers/embedding'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Legion::Extensions::Apollo::Helpers::Embedding do
|
|
7
|
+
describe '.generate' do
|
|
8
|
+
context 'when Legion::LLM is not defined' do
|
|
9
|
+
before do
|
|
10
|
+
hide_const('Legion::LLM') if defined?(Legion::LLM)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'returns a zero vector of the correct dimension' do
|
|
14
|
+
result = described_class.generate(text: 'hello world')
|
|
15
|
+
expect(result).to eq(Array.new(1536, 0.0))
|
|
16
|
+
expect(result.size).to eq(1536)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
context 'when Legion::LLM is defined but not started' do
|
|
21
|
+
before do
|
|
22
|
+
stub_const('Legion::LLM', Module.new { def self.started? = false })
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'returns a zero vector' do
|
|
26
|
+
result = described_class.generate(text: 'hello world')
|
|
27
|
+
expect(result).to eq(Array.new(1536, 0.0))
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
context 'when Legion::LLM is available and started' do
|
|
32
|
+
let(:mock_embedding) { Array.new(1536) { rand(-1.0..1.0) } }
|
|
33
|
+
|
|
34
|
+
before do
|
|
35
|
+
llm = Module.new do
|
|
36
|
+
define_method(:started?) { true }
|
|
37
|
+
define_method(:embed) { |_text:| nil }
|
|
38
|
+
extend self
|
|
39
|
+
end
|
|
40
|
+
stub_const('Legion::LLM', llm)
|
|
41
|
+
allow(Legion::LLM).to receive(:embed).and_return(mock_embedding)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'returns the embedding from LLM' do
|
|
45
|
+
result = described_class.generate(text: 'hello world')
|
|
46
|
+
expect(result).to eq(mock_embedding)
|
|
47
|
+
expect(Legion::LLM).to have_received(:embed).with(text: 'hello world')
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
context 'when Legion::LLM returns invalid embedding' do
|
|
52
|
+
before do
|
|
53
|
+
llm = Module.new do
|
|
54
|
+
define_method(:started?) { true }
|
|
55
|
+
define_method(:embed) { |_text:| nil }
|
|
56
|
+
extend self
|
|
57
|
+
end
|
|
58
|
+
stub_const('Legion::LLM', llm)
|
|
59
|
+
allow(Legion::LLM).to receive(:embed).and_return([0.1, 0.2])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'returns a zero vector as fallback' do
|
|
63
|
+
result = described_class.generate(text: 'hello world')
|
|
64
|
+
expect(result).to eq(Array.new(1536, 0.0))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/apollo/helpers/graph_query'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Legion::Extensions::Apollo::Helpers::GraphQuery do
|
|
7
|
+
describe 'constants' do
|
|
8
|
+
it 'defines SPREAD_FACTOR' do
|
|
9
|
+
expect(described_class::SPREAD_FACTOR).to eq(0.6)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'defines DEFAULT_DEPTH' do
|
|
13
|
+
expect(described_class::DEFAULT_DEPTH).to eq(2)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'defines MIN_ACTIVATION' do
|
|
17
|
+
expect(described_class::MIN_ACTIVATION).to eq(0.1)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe '.build_traversal_sql' do
|
|
22
|
+
it 'returns SQL string with entry_id placeholder' do
|
|
23
|
+
sql = described_class.build_traversal_sql(depth: 2)
|
|
24
|
+
expect(sql).to include('apollo_entries')
|
|
25
|
+
expect(sql).to include('apollo_relations')
|
|
26
|
+
expect(sql).to include('WITH RECURSIVE')
|
|
27
|
+
expect(sql).to include('$entry_id')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'includes relation type filter when specified' do
|
|
31
|
+
sql = described_class.build_traversal_sql(depth: 2, relation_types: %w[causes depends_on])
|
|
32
|
+
expect(sql).to include("'causes'")
|
|
33
|
+
expect(sql).to include("'depends_on'")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'respects custom depth' do
|
|
37
|
+
sql = described_class.build_traversal_sql(depth: 3)
|
|
38
|
+
expect(sql).to include('g.depth < 3')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'applies spread factor and min activation' do
|
|
42
|
+
sql = described_class.build_traversal_sql(depth: 2)
|
|
43
|
+
expect(sql).to include('0.6')
|
|
44
|
+
expect(sql).to include('0.1')
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe '.build_semantic_search_sql' do
|
|
49
|
+
it 'returns SQL with vector placeholder' do
|
|
50
|
+
sql = described_class.build_semantic_search_sql(limit: 5, min_confidence: 0.3)
|
|
51
|
+
expect(sql).to include('apollo_entries')
|
|
52
|
+
expect(sql).to include('$embedding')
|
|
53
|
+
expect(sql).to include('<=>')
|
|
54
|
+
expect(sql).to include('LIMIT 5')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'includes status filter' do
|
|
58
|
+
sql = described_class.build_semantic_search_sql(limit: 10, statuses: %w[confirmed])
|
|
59
|
+
expect(sql).to include("'confirmed'")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'includes tag filter when specified' do
|
|
63
|
+
sql = described_class.build_semantic_search_sql(limit: 10, tags: %w[vault dns])
|
|
64
|
+
expect(sql).to include("'vault'")
|
|
65
|
+
expect(sql).to include("'dns'")
|
|
66
|
+
expect(sql).to include('&&')
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/apollo/helpers/similarity'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Legion::Extensions::Apollo::Helpers::Similarity do
|
|
7
|
+
describe '.cosine_similarity' do
|
|
8
|
+
it 'returns 1.0 for identical vectors' do
|
|
9
|
+
vec = [1.0, 0.0, 0.0]
|
|
10
|
+
expect(described_class.cosine_similarity(vec_a: vec, vec_b: vec)).to be_within(0.001).of(1.0)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'returns 0.0 for orthogonal vectors' do
|
|
14
|
+
vec_a = [1.0, 0.0]
|
|
15
|
+
vec_b = [0.0, 1.0]
|
|
16
|
+
expect(described_class.cosine_similarity(vec_a: vec_a, vec_b: vec_b)).to be_within(0.001).of(0.0)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'returns correct similarity for known vectors' do
|
|
20
|
+
vec_a = [1.0, 2.0, 3.0]
|
|
21
|
+
vec_b = [4.0, 5.0, 6.0]
|
|
22
|
+
expect(described_class.cosine_similarity(vec_a: vec_a, vec_b: vec_b)).to be_within(0.001).of(0.9746)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'returns 0.0 for zero vectors' do
|
|
26
|
+
vec_a = [0.0, 0.0]
|
|
27
|
+
vec_b = [1.0, 1.0]
|
|
28
|
+
expect(described_class.cosine_similarity(vec_a: vec_a, vec_b: vec_b)).to eq(0.0)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe '.above_corroboration_threshold?' do
|
|
33
|
+
it 'returns true when similarity exceeds threshold' do
|
|
34
|
+
expect(described_class.above_corroboration_threshold?(similarity: 0.95)).to be true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'returns false when similarity below threshold' do
|
|
38
|
+
expect(described_class.above_corroboration_threshold?(similarity: 0.85)).to be false
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe '.classify_match' do
|
|
43
|
+
it 'returns :corroboration for high similarity same type' do
|
|
44
|
+
result = described_class.classify_match(similarity: 0.95, same_content_type: true)
|
|
45
|
+
expect(result).to eq(:corroboration)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'returns :contradiction for high similarity with contradicts relation' do
|
|
49
|
+
result = described_class.classify_match(similarity: 0.95, same_content_type: true, contradicts: true)
|
|
50
|
+
expect(result).to eq(:contradiction)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'returns :novel for low similarity' do
|
|
54
|
+
result = described_class.classify_match(similarity: 0.5, same_content_type: true)
|
|
55
|
+
expect(result).to eq(:novel)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/apollo/runners/expertise'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Legion::Extensions::Apollo::Runners::Expertise do
|
|
7
|
+
let(:runner) do
|
|
8
|
+
obj = Object.new
|
|
9
|
+
obj.extend(described_class)
|
|
10
|
+
obj
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe '#get_expertise' do
|
|
14
|
+
it 'returns an expertise query payload' do
|
|
15
|
+
result = runner.get_expertise(domain: 'vault')
|
|
16
|
+
expect(result[:action]).to eq(:expertise_query)
|
|
17
|
+
expect(result[:domain]).to eq('vault')
|
|
18
|
+
expect(result[:min_proficiency]).to eq(0.0)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe '#domains_at_risk' do
|
|
23
|
+
it 'returns an at-risk query payload' do
|
|
24
|
+
result = runner.domains_at_risk(min_agents: 2)
|
|
25
|
+
expect(result[:action]).to eq(:domains_at_risk)
|
|
26
|
+
expect(result[:min_agents]).to eq(2)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe '#agent_profile' do
|
|
31
|
+
it 'returns a profile query payload' do
|
|
32
|
+
result = runner.agent_profile(agent_id: 'worker-1')
|
|
33
|
+
expect(result[:action]).to eq(:agent_profile)
|
|
34
|
+
expect(result[:agent_id]).to eq('worker-1')
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe '#aggregate' do
|
|
39
|
+
let(:host) { Object.new.extend(described_class) }
|
|
40
|
+
|
|
41
|
+
context 'when Apollo data is not available' do
|
|
42
|
+
before { hide_const('Legion::Data::Model::ApolloEntry') if defined?(Legion::Data::Model::ApolloEntry) }
|
|
43
|
+
|
|
44
|
+
it 'returns a structured error' do
|
|
45
|
+
result = host.aggregate
|
|
46
|
+
expect(result[:success]).to be false
|
|
47
|
+
expect(result[:error]).to eq('apollo_data_not_available')
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
context 'when entries exist' do
|
|
52
|
+
let(:mock_entry_class) { double('ApolloEntry') }
|
|
53
|
+
let(:mock_expertise_class) { double('ApolloExpertise') }
|
|
54
|
+
let(:entries) do
|
|
55
|
+
[
|
|
56
|
+
double('e1', source_agent: 'agent-1', tags: ['ruby'], confidence: 0.8),
|
|
57
|
+
double('e2', source_agent: 'agent-1', tags: ['ruby'], confidence: 0.6),
|
|
58
|
+
double('e3', source_agent: 'agent-2', tags: ['python'], confidence: 0.9)
|
|
59
|
+
]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
before do
|
|
63
|
+
stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
|
|
64
|
+
stub_const('Legion::Data::Model::ApolloExpertise', mock_expertise_class)
|
|
65
|
+
allow(mock_entry_class).to receive(:select).and_return(double(exclude: double(all: entries)))
|
|
66
|
+
allow(mock_expertise_class).to receive(:where).and_return(double(first: nil))
|
|
67
|
+
allow(mock_expertise_class).to receive(:create)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'returns agent and domain counts' do
|
|
71
|
+
result = host.aggregate
|
|
72
|
+
expect(result[:success]).to be true
|
|
73
|
+
expect(result[:agents]).to eq(2)
|
|
74
|
+
expect(result[:domains]).to eq(2)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'creates expertise records' do
|
|
78
|
+
expect(mock_expertise_class).to receive(:create).twice
|
|
79
|
+
host.aggregate
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'computes proficiency using log2 formula' do
|
|
83
|
+
# agent-1 ruby: avg=0.7, count=2, proficiency = 0.7 * log2(3) = 0.7 * 1.585 = 1.109 -> capped at 1.0
|
|
84
|
+
expect(mock_expertise_class).to receive(:create).with(
|
|
85
|
+
hash_including(agent_id: 'agent-1', domain: 'ruby', proficiency: 1.0)
|
|
86
|
+
)
|
|
87
|
+
# agent-2 python: avg=0.9, count=1, proficiency = 0.9 * log2(2) = 0.9 * 1.0 = 0.9
|
|
88
|
+
expect(mock_expertise_class).to receive(:create).with(
|
|
89
|
+
hash_including(agent_id: 'agent-2', domain: 'python', proficiency: 0.9)
|
|
90
|
+
)
|
|
91
|
+
host.aggregate
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
context 'when no entries exist' do
|
|
96
|
+
let(:mock_entry_class) { double('ApolloEntry') }
|
|
97
|
+
|
|
98
|
+
before do
|
|
99
|
+
stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
|
|
100
|
+
allow(mock_entry_class).to receive(:select).and_return(double(exclude: double(all: [])))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'returns zero counts' do
|
|
104
|
+
result = host.aggregate
|
|
105
|
+
expect(result[:success]).to be true
|
|
106
|
+
expect(result[:agents]).to eq(0)
|
|
107
|
+
expect(result[:domains]).to eq(0)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|