lex-joint-attention 0.1.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/Gemfile +11 -0
- data/lex-joint-attention.gemspec +31 -0
- data/lib/legion/extensions/joint_attention/actors/decay.rb +41 -0
- data/lib/legion/extensions/joint_attention/client.rb +24 -0
- data/lib/legion/extensions/joint_attention/helpers/attention_target.rb +120 -0
- data/lib/legion/extensions/joint_attention/helpers/constants.rb +30 -0
- data/lib/legion/extensions/joint_attention/helpers/joint_focus_manager.rb +153 -0
- data/lib/legion/extensions/joint_attention/runners/joint_attention.rb +84 -0
- data/lib/legion/extensions/joint_attention/version.rb +9 -0
- data/lib/legion/extensions/joint_attention.rb +17 -0
- data/spec/legion/extensions/joint_attention/client_spec.rb +36 -0
- data/spec/legion/extensions/joint_attention/helpers/attention_target_spec.rb +258 -0
- data/spec/legion/extensions/joint_attention/helpers/joint_focus_manager_spec.rb +238 -0
- data/spec/legion/extensions/joint_attention/runners/joint_attention_spec.rb +228 -0
- data/spec/spec_helper.rb +20 -0
- metadata +77 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::JointAttention::Helpers::AttentionTarget do
|
|
4
|
+
subject(:target) do
|
|
5
|
+
described_class.new(name: 'task-planning', domain: :collaboration, priority: 0.7, creator_agent_id: 'agent-1')
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
describe '#initialize' do
|
|
9
|
+
it 'assigns fields' do
|
|
10
|
+
expect(target.name).to eq('task-planning')
|
|
11
|
+
expect(target.domain).to eq(:collaboration)
|
|
12
|
+
expect(target.priority).to be_within(0.001).of(0.7)
|
|
13
|
+
expect(target.creator_agent_id).to eq('agent-1')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'assigns a uuid id' do
|
|
17
|
+
expect(target.id).to match(/\A[0-9a-f-]{36}\z/)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'assigns created_at timestamp' do
|
|
21
|
+
expect(target.created_at).to be_a(Time)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'initializes with empty attendees' do
|
|
25
|
+
expect(target.attendees).to be_empty
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'clamps priority to 0..1' do
|
|
29
|
+
t = described_class.new(name: 'x', domain: :d, priority: 2.0, creator_agent_id: 'a')
|
|
30
|
+
expect(t.priority).to eq(1.0)
|
|
31
|
+
|
|
32
|
+
t2 = described_class.new(name: 'x', domain: :d, priority: -0.5, creator_agent_id: 'a')
|
|
33
|
+
expect(t2.priority).to eq(0.0)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'sets default focus_strength' do
|
|
37
|
+
const = Legion::Extensions::JointAttention::Helpers::Constants::DEFAULT_FOCUS
|
|
38
|
+
expect(target.focus_strength).to eq(const)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe '#add_attendee' do
|
|
43
|
+
it 'adds a new attendee and returns :joined' do
|
|
44
|
+
result = target.add_attendee(agent_id: 'agent-2')
|
|
45
|
+
expect(result).to eq(:joined)
|
|
46
|
+
expect(target.attendees).to have_key('agent-2')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'returns :already_attending when agent is already present' do
|
|
50
|
+
target.add_attendee(agent_id: 'agent-2')
|
|
51
|
+
result = target.add_attendee(agent_id: 'agent-2')
|
|
52
|
+
expect(result).to eq(:already_attending)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'stores gaze direction when provided' do
|
|
56
|
+
target.add_attendee(agent_id: 'agent-2', gaze: :risk_assessment)
|
|
57
|
+
expect(target.attendees['agent-2'][:gaze]).to eq(:risk_assessment)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'sets mutual_awareness to false by default' do
|
|
61
|
+
target.add_attendee(agent_id: 'agent-2')
|
|
62
|
+
expect(target.attendees['agent-2'][:mutual_awareness]).to be false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'returns :at_capacity when MAX_ATTENDEES_PER_TARGET is reached' do
|
|
66
|
+
max = Legion::Extensions::JointAttention::Helpers::Constants::MAX_ATTENDEES_PER_TARGET
|
|
67
|
+
max.times { |i| target.add_attendee(agent_id: "agent-#{i + 100}") }
|
|
68
|
+
result = target.add_attendee(agent_id: 'overflow-agent')
|
|
69
|
+
expect(result).to eq(:at_capacity)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe '#remove_attendee' do
|
|
74
|
+
before { target.add_attendee(agent_id: 'agent-2') }
|
|
75
|
+
|
|
76
|
+
it 'removes attendee and returns :removed' do
|
|
77
|
+
result = target.remove_attendee(agent_id: 'agent-2')
|
|
78
|
+
expect(result).to eq(:removed)
|
|
79
|
+
expect(target.attendees).not_to have_key('agent-2')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'returns :not_found for unknown agent' do
|
|
83
|
+
expect(target.remove_attendee(agent_id: 'unknown')).to eq(:not_found)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
describe '#update_gaze' do
|
|
88
|
+
before { target.add_attendee(agent_id: 'agent-2') }
|
|
89
|
+
|
|
90
|
+
it 'updates gaze and returns :updated' do
|
|
91
|
+
result = target.update_gaze(agent_id: 'agent-2', gaze: :timeline)
|
|
92
|
+
expect(result).to eq(:updated)
|
|
93
|
+
expect(target.attendees['agent-2'][:gaze]).to eq(:timeline)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'returns :not_found for unknown agent' do
|
|
97
|
+
expect(target.update_gaze(agent_id: 'ghost', gaze: :something)).to eq(:not_found)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
describe '#boost_focus' do
|
|
102
|
+
before { target.add_attendee(agent_id: 'agent-2') }
|
|
103
|
+
|
|
104
|
+
it 'increases focus for the agent' do
|
|
105
|
+
before_val = target.attendees['agent-2'][:focus]
|
|
106
|
+
target.boost_focus(agent_id: 'agent-2', amount: 0.2)
|
|
107
|
+
expect(target.attendees['agent-2'][:focus]).to be > before_val
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'caps focus at 1.0' do
|
|
111
|
+
target.boost_focus(agent_id: 'agent-2', amount: 5.0)
|
|
112
|
+
expect(target.attendees['agent-2'][:focus]).to eq(1.0)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'returns :not_found for unknown agent' do
|
|
116
|
+
expect(target.boost_focus(agent_id: 'ghost', amount: 0.1)).to eq(:not_found)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
describe '#establish_mutual_awareness' do
|
|
121
|
+
before do
|
|
122
|
+
target.add_attendee(agent_id: 'agent-1')
|
|
123
|
+
target.add_attendee(agent_id: 'agent-2')
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'sets mutual_awareness for both agents and returns :established' do
|
|
127
|
+
result = target.establish_mutual_awareness(agent_a: 'agent-1', agent_b: 'agent-2')
|
|
128
|
+
expect(result).to eq(:established)
|
|
129
|
+
expect(target.attendees['agent-1'][:mutual_awareness]).to be true
|
|
130
|
+
expect(target.attendees['agent-2'][:mutual_awareness]).to be true
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'boosts focus for both agents' do
|
|
134
|
+
before_a = target.attendees['agent-1'][:focus]
|
|
135
|
+
before_b = target.attendees['agent-2'][:focus]
|
|
136
|
+
target.establish_mutual_awareness(agent_a: 'agent-1', agent_b: 'agent-2')
|
|
137
|
+
expect(target.attendees['agent-1'][:focus]).to be > before_a
|
|
138
|
+
expect(target.attendees['agent-2'][:focus]).to be > before_b
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it 'returns :not_found when one agent is missing' do
|
|
142
|
+
result = target.establish_mutual_awareness(agent_a: 'agent-1', agent_b: 'ghost')
|
|
143
|
+
expect(result).to eq(:not_found)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
describe '#attendee_count' do
|
|
148
|
+
it 'returns 0 initially' do
|
|
149
|
+
expect(target.attendee_count).to eq(0)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it 'increments when attendees join' do
|
|
153
|
+
target.add_attendee(agent_id: 'agent-2')
|
|
154
|
+
target.add_attendee(agent_id: 'agent-3')
|
|
155
|
+
expect(target.attendee_count).to eq(2)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
describe '#shared_awareness?' do
|
|
160
|
+
it 'returns false with no mutual awareness' do
|
|
161
|
+
target.add_attendee(agent_id: 'agent-2')
|
|
162
|
+
expect(target.shared_awareness?).to be false
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it 'returns true when two or more agents have mutual awareness' do
|
|
166
|
+
target.add_attendee(agent_id: 'agent-1')
|
|
167
|
+
target.add_attendee(agent_id: 'agent-2')
|
|
168
|
+
target.establish_mutual_awareness(agent_a: 'agent-1', agent_b: 'agent-2')
|
|
169
|
+
expect(target.shared_awareness?).to be true
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
describe '#decay' do
|
|
174
|
+
before { target.add_attendee(agent_id: 'agent-2') }
|
|
175
|
+
|
|
176
|
+
it 'reduces attendee focus' do
|
|
177
|
+
before_val = target.attendees['agent-2'][:focus]
|
|
178
|
+
target.decay
|
|
179
|
+
expect(target.attendees['agent-2'][:focus]).to be < before_val
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it 'does not drop focus below FOCUS_FLOOR' do
|
|
183
|
+
floor = Legion::Extensions::JointAttention::Helpers::Constants::FOCUS_FLOOR
|
|
184
|
+
200.times { target.decay }
|
|
185
|
+
expect(target.attendees['agent-2'][:focus]).to be >= floor
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it 'updates overall focus_strength based on attendees' do
|
|
189
|
+
original = target.focus_strength
|
|
190
|
+
200.times { target.decay }
|
|
191
|
+
expect(target.focus_strength).to be <= original
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
describe '#faded?' do
|
|
196
|
+
it 'returns false when focus is above floor or attendees present' do
|
|
197
|
+
expect(target.faded?).to be false
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it 'returns true when focus is at floor and no attendees' do
|
|
201
|
+
200.times { target.decay }
|
|
202
|
+
expect(target.faded?).to be true
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
describe '#prune_faded_attendees' do
|
|
207
|
+
it 'removes attendees whose focus is at or below FOCUS_FLOOR' do
|
|
208
|
+
target.add_attendee(agent_id: 'fading')
|
|
209
|
+
target.attendees['fading'][:focus] = Legion::Extensions::JointAttention::Helpers::Constants::FOCUS_FLOOR
|
|
210
|
+
target.prune_faded_attendees
|
|
211
|
+
expect(target.attendees).not_to have_key('fading')
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
it 'keeps attendees above floor' do
|
|
215
|
+
target.add_attendee(agent_id: 'active')
|
|
216
|
+
target.prune_faded_attendees
|
|
217
|
+
expect(target.attendees).to have_key('active')
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
describe '#focus_label' do
|
|
222
|
+
it 'returns :locked_on for high focus' do
|
|
223
|
+
target.instance_variable_set(:@focus_strength, 0.9)
|
|
224
|
+
expect(target.focus_label).to eq(:locked_on)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it 'returns :focused for 0.6..0.8 range' do
|
|
228
|
+
target.instance_variable_set(:@focus_strength, 0.7)
|
|
229
|
+
expect(target.focus_label).to eq(:focused)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
it 'returns :attending for 0.4..0.6 range' do
|
|
233
|
+
target.instance_variable_set(:@focus_strength, 0.5)
|
|
234
|
+
expect(target.focus_label).to eq(:attending)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
it 'returns :fading for very low focus' do
|
|
238
|
+
target.instance_variable_set(:@focus_strength, 0.1)
|
|
239
|
+
expect(target.focus_label).to eq(:fading)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
describe '#to_h' do
|
|
244
|
+
it 'returns a hash with all required fields' do
|
|
245
|
+
h = target.to_h
|
|
246
|
+
expect(h).to include(
|
|
247
|
+
:id, :name, :domain, :priority, :creator_agent_id,
|
|
248
|
+
:created_at, :focus_strength, :focus_label,
|
|
249
|
+
:attendee_count, :shared_awareness, :attendees
|
|
250
|
+
)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it 'rounds focus_strength to 4 decimal places' do
|
|
254
|
+
target.instance_variable_set(:@focus_strength, 0.123456789)
|
|
255
|
+
expect(target.to_h[:focus_strength]).to eq(0.1235)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::JointAttention::Helpers::JointFocusManager do
|
|
4
|
+
subject(:manager) { described_class.new }
|
|
5
|
+
|
|
6
|
+
let!(:target) do
|
|
7
|
+
manager.create_target(name: 'task-alpha', domain: :planning, priority: 0.8, creator: 'agent-1')
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
describe '#create_target' do
|
|
11
|
+
it 'creates a target and returns an AttentionTarget' do
|
|
12
|
+
expect(target).to be_a(Legion::Extensions::JointAttention::Helpers::AttentionTarget)
|
|
13
|
+
expect(target.name).to eq('task-alpha')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'adds the target to @targets' do
|
|
17
|
+
expect(manager.targets).to have_key(target.id)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'auto-joins the creator' do
|
|
21
|
+
expect(target.attendees).to have_key('agent-1')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'records history' do
|
|
25
|
+
expect(manager.history).not_to be_empty
|
|
26
|
+
expect(manager.history.last[:event]).to eq(:agent_joined)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'increments target_count' do
|
|
30
|
+
expect(manager.target_count).to eq(1)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe '#join_target' do
|
|
35
|
+
it 'adds a second agent to the target' do
|
|
36
|
+
result = manager.join_target(target_id: target.id, agent_id: 'agent-2')
|
|
37
|
+
expect(result).to eq(:joined)
|
|
38
|
+
expect(target.attendees).to have_key('agent-2')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'returns :target_not_found for unknown target' do
|
|
42
|
+
result = manager.join_target(target_id: 'bogus', agent_id: 'agent-2')
|
|
43
|
+
expect(result).to eq(:target_not_found)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'returns :already_attending when agent already there' do
|
|
47
|
+
manager.join_target(target_id: target.id, agent_id: 'agent-2')
|
|
48
|
+
result = manager.join_target(target_id: target.id, agent_id: 'agent-2')
|
|
49
|
+
expect(result).to eq(:already_attending)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'enforces MAX_SIMULTANEOUS_TARGETS per agent' do
|
|
53
|
+
max = Legion::Extensions::JointAttention::Helpers::Constants::MAX_SIMULTANEOUS_TARGETS
|
|
54
|
+
max.times do |i|
|
|
55
|
+
t = manager.create_target(name: "agent-x-target-#{i}", domain: :d, priority: 0.5, creator: 'other')
|
|
56
|
+
manager.join_target(target_id: t.id, agent_id: 'agent-x')
|
|
57
|
+
end
|
|
58
|
+
extra = manager.create_target(name: 'overflow', domain: :d, priority: 0.5, creator: 'other2')
|
|
59
|
+
result = manager.join_target(target_id: extra.id, agent_id: 'agent-x')
|
|
60
|
+
expect(result).to eq(:capacity_exceeded)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'records join history' do
|
|
64
|
+
manager.join_target(target_id: target.id, agent_id: 'agent-3')
|
|
65
|
+
events = manager.history.map { |h| h[:event] }
|
|
66
|
+
expect(events).to include(:agent_joined)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe '#leave_target' do
|
|
71
|
+
before { manager.join_target(target_id: target.id, agent_id: 'agent-2') }
|
|
72
|
+
|
|
73
|
+
it 'removes the agent and returns :removed' do
|
|
74
|
+
result = manager.leave_target(target_id: target.id, agent_id: 'agent-2')
|
|
75
|
+
expect(result).to eq(:removed)
|
|
76
|
+
expect(target.attendees).not_to have_key('agent-2')
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'returns :target_not_found for unknown target' do
|
|
80
|
+
result = manager.leave_target(target_id: 'bogus', agent_id: 'agent-2')
|
|
81
|
+
expect(result).to eq(:target_not_found)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'returns :not_found when agent not attending' do
|
|
85
|
+
result = manager.leave_target(target_id: target.id, agent_id: 'never-joined')
|
|
86
|
+
expect(result).to eq(:not_found)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'records leave history' do
|
|
90
|
+
manager.leave_target(target_id: target.id, agent_id: 'agent-2')
|
|
91
|
+
events = manager.history.map { |h| h[:event] }
|
|
92
|
+
expect(events).to include(:agent_left)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
describe '#direct_attention' do
|
|
97
|
+
before { manager.join_target(target_id: target.id, agent_id: 'agent-1') }
|
|
98
|
+
|
|
99
|
+
it 'joins recipient to target and returns :directed' do
|
|
100
|
+
result = manager.direct_attention(from_agent: 'agent-1', to_agent: 'agent-2', target_id: target.id)
|
|
101
|
+
expect(result).to eq(:directed)
|
|
102
|
+
expect(target.attendees).to have_key('agent-2')
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it 'returns :target_not_found for unknown target' do
|
|
106
|
+
result = manager.direct_attention(from_agent: 'agent-1', to_agent: 'agent-2', target_id: 'bogus')
|
|
107
|
+
expect(result).to eq(:target_not_found)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'returns :referrer_not_attending when from_agent is not on target' do
|
|
111
|
+
result = manager.direct_attention(from_agent: 'nobody', to_agent: 'agent-2', target_id: target.id)
|
|
112
|
+
expect(result).to eq(:referrer_not_attending)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'boosts focus of recipient' do
|
|
116
|
+
manager.direct_attention(from_agent: 'agent-1', to_agent: 'agent-2', target_id: target.id)
|
|
117
|
+
const = Legion::Extensions::JointAttention::Helpers::Constants
|
|
118
|
+
expected_min = const::DEFAULT_FOCUS + const::REFERRAL_BOOST
|
|
119
|
+
expect(target.attendees['agent-2'][:focus]).to be >= expected_min
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it 'records direction history' do
|
|
123
|
+
manager.direct_attention(from_agent: 'agent-1', to_agent: 'agent-2', target_id: target.id)
|
|
124
|
+
events = manager.history.map { |h| h[:event] }
|
|
125
|
+
expect(events).to include(:attention_directed)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
describe '#establish_shared' do
|
|
130
|
+
before do
|
|
131
|
+
manager.join_target(target_id: target.id, agent_id: 'agent-1')
|
|
132
|
+
manager.join_target(target_id: target.id, agent_id: 'agent-2')
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it 'establishes mutual awareness and returns :established' do
|
|
136
|
+
result = manager.establish_shared(target_id: target.id, agent_a: 'agent-1', agent_b: 'agent-2')
|
|
137
|
+
expect(result).to eq(:established)
|
|
138
|
+
expect(target.attendees['agent-1'][:mutual_awareness]).to be true
|
|
139
|
+
expect(target.attendees['agent-2'][:mutual_awareness]).to be true
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it 'returns :target_not_found for unknown target' do
|
|
143
|
+
result = manager.establish_shared(target_id: 'bogus', agent_a: 'agent-1', agent_b: 'agent-2')
|
|
144
|
+
expect(result).to eq(:target_not_found)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it 'records shared_awareness history event' do
|
|
148
|
+
manager.establish_shared(target_id: target.id, agent_a: 'agent-1', agent_b: 'agent-2')
|
|
149
|
+
events = manager.history.map { |h| h[:event] }
|
|
150
|
+
expect(events).to include(:shared_awareness)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
describe '#update_gaze' do
|
|
155
|
+
before { manager.join_target(target_id: target.id, agent_id: 'agent-1') }
|
|
156
|
+
|
|
157
|
+
it 'updates gaze direction' do
|
|
158
|
+
result = manager.update_gaze(target_id: target.id, agent_id: 'agent-1', gaze: :timeline)
|
|
159
|
+
expect(result).to eq(:updated)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it 'returns :target_not_found for unknown target' do
|
|
163
|
+
result = manager.update_gaze(target_id: 'bogus', agent_id: 'agent-1', gaze: :x)
|
|
164
|
+
expect(result).to eq(:target_not_found)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
describe '#targets_for_agent' do
|
|
169
|
+
it 'returns targets the agent is attending' do
|
|
170
|
+
targets = manager.targets_for_agent(agent_id: 'agent-1')
|
|
171
|
+
expect(targets).to include(target)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it 'returns empty array for unknown agent' do
|
|
175
|
+
expect(manager.targets_for_agent(agent_id: 'nobody')).to eq([])
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
describe '#attendees_for_target' do
|
|
180
|
+
before { manager.join_target(target_id: target.id, agent_id: 'agent-2') }
|
|
181
|
+
|
|
182
|
+
it 'returns attendee ids for target' do
|
|
183
|
+
ids = manager.attendees_for_target(target_id: target.id)
|
|
184
|
+
expect(ids).to include('agent-1', 'agent-2')
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it 'returns empty array for unknown target' do
|
|
188
|
+
expect(manager.attendees_for_target(target_id: 'bogus')).to eq([])
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
describe '#shared_targets' do
|
|
193
|
+
it 'returns targets attended by both agents' do
|
|
194
|
+
manager.join_target(target_id: target.id, agent_id: 'agent-2')
|
|
195
|
+
shared = manager.shared_targets(agent_a: 'agent-1', agent_b: 'agent-2')
|
|
196
|
+
expect(shared).to include(target)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it 'returns empty array when no shared targets' do
|
|
200
|
+
shared = manager.shared_targets(agent_a: 'agent-1', agent_b: 'loner')
|
|
201
|
+
expect(shared).to be_empty
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
describe '#decay_all' do
|
|
206
|
+
it 'decays focus on all targets' do
|
|
207
|
+
focus_before = target.focus_strength
|
|
208
|
+
manager.decay_all
|
|
209
|
+
expect(target.focus_strength).to be <= focus_before
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it 'prunes faded targets' do
|
|
213
|
+
t = manager.create_target(name: 'doomed', domain: :temp, priority: 0.1, creator: 'agent-x')
|
|
214
|
+
# Drain all attendees so target can become faded
|
|
215
|
+
manager.leave_target(target_id: t.id, agent_id: 'agent-x')
|
|
216
|
+
200.times { manager.decay_all }
|
|
217
|
+
expect(manager.targets).not_to have_key(t.id)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
describe '#target_count' do
|
|
222
|
+
it 'returns number of active targets' do
|
|
223
|
+
manager.create_target(name: 'b', domain: :d, priority: 0.5, creator: 'a')
|
|
224
|
+
expect(manager.target_count).to eq(2)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
describe '#to_h' do
|
|
229
|
+
it 'returns a summary hash' do
|
|
230
|
+
h = manager.to_h
|
|
231
|
+
expect(h).to include(:target_count, :agent_count, :history_size, :targets)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
it 'reflects correct target count' do
|
|
235
|
+
expect(manager.to_h[:target_count]).to eq(1)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|