|
0981a08…
|
noreply
|
1 |
"""Tests for agent skill execute() methods with mocked context.""" |
|
0981a08…
|
noreply
|
2 |
|
|
0981a08…
|
noreply
|
3 |
import json |
|
0981a08…
|
noreply
|
4 |
from dataclasses import dataclass |
|
0981a08…
|
noreply
|
5 |
from unittest.mock import MagicMock |
|
0981a08…
|
noreply
|
6 |
|
|
0981a08…
|
noreply
|
7 |
import pytest |
|
0981a08…
|
noreply
|
8 |
|
|
0981a08…
|
noreply
|
9 |
from video_processor.agent.skills.base import ( |
|
0981a08…
|
noreply
|
10 |
AgentContext, |
|
0981a08…
|
noreply
|
11 |
Artifact, |
|
0981a08…
|
noreply
|
12 |
_skills, |
|
0981a08…
|
noreply
|
13 |
) |
|
0981a08…
|
noreply
|
14 |
|
|
0981a08…
|
noreply
|
15 |
# --------------------------------------------------------------------------- |
|
0981a08…
|
noreply
|
16 |
# Fixtures |
|
0981a08…
|
noreply
|
17 |
# --------------------------------------------------------------------------- |
|
0981a08…
|
noreply
|
18 |
|
|
0981a08…
|
noreply
|
19 |
|
|
0981a08…
|
noreply
|
20 |
@pytest.fixture(autouse=True) |
|
0981a08…
|
noreply
|
21 |
def _clean_skill_registry(): |
|
0981a08…
|
noreply
|
22 |
"""Save and restore the global skill registry between tests.""" |
|
0981a08…
|
noreply
|
23 |
original = dict(_skills) |
|
0981a08…
|
noreply
|
24 |
yield |
|
0981a08…
|
noreply
|
25 |
_skills.clear() |
|
0981a08…
|
noreply
|
26 |
_skills.update(original) |
|
0981a08…
|
noreply
|
27 |
|
|
0981a08…
|
noreply
|
28 |
|
|
0981a08…
|
noreply
|
29 |
@dataclass |
|
0981a08…
|
noreply
|
30 |
class FakeEntity: |
|
0981a08…
|
noreply
|
31 |
name: str |
|
0981a08…
|
noreply
|
32 |
type: str |
|
0981a08…
|
noreply
|
33 |
|
|
0981a08…
|
noreply
|
34 |
def __str__(self): |
|
0981a08…
|
noreply
|
35 |
return self.name |
|
0981a08…
|
noreply
|
36 |
|
|
0981a08…
|
noreply
|
37 |
|
|
0981a08…
|
noreply
|
38 |
class FakeQueryResult: |
|
0981a08…
|
noreply
|
39 |
"""Mimics QueryResult.to_text().""" |
|
0981a08…
|
noreply
|
40 |
|
|
0981a08…
|
noreply
|
41 |
def __init__(self, text="Stats: 10 entities, 5 relationships"): |
|
0981a08…
|
noreply
|
42 |
self._text = text |
|
0981a08…
|
noreply
|
43 |
|
|
0981a08…
|
noreply
|
44 |
def to_text(self): |
|
0981a08…
|
noreply
|
45 |
return self._text |
|
0981a08…
|
noreply
|
46 |
|
|
0981a08…
|
noreply
|
47 |
|
|
0981a08…
|
noreply
|
48 |
def _make_context( |
|
0981a08…
|
noreply
|
49 |
chat_response="# Generated Content\n\nSome markdown content.", |
|
0981a08…
|
noreply
|
50 |
planning_entities=None, |
|
0981a08…
|
noreply
|
51 |
): |
|
0981a08…
|
noreply
|
52 |
"""Build an AgentContext with mocked query_engine and provider_manager.""" |
|
0981a08…
|
noreply
|
53 |
ctx = AgentContext() |
|
0981a08…
|
noreply
|
54 |
|
|
0981a08…
|
noreply
|
55 |
qe = MagicMock() |
|
0981a08…
|
noreply
|
56 |
qe.stats.return_value = FakeQueryResult("Stats: 10 entities, 5 rels") |
|
0981a08…
|
noreply
|
57 |
qe.entities.return_value = FakeQueryResult("Entity1, Entity2") |
|
0981a08…
|
noreply
|
58 |
qe.relationships.return_value = FakeQueryResult("Entity1 -> Entity2") |
|
0981a08…
|
noreply
|
59 |
ctx.query_engine = qe |
|
0981a08…
|
noreply
|
60 |
|
|
0981a08…
|
noreply
|
61 |
pm = MagicMock() |
|
0981a08…
|
noreply
|
62 |
pm.chat.return_value = chat_response |
|
0981a08…
|
noreply
|
63 |
ctx.provider_manager = pm |
|
0981a08…
|
noreply
|
64 |
|
|
0981a08…
|
noreply
|
65 |
ctx.knowledge_graph = MagicMock() |
|
0981a08…
|
noreply
|
66 |
|
|
0981a08…
|
noreply
|
67 |
if planning_entities is not None: |
|
0981a08…
|
noreply
|
68 |
ctx.planning_entities = planning_entities |
|
0981a08…
|
noreply
|
69 |
else: |
|
0981a08…
|
noreply
|
70 |
ctx.planning_entities = [ |
|
0981a08…
|
noreply
|
71 |
FakeEntity(name="Auth system", type="feature"), |
|
0981a08…
|
noreply
|
72 |
FakeEntity(name="Launch v1", type="milestone"), |
|
0981a08…
|
noreply
|
73 |
FakeEntity(name="Must be fast", type="constraint"), |
|
0981a08…
|
noreply
|
74 |
FakeEntity(name="Build dashboard", type="goal"), |
|
0981a08…
|
noreply
|
75 |
FakeEntity(name="API depends on auth", type="dependency"), |
|
0981a08…
|
noreply
|
76 |
FakeEntity(name="User login", type="requirement"), |
|
0981a08…
|
noreply
|
77 |
] |
|
0981a08…
|
noreply
|
78 |
|
|
0981a08…
|
noreply
|
79 |
return ctx |
|
0981a08…
|
noreply
|
80 |
|
|
0981a08…
|
noreply
|
81 |
|
|
0981a08…
|
noreply
|
82 |
# --------------------------------------------------------------------------- |
|
0981a08…
|
noreply
|
83 |
# ProjectPlanSkill |
|
0981a08…
|
noreply
|
84 |
# --------------------------------------------------------------------------- |
|
0981a08…
|
noreply
|
85 |
|
|
0981a08…
|
noreply
|
86 |
|
|
0981a08…
|
noreply
|
87 |
class TestProjectPlanSkill: |
|
0981a08…
|
noreply
|
88 |
def test_execute_returns_artifact(self): |
|
0981a08…
|
noreply
|
89 |
from video_processor.agent.skills.project_plan import ProjectPlanSkill |
|
0981a08…
|
noreply
|
90 |
|
|
0981a08…
|
noreply
|
91 |
skill = ProjectPlanSkill() |
|
0981a08…
|
noreply
|
92 |
ctx = _make_context() |
|
0981a08…
|
noreply
|
93 |
artifact = skill.execute(ctx) |
|
0981a08…
|
noreply
|
94 |
|
|
0981a08…
|
noreply
|
95 |
assert isinstance(artifact, Artifact) |
|
0981a08…
|
noreply
|
96 |
assert artifact.artifact_type == "project_plan" |
|
0981a08…
|
noreply
|
97 |
assert artifact.format == "markdown" |
|
0981a08…
|
noreply
|
98 |
assert len(artifact.content) > 0 |
|
0981a08…
|
noreply
|
99 |
|
|
0981a08…
|
noreply
|
100 |
def test_execute_calls_provider(self): |
|
0981a08…
|
noreply
|
101 |
from video_processor.agent.skills.project_plan import ProjectPlanSkill |
|
0981a08…
|
noreply
|
102 |
|
|
0981a08…
|
noreply
|
103 |
skill = ProjectPlanSkill() |
|
0981a08…
|
noreply
|
104 |
ctx = _make_context() |
|
0981a08…
|
noreply
|
105 |
skill.execute(ctx) |
|
0981a08…
|
noreply
|
106 |
|
|
0981a08…
|
noreply
|
107 |
ctx.provider_manager.chat.assert_called_once() |
|
0981a08…
|
noreply
|
108 |
call_args = ctx.provider_manager.chat.call_args |
|
0981a08…
|
noreply
|
109 |
messages = call_args[1]["messages"] if "messages" in call_args[1] else call_args[0][0] |
|
0981a08…
|
noreply
|
110 |
assert len(messages) == 1 |
|
0981a08…
|
noreply
|
111 |
assert messages[0]["role"] == "user" |
|
0981a08…
|
noreply
|
112 |
|
|
0981a08…
|
noreply
|
113 |
def test_execute_queries_graph(self): |
|
0981a08…
|
noreply
|
114 |
from video_processor.agent.skills.project_plan import ProjectPlanSkill |
|
0981a08…
|
noreply
|
115 |
|
|
0981a08…
|
noreply
|
116 |
skill = ProjectPlanSkill() |
|
0981a08…
|
noreply
|
117 |
ctx = _make_context() |
|
0981a08…
|
noreply
|
118 |
skill.execute(ctx) |
|
0981a08…
|
noreply
|
119 |
|
|
0981a08…
|
noreply
|
120 |
ctx.query_engine.stats.assert_called_once() |
|
0981a08…
|
noreply
|
121 |
ctx.query_engine.entities.assert_called_once() |
|
0981a08…
|
noreply
|
122 |
ctx.query_engine.relationships.assert_called_once() |
|
0981a08…
|
noreply
|
123 |
|
|
0981a08…
|
noreply
|
124 |
|
|
0981a08…
|
noreply
|
125 |
# --------------------------------------------------------------------------- |
|
0981a08…
|
noreply
|
126 |
# PRDSkill |
|
0981a08…
|
noreply
|
127 |
# --------------------------------------------------------------------------- |
|
0981a08…
|
noreply
|
128 |
|
|
0981a08…
|
noreply
|
129 |
|
|
0981a08…
|
noreply
|
130 |
class TestPRDSkill: |
|
0981a08…
|
noreply
|
131 |
def test_execute_returns_artifact(self): |
|
0981a08…
|
noreply
|
132 |
from video_processor.agent.skills.prd import PRDSkill |
|
0981a08…
|
noreply
|
133 |
|
|
0981a08…
|
noreply
|
134 |
skill = PRDSkill() |
|
0981a08…
|
noreply
|
135 |
ctx = _make_context() |
|
0981a08…
|
noreply
|
136 |
artifact = skill.execute(ctx) |
|
0981a08…
|
noreply
|
137 |
|
|
0981a08…
|
noreply
|
138 |
assert isinstance(artifact, Artifact) |
|
0981a08…
|
noreply
|
139 |
assert artifact.artifact_type == "prd" |
|
0981a08…
|
noreply
|
140 |
assert artifact.format == "markdown" |
|
0981a08…
|
noreply
|
141 |
|
|
0981a08…
|
noreply
|
142 |
def test_execute_filters_relevant_entities(self): |
|
0981a08…
|
noreply
|
143 |
from video_processor.agent.skills.prd import PRDSkill |
|
0981a08…
|
noreply
|
144 |
|
|
0981a08…
|
noreply
|
145 |
skill = PRDSkill() |
|
0981a08…
|
noreply
|
146 |
ctx = _make_context() |
|
0981a08…
|
noreply
|
147 |
skill.execute(ctx) |
|
0981a08…
|
noreply
|
148 |
|
|
0981a08…
|
noreply
|
149 |
# Should still call provider |
|
0981a08…
|
noreply
|
150 |
ctx.provider_manager.chat.assert_called_once() |
|
0981a08…
|
noreply
|
151 |
|
|
0981a08…
|
noreply
|
152 |
def test_execute_with_no_relevant_entities(self): |
|
0981a08…
|
noreply
|
153 |
from video_processor.agent.skills.prd import PRDSkill |
|
0981a08…
|
noreply
|
154 |
|
|
0981a08…
|
noreply
|
155 |
skill = PRDSkill() |
|
0981a08…
|
noreply
|
156 |
ctx = _make_context( |
|
0981a08…
|
noreply
|
157 |
planning_entities=[ |
|
0981a08…
|
noreply
|
158 |
FakeEntity(name="Some goal", type="goal"), |
|
0981a08…
|
noreply
|
159 |
] |
|
0981a08…
|
noreply
|
160 |
) |
|
0981a08…
|
noreply
|
161 |
artifact = skill.execute(ctx) |
|
0981a08…
|
noreply
|
162 |
|
|
0981a08…
|
noreply
|
163 |
assert isinstance(artifact, Artifact) |
|
0981a08…
|
noreply
|
164 |
assert artifact.artifact_type == "prd" |
|
0981a08…
|
noreply
|
165 |
|
|
0981a08…
|
noreply
|
166 |
|
|
0981a08…
|
noreply
|
167 |
# --------------------------------------------------------------------------- |
|
0981a08…
|
noreply
|
168 |
# RoadmapSkill |
|
0981a08…
|
noreply
|
169 |
# --------------------------------------------------------------------------- |
|
0981a08…
|
noreply
|
170 |
|
|
0981a08…
|
noreply
|
171 |
|
|
0981a08…
|
noreply
|
172 |
class TestRoadmapSkill: |
|
0981a08…
|
noreply
|
173 |
def test_execute_returns_artifact(self): |
|
0981a08…
|
noreply
|
174 |
from video_processor.agent.skills.roadmap import RoadmapSkill |
|
0981a08…
|
noreply
|
175 |
|
|
0981a08…
|
noreply
|
176 |
skill = RoadmapSkill() |
|
0981a08…
|
noreply
|
177 |
ctx = _make_context() |
|
0981a08…
|
noreply
|
178 |
artifact = skill.execute(ctx) |
|
0981a08…
|
noreply
|
179 |
|
|
0981a08…
|
noreply
|
180 |
assert isinstance(artifact, Artifact) |
|
0981a08…
|
noreply
|
181 |
assert artifact.artifact_type == "roadmap" |
|
0981a08…
|
noreply
|
182 |
assert artifact.format == "markdown" |
|
0981a08…
|
noreply
|
183 |
|
|
0981a08…
|
noreply
|
184 |
def test_execute_with_no_relevant_entities(self): |
|
0981a08…
|
noreply
|
185 |
from video_processor.agent.skills.roadmap import RoadmapSkill |
|
0981a08…
|
noreply
|
186 |
|
|
0981a08…
|
noreply
|
187 |
skill = RoadmapSkill() |
|
0981a08…
|
noreply
|
188 |
ctx = _make_context( |
|
0981a08…
|
noreply
|
189 |
planning_entities=[ |
|
0981a08…
|
noreply
|
190 |
FakeEntity(name="Some constraint", type="constraint"), |
|
0981a08…
|
noreply
|
191 |
] |
|
0981a08…
|
noreply
|
192 |
) |
|
0981a08…
|
noreply
|
193 |
artifact = skill.execute(ctx) |
|
0981a08…
|
noreply
|
194 |
|
|
0981a08…
|
noreply
|
195 |
assert isinstance(artifact, Artifact) |
|
0981a08…
|
noreply
|
196 |
|
|
0981a08…
|
noreply
|
197 |
|
|
0981a08…
|
noreply
|
198 |
# --------------------------------------------------------------------------- |
|
0981a08…
|
noreply
|
199 |
# TaskBreakdownSkill |
|
0981a08…
|
noreply
|
200 |
# --------------------------------------------------------------------------- |
|
0981a08…
|
noreply
|
201 |
|
|
0981a08…
|
noreply
|
202 |
|
|
0981a08…
|
noreply
|
203 |
class TestTaskBreakdownSkill: |
|
0981a08…
|
noreply
|
204 |
def test_execute_returns_artifact_json(self): |
|
0981a08…
|
noreply
|
205 |
from video_processor.agent.skills.task_breakdown import TaskBreakdownSkill |
|
0981a08…
|
noreply
|
206 |
|
|
0981a08…
|
noreply
|
207 |
tasks_json = json.dumps( |
|
0981a08…
|
noreply
|
208 |
[ |
|
0981a08…
|
noreply
|
209 |
{ |
|
0981a08…
|
noreply
|
210 |
"id": "T1", |
|
0981a08…
|
noreply
|
211 |
"title": "Setup", |
|
0981a08…
|
noreply
|
212 |
"description": "Init", |
|
0981a08…
|
noreply
|
213 |
"depends_on": [], |
|
0981a08…
|
noreply
|
214 |
"priority": "high", |
|
0981a08…
|
noreply
|
215 |
"estimate": "1d", |
|
0981a08…
|
noreply
|
216 |
"assignee_role": "dev", |
|
0981a08…
|
noreply
|
217 |
}, |
|
0981a08…
|
noreply
|
218 |
] |
|
0981a08…
|
noreply
|
219 |
) |
|
0981a08…
|
noreply
|
220 |
skill = TaskBreakdownSkill() |
|
0981a08…
|
noreply
|
221 |
ctx = _make_context(chat_response=tasks_json) |
|
0981a08…
|
noreply
|
222 |
artifact = skill.execute(ctx) |
|
0981a08…
|
noreply
|
223 |
|
|
0981a08…
|
noreply
|
224 |
assert isinstance(artifact, Artifact) |
|
0981a08…
|
noreply
|
225 |
assert artifact.artifact_type == "task_list" |
|
0981a08…
|
noreply
|
226 |
assert artifact.format == "json" |
|
0981a08…
|
noreply
|
227 |
assert "tasks" in artifact.metadata |
|
0981a08…
|
noreply
|
228 |
assert len(artifact.metadata["tasks"]) == 1 |
|
0981a08…
|
noreply
|
229 |
|
|
0981a08…
|
noreply
|
230 |
def test_execute_with_non_json_response(self): |
|
0981a08…
|
noreply
|
231 |
from video_processor.agent.skills.task_breakdown import TaskBreakdownSkill |
|
0981a08…
|
noreply
|
232 |
|
|
0981a08…
|
noreply
|
233 |
skill = TaskBreakdownSkill() |
|
0981a08…
|
noreply
|
234 |
ctx = _make_context(chat_response="Not valid JSON at all") |
|
0981a08…
|
noreply
|
235 |
artifact = skill.execute(ctx) |
|
0981a08…
|
noreply
|
236 |
|
|
0981a08…
|
noreply
|
237 |
assert isinstance(artifact, Artifact) |
|
0981a08…
|
noreply
|
238 |
assert artifact.artifact_type == "task_list" |
|
0981a08…
|
noreply
|
239 |
|
|
0981a08…
|
noreply
|
240 |
def test_execute_with_no_relevant_entities(self): |
|
0981a08…
|
noreply
|
241 |
from video_processor.agent.skills.task_breakdown import TaskBreakdownSkill |
|
0981a08…
|
noreply
|
242 |
|
|
0981a08…
|
noreply
|
243 |
tasks_json = json.dumps([]) |
|
0981a08…
|
noreply
|
244 |
skill = TaskBreakdownSkill() |
|
0981a08…
|
noreply
|
245 |
ctx = _make_context( |
|
0981a08…
|
noreply
|
246 |
chat_response=tasks_json, |
|
0981a08…
|
noreply
|
247 |
planning_entities=[FakeEntity(name="X", type="constraint")], |
|
0981a08…
|
noreply
|
248 |
) |
|
0981a08…
|
noreply
|
249 |
artifact = skill.execute(ctx) |
|
0981a08…
|
noreply
|
250 |
assert artifact.metadata["tasks"] == [] |
|
0981a08…
|
noreply
|
251 |
|
|
0981a08…
|
noreply
|
252 |
|
|
0981a08…
|
noreply
|
253 |
# --------------------------------------------------------------------------- |
|
0981a08…
|
noreply
|
254 |
# DocGeneratorSkill |
|
0981a08…
|
noreply
|
255 |
# --------------------------------------------------------------------------- |
|
0981a08…
|
noreply
|
256 |
|
|
0981a08…
|
noreply
|
257 |
|
|
0981a08…
|
noreply
|
258 |
class TestDocGeneratorSkill: |
|
0981a08…
|
noreply
|
259 |
def test_execute_default_type(self): |
|
0981a08…
|
noreply
|
260 |
from video_processor.agent.skills.doc_generator import DocGeneratorSkill |
|
0981a08…
|
noreply
|
261 |
|
|
0981a08…
|
noreply
|
262 |
skill = DocGeneratorSkill() |
|
0981a08…
|
noreply
|
263 |
ctx = _make_context() |
|
0981a08…
|
noreply
|
264 |
artifact = skill.execute(ctx) |
|
0981a08…
|
noreply
|
265 |
|
|
0981a08…
|
noreply
|
266 |
assert isinstance(artifact, Artifact) |
|
0981a08…
|
noreply
|
267 |
assert artifact.artifact_type == "document" |
|
0981a08…
|
noreply
|
268 |
assert artifact.format == "markdown" |
|
0981a08…
|
noreply
|
269 |
assert artifact.metadata["doc_type"] == "technical_doc" |
|
0981a08…
|
noreply
|
270 |
|
|
0981a08…
|
noreply
|
271 |
def test_execute_adr_type(self): |
|
0981a08…
|
noreply
|
272 |
from video_processor.agent.skills.doc_generator import DocGeneratorSkill |
|
0981a08…
|
noreply
|
273 |
|
|
0981a08…
|
noreply
|
274 |
skill = DocGeneratorSkill() |
|
0981a08…
|
noreply
|
275 |
ctx = _make_context() |
|
0981a08…
|
noreply
|
276 |
artifact = skill.execute(ctx, doc_type="adr") |
|
0981a08…
|
noreply
|
277 |
|
|
0981a08…
|
noreply
|
278 |
assert artifact.metadata["doc_type"] == "adr" |
|
0981a08…
|
noreply
|
279 |
|
|
0981a08…
|
noreply
|
280 |
def test_execute_meeting_notes_type(self): |
|
0981a08…
|
noreply
|
281 |
from video_processor.agent.skills.doc_generator import DocGeneratorSkill |
|
0981a08…
|
noreply
|
282 |
|
|
0981a08…
|
noreply
|
283 |
skill = DocGeneratorSkill() |
|
0981a08…
|
noreply
|
284 |
ctx = _make_context() |
|
0981a08…
|
noreply
|
285 |
artifact = skill.execute(ctx, doc_type="meeting_notes") |
|
0981a08…
|
noreply
|
286 |
|
|
0981a08…
|
noreply
|
287 |
assert artifact.metadata["doc_type"] == "meeting_notes" |
|
0981a08…
|
noreply
|
288 |
|
|
0981a08…
|
noreply
|
289 |
def test_execute_unknown_type_falls_back(self): |
|
0981a08…
|
noreply
|
290 |
from video_processor.agent.skills.doc_generator import DocGeneratorSkill |
|
0981a08…
|
noreply
|
291 |
|
|
0981a08…
|
noreply
|
292 |
skill = DocGeneratorSkill() |
|
0981a08…
|
noreply
|
293 |
ctx = _make_context() |
|
0981a08…
|
noreply
|
294 |
artifact = skill.execute(ctx, doc_type="unknown_type") |
|
0981a08…
|
noreply
|
295 |
|
|
0981a08…
|
noreply
|
296 |
# Falls back to technical_doc prompt |
|
0981a08…
|
noreply
|
297 |
assert artifact.artifact_type == "document" |
|
0981a08…
|
noreply
|
298 |
|
|
0981a08…
|
noreply
|
299 |
|
|
0981a08…
|
noreply
|
300 |
# --------------------------------------------------------------------------- |
|
0981a08…
|
noreply
|
301 |
# RequirementsChatSkill |
|
0981a08…
|
noreply
|
302 |
# --------------------------------------------------------------------------- |
|
0981a08…
|
noreply
|
303 |
|
|
0981a08…
|
noreply
|
304 |
|
|
0981a08…
|
noreply
|
305 |
class TestRequirementsChatSkill: |
|
0981a08…
|
noreply
|
306 |
def test_execute_returns_artifact(self): |
|
0981a08…
|
noreply
|
307 |
from video_processor.agent.skills.requirements_chat import RequirementsChatSkill |
|
0981a08…
|
noreply
|
308 |
|
|
0981a08…
|
noreply
|
309 |
questions = { |
|
0981a08…
|
noreply
|
310 |
"questions": [ |
|
0981a08…
|
noreply
|
311 |
{"id": "Q1", "category": "goals", "question": "What?", "context": "Why"}, |
|
0981a08…
|
noreply
|
312 |
] |
|
0981a08…
|
noreply
|
313 |
} |
|
0981a08…
|
noreply
|
314 |
skill = RequirementsChatSkill() |
|
0981a08…
|
noreply
|
315 |
ctx = _make_context(chat_response=json.dumps(questions)) |
|
0981a08…
|
noreply
|
316 |
artifact = skill.execute(ctx) |
|
0981a08…
|
noreply
|
317 |
|
|
0981a08…
|
noreply
|
318 |
assert isinstance(artifact, Artifact) |
|
0981a08…
|
noreply
|
319 |
assert artifact.artifact_type == "requirements" |
|
0981a08…
|
noreply
|
320 |
assert artifact.format == "json" |
|
0981a08…
|
noreply
|
321 |
assert artifact.metadata["stage"] == "questionnaire" |
|
0981a08…
|
noreply
|
322 |
|
|
0981a08…
|
noreply
|
323 |
def test_gather_requirements(self): |
|
0981a08…
|
noreply
|
324 |
from video_processor.agent.skills.requirements_chat import RequirementsChatSkill |
|
0981a08…
|
noreply
|
325 |
|
|
0981a08…
|
noreply
|
326 |
reqs = { |
|
0981a08…
|
noreply
|
327 |
"goals": ["Build auth"], |
|
0981a08…
|
noreply
|
328 |
"constraints": ["Budget < 10k"], |
|
0981a08…
|
noreply
|
329 |
"priorities": ["Security"], |
|
0981a08…
|
noreply
|
330 |
"scope": {"in_scope": ["Login"], "out_of_scope": ["SSO"]}, |
|
0981a08…
|
noreply
|
331 |
} |
|
0981a08…
|
noreply
|
332 |
skill = RequirementsChatSkill() |
|
0981a08…
|
noreply
|
333 |
ctx = _make_context(chat_response=json.dumps(reqs)) |
|
0981a08…
|
noreply
|
334 |
result = skill.gather_requirements(ctx, {"Q1": "We need auth", "Q2": "Budget is limited"}) |
|
0981a08…
|
noreply
|
335 |
|
|
0981a08…
|
noreply
|
336 |
assert isinstance(result, dict) |
|
0981a08…
|
noreply
|
337 |
|
|
0981a08…
|
noreply
|
338 |
def test_gather_requirements_non_json_response(self): |
|
0981a08…
|
noreply
|
339 |
from video_processor.agent.skills.requirements_chat import RequirementsChatSkill |
|
0981a08…
|
noreply
|
340 |
|
|
0981a08…
|
noreply
|
341 |
skill = RequirementsChatSkill() |
|
0981a08…
|
noreply
|
342 |
ctx = _make_context(chat_response="Not JSON") |
|
0981a08…
|
noreply
|
343 |
result = skill.gather_requirements(ctx, {"Q1": "answer"}) |
|
0981a08…
|
noreply
|
344 |
|
|
0981a08…
|
noreply
|
345 |
assert isinstance(result, dict) |
|
0981a08…
|
noreply
|
346 |
|
|
0981a08…
|
noreply
|
347 |
|
|
0981a08…
|
noreply
|
348 |
# --------------------------------------------------------------------------- |
|
0981a08…
|
noreply
|
349 |
# Skill metadata |
|
0981a08…
|
noreply
|
350 |
# --------------------------------------------------------------------------- |
|
0981a08…
|
noreply
|
351 |
|
|
0981a08…
|
noreply
|
352 |
|
|
0981a08…
|
noreply
|
353 |
class TestSkillMetadata: |
|
0981a08…
|
noreply
|
354 |
def test_project_plan_name(self): |
|
0981a08…
|
noreply
|
355 |
from video_processor.agent.skills.project_plan import ProjectPlanSkill |
|
0981a08…
|
noreply
|
356 |
|
|
0981a08…
|
noreply
|
357 |
assert ProjectPlanSkill.name == "project_plan" |
|
0981a08…
|
noreply
|
358 |
|
|
0981a08…
|
noreply
|
359 |
def test_prd_name(self): |
|
0981a08…
|
noreply
|
360 |
from video_processor.agent.skills.prd import PRDSkill |
|
0981a08…
|
noreply
|
361 |
|
|
0981a08…
|
noreply
|
362 |
assert PRDSkill.name == "prd" |
|
0981a08…
|
noreply
|
363 |
|
|
0981a08…
|
noreply
|
364 |
def test_roadmap_name(self): |
|
0981a08…
|
noreply
|
365 |
from video_processor.agent.skills.roadmap import RoadmapSkill |
|
0981a08…
|
noreply
|
366 |
|
|
0981a08…
|
noreply
|
367 |
assert RoadmapSkill.name == "roadmap" |
|
0981a08…
|
noreply
|
368 |
|
|
0981a08…
|
noreply
|
369 |
def test_task_breakdown_name(self): |
|
0981a08…
|
noreply
|
370 |
from video_processor.agent.skills.task_breakdown import TaskBreakdownSkill |
|
0981a08…
|
noreply
|
371 |
|
|
0981a08…
|
noreply
|
372 |
assert TaskBreakdownSkill.name == "task_breakdown" |
|
0981a08…
|
noreply
|
373 |
|
|
0981a08…
|
noreply
|
374 |
def test_doc_generator_name(self): |
|
0981a08…
|
noreply
|
375 |
from video_processor.agent.skills.doc_generator import DocGeneratorSkill |
|
0981a08…
|
noreply
|
376 |
|
|
0981a08…
|
noreply
|
377 |
assert DocGeneratorSkill.name == "doc_generator" |
|
0981a08…
|
noreply
|
378 |
|
|
0981a08…
|
noreply
|
379 |
def test_requirements_chat_name(self): |
|
0981a08…
|
noreply
|
380 |
from video_processor.agent.skills.requirements_chat import RequirementsChatSkill |
|
0981a08…
|
noreply
|
381 |
|
|
0981a08…
|
noreply
|
382 |
assert RequirementsChatSkill.name == "requirements_chat" |
|
0981a08…
|
noreply
|
383 |
|
|
0981a08…
|
noreply
|
384 |
def test_can_execute_with_context(self): |
|
0981a08…
|
noreply
|
385 |
from video_processor.agent.skills.project_plan import ProjectPlanSkill |
|
0981a08…
|
noreply
|
386 |
|
|
0981a08…
|
noreply
|
387 |
skill = ProjectPlanSkill() |
|
0981a08…
|
noreply
|
388 |
ctx = _make_context() |
|
0981a08…
|
noreply
|
389 |
assert skill.can_execute(ctx) is True |
|
0981a08…
|
noreply
|
390 |
|
|
0981a08…
|
noreply
|
391 |
def test_can_execute_without_kg(self): |
|
0981a08…
|
noreply
|
392 |
from video_processor.agent.skills.project_plan import ProjectPlanSkill |
|
0981a08…
|
noreply
|
393 |
|
|
0981a08…
|
noreply
|
394 |
skill = ProjectPlanSkill() |
|
0981a08…
|
noreply
|
395 |
ctx = _make_context() |
|
0981a08…
|
noreply
|
396 |
ctx.knowledge_graph = None |
|
0981a08…
|
noreply
|
397 |
assert skill.can_execute(ctx) is False |
|
0981a08…
|
noreply
|
398 |
|
|
0981a08…
|
noreply
|
399 |
def test_can_execute_without_provider(self): |
|
0981a08…
|
noreply
|
400 |
from video_processor.agent.skills.project_plan import ProjectPlanSkill |
|
0981a08…
|
noreply
|
401 |
|
|
0981a08…
|
noreply
|
402 |
skill = ProjectPlanSkill() |
|
0981a08…
|
noreply
|
403 |
ctx = _make_context() |
|
0981a08…
|
noreply
|
404 |
ctx.provider_manager = None |
|
0981a08…
|
noreply
|
405 |
assert skill.can_execute(ctx) is False |
|
0981a08…
|
noreply
|
406 |
|
|
0981a08…
|
noreply
|
407 |
|
|
0981a08…
|
noreply
|
408 |
# --------------------------------------------------------------------------- |
|
0981a08…
|
noreply
|
409 |
# WikiGeneratorSkill |
|
0981a08…
|
noreply
|
410 |
# --------------------------------------------------------------------------- |
|
0981a08…
|
noreply
|
411 |
|
|
0981a08…
|
noreply
|
412 |
|
|
0981a08…
|
noreply
|
413 |
class TestWikiGeneratorSkill: |
|
0981a08…
|
noreply
|
414 |
def _sample_kg_data(self): |
|
0981a08…
|
noreply
|
415 |
return { |
|
0981a08…
|
noreply
|
416 |
"nodes": [ |
|
0981a08…
|
noreply
|
417 |
{ |
|
0981a08…
|
noreply
|
418 |
"name": "Python", |
|
0981a08…
|
noreply
|
419 |
"type": "technology", |
|
0981a08…
|
noreply
|
420 |
"descriptions": ["A programming language"], |
|
0981a08…
|
noreply
|
421 |
}, |
|
0981a08…
|
noreply
|
422 |
{ |
|
0981a08…
|
noreply
|
423 |
"name": "Alice", |
|
0981a08…
|
noreply
|
424 |
"type": "person", |
|
0981a08…
|
noreply
|
425 |
"descriptions": ["Lead developer"], |
|
0981a08…
|
noreply
|
426 |
}, |
|
0981a08…
|
noreply
|
427 |
{ |
|
0981a08…
|
noreply
|
428 |
"name": "FastAPI", |
|
0981a08…
|
noreply
|
429 |
"type": "technology", |
|
0981a08…
|
noreply
|
430 |
"descriptions": ["Web framework"], |
|
0981a08…
|
noreply
|
431 |
}, |
|
0981a08…
|
noreply
|
432 |
], |
|
0981a08…
|
noreply
|
433 |
"relationships": [ |
|
0981a08…
|
noreply
|
434 |
{"source": "Alice", "target": "Python", "type": "uses"}, |
|
0981a08…
|
noreply
|
435 |
{"source": "FastAPI", "target": "Python", "type": "built_with"}, |
|
0981a08…
|
noreply
|
436 |
], |
|
0981a08…
|
noreply
|
437 |
} |
|
0981a08…
|
noreply
|
438 |
|
|
0981a08…
|
noreply
|
439 |
def test_generate_wiki(self): |
|
0981a08…
|
noreply
|
440 |
from video_processor.agent.skills.wiki_generator import generate_wiki |
|
0981a08…
|
noreply
|
441 |
|
|
0981a08…
|
noreply
|
442 |
pages = generate_wiki(self._sample_kg_data(), title="Test Wiki") |
|
0981a08…
|
noreply
|
443 |
|
|
0981a08…
|
noreply
|
444 |
assert "Home" in pages |
|
0981a08…
|
noreply
|
445 |
assert "_Sidebar" in pages |
|
0981a08…
|
noreply
|
446 |
assert "Test Wiki" in pages["Home"] |
|
0981a08…
|
noreply
|
447 |
assert "3" in pages["Home"] # 3 entities |
|
0981a08…
|
noreply
|
448 |
assert "2" in pages["Home"] # 2 relationships |
|
0981a08…
|
noreply
|
449 |
|
|
0981a08…
|
noreply
|
450 |
# Entity pages should exist |
|
0981a08…
|
noreply
|
451 |
assert "Python" in pages |
|
0981a08…
|
noreply
|
452 |
assert "Alice" in pages |
|
0981a08…
|
noreply
|
453 |
assert "FastAPI" in pages |
|
0981a08…
|
noreply
|
454 |
|
|
0981a08…
|
noreply
|
455 |
# Type index pages should exist |
|
0981a08…
|
noreply
|
456 |
assert "Technology" in pages |
|
0981a08…
|
noreply
|
457 |
assert "Person" in pages |
|
0981a08…
|
noreply
|
458 |
|
|
0981a08…
|
noreply
|
459 |
# Alice's page should reference Python |
|
0981a08…
|
noreply
|
460 |
assert "Python" in pages["Alice"] |
|
0981a08…
|
noreply
|
461 |
assert "uses" in pages["Alice"] |
|
0981a08…
|
noreply
|
462 |
|
|
0981a08…
|
noreply
|
463 |
def test_generate_wiki_with_artifacts(self): |
|
0981a08…
|
noreply
|
464 |
from video_processor.agent.skills.wiki_generator import generate_wiki |
|
0981a08…
|
noreply
|
465 |
|
|
0981a08…
|
noreply
|
466 |
art = Artifact( |
|
0981a08…
|
noreply
|
467 |
name="Project Plan", |
|
0981a08…
|
noreply
|
468 |
content="# Plan\n\nDo the thing.", |
|
0981a08…
|
noreply
|
469 |
artifact_type="project_plan", |
|
0981a08…
|
noreply
|
470 |
format="markdown", |
|
0981a08…
|
noreply
|
471 |
) |
|
0981a08…
|
noreply
|
472 |
pages = generate_wiki(self._sample_kg_data(), artifacts=[art]) |
|
0981a08…
|
noreply
|
473 |
|
|
0981a08…
|
noreply
|
474 |
assert "Project-Plan" in pages |
|
0981a08…
|
noreply
|
475 |
assert "Do the thing." in pages["Project-Plan"] |
|
0981a08…
|
noreply
|
476 |
assert "Planning Artifacts" in pages["Home"] |
|
0981a08…
|
noreply
|
477 |
|
|
0981a08…
|
noreply
|
478 |
def test_write_wiki(self, tmp_path): |
|
0981a08…
|
noreply
|
479 |
from video_processor.agent.skills.wiki_generator import write_wiki |
|
0981a08…
|
noreply
|
480 |
|
|
0981a08…
|
noreply
|
481 |
pages = { |
|
0981a08…
|
noreply
|
482 |
"Home": "# Home\n\nWelcome.", |
|
0981a08…
|
noreply
|
483 |
"Page-One": "# Page One\n\nContent.", |
|
0981a08…
|
noreply
|
484 |
} |
|
0981a08…
|
noreply
|
485 |
paths = write_wiki(pages, tmp_path / "wiki") |
|
0981a08…
|
noreply
|
486 |
|
|
0981a08…
|
noreply
|
487 |
assert len(paths) == 2 |
|
0981a08…
|
noreply
|
488 |
assert (tmp_path / "wiki" / "Home.md").exists() |
|
0981a08…
|
noreply
|
489 |
assert (tmp_path / "wiki" / "Page-One.md").exists() |
|
0981a08…
|
noreply
|
490 |
assert "Welcome." in (tmp_path / "wiki" / "Home.md").read_text() |
|
0981a08…
|
noreply
|
491 |
|
|
0981a08…
|
noreply
|
492 |
def test_sanitize_filename(self): |
|
0981a08…
|
noreply
|
493 |
from video_processor.agent.skills.wiki_generator import _sanitize_filename |
|
0981a08…
|
noreply
|
494 |
|
|
0981a08…
|
noreply
|
495 |
assert _sanitize_filename("Hello World") == "Hello-World" |
|
0981a08…
|
noreply
|
496 |
assert _sanitize_filename("path/to\\file") == "path-to-file" |
|
0981a08…
|
noreply
|
497 |
assert _sanitize_filename("version.2") == "version-2" |
|
0981a08…
|
noreply
|
498 |
|
|
0981a08…
|
noreply
|
499 |
def test_wiki_link(self): |
|
0981a08…
|
noreply
|
500 |
from video_processor.agent.skills.wiki_generator import _wiki_link |
|
0981a08…
|
noreply
|
501 |
|
|
0981a08…
|
noreply
|
502 |
result = _wiki_link("My Page") |
|
0981a08…
|
noreply
|
503 |
assert result == "[My Page](My-Page)" |
|
0981a08…
|
noreply
|
504 |
|
|
0981a08…
|
noreply
|
505 |
result = _wiki_link("Simple") |
|
0981a08…
|
noreply
|
506 |
assert result == "[Simple](Simple)" |
|
0981a08…
|
noreply
|
507 |
|
|
0981a08…
|
noreply
|
508 |
|
|
0981a08…
|
noreply
|
509 |
# --------------------------------------------------------------------------- |
|
0981a08…
|
noreply
|
510 |
# NotesExportSkill |
|
0981a08…
|
noreply
|
511 |
# --------------------------------------------------------------------------- |
|
0981a08…
|
noreply
|
512 |
|
|
0981a08…
|
noreply
|
513 |
|
|
0981a08…
|
noreply
|
514 |
class TestNotesExportSkill: |
|
0981a08…
|
noreply
|
515 |
def _sample_kg_data(self): |
|
0981a08…
|
noreply
|
516 |
return { |
|
0981a08…
|
noreply
|
517 |
"nodes": [ |
|
0981a08…
|
noreply
|
518 |
{ |
|
0981a08…
|
noreply
|
519 |
"name": "Python", |
|
0981a08…
|
noreply
|
520 |
"type": "technology", |
|
0981a08…
|
noreply
|
521 |
"descriptions": ["A programming language"], |
|
0981a08…
|
noreply
|
522 |
}, |
|
0981a08…
|
noreply
|
523 |
{ |
|
0981a08…
|
noreply
|
524 |
"name": "Alice", |
|
0981a08…
|
noreply
|
525 |
"type": "person", |
|
0981a08…
|
noreply
|
526 |
"descriptions": ["Lead developer"], |
|
0981a08…
|
noreply
|
527 |
}, |
|
0981a08…
|
noreply
|
528 |
], |
|
0981a08…
|
noreply
|
529 |
"relationships": [ |
|
0981a08…
|
noreply
|
530 |
{"source": "Alice", "target": "Python", "type": "uses"}, |
|
0981a08…
|
noreply
|
531 |
], |
|
0981a08…
|
noreply
|
532 |
} |
|
0981a08…
|
noreply
|
533 |
|
|
0981a08…
|
noreply
|
534 |
def test_export_to_obsidian(self, tmp_path): |
|
0981a08…
|
noreply
|
535 |
from video_processor.agent.skills.notes_export import export_to_obsidian |
|
0981a08…
|
noreply
|
536 |
|
|
0981a08…
|
noreply
|
537 |
output_dir = tmp_path / "obsidian_vault" |
|
0981a08…
|
noreply
|
538 |
export_to_obsidian(self._sample_kg_data(), output_dir) |
|
0981a08…
|
noreply
|
539 |
|
|
0981a08…
|
noreply
|
540 |
assert output_dir.is_dir() |
|
0981a08…
|
noreply
|
541 |
|
|
0981a08…
|
noreply
|
542 |
# Check entity files exist |
|
0981a08…
|
noreply
|
543 |
python_file = output_dir / "Python.md" |
|
0981a08…
|
noreply
|
544 |
alice_file = output_dir / "Alice.md" |
|
0981a08…
|
noreply
|
545 |
assert python_file.exists() |
|
0981a08…
|
noreply
|
546 |
assert alice_file.exists() |
|
0981a08…
|
noreply
|
547 |
|
|
0981a08…
|
noreply
|
548 |
# Check frontmatter in entity file |
|
0981a08…
|
noreply
|
549 |
python_content = python_file.read_text() |
|
0981a08…
|
noreply
|
550 |
assert "---" in python_content |
|
0981a08…
|
noreply
|
551 |
assert "type: technology" in python_content |
|
0981a08…
|
noreply
|
552 |
assert "# Python" in python_content |
|
0981a08…
|
noreply
|
553 |
|
|
0981a08…
|
noreply
|
554 |
# Check wiki-links in Alice file |
|
0981a08…
|
noreply
|
555 |
alice_content = alice_file.read_text() |
|
0981a08…
|
noreply
|
556 |
assert "[[Python]]" in alice_content |
|
0981a08…
|
noreply
|
557 |
assert "uses" in alice_content |
|
0981a08…
|
noreply
|
558 |
|
|
0981a08…
|
noreply
|
559 |
# Check index file |
|
0981a08…
|
noreply
|
560 |
index_file = output_dir / "_Index.md" |
|
0981a08…
|
noreply
|
561 |
assert index_file.exists() |
|
0981a08…
|
noreply
|
562 |
index_content = index_file.read_text() |
|
0981a08…
|
noreply
|
563 |
assert "[[Python]]" in index_content |
|
0981a08…
|
noreply
|
564 |
assert "[[Alice]]" in index_content |
|
0981a08…
|
noreply
|
565 |
|
|
0981a08…
|
noreply
|
566 |
def test_export_to_obsidian_with_artifacts(self, tmp_path): |
|
0981a08…
|
noreply
|
567 |
from video_processor.agent.skills.notes_export import export_to_obsidian |
|
0981a08…
|
noreply
|
568 |
|
|
0981a08…
|
noreply
|
569 |
art = Artifact( |
|
0981a08…
|
noreply
|
570 |
name="Test Plan", |
|
0981a08…
|
noreply
|
571 |
content="# Plan\n\nSteps here.", |
|
0981a08…
|
noreply
|
572 |
artifact_type="project_plan", |
|
0981a08…
|
noreply
|
573 |
format="markdown", |
|
0981a08…
|
noreply
|
574 |
) |
|
0981a08…
|
noreply
|
575 |
output_dir = tmp_path / "obsidian_arts" |
|
0981a08…
|
noreply
|
576 |
export_to_obsidian(self._sample_kg_data(), output_dir, artifacts=[art]) |
|
0981a08…
|
noreply
|
577 |
|
|
0981a08…
|
noreply
|
578 |
art_file = output_dir / "Test Plan.md" |
|
0981a08…
|
noreply
|
579 |
assert art_file.exists() |
|
0981a08…
|
noreply
|
580 |
art_content = art_file.read_text() |
|
0981a08…
|
noreply
|
581 |
assert "artifact" in art_content |
|
0981a08…
|
noreply
|
582 |
assert "Steps here." in art_content |
|
0981a08…
|
noreply
|
583 |
|
|
0981a08…
|
noreply
|
584 |
def test_export_to_notion_md(self, tmp_path): |
|
0981a08…
|
noreply
|
585 |
from video_processor.agent.skills.notes_export import export_to_notion_md |
|
0981a08…
|
noreply
|
586 |
|
|
0981a08…
|
noreply
|
587 |
output_dir = tmp_path / "notion_export" |
|
0981a08…
|
noreply
|
588 |
export_to_notion_md(self._sample_kg_data(), output_dir) |
|
0981a08…
|
noreply
|
589 |
|
|
0981a08…
|
noreply
|
590 |
assert output_dir.is_dir() |
|
0981a08…
|
noreply
|
591 |
|
|
0981a08…
|
noreply
|
592 |
# Check CSV database file |
|
0981a08…
|
noreply
|
593 |
csv_file = output_dir / "entities_database.csv" |
|
0981a08…
|
noreply
|
594 |
assert csv_file.exists() |
|
0981a08…
|
noreply
|
595 |
csv_content = csv_file.read_text() |
|
0981a08…
|
noreply
|
596 |
assert "Name" in csv_content |
|
0981a08…
|
noreply
|
597 |
assert "Type" in csv_content |
|
0981a08…
|
noreply
|
598 |
assert "Python" in csv_content |
|
0981a08…
|
noreply
|
599 |
assert "Alice" in csv_content |
|
0981a08…
|
noreply
|
600 |
|
|
0981a08…
|
noreply
|
601 |
# Check entity markdown files |
|
0981a08…
|
noreply
|
602 |
python_file = output_dir / "Python.md" |
|
0981a08…
|
noreply
|
603 |
assert python_file.exists() |
|
0981a08…
|
noreply
|
604 |
python_content = python_file.read_text() |
|
0981a08…
|
noreply
|
605 |
assert "# Python" in python_content |
|
0981a08…
|
noreply
|
606 |
assert "technology" in python_content |
|
0981a08…
|
noreply
|
607 |
|
|
0981a08…
|
noreply
|
608 |
# Check overview file |
|
0981a08…
|
noreply
|
609 |
overview_file = output_dir / "Overview.md" |
|
0981a08…
|
noreply
|
610 |
assert overview_file.exists() |
|
0981a08…
|
noreply
|
611 |
|
|
0981a08…
|
noreply
|
612 |
def test_export_to_notion_md_with_artifacts(self, tmp_path): |
|
0981a08…
|
noreply
|
613 |
from video_processor.agent.skills.notes_export import export_to_notion_md |
|
0981a08…
|
noreply
|
614 |
|
|
0981a08…
|
noreply
|
615 |
art = Artifact( |
|
0981a08…
|
noreply
|
616 |
name="Roadmap", |
|
0981a08…
|
noreply
|
617 |
content="# Roadmap\n\nQ1 goals.", |
|
0981a08…
|
noreply
|
618 |
artifact_type="roadmap", |
|
0981a08…
|
noreply
|
619 |
format="markdown", |
|
0981a08…
|
noreply
|
620 |
) |
|
0981a08…
|
noreply
|
621 |
output_dir = tmp_path / "notion_arts" |
|
0981a08…
|
noreply
|
622 |
export_to_notion_md(self._sample_kg_data(), output_dir, artifacts=[art]) |
|
0981a08…
|
noreply
|
623 |
|
|
0981a08…
|
noreply
|
624 |
art_file = output_dir / "Roadmap.md" |
|
0981a08…
|
noreply
|
625 |
assert art_file.exists() |
|
0981a08…
|
noreply
|
626 |
art_content = art_file.read_text() |
|
0981a08…
|
noreply
|
627 |
assert "Q1 goals." in art_content |