PlanOpticon

planopticon / tests / test_action_detector.py
Blame History Raw 250 lines
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

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button