|
1
|
"""Tests for enhanced action item detection.""" |
|
2
|
|
|
3
|
import json |
|
4
|
from unittest.mock import MagicMock |
|
5
|
|
|
6
|
from video_processor.analyzers.action_detector import ActionDetector |
|
7
|
from video_processor.models import ActionItem, TranscriptSegment |
|
8
|
|
|
9
|
|
|
10
|
class TestPatternExtract: |
|
11
|
def test_detects_need_to(self): |
|
12
|
detector = ActionDetector() |
|
13
|
items = detector.detect_from_transcript( |
|
14
|
"We need to update the database schema before release." |
|
15
|
) |
|
16
|
assert len(items) >= 1 |
|
17
|
assert any("database" in i.action.lower() for i in items) |
|
18
|
|
|
19
|
def test_detects_should(self): |
|
20
|
detector = ActionDetector() |
|
21
|
items = detector.detect_from_transcript("Alice should review the pull request by Friday.") |
|
22
|
assert len(items) >= 1 |
|
23
|
|
|
24
|
def test_detects_action_item_keyword(self): |
|
25
|
detector = ActionDetector() |
|
26
|
items = detector.detect_from_transcript( |
|
27
|
"Action item: set up monitoring for the new service." |
|
28
|
) |
|
29
|
assert len(items) >= 1 |
|
30
|
|
|
31
|
def test_detects_follow_up(self): |
|
32
|
detector = ActionDetector() |
|
33
|
items = detector.detect_from_transcript("Follow up with the client about requirements.") |
|
34
|
assert len(items) >= 1 |
|
35
|
|
|
36
|
def test_detects_lets(self): |
|
37
|
detector = ActionDetector() |
|
38
|
items = detector.detect_from_transcript("Let's schedule a meeting to discuss the roadmap.") |
|
39
|
assert len(items) >= 1 |
|
40
|
|
|
41
|
def test_ignores_short_sentences(self): |
|
42
|
detector = ActionDetector() |
|
43
|
items = detector.detect_from_transcript("Do it.") |
|
44
|
assert len(items) == 0 |
|
45
|
|
|
46
|
def test_no_action_patterns(self): |
|
47
|
detector = ActionDetector() |
|
48
|
items = detector.detect_from_transcript("The weather was nice today. We had lunch at noon.") |
|
49
|
assert len(items) == 0 |
|
50
|
|
|
51
|
def test_multiple_sentences(self): |
|
52
|
detector = ActionDetector() |
|
53
|
text = "We need to deploy the fix. Alice should test it first. The sky is blue." |
|
54
|
items = detector.detect_from_transcript(text) |
|
55
|
assert len(items) == 2 |
|
56
|
|
|
57
|
def test_source_is_transcript(self): |
|
58
|
detector = ActionDetector() |
|
59
|
items = detector.detect_from_transcript("We need to fix the authentication module.") |
|
60
|
for item in items: |
|
61
|
assert item.source == "transcript" |
|
62
|
|
|
63
|
|
|
64
|
class TestLLMExtract: |
|
65
|
def test_llm_extraction(self): |
|
66
|
pm = MagicMock() |
|
67
|
pm.chat.return_value = json.dumps( |
|
68
|
[ |
|
69
|
{ |
|
70
|
"action": "Deploy new version", |
|
71
|
"assignee": "Bob", |
|
72
|
"deadline": "Friday", |
|
73
|
"priority": "high", |
|
74
|
"context": "Production release", |
|
75
|
} |
|
76
|
] |
|
77
|
) |
|
78
|
detector = ActionDetector(provider_manager=pm) |
|
79
|
items = detector.detect_from_transcript("Deploy new version by Friday.") |
|
80
|
assert len(items) == 1 |
|
81
|
assert items[0].action == "Deploy new version" |
|
82
|
assert items[0].assignee == "Bob" |
|
83
|
assert items[0].deadline == "Friday" |
|
84
|
assert items[0].priority == "high" |
|
85
|
assert items[0].source == "transcript" |
|
86
|
|
|
87
|
def test_llm_returns_empty(self): |
|
88
|
pm = MagicMock() |
|
89
|
pm.chat.return_value = "[]" |
|
90
|
detector = ActionDetector(provider_manager=pm) |
|
91
|
items = detector.detect_from_transcript("No action items here.") |
|
92
|
assert items == [] |
|
93
|
|
|
94
|
def test_llm_error_returns_empty(self): |
|
95
|
pm = MagicMock() |
|
96
|
pm.chat.side_effect = Exception("API error") |
|
97
|
detector = ActionDetector(provider_manager=pm) |
|
98
|
items = detector.detect_from_transcript("We need to fix this.") |
|
99
|
assert items == [] |
|
100
|
|
|
101
|
def test_llm_bad_json(self): |
|
102
|
pm = MagicMock() |
|
103
|
pm.chat.return_value = "not valid json" |
|
104
|
detector = ActionDetector(provider_manager=pm) |
|
105
|
items = detector.detect_from_transcript("Update the docs.") |
|
106
|
assert items == [] |
|
107
|
|
|
108
|
def test_llm_skips_items_without_action(self): |
|
109
|
pm = MagicMock() |
|
110
|
pm.chat.return_value = json.dumps( |
|
111
|
[ |
|
112
|
{"action": "Valid action", "assignee": None}, |
|
113
|
{"assignee": "Alice"}, # No action field |
|
114
|
{"action": "", "assignee": "Bob"}, # Empty action |
|
115
|
] |
|
116
|
) |
|
117
|
detector = ActionDetector(provider_manager=pm) |
|
118
|
items = detector.detect_from_transcript("Some text.") |
|
119
|
assert len(items) == 1 |
|
120
|
assert items[0].action == "Valid action" |
|
121
|
|
|
122
|
|
|
123
|
class TestDetectFromDiagrams: |
|
124
|
def test_dict_diagrams(self): |
|
125
|
pm = MagicMock() |
|
126
|
pm.chat.return_value = json.dumps( |
|
127
|
[ |
|
128
|
{ |
|
129
|
"action": "Migrate database", |
|
130
|
"assignee": None, |
|
131
|
"deadline": None, |
|
132
|
"priority": None, |
|
133
|
"context": None, |
|
134
|
}, |
|
135
|
] |
|
136
|
) |
|
137
|
detector = ActionDetector(provider_manager=pm) |
|
138
|
diagrams = [ |
|
139
|
{"text_content": "Step 1: Migrate database", "elements": ["DB", "Migration"]}, |
|
140
|
] |
|
141
|
items = detector.detect_from_diagrams(diagrams) |
|
142
|
assert len(items) == 1 |
|
143
|
assert items[0].source == "diagram" |
|
144
|
|
|
145
|
def test_object_diagrams(self): |
|
146
|
pm = MagicMock() |
|
147
|
pm.chat.return_value = json.dumps( |
|
148
|
[ |
|
149
|
{ |
|
150
|
"action": "Update API", |
|
151
|
"assignee": None, |
|
152
|
"deadline": None, |
|
153
|
"priority": None, |
|
154
|
"context": None, |
|
155
|
}, |
|
156
|
] |
|
157
|
) |
|
158
|
detector = ActionDetector(provider_manager=pm) |
|
159
|
|
|
160
|
class FakeDiagram: |
|
161
|
text_content = "Update API endpoints" |
|
162
|
elements = ["API", "Gateway"] |
|
163
|
|
|
164
|
items = detector.detect_from_diagrams([FakeDiagram()]) |
|
165
|
assert len(items) >= 1 |
|
166
|
assert items[0].source == "diagram" |
|
167
|
|
|
168
|
def test_empty_diagram_skipped(self): |
|
169
|
detector = ActionDetector() |
|
170
|
diagrams = [{"text_content": "", "elements": []}] |
|
171
|
items = detector.detect_from_diagrams(diagrams) |
|
172
|
assert items == [] |
|
173
|
|
|
174
|
def test_pattern_fallback_for_diagrams(self): |
|
175
|
detector = ActionDetector() # No provider |
|
176
|
diagrams = [ |
|
177
|
{ |
|
178
|
"text_content": "We need to update the configuration before deployment.", |
|
179
|
"elements": [], |
|
180
|
}, |
|
181
|
] |
|
182
|
items = detector.detect_from_diagrams(diagrams) |
|
183
|
assert len(items) >= 1 |
|
184
|
assert items[0].source == "diagram" |
|
185
|
|
|
186
|
|
|
187
|
class TestMergeActionItems: |
|
188
|
def test_deduplicates(self): |
|
189
|
detector = ActionDetector() |
|
190
|
t_items = [ActionItem(action="Deploy fix", source="transcript")] |
|
191
|
d_items = [ActionItem(action="Deploy fix", source="diagram")] |
|
192
|
merged = detector.merge_action_items(t_items, d_items) |
|
193
|
assert len(merged) == 1 |
|
194
|
|
|
195
|
def test_case_insensitive_dedup(self): |
|
196
|
detector = ActionDetector() |
|
197
|
t_items = [ActionItem(action="deploy fix", source="transcript")] |
|
198
|
d_items = [ActionItem(action="Deploy Fix", source="diagram")] |
|
199
|
merged = detector.merge_action_items(t_items, d_items) |
|
200
|
assert len(merged) == 1 |
|
201
|
|
|
202
|
def test_keeps_unique(self): |
|
203
|
detector = ActionDetector() |
|
204
|
t_items = [ActionItem(action="Task A", source="transcript")] |
|
205
|
d_items = [ActionItem(action="Task B", source="diagram")] |
|
206
|
merged = detector.merge_action_items(t_items, d_items) |
|
207
|
assert len(merged) == 2 |
|
208
|
|
|
209
|
def test_empty_inputs(self): |
|
210
|
detector = ActionDetector() |
|
211
|
merged = detector.merge_action_items([], []) |
|
212
|
assert merged == [] |
|
213
|
|
|
214
|
|
|
215
|
class TestAttachTimestamps: |
|
216
|
def test_attaches_matching_segment(self): |
|
217
|
detector = ActionDetector() |
|
218
|
[ |
|
219
|
ActionItem(action="We need to update the database schema before release"), |
|
220
|
] |
|
221
|
segments = [ |
|
222
|
TranscriptSegment(start=0.0, end=5.0, text="Welcome to the meeting."), |
|
223
|
TranscriptSegment( |
|
224
|
start=5.0, end=15.0, text="We need to update the database schema before release." |
|
225
|
), |
|
226
|
TranscriptSegment(start=15.0, end=20.0, text="Any questions?"), |
|
227
|
] |
|
228
|
detector.detect_from_transcript( |
|
229
|
"We need to update the database schema before release.", |
|
230
|
segments=segments, |
|
231
|
) |
|
232
|
# Pattern extract will create items; check them |
|
233
|
result = detector.detect_from_transcript( |
|
234
|
"We need to update the database schema before release.", |
|
235
|
segments=segments, |
|
236
|
) |
|
237
|
assert len(result) >= 1 |
|
238
|
# Context should be set with timestamp |
|
239
|
assert any(i.context and "5s" in i.context for i in result) |
|
240
|
|
|
241
|
def test_no_match_no_context(self): |
|
242
|
detector = ActionDetector() |
|
243
|
items = [ActionItem(action="Completely unrelated action")] |
|
244
|
segments = [ |
|
245
|
TranscriptSegment(start=0.0, end=5.0, text="Hello world."), |
|
246
|
] |
|
247
|
# Manually test the private method |
|
248
|
detector._attach_timestamps(items, segments) |
|
249
|
assert items[0].context is None |
|
250
|
|