lex-mesh 0.1.1 → 0.2.3
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/Gemfile +0 -2
- data/lex-mesh.gemspec +0 -1
- data/lib/legion/extensions/mesh/actors/pending_expiry.rb +37 -0
- data/lib/legion/extensions/mesh/actors/preference_listener.rb +44 -0
- data/lib/legion/extensions/mesh/actors/silence_watchdog.rb +37 -0
- data/lib/legion/extensions/mesh/client.rb +7 -2
- data/lib/legion/extensions/mesh/helpers/pending_requests.rb +57 -0
- data/lib/legion/extensions/mesh/helpers/registry.rb +12 -0
- data/lib/legion/extensions/mesh/runners/mesh.rb +20 -0
- data/lib/legion/extensions/mesh/runners/preferences.rb +120 -0
- data/lib/legion/extensions/mesh/transport/messages/mesh_departure.rb +34 -0
- data/lib/legion/extensions/mesh/transport/messages/preference_query.rb +34 -0
- data/lib/legion/extensions/mesh/transport/messages/preference_response.rb +34 -0
- data/lib/legion/extensions/mesh/transport/queues/preference.rb +27 -0
- data/lib/legion/extensions/mesh/version.rb +1 -1
- data/lib/legion/extensions/mesh.rb +9 -0
- data/spec/legion/extensions/mesh/actors/pending_expiry_spec.rb +57 -0
- data/spec/legion/extensions/mesh/actors/preference_listener_spec.rb +87 -0
- data/spec/legion/extensions/mesh/actors/silence_watchdog_spec.rb +55 -0
- data/spec/legion/extensions/mesh/client_spec.rb +40 -1
- data/spec/legion/extensions/mesh/helpers/pending_requests_spec.rb +80 -0
- data/spec/legion/extensions/mesh/helpers/registry_spec.rb +41 -0
- data/spec/legion/extensions/mesh/runners/mesh_spec.rb +25 -0
- data/spec/legion/extensions/mesh/runners/preferences_spec.rb +164 -0
- data/spec/legion/extensions/mesh/transport/messages/mesh_departure_spec.rb +81 -0
- data/spec/legion/extensions/mesh/transport/messages/preference_query_spec.rb +85 -0
- data/spec/legion/extensions/mesh/transport/messages/preference_response_spec.rb +85 -0
- data/spec/legion/extensions/mesh/transport/queues/preference_spec.rb +61 -0
- metadata +20 -16
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Stub the framework subscription base class
|
|
4
|
+
unless defined?(Legion::Extensions::Actors::Subscription)
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Actors
|
|
8
|
+
class Subscription # rubocop:disable Lint/EmptyClass
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
$LOADED_FEATURES << 'legion/extensions/actors/subscription'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Stub transport queue base classes if not loaded
|
|
18
|
+
unless defined?(Legion::Transport::Queues::Agent)
|
|
19
|
+
module Legion
|
|
20
|
+
module Transport
|
|
21
|
+
module Queues
|
|
22
|
+
class Agent # rubocop:disable Lint/EmptyClass
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
require 'legion/extensions/mesh/transport/queues/preference'
|
|
30
|
+
require 'legion/extensions/mesh/runners/preferences'
|
|
31
|
+
require 'legion/extensions/mesh/actors/preference_listener'
|
|
32
|
+
|
|
33
|
+
RSpec.describe Legion::Extensions::Mesh::Actor::PreferenceListener do
|
|
34
|
+
subject(:actor) { described_class.new }
|
|
35
|
+
|
|
36
|
+
describe '#runner_class' do
|
|
37
|
+
it 'returns the Preferences runner module' do
|
|
38
|
+
expect(actor.runner_class).to eq(Legion::Extensions::Mesh::Runners::Preferences)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe '#runner_function' do
|
|
43
|
+
it 'returns dispatch_preference_message' do
|
|
44
|
+
expect(actor.runner_function).to eq('dispatch_preference_message')
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe '#check_subtask?' do
|
|
49
|
+
it 'returns false' do
|
|
50
|
+
expect(actor.check_subtask?).to be false
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe '#generate_task?' do
|
|
55
|
+
it 'returns false' do
|
|
56
|
+
expect(actor.generate_task?).to be false
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe '#use_runner?' do
|
|
61
|
+
it 'returns false' do
|
|
62
|
+
expect(actor.use_runner?).to be false
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe '#queue' do
|
|
67
|
+
it 'returns the Preference queue class' do
|
|
68
|
+
expect(actor.queue).to eq(Legion::Extensions::Mesh::Transport::Queues::Preference)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe '#enabled?' do
|
|
73
|
+
context 'when transport is available' do
|
|
74
|
+
it 'returns truthy' do
|
|
75
|
+
stub_const('Legion::Transport', Module.new)
|
|
76
|
+
expect(actor.enabled?).to be_truthy
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
context 'when transport is not available' do
|
|
81
|
+
it 'returns falsey' do
|
|
82
|
+
hide_const('Legion::Transport')
|
|
83
|
+
expect(actor.enabled?).to be_falsey
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
unless defined?(Legion::Extensions::Actors::Every)
|
|
6
|
+
module Legion
|
|
7
|
+
module Extensions
|
|
8
|
+
module Actors
|
|
9
|
+
class Every; end # rubocop:disable Lint/EmptyClass
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
require 'legion/extensions/mesh/actors/silence_watchdog'
|
|
16
|
+
|
|
17
|
+
RSpec.describe Legion::Extensions::Mesh::Actor::SilenceWatchdog do
|
|
18
|
+
subject(:actor) { described_class.allocate }
|
|
19
|
+
|
|
20
|
+
describe '#runner_class' do
|
|
21
|
+
it 'returns the Mesh runner module' do
|
|
22
|
+
expect(actor.runner_class).to eq(Legion::Extensions::Mesh::Runners::Mesh)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe '#runner_function' do
|
|
27
|
+
it 'returns expire_silent_agents' do
|
|
28
|
+
expect(actor.runner_function).to eq('expire_silent_agents')
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe '#time' do
|
|
33
|
+
it 'returns 15' do
|
|
34
|
+
expect(actor.time).to eq(15)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe '#use_runner?' do
|
|
39
|
+
it 'returns false' do
|
|
40
|
+
expect(actor.use_runner?).to be false
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe '#check_subtask?' do
|
|
45
|
+
it 'returns false' do
|
|
46
|
+
expect(actor.check_subtask?).to be false
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe '#generate_task?' do
|
|
51
|
+
it 'returns false' do
|
|
52
|
+
expect(actor.generate_task?).to be false
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
require 'legion/extensions/mesh/client'
|
|
4
4
|
|
|
5
5
|
RSpec.describe Legion::Extensions::Mesh::Client do
|
|
6
|
+
let(:client) { described_class.new }
|
|
7
|
+
|
|
6
8
|
it 'responds to mesh runner methods' do
|
|
7
|
-
client = described_class.new
|
|
8
9
|
expect(client).to respond_to(:register)
|
|
9
10
|
expect(client).to respond_to(:unregister)
|
|
10
11
|
expect(client).to respond_to(:heartbeat)
|
|
@@ -12,4 +13,42 @@ RSpec.describe Legion::Extensions::Mesh::Client do
|
|
|
12
13
|
expect(client).to respond_to(:find_agents)
|
|
13
14
|
expect(client).to respond_to(:mesh_status)
|
|
14
15
|
end
|
|
16
|
+
|
|
17
|
+
it 'responds to preference runner methods' do
|
|
18
|
+
expect(client).to respond_to(:query_preferences)
|
|
19
|
+
expect(client).to respond_to(:handle_preference_query)
|
|
20
|
+
expect(client).to respond_to(:handle_preference_response)
|
|
21
|
+
expect(client).to respond_to(:expire_pending_requests)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe '#query_preferences' do
|
|
25
|
+
it 'returns a result hash with success key' do
|
|
26
|
+
result = client.query_preferences(target_agent_id: 'agent-1')
|
|
27
|
+
expect(result).to have_key(:success)
|
|
28
|
+
expect(result[:success]).to be true
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe '#handle_preference_query' do
|
|
33
|
+
it 'returns local profile' do
|
|
34
|
+
result = client.handle_preference_query(requesting_agent_id: 'agent-1')
|
|
35
|
+
expect(result[:success]).to be true
|
|
36
|
+
expect(result[:profile]).to be_a(Hash)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe '#handle_preference_response' do
|
|
41
|
+
it 'returns resolved false for unknown id' do
|
|
42
|
+
result = client.handle_preference_response(correlation_id: 'nope', profile: {})
|
|
43
|
+
expect(result[:resolved]).to be false
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe '#expire_pending_requests' do
|
|
48
|
+
it 'returns expiry summary' do
|
|
49
|
+
result = client.expire_pending_requests
|
|
50
|
+
expect(result).to have_key(:expired)
|
|
51
|
+
expect(result[:expired]).to eq(0)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
15
54
|
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/mesh/helpers/pending_requests'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Legion::Extensions::Mesh::Helpers::PendingRequests do
|
|
7
|
+
subject(:tracker) { described_class.new(default_ttl: 5) }
|
|
8
|
+
|
|
9
|
+
describe '#register' do
|
|
10
|
+
it 'stores a pending request' do
|
|
11
|
+
tracker.register(correlation_id: 'abc', callback: ->(_r) {})
|
|
12
|
+
expect(tracker.pending?('abc')).to be true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'increments pending_count' do
|
|
16
|
+
expect { tracker.register(correlation_id: 'x', callback: nil) }
|
|
17
|
+
.to change { tracker.pending_count }.by(1)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe '#resolve' do
|
|
22
|
+
it 'invokes callback and removes entry' do
|
|
23
|
+
received = nil
|
|
24
|
+
tracker.register(correlation_id: 'r1', callback: ->(result) { received = result })
|
|
25
|
+
result = tracker.resolve(correlation_id: 'r1', result: { verbosity: :concise })
|
|
26
|
+
expect(result).to be true
|
|
27
|
+
expect(received).to eq({ verbosity: :concise })
|
|
28
|
+
expect(tracker.pending?('r1')).to be false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'returns false for unknown correlation_id' do
|
|
32
|
+
result = tracker.resolve(correlation_id: 'nonexistent', result: {})
|
|
33
|
+
expect(result).to be false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'handles nil callback gracefully' do
|
|
37
|
+
tracker.register(correlation_id: 'nil-cb', callback: nil)
|
|
38
|
+
expect { tracker.resolve(correlation_id: 'nil-cb', result: {}) }.not_to raise_error
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe '#pending?' do
|
|
43
|
+
it 'returns false for unregistered id' do
|
|
44
|
+
expect(tracker.pending?('missing')).to be false
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe '#pending_count' do
|
|
49
|
+
it 'returns correct count' do
|
|
50
|
+
tracker.register(correlation_id: 'a', callback: nil)
|
|
51
|
+
tracker.register(correlation_id: 'b', callback: nil)
|
|
52
|
+
expect(tracker.pending_count).to eq(2)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe '#expire' do
|
|
57
|
+
it 'removes entries past TTL' do
|
|
58
|
+
tracker.register(correlation_id: 'old', callback: nil, ttl: 1)
|
|
59
|
+
entry = tracker.instance_variable_get(:@requests)['old']
|
|
60
|
+
entry[:registered_at] = Time.now - 10
|
|
61
|
+
expired = tracker.expire
|
|
62
|
+
expect(expired).to include('old')
|
|
63
|
+
expect(tracker.pending?('old')).to be false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'does not remove entries within TTL' do
|
|
67
|
+
tracker.register(correlation_id: 'fresh', callback: nil, ttl: 60)
|
|
68
|
+
tracker.expire
|
|
69
|
+
expect(tracker.pending?('fresh')).to be true
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'returns array of expired correlation ids' do
|
|
73
|
+
tracker.register(correlation_id: 'e1', callback: nil, ttl: 1)
|
|
74
|
+
tracker.register(correlation_id: 'e2', callback: nil, ttl: 1)
|
|
75
|
+
tracker.instance_variable_get(:@requests).each_value { |e| e[:registered_at] = Time.now - 10 }
|
|
76
|
+
expired = tracker.expire
|
|
77
|
+
expect(expired).to contain_exactly('e1', 'e2')
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -272,6 +272,47 @@ RSpec.describe Legion::Extensions::Mesh::Helpers::Registry do
|
|
|
272
272
|
end
|
|
273
273
|
end
|
|
274
274
|
|
|
275
|
+
describe '#expire_silent_agents' do
|
|
276
|
+
it 'marks stale agents as offline' do
|
|
277
|
+
registry.register_agent('a1')
|
|
278
|
+
registry.agents['a1'][:last_seen] = Time.now.utc - 60
|
|
279
|
+
expired = registry.expire_silent_agents(timeout: 30)
|
|
280
|
+
expect(expired).to eq(['a1'])
|
|
281
|
+
expect(registry.agents['a1'][:status]).to eq(:offline)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
it 'does not expire agents within timeout' do
|
|
285
|
+
registry.register_agent('a1')
|
|
286
|
+
expired = registry.expire_silent_agents(timeout: 30)
|
|
287
|
+
expect(expired).to eq([])
|
|
288
|
+
expect(registry.agents['a1'][:status]).to eq(:online)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
it 'does not expire already-offline agents' do
|
|
292
|
+
registry.register_agent('a1')
|
|
293
|
+
registry.agents['a1'][:last_seen] = Time.now.utc - 60
|
|
294
|
+
registry.agents['a1'][:status] = :offline
|
|
295
|
+
expired = registry.expire_silent_agents(timeout: 30)
|
|
296
|
+
expect(expired).to eq([])
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
it 'returns multiple expired agent ids' do
|
|
300
|
+
registry.register_agent('a1')
|
|
301
|
+
registry.register_agent('a2')
|
|
302
|
+
registry.agents['a1'][:last_seen] = Time.now.utc - 60
|
|
303
|
+
registry.agents['a2'][:last_seen] = Time.now.utc - 60
|
|
304
|
+
expired = registry.expire_silent_agents(timeout: 30)
|
|
305
|
+
expect(expired).to contain_exactly('a1', 'a2')
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
it 'uses MESH_SILENCE_TIMEOUT as default' do
|
|
309
|
+
registry.register_agent('a1')
|
|
310
|
+
registry.agents['a1'][:last_seen] = Time.now.utc - 31
|
|
311
|
+
expired = registry.expire_silent_agents
|
|
312
|
+
expect(expired).to eq(['a1'])
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
275
316
|
describe '#count' do
|
|
276
317
|
it 'returns 0 for an empty registry' do
|
|
277
318
|
expect(registry.count).to eq(0)
|
|
@@ -23,6 +23,22 @@ RSpec.describe Legion::Extensions::Mesh::Runners::Mesh do
|
|
|
23
23
|
result = client.unregister(agent_id: 'unknown')
|
|
24
24
|
expect(result[:error]).to eq(:not_found)
|
|
25
25
|
end
|
|
26
|
+
|
|
27
|
+
it 'publishes mesh departure signal when transport available' do
|
|
28
|
+
client.register(agent_id: 'agent-1', capabilities: [:search])
|
|
29
|
+
|
|
30
|
+
mock_msg = instance_double('MeshDeparture', publish: nil)
|
|
31
|
+
stub_const('Legion::Extensions::Mesh::Transport::Messages::MeshDeparture', class_double('MeshDeparture', new: mock_msg))
|
|
32
|
+
|
|
33
|
+
client.unregister(agent_id: 'agent-1')
|
|
34
|
+
expect(mock_msg).to have_received(:publish)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'succeeds without transport available' do
|
|
38
|
+
client.register(agent_id: 'agent-1')
|
|
39
|
+
result = client.unregister(agent_id: 'agent-1')
|
|
40
|
+
expect(result[:unregistered]).to be true
|
|
41
|
+
end
|
|
26
42
|
end
|
|
27
43
|
|
|
28
44
|
describe '#send_message' do
|
|
@@ -65,4 +81,13 @@ RSpec.describe Legion::Extensions::Mesh::Runners::Mesh do
|
|
|
65
81
|
expect(status[:online]).to eq(1)
|
|
66
82
|
end
|
|
67
83
|
end
|
|
84
|
+
|
|
85
|
+
describe '#expire_silent_agents' do
|
|
86
|
+
it 'returns expired agent count' do
|
|
87
|
+
client.register(agent_id: 'a1')
|
|
88
|
+
result = client.expire_silent_agents
|
|
89
|
+
expect(result[:success]).to be true
|
|
90
|
+
expect(result[:count]).to eq(0)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
68
93
|
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/mesh/helpers/pending_requests'
|
|
5
|
+
require 'legion/extensions/mesh/helpers/preference_profile'
|
|
6
|
+
require 'legion/extensions/mesh/runners/preferences'
|
|
7
|
+
|
|
8
|
+
RSpec.describe Legion::Extensions::Mesh::Runners::Preferences do
|
|
9
|
+
let(:runner) { Object.new.extend(described_class) }
|
|
10
|
+
|
|
11
|
+
describe '#query_preferences' do
|
|
12
|
+
context 'without transport' do
|
|
13
|
+
it 'returns local_default profile' do
|
|
14
|
+
result = runner.query_preferences(target_agent_id: 'agent-99')
|
|
15
|
+
expect(result[:success]).to be true
|
|
16
|
+
expect(result[:source]).to eq(:local_default)
|
|
17
|
+
expect(result[:profile]).to be_a(Hash)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'includes default preference keys in profile' do
|
|
21
|
+
result = runner.query_preferences(target_agent_id: 'agent-99')
|
|
22
|
+
expect(result[:profile]).to have_key(:verbosity)
|
|
23
|
+
expect(result[:profile]).to have_key(:tone)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
context 'with transport available' do
|
|
28
|
+
before do
|
|
29
|
+
stub_const('Legion::Transport::Connection', Class.new do
|
|
30
|
+
def self.session; end
|
|
31
|
+
end)
|
|
32
|
+
stub_const('Legion::Extensions::Mesh::Transport::Messages::PreferenceQuery',
|
|
33
|
+
Class.new do
|
|
34
|
+
def initialize(**_opts); end
|
|
35
|
+
|
|
36
|
+
def publish; end
|
|
37
|
+
end)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'returns pending status with correlation_id' do
|
|
41
|
+
result = runner.query_preferences(target_agent_id: 'agent-42')
|
|
42
|
+
expect(result[:success]).to be true
|
|
43
|
+
expect(result[:source]).to eq(:pending)
|
|
44
|
+
expect(result[:correlation_id]).to be_a(String)
|
|
45
|
+
expect(result[:correlation_id]).not_to be_empty
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'registers a pending request' do
|
|
49
|
+
result = runner.query_preferences(target_agent_id: 'agent-42')
|
|
50
|
+
pending_req = runner.send(:pending_requests)
|
|
51
|
+
expect(pending_req.pending?(result[:correlation_id])).to be true
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe '#handle_preference_query' do
|
|
57
|
+
it 'returns local profile' do
|
|
58
|
+
result = runner.handle_preference_query(requesting_agent_id: 'agent-1')
|
|
59
|
+
expect(result[:success]).to be true
|
|
60
|
+
expect(result[:profile]).to be_a(Hash)
|
|
61
|
+
expect(result[:responding_agent_id]).not_to be_nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'includes verbosity in profile' do
|
|
65
|
+
result = runner.handle_preference_query(requesting_agent_id: 'agent-1')
|
|
66
|
+
expect(result[:profile]).to have_key(:verbosity)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe '#handle_preference_response' do
|
|
71
|
+
it 'resolves a pending request' do
|
|
72
|
+
received = nil
|
|
73
|
+
pending_req = runner.send(:pending_requests)
|
|
74
|
+
pending_req.register(
|
|
75
|
+
correlation_id: 'corr-1',
|
|
76
|
+
callback: ->(profile) { received = profile }
|
|
77
|
+
)
|
|
78
|
+
result = runner.handle_preference_response(
|
|
79
|
+
correlation_id: 'corr-1',
|
|
80
|
+
profile: { verbosity: :concise }
|
|
81
|
+
)
|
|
82
|
+
expect(result[:resolved]).to be true
|
|
83
|
+
expect(received).to eq({ verbosity: :concise })
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'returns false for unknown correlation_id' do
|
|
87
|
+
result = runner.handle_preference_response(
|
|
88
|
+
correlation_id: 'unknown-corr',
|
|
89
|
+
profile: {}
|
|
90
|
+
)
|
|
91
|
+
expect(result[:resolved]).to be false
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
describe '#dispatch_preference_message' do
|
|
96
|
+
context 'with preference_query type' do
|
|
97
|
+
it 'calls handle_preference_query and returns profile' do
|
|
98
|
+
result = runner.dispatch_preference_message(
|
|
99
|
+
type: 'preference_query',
|
|
100
|
+
requesting_agent_id: 'agent-1',
|
|
101
|
+
correlation_id: 'corr-123'
|
|
102
|
+
)
|
|
103
|
+
expect(result[:success]).to be true
|
|
104
|
+
expect(result[:profile]).to be_a(Hash)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
context 'with preference_response type' do
|
|
109
|
+
it 'resolves a pending request' do
|
|
110
|
+
pending_req = runner.send(:pending_requests)
|
|
111
|
+
pending_req.register(correlation_id: 'corr-abc', callback: ->(_p) {})
|
|
112
|
+
result = runner.dispatch_preference_message(
|
|
113
|
+
type: 'preference_response',
|
|
114
|
+
correlation_id: 'corr-abc',
|
|
115
|
+
profile: { verbosity: :concise }
|
|
116
|
+
)
|
|
117
|
+
expect(result[:resolved]).to be true
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'returns false for unknown correlation_id' do
|
|
121
|
+
result = runner.dispatch_preference_message(
|
|
122
|
+
type: 'preference_response',
|
|
123
|
+
correlation_id: 'unknown',
|
|
124
|
+
profile: {}
|
|
125
|
+
)
|
|
126
|
+
expect(result[:resolved]).to be false
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
context 'with unknown type' do
|
|
131
|
+
it 'returns error' do
|
|
132
|
+
result = runner.dispatch_preference_message(type: 'bogus')
|
|
133
|
+
expect(result[:success]).to be false
|
|
134
|
+
expect(result[:error]).to include('unknown')
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
context 'with nil type' do
|
|
139
|
+
it 'returns error' do
|
|
140
|
+
result = runner.dispatch_preference_message(type: nil)
|
|
141
|
+
expect(result[:success]).to be false
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
describe '#expire_pending_requests' do
|
|
147
|
+
it 'cleans up expired entries and returns count' do
|
|
148
|
+
pending_req = runner.send(:pending_requests)
|
|
149
|
+
pending_req.register(correlation_id: 'old-1', callback: nil, ttl: 1)
|
|
150
|
+
pending_req.register(correlation_id: 'old-2', callback: nil, ttl: 1)
|
|
151
|
+
pending_req.instance_variable_get(:@requests).each_value { |e| e[:registered_at] = Time.now - 60 }
|
|
152
|
+
|
|
153
|
+
result = runner.expire_pending_requests
|
|
154
|
+
expect(result[:expired]).to eq(2)
|
|
155
|
+
expect(result[:correlation_ids]).to contain_exactly('old-1', 'old-2')
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it 'returns zero when nothing expired' do
|
|
159
|
+
result = runner.expire_pending_requests
|
|
160
|
+
expect(result[:expired]).to eq(0)
|
|
161
|
+
expect(result[:correlation_ids]).to be_empty
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
def initialize(**options)
|
|
10
|
+
@options = options
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
unless defined?(Legion::Transport::Exchanges::Node)
|
|
18
|
+
module Legion
|
|
19
|
+
module Transport
|
|
20
|
+
module Exchanges
|
|
21
|
+
class Node # rubocop:disable Lint/EmptyClass
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
require 'legion/extensions/mesh/transport/messages/mesh_departure'
|
|
29
|
+
|
|
30
|
+
RSpec.describe Legion::Extensions::Mesh::Transport::Messages::MeshDeparture do
|
|
31
|
+
subject(:msg) do
|
|
32
|
+
described_class.new(
|
|
33
|
+
agent_id: 'agent-42',
|
|
34
|
+
capabilities: %i[code_review search]
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe '#exchange' do
|
|
39
|
+
it 'returns the Node exchange' do
|
|
40
|
+
expect(msg.exchange).to eq(Legion::Transport::Exchanges::Node)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe '#routing_key' do
|
|
45
|
+
it 'uses mesh.departure' do
|
|
46
|
+
expect(msg.routing_key).to eq('mesh.departure')
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe '#message' do
|
|
51
|
+
it 'includes type mesh_departure' do
|
|
52
|
+
expect(msg.message[:type]).to eq('mesh_departure')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'includes agent_id' do
|
|
56
|
+
expect(msg.message[:agent_id]).to eq('agent-42')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'includes capabilities' do
|
|
60
|
+
expect(msg.message[:capabilities]).to eq(%i[code_review search])
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'includes departed_at timestamp' do
|
|
64
|
+
expect(msg.message[:departed_at]).to be_a(String)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe '#type' do
|
|
69
|
+
it 'returns mesh_departure' do
|
|
70
|
+
expect(msg.type).to eq('mesh_departure')
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
describe 'empty capabilities default' do
|
|
75
|
+
subject(:msg_no_caps) { described_class.new(agent_id: 'x') }
|
|
76
|
+
|
|
77
|
+
it 'defaults capabilities to empty array' do
|
|
78
|
+
expect(msg_no_caps.message[:capabilities]).to eq([])
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
def initialize(**options)
|
|
10
|
+
@options = options
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
unless defined?(Legion::Transport::Exchanges::Agent)
|
|
18
|
+
module Legion
|
|
19
|
+
module Transport
|
|
20
|
+
module Exchanges
|
|
21
|
+
class Agent
|
|
22
|
+
def self.name
|
|
23
|
+
'Legion::Transport::Exchanges::Agent'
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
require 'legion/extensions/mesh/transport/messages/preference_query'
|
|
32
|
+
|
|
33
|
+
RSpec.describe Legion::Extensions::Mesh::Transport::Messages::PreferenceQuery do
|
|
34
|
+
subject(:msg) do
|
|
35
|
+
described_class.new(
|
|
36
|
+
target_agent_id: 'agent-42',
|
|
37
|
+
requesting_agent_id: 'agent-1',
|
|
38
|
+
domains: %i[verbosity tone]
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe '#exchange' do
|
|
43
|
+
it 'returns the Agent exchange' do
|
|
44
|
+
expect(msg.exchange).to eq(Legion::Transport::Exchanges::Agent)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe '#routing_key' do
|
|
49
|
+
it 'routes to target agent' do
|
|
50
|
+
expect(msg.routing_key).to eq('agent.agent-42.preferences')
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe '#message' do
|
|
55
|
+
it 'includes type preference_query' do
|
|
56
|
+
expect(msg.message[:type]).to eq('preference_query')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'includes requesting_agent_id' do
|
|
60
|
+
expect(msg.message[:requesting_agent_id]).to eq('agent-1')
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'includes domains' do
|
|
64
|
+
expect(msg.message[:domains]).to eq(%i[verbosity tone])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'includes requested_at timestamp' do
|
|
68
|
+
expect(msg.message[:requested_at]).to be_a(String)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe '#type' do
|
|
73
|
+
it 'returns preference_query' do
|
|
74
|
+
expect(msg.type).to eq('preference_query')
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe 'empty domains default' do
|
|
79
|
+
subject(:msg_no_domains) { described_class.new(target_agent_id: 'x', requesting_agent_id: 'y') }
|
|
80
|
+
|
|
81
|
+
it 'defaults domains to empty array' do
|
|
82
|
+
expect(msg_no_domains.message[:domains]).to eq([])
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|