lex-mesh 0.2.4 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 25cafa977ed6aaf671c903e71895312253a412c32c141e8de9674b1a86747bff
4
- data.tar.gz: c7bbdde6f4c065f0fdaea58a5a5124bd53e49f0c48e0d275de0828bd93ec2439
3
+ metadata.gz: d736de5644beff024c6acc40ededd1eface48a64dc919b4f1033e0971a7af71f
4
+ data.tar.gz: e58b0661aab721ebaf1c7f413691a41f2a5f0f1e455710b3d27f79ad687655fb
5
5
  SHA512:
6
- metadata.gz: 76357a6ca4c31089fd7f5317097f935b07df6349476b344bd2681dccfbd796d79b1061922b22d42ca45b92d045bdbf901ed8d6c502f525cff3a668099dfea56c
7
- data.tar.gz: 9e17a698c0b941fd2000ab9affffa87cd3bad983b37acacce794ee0fb4d975a8cdc04eee44480f976856e712fb6af8dfa80f976d07c2fd94ea302ba61f31b389
6
+ metadata.gz: d1abfff1fe01a29d3b9be8ee75c5f949f753a9fdf33df340e1362ef758236fb411a826eb0ae3c448bc330b8bc06cff725f35d10b5767f39ca7a8ffcb06969782
7
+ data.tar.gz: e1266fc12dd23f183439f2ed5c1d2a21a1c11b824652e918b6fc398df40eea46cc5aa0dbabbb14e99b8f41aa4b0cd2e99d71a259aaba8de595088c51336c7a62
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/actors/every'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Mesh
8
+ module Actor
9
+ class Gossip < Legion::Extensions::Actors::Every
10
+ def runner_class
11
+ Legion::Extensions::Mesh::Runners::Mesh
12
+ end
13
+
14
+ def runner_function
15
+ :publish_gossip
16
+ end
17
+
18
+ def time
19
+ 15
20
+ end
21
+
22
+ def use_runner?
23
+ true
24
+ end
25
+
26
+ def check_subtask?
27
+ false
28
+ end
29
+
30
+ def generate_task?
31
+ false
32
+ end
33
+
34
+ def args
35
+ {}
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'legion/extensions/mesh/runners/mesh'
4
4
  require 'legion/extensions/mesh/runners/preferences'
5
+ require 'legion/extensions/mesh/runners/delegation'
6
+ require 'legion/extensions/mesh/runners/task_request'
5
7
  require 'legion/extensions/mesh/helpers/preference_profile'
6
8
  require 'legion/extensions/mesh/helpers/pending_requests'
7
9
  require 'legion/extensions/mesh/helpers/topology'
@@ -13,6 +15,8 @@ module Legion
13
15
  class Client
14
16
  include Runners::Mesh
15
17
  include Runners::Preferences
18
+ include Runners::Delegation
19
+ include Runners::TaskRequest
16
20
 
17
21
  def initialize(**opts)
18
22
  @opts = opts
@@ -13,11 +13,14 @@ module Legion
13
13
  @messages = []
14
14
  end
15
15
 
16
- def register_agent(agent_id, capabilities: [], endpoint: nil)
16
+ def register_agent(agent_id, capabilities: [], endpoint: nil, source: :native, node: nil)
17
17
  @agents[agent_id] = {
18
18
  agent_id: agent_id,
19
19
  capabilities: capabilities,
20
20
  endpoint: endpoint,
21
+ source: source,
22
+ node: node || local_node_name,
23
+ generation: 1,
21
24
  registered_at: Time.now.utc,
22
25
  last_seen: Time.now.utc,
23
26
  status: :online
@@ -39,6 +42,7 @@ module Legion
39
42
 
40
43
  agent[:last_seen] = Time.now.utc
41
44
  agent[:status] = :online
45
+ agent[:generation] = (agent[:generation] || 0) + 1
42
46
  end
43
47
 
44
48
  def find_by_capability(capability)
@@ -81,9 +85,21 @@ module Legion
81
85
  @agents.values.select { |a| a[:status] == :online }
82
86
  end
83
87
 
88
+ def all_agents
89
+ @agents.values
90
+ end
91
+
84
92
  def count
85
93
  @agents.size
86
94
  end
95
+
96
+ private
97
+
98
+ def local_node_name
99
+ Legion::Settings[:client][:name]
100
+ rescue StandardError
101
+ 'unknown'
102
+ end
87
103
  end
88
104
  end
89
105
  end
@@ -64,6 +64,50 @@ module Legion
64
64
  { success: true, expired: expired, count: expired.size }
65
65
  end
66
66
 
67
+ def publish_gossip(**)
68
+ registry = mesh_registry
69
+ peers = registry.all_agents.first(gossip_max_peers).map do |agent|
70
+ agent.slice(:agent_id, :capabilities, :node, :source, :status, :generation,
71
+ :last_seen, :registered_at).transform_values { |v| v.is_a?(Time) ? v.to_s : v }
72
+ end
73
+
74
+ @gossip_round = (@gossip_round || 0) + 1
75
+ publish_gossip_message(peers)
76
+ { success: true, peers_broadcast: peers.size, gossip_round: @gossip_round }
77
+ rescue StandardError => e
78
+ { success: false, reason: :error, message: e.message }
79
+ end
80
+
81
+ def merge_gossip(incoming_peers:, sender: nil, **) # rubocop:disable Lint/UnusedMethodArgument
82
+ registry = mesh_registry
83
+ merged = 0
84
+
85
+ incoming_peers.each do |peer|
86
+ peer = peer.transform_keys(&:to_sym)
87
+ next if peer[:node] == local_node_name
88
+
89
+ local = registry.agents[peer[:agent_id]]
90
+ if local.nil?
91
+ registry.register_agent(
92
+ peer[:agent_id],
93
+ capabilities: (peer[:capabilities] || []).map(&:to_sym),
94
+ source: (peer[:source] || :native).to_sym,
95
+ node: peer[:node]
96
+ )
97
+ registry.agents[peer[:agent_id]][:generation] = peer[:generation] || 1
98
+ merged += 1
99
+ elsif (peer[:generation] || 0) > (local[:generation] || 0)
100
+ local.merge!(peer.slice(:capabilities, :status, :generation, :last_seen))
101
+ local[:capabilities] = (local[:capabilities] || []).map(&:to_sym)
102
+ merged += 1
103
+ end
104
+ end
105
+
106
+ { success: true, merged: merged, total_peers: incoming_peers.size }
107
+ rescue StandardError => e
108
+ { success: false, reason: :error, message: e.message }
109
+ end
110
+
67
111
  private
68
112
 
69
113
  def publish_mesh_departure(agent_id:, capabilities:)
@@ -77,6 +121,29 @@ module Legion
77
121
  Legion::Logging.warn "[mesh] failed to publish departure signal: #{e.message}"
78
122
  end
79
123
 
124
+ def publish_gossip_message(peers)
125
+ return unless defined?(Legion::Extensions::Mesh::Transport::Messages::Gossip)
126
+
127
+ Legion::Extensions::Mesh::Transport::Messages::Gossip.new(
128
+ sender: local_node_name,
129
+ gossip_round: @gossip_round,
130
+ peers: peers
131
+ ).publish
132
+ end
133
+
134
+ def gossip_max_peers
135
+ settings = Legion::Settings.dig(:mesh, :gossip)
136
+ (settings.is_a?(Hash) ? settings[:max_peers_per_message] : nil) || 100
137
+ rescue StandardError
138
+ 100
139
+ end
140
+
141
+ def local_node_name
142
+ Legion::Settings[:client][:name]
143
+ rescue StandardError
144
+ 'unknown'
145
+ end
146
+
80
147
  def mesh_registry
81
148
  @mesh_registry ||= Helpers::Registry.new
82
149
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require_relative '../helpers/pending_requests'
5
+ require_relative '../helpers/delegation'
6
+ require_relative '../helpers/registry'
7
+ require_relative 'mesh'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module Mesh
12
+ module Runners
13
+ module TaskRequest
14
+ include Runners::Mesh
15
+
16
+ DEFAULT_TIMEOUT = 30
17
+
18
+ def request_task(from:, to:, task:, payload:, timeout: DEFAULT_TIMEOUT, consent_level: :execute, **) # rubocop:disable Metrics/ParameterLists
19
+ target_id = resolve_target(to)
20
+ return { success: false, reason: :no_agent_found, requested: to } unless target_id
21
+
22
+ correlation_id = "task-#{SecureRandom.uuid}"
23
+
24
+ delegation = delegation_tracker.create(
25
+ from: from, to: target_id,
26
+ task_context: task, consent_level: consent_level
27
+ )
28
+ return { success: false, reason: delegation[:error] } if delegation[:error]
29
+
30
+ task_pending.register(
31
+ correlation_id: correlation_id,
32
+ callback: nil,
33
+ ttl: timeout
34
+ )
35
+
36
+ send_message(from: from, to: target_id, pattern: :unicast,
37
+ payload: { type: :task_request, correlation_id: correlation_id,
38
+ task: task, payload: payload, reply_to: from })
39
+
40
+ Legion::Logging.info "[mesh-task] request: from=#{from} to=#{target_id} task=#{task} cid=#{correlation_id[0..11]}"
41
+ { success: true, correlation_id: correlation_id, delegation_id: delegation[:delegation_id],
42
+ target_agent: target_id }
43
+ end
44
+
45
+ def handle_task_reply(correlation_id:, result:, **)
46
+ resolved = task_pending.resolve(correlation_id: correlation_id, result: result)
47
+ if resolved
48
+ Legion::Logging.debug "[mesh-task] reply resolved: cid=#{correlation_id[0..11]}"
49
+ { success: true, resolved: true, correlation_id: correlation_id }
50
+ else
51
+ { success: false, reason: :not_found, correlation_id: correlation_id }
52
+ end
53
+ end
54
+
55
+ def pending_task_stats(**)
56
+ { success: true, pending_count: task_pending.pending_count }
57
+ end
58
+
59
+ def expire_pending_tasks(**)
60
+ expired = task_pending.expire
61
+ Legion::Logging.debug "[mesh-task] expired #{expired.size} pending tasks" unless expired.empty?
62
+ { success: true, expired_count: expired.size, expired_ids: expired }
63
+ end
64
+
65
+ private
66
+
67
+ def resolve_target(to)
68
+ return to if mesh_registry.agents.key?(to)
69
+
70
+ agents = mesh_registry.find_by_capability(to.to_sym)
71
+ return nil if agents.empty?
72
+
73
+ online = agents.select { |a| a[:status] == :online }
74
+ (online.empty? ? agents : online).sample[:agent_id]
75
+ end
76
+
77
+ def task_pending
78
+ @task_pending ||= Helpers::PendingRequests.new(default_ttl: DEFAULT_TIMEOUT)
79
+ end
80
+
81
+ def delegation_tracker
82
+ @delegation_tracker ||= Helpers::Delegation.new
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mesh
6
+ module Transport
7
+ module Messages
8
+ class Gossip < Legion::Transport::Message
9
+ def exchange
10
+ Legion::Transport::Exchanges::Node
11
+ end
12
+
13
+ def routing_key
14
+ 'mesh.gossip'
15
+ end
16
+
17
+ def message
18
+ {
19
+ type: 'mesh_gossip',
20
+ sender: @options[:sender],
21
+ gossip_round: @options[:gossip_round] || 0,
22
+ peers: @options[:peers] || []
23
+ }
24
+ end
25
+
26
+ def type
27
+ 'mesh_gossip'
28
+ end
29
+
30
+ def encrypt?
31
+ false
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mesh
6
+ module Transport
7
+ module Queues
8
+ class Gossip < Legion::Transport::Queue
9
+ def queue_name
10
+ 'mesh.gossip'
11
+ end
12
+
13
+ def exchange
14
+ Legion::Transport::Exchanges::Node
15
+ end
16
+
17
+ def routing_key
18
+ 'mesh.gossip'
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Mesh
6
- VERSION = '0.2.4'
6
+ VERSION = '0.3.0'
7
7
  end
8
8
  end
9
9
  end
@@ -10,6 +10,7 @@ require 'legion/extensions/mesh/helpers/peer_verify'
10
10
  require 'legion/extensions/mesh/runners/mesh'
11
11
  require 'legion/extensions/mesh/runners/preferences'
12
12
  require 'legion/extensions/mesh/runners/delegation'
13
+ require 'legion/extensions/mesh/runners/task_request'
13
14
 
14
15
  if defined?(Legion::Transport)
15
16
  require 'legion/extensions/mesh/transport/messages/preference_query'
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Stub the framework actor base class since legionio gem is not available in test
4
+ unless defined?(Legion::Extensions::Actors::Every)
5
+ module Legion
6
+ module Extensions
7
+ module Actors
8
+ class Every # rubocop:disable Lint/EmptyClass
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+
15
+ $LOADED_FEATURES << 'legion/extensions/actors/every' unless $LOADED_FEATURES.include?('legion/extensions/actors/every')
16
+
17
+ require 'legion/extensions/mesh/actors/gossip'
18
+
19
+ RSpec.describe Legion::Extensions::Mesh::Actor::Gossip do
20
+ it 'fires every 15 seconds' do
21
+ expect(described_class.new.time).to eq(15)
22
+ end
23
+
24
+ it 'calls publish_gossip runner function' do
25
+ instance = described_class.allocate
26
+ expect(instance.runner_function).to eq(:publish_gossip)
27
+ end
28
+ end
@@ -51,4 +51,26 @@ RSpec.describe Legion::Extensions::Mesh::Client do
51
51
  expect(result[:expired]).to eq(0)
52
52
  end
53
53
  end
54
+
55
+ it 'responds to delegation runner methods' do
56
+ expect(client).to respond_to(:delegate)
57
+ expect(client).to respond_to(:complete_delegation)
58
+ expect(client).to respond_to(:revoke_delegation)
59
+ end
60
+
61
+ it 'responds to task request runner methods' do
62
+ expect(client).to respond_to(:request_task)
63
+ expect(client).to respond_to(:handle_task_reply)
64
+ expect(client).to respond_to(:pending_task_stats)
65
+ end
66
+
67
+ describe '#request_task' do
68
+ it 'sends a task request via the client' do
69
+ client.register(agent_id: 'a', capabilities: [:orchestration])
70
+ client.register(agent_id: 'b', capabilities: [:review])
71
+ result = client.request_task(from: 'a', to: 'b', task: 'review', payload: {})
72
+ expect(result[:success]).to be true
73
+ expect(result[:target_agent]).to eq('b')
74
+ end
75
+ end
54
76
  end
@@ -313,6 +313,32 @@ RSpec.describe Legion::Extensions::Mesh::Helpers::Registry do
313
313
  end
314
314
  end
315
315
 
316
+ describe 'gossip fields' do
317
+ it 'stores source, node, and generation on registration' do
318
+ registry.register_agent('agent-1', capabilities: [:test], source: :native, node: 'node-01')
319
+ agent = registry.agents['agent-1']
320
+ expect(agent[:source]).to eq(:native)
321
+ expect(agent[:node]).to eq('node-01')
322
+ expect(agent[:generation]).to eq(1)
323
+ end
324
+
325
+ it 'increments generation on heartbeat' do
326
+ registry.register_agent('agent-1', capabilities: [:test])
327
+ registry.heartbeat('agent-1')
328
+ expect(registry.agents['agent-1'][:generation]).to eq(2)
329
+ end
330
+ end
331
+
332
+ describe '#all_agents' do
333
+ it 'returns all agent records as an array' do
334
+ registry.register_agent('a1', capabilities: [:search])
335
+ registry.register_agent('a2', capabilities: [:compute])
336
+ all = registry.all_agents
337
+ expect(all).to be_an(Array)
338
+ expect(all.size).to eq(2)
339
+ end
340
+ end
341
+
316
342
  describe '#count' do
317
343
  it 'returns 0 for an empty registry' do
318
344
  expect(registry.count).to eq(0)
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe 'Gossip runner methods' do
6
+ subject { Object.new.extend(Legion::Extensions::Mesh::Runners::Mesh) }
7
+
8
+ before do
9
+ allow(subject).to receive(:mesh_registry).and_return(
10
+ Legion::Extensions::Mesh::Helpers::Registry.new
11
+ )
12
+ end
13
+
14
+ describe '#merge_gossip' do
15
+ it 'adds unknown agents from incoming gossip' do
16
+ incoming = [
17
+ { agent_id: 'remote-1', capabilities: [:test], node: 'node-02',
18
+ source: :native, status: :online, generation: 5,
19
+ last_seen: Time.now.utc.to_s, registered_at: Time.now.utc.to_s }
20
+ ]
21
+ result = subject.merge_gossip(incoming_peers: incoming, sender: 'node-02')
22
+ expect(result[:success]).to be true
23
+ expect(result[:merged]).to eq(1)
24
+ end
25
+
26
+ it 'updates agents with higher generation' do
27
+ registry = subject.send(:mesh_registry)
28
+ registry.register_agent('agent-1', capabilities: [:old], node: 'node-01')
29
+
30
+ incoming = [
31
+ { agent_id: 'agent-1', capabilities: [:updated], node: 'node-01',
32
+ source: :native, status: :online, generation: 99,
33
+ last_seen: Time.now.utc.to_s, registered_at: Time.now.utc.to_s }
34
+ ]
35
+ result = subject.merge_gossip(incoming_peers: incoming, sender: 'node-01')
36
+ expect(result[:merged]).to eq(1)
37
+ expect(registry.agents['agent-1'][:generation]).to eq(99)
38
+ end
39
+
40
+ it 'skips agents from local node' do
41
+ local_name = 'local-node'
42
+ allow(subject).to receive(:local_node_name).and_return(local_name)
43
+
44
+ incoming = [
45
+ { agent_id: 'local-agent', node: local_name, generation: 1 }
46
+ ]
47
+ result = subject.merge_gossip(incoming_peers: incoming, sender: 'other-node')
48
+ expect(result[:merged]).to eq(0)
49
+ end
50
+
51
+ it 'ignores agents with lower or equal generation' do
52
+ registry = subject.send(:mesh_registry)
53
+ registry.register_agent('agent-1', capabilities: [:test], node: 'node-01')
54
+ # generation is 1 after register
55
+
56
+ incoming = [
57
+ { agent_id: 'agent-1', capabilities: [:test], node: 'node-01',
58
+ generation: 1, source: :native, status: :online }
59
+ ]
60
+ result = subject.merge_gossip(incoming_peers: incoming, sender: 'node-02')
61
+ expect(result[:merged]).to eq(0)
62
+ end
63
+ end
64
+
65
+ describe '#publish_gossip' do
66
+ it 'returns success with peer count' do
67
+ allow(subject).to receive(:publish_gossip_message)
68
+ result = subject.publish_gossip
69
+ expect(result[:success]).to be true
70
+ expect(result[:peers_broadcast]).to be_a(Integer)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'legion/extensions/mesh/runners/task_request'
5
+
6
+ RSpec.describe Legion::Extensions::Mesh::Runners::TaskRequest do
7
+ subject(:runner) { Object.new.extend(described_class) }
8
+
9
+ before do
10
+ runner.instance_variable_set(:@mesh_registry, nil)
11
+ runner.instance_variable_set(:@task_pending, nil)
12
+ runner.instance_variable_set(:@delegation_tracker, nil)
13
+ end
14
+
15
+ describe '#request_task' do
16
+ before do
17
+ runner.register(agent_id: 'requester', capabilities: [:orchestration])
18
+ runner.register(agent_id: 'worker-1', capabilities: [:code_review])
19
+ runner.register(agent_id: 'worker-2', capabilities: %i[code_review testing])
20
+ end
21
+
22
+ it 'sends a task request to a specific agent by ID' do
23
+ result = runner.request_task(from: 'requester', to: 'worker-1',
24
+ task: 'review_code', payload: { file: 'app.rb' })
25
+ expect(result[:success]).to be true
26
+ expect(result[:correlation_id]).to be_a(String)
27
+ expect(result[:delegation_id]).to start_with('del-')
28
+ expect(result[:target_agent]).to eq('worker-1')
29
+ end
30
+
31
+ it 'routes to an agent by capability when to: is not a registered agent' do
32
+ result = runner.request_task(from: 'requester', to: 'code_review',
33
+ task: 'review_code', payload: {})
34
+ expect(result[:success]).to be true
35
+ expect(%w[worker-1 worker-2]).to include(result[:target_agent])
36
+ end
37
+
38
+ it 'returns error when no agent found for capability' do
39
+ result = runner.request_task(from: 'requester', to: 'nonexistent_capability',
40
+ task: 'do_thing', payload: {})
41
+ expect(result[:success]).to be false
42
+ expect(result[:reason]).to eq(:no_agent_found)
43
+ end
44
+
45
+ it 'registers a pending request' do
46
+ result = runner.request_task(from: 'requester', to: 'worker-1',
47
+ task: 'review', payload: {}, timeout: 60)
48
+ expect(runner.send(:task_pending).pending?(result[:correlation_id])).to be true
49
+ end
50
+ end
51
+
52
+ describe '#handle_task_reply' do
53
+ it 'resolves a pending request' do
54
+ runner.register(agent_id: 'req', capabilities: [])
55
+ runner.register(agent_id: 'wrk', capabilities: [:work])
56
+
57
+ req = runner.request_task(from: 'req', to: 'wrk', task: 'test', payload: {})
58
+ result = runner.handle_task_reply(
59
+ correlation_id: req[:correlation_id],
60
+ result: { status: :completed, output: 'done' }
61
+ )
62
+ expect(result[:success]).to be true
63
+ expect(result[:resolved]).to be true
64
+ end
65
+
66
+ it 'returns not_found for unknown correlation_id' do
67
+ result = runner.handle_task_reply(correlation_id: 'unknown', result: {})
68
+ expect(result[:success]).to be false
69
+ expect(result[:reason]).to eq(:not_found)
70
+ end
71
+ end
72
+
73
+ describe '#pending_task_stats' do
74
+ it 'returns count of pending requests' do
75
+ runner.register(agent_id: 'a', capabilities: [])
76
+ runner.register(agent_id: 'b', capabilities: [:work])
77
+ runner.request_task(from: 'a', to: 'b', task: 't', payload: {})
78
+ stats = runner.pending_task_stats
79
+ expect(stats[:success]).to be true
80
+ expect(stats[:pending_count]).to be >= 1
81
+ end
82
+ end
83
+
84
+ describe '#expire_pending_tasks' do
85
+ it 'expires timed-out requests' do
86
+ result = runner.expire_pending_tasks
87
+ expect(result[:success]).to be true
88
+ expect(result[:expired_count]).to eq(0)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,54 @@
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/gossip'
29
+
30
+ RSpec.describe Legion::Extensions::Mesh::Transport::Messages::Gossip do
31
+ subject do
32
+ described_class.new(
33
+ sender: 'node-01',
34
+ gossip_round: 5,
35
+ peers: [{ agent_id: 'w1', capabilities: [:test], node: 'node-01', generation: 3 }]
36
+ )
37
+ end
38
+
39
+ it 'uses the node exchange' do
40
+ expect(subject.exchange).to eq(Legion::Transport::Exchanges::Node)
41
+ end
42
+
43
+ it 'routes to mesh.gossip' do
44
+ expect(subject.routing_key).to eq('mesh.gossip')
45
+ end
46
+
47
+ it 'includes sender and peers in the message body' do
48
+ msg = subject.message
49
+ expect(msg[:type]).to eq('mesh_gossip')
50
+ expect(msg[:sender]).to eq('node-01')
51
+ expect(msg[:peers]).to be_an(Array)
52
+ expect(msg[:gossip_round]).to eq(5)
53
+ end
54
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-mesh
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -48,6 +48,7 @@ files:
48
48
  - Gemfile
49
49
  - lex-mesh.gemspec
50
50
  - lib/legion/extensions/mesh.rb
51
+ - lib/legion/extensions/mesh/actors/gossip.rb
51
52
  - lib/legion/extensions/mesh/actors/heartbeat.rb
52
53
  - lib/legion/extensions/mesh/actors/pending_expiry.rb
53
54
  - lib/legion/extensions/mesh/actors/preference_listener.rb
@@ -62,11 +63,15 @@ files:
62
63
  - lib/legion/extensions/mesh/runners/delegation.rb
63
64
  - lib/legion/extensions/mesh/runners/mesh.rb
64
65
  - lib/legion/extensions/mesh/runners/preferences.rb
66
+ - lib/legion/extensions/mesh/runners/task_request.rb
67
+ - lib/legion/extensions/mesh/transport/messages/gossip.rb
65
68
  - lib/legion/extensions/mesh/transport/messages/mesh_departure.rb
66
69
  - lib/legion/extensions/mesh/transport/messages/preference_query.rb
67
70
  - lib/legion/extensions/mesh/transport/messages/preference_response.rb
71
+ - lib/legion/extensions/mesh/transport/queues/gossip.rb
68
72
  - lib/legion/extensions/mesh/transport/queues/preference.rb
69
73
  - lib/legion/extensions/mesh/version.rb
74
+ - spec/legion/extensions/mesh/actors/gossip_spec.rb
70
75
  - spec/legion/extensions/mesh/actors/heartbeat_spec.rb
71
76
  - spec/legion/extensions/mesh/actors/pending_expiry_spec.rb
72
77
  - spec/legion/extensions/mesh/actors/preference_listener_spec.rb
@@ -79,8 +84,11 @@ files:
79
84
  - spec/legion/extensions/mesh/helpers/registry_spec.rb
80
85
  - spec/legion/extensions/mesh/helpers/topology_spec.rb
81
86
  - spec/legion/extensions/mesh/runners/delegation_spec.rb
87
+ - spec/legion/extensions/mesh/runners/mesh_gossip_spec.rb
82
88
  - spec/legion/extensions/mesh/runners/mesh_spec.rb
83
89
  - spec/legion/extensions/mesh/runners/preferences_spec.rb
90
+ - spec/legion/extensions/mesh/runners/task_request_spec.rb
91
+ - spec/legion/extensions/mesh/transport/messages/gossip_spec.rb
84
92
  - spec/legion/extensions/mesh/transport/messages/mesh_departure_spec.rb
85
93
  - spec/legion/extensions/mesh/transport/messages/preference_query_spec.rb
86
94
  - spec/legion/extensions/mesh/transport/messages/preference_response_spec.rb