forked from hyphen-2025/cyber-pilot
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_context.py
More file actions
298 lines (248 loc) · 11.1 KB
/
test_context.py
File metadata and controls
298 lines (248 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
"""
Tests for CypilotContext and related functions.
Tests cover:
- CypilotContext methods: get_template, get_template_for_kind, get_known_id_kinds
- Global context functions: get_context, set_context, ensure_context
"""
import json
import sys
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import MagicMock, patch
sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "cypilot" / "scripts"))
from cypilot.utils.context import (
CypilotContext,
LoadedKit,
get_context,
set_context,
ensure_context,
_global_context,
)
from cypilot.utils.artifacts_meta import ArtifactsMeta, Kit
from cypilot.utils.constraints import ArtifactKindConstraints, IdConstraint, KitConstraints
def _make_mock_template(kind: str, blocks: list = None) -> MagicMock:
"""Create a mock Template with kind and blocks."""
tmpl = MagicMock()
tmpl.kind = kind
tmpl.blocks = blocks or []
return tmpl
def _make_mock_block(block_type: str, name: str) -> MagicMock:
"""Create a mock TemplateBlock."""
block = MagicMock()
block.type = block_type
block.name = name
return block
class TestCypilotContextMethods:
"""Tests for CypilotContext instance methods."""
def _make_context(self) -> CypilotContext:
"""Create a mock CypilotContext with templates."""
# Create mock templates
prd_tmpl = _make_mock_template("PRD", [
_make_mock_block("id", "fr"),
_make_mock_block("id", "actor"),
])
design_tmpl = _make_mock_template("DESIGN", [
_make_mock_block("id", "component"),
_make_mock_block("id", "seq"),
])
spec_tmpl = _make_mock_template("SPEC", [
_make_mock_block("id", "flow"),
_make_mock_block("id", "algo"),
])
# Create kits
kit1 = Kit(kit_id="cypilot-sdlc", format="Cypilot", path="kits/sdlc")
kit2 = Kit(kit_id="custom", format="Cypilot", path="kits/custom")
loaded_kit1 = LoadedKit(
kit=kit1,
templates={"PRD": prd_tmpl, "DESIGN": design_tmpl},
constraints=KitConstraints(by_kind={
"PRD": ArtifactKindConstraints(name=None, description=None, defined_id=[
IdConstraint(kind="fr"),
IdConstraint(kind="actor"),
]),
"DESIGN": ArtifactKindConstraints(name=None, description=None, defined_id=[
IdConstraint(kind="component"),
IdConstraint(kind="seq"),
]),
}),
)
loaded_kit2 = LoadedKit(
kit=kit2,
templates={"SPEC": spec_tmpl},
constraints=KitConstraints(by_kind={
"SPEC": ArtifactKindConstraints(name=None, description=None, defined_id=[
IdConstraint(kind="flow"),
IdConstraint(kind="algo"),
]),
}),
)
# Create mock meta
meta = MagicMock(spec=ArtifactsMeta)
meta.project_root = ".."
return CypilotContext(
adapter_dir=Path("/fake/adapter"),
project_root=Path("/fake/project"),
meta=meta,
kits={"cypilot-sdlc": loaded_kit1, "custom": loaded_kit2},
registered_systems={"myapp", "test-system"},
_errors=[{"type": "context", "message": "error1"}, {"type": "context", "message": "error2"}],
)
def test_get_known_id_kinds(self):
"""get_known_id_kinds extracts id kinds from template markers."""
ctx = self._make_context()
id_kinds = ctx.get_known_id_kinds()
# PRD has fr, actor; DESIGN has component, seq; SPEC has flow, algo
assert id_kinds == {"fr", "actor", "component", "seq", "flow", "algo"}
class TestGlobalContextFunctions:
"""Tests for global context getter/setter functions."""
def teardown_method(self, method):
"""Reset global context after each test."""
set_context(None)
def test_get_context_initially_none(self):
"""get_context returns None when not set."""
set_context(None)
assert get_context() is None
@patch("cypilot.utils.context.WorkspaceContext.load", return_value=None)
def test_set_and_get_context(self, _mock_ws_load):
"""set_context stores context retrievable by get_context."""
mock_ctx = MagicMock(spec=CypilotContext)
set_context(mock_ctx)
# get_context() lazily attempts workspace upgrade on first call
assert get_context() is mock_ctx
def test_set_context_to_none(self):
"""set_context(None) clears the context."""
mock_ctx = MagicMock(spec=CypilotContext)
set_context(mock_ctx)
set_context(None)
assert get_context() is None
@patch("cypilot.utils.context.WorkspaceContext.load", return_value=None)
@patch("cypilot.utils.context.CypilotContext.load")
def test_ensure_context_loads_when_none(self, mock_load, _mock_ws_load):
"""ensure_context loads context when global is None."""
set_context(None)
mock_ctx = MagicMock(spec=CypilotContext)
mock_load.return_value = mock_ctx
result = ensure_context()
mock_load.assert_called_once_with(None)
assert result is mock_ctx
assert get_context() is mock_ctx
@patch("cypilot.utils.context.WorkspaceContext.load", return_value=None)
@patch("cypilot.utils.context.CypilotContext.load")
def test_ensure_context_passes_start_path(self, mock_load, _mock_ws_load):
"""ensure_context passes start_path to CypilotContext.load."""
set_context(None)
mock_ctx = MagicMock(spec=CypilotContext)
mock_load.return_value = mock_ctx
start = Path("/some/path")
result = ensure_context(start)
mock_load.assert_called_once_with(start)
def test_ensure_context_returns_existing(self):
"""ensure_context returns existing context without reloading."""
existing_ctx = MagicMock(spec=CypilotContext)
set_context(existing_ctx)
with patch("cypilot.utils.context.CypilotContext.load") as mock_load:
result = ensure_context()
mock_load.assert_not_called()
assert result is existing_ctx
class TestCypilotContextLoad:
"""Tests for CypilotContext.load() method."""
def teardown_method(self, method):
"""Reset global context after each test."""
set_context(None)
@patch("cypilot.utils.files.find_cypilot_directory")
def test_load_returns_none_when_no_adapter(self, mock_find):
"""load returns None when adapter directory not found."""
mock_find.return_value = None
result = CypilotContext.load()
assert result is None
@patch("cypilot.utils.context.load_constraints_toml")
@patch("cypilot.utils.context.load_artifacts_meta")
@patch("cypilot.utils.files.find_cypilot_directory")
def test_load_success_loads_templates_and_expands_autodetect(
self,
mock_find,
mock_load_meta,
mock_load_constraints,
):
with TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
adapter_dir = tmp / "adapter"
adapter_dir.mkdir(parents=True)
# project_root = adapter_dir / '..' => tmp
# Create kit template structure: <tmp>/kits/sdlc/artifacts/PRD/template.md
tmpl_dir = tmp / "kits" / "sdlc" / "artifacts" / "PRD"
tmpl_dir.mkdir(parents=True)
(tmpl_dir / "template.md").write_text("x", encoding="utf-8")
# Create autodetect target file: <tmp>/subsystems/docs/PRD.md
docs_dir = tmp / "subsystems" / "docs"
docs_dir.mkdir(parents=True)
(docs_dir / "PRD.md").write_text("x", encoding="utf-8")
meta = ArtifactsMeta.from_dict({
"version": "1.1",
"project_root": "..",
"kits": {"k": {"format": "Cypilot", "path": "kits/sdlc"}},
"systems": [
{
"name": "App",
"slug": "app",
"kit": "k",
"autodetect": [
{
"kit": "k",
"system_root": "{project_root}/subsystems",
"artifacts_root": "{system_root}/docs",
"artifacts": {
"PRD": {"pattern": "PRD.md", "traceability": "FULL"},
"UNKNOWN": {"pattern": "missing.md", "traceability": "FULL"},
},
"validation": {"require_kind_registered_in_kit": True},
}
],
}
],
})
# Make constraints error branch execute, but still provide kit_constraints
kit_constraints = MagicMock()
kit_constraints.by_kind = {"PRD": MagicMock()}
mock_load_constraints.return_value = (kit_constraints, ["bad constraints"])
# Template.from_path returns a template for PRD
mock_find.return_value = adapter_dir
mock_load_meta.return_value = (meta, None)
ctx = CypilotContext.load()
assert ctx is not None
# We should have:
# - constraints.toml parse error surfaced
# - autodetect kind-not-registered error surfaced
msgs = [str(e.get("message", "")) for e in (ctx._errors or [])]
assert any("Invalid constraints.toml" in m for m in msgs)
assert any("Autodetect validation error" in m for m in msgs)
@patch("cypilot.utils.context.load_artifacts_meta")
@patch("cypilot.utils.files.find_cypilot_directory")
def test_load_autodetect_exception_is_captured(self, mock_find, mock_load_meta):
with TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
adapter_dir = tmp / "adapter"
adapter_dir.mkdir(parents=True)
meta = ArtifactsMeta.from_dict({
"version": "1.1",
"project_root": "..",
"kits": {},
"systems": [],
})
def boom(*args, **kwargs):
raise ValueError("boom")
meta.expand_autodetect = boom # type: ignore[assignment]
mock_find.return_value = adapter_dir
mock_load_meta.return_value = (meta, None)
ctx = CypilotContext.load()
assert ctx is not None
msgs = [str(e.get("message", "")) for e in (ctx._errors or [])]
assert any("Autodetect expansion failed" in m for m in msgs)
@patch("cypilot.utils.context.load_artifacts_meta")
@patch("cypilot.utils.files.find_cypilot_directory")
def test_load_returns_none_on_meta_error(self, mock_find, mock_load_meta):
"""load returns None when artifacts registry fails to load."""
mock_find.return_value = Path("/fake/adapter")
mock_load_meta.return_value = (None, "Some error")
result = CypilotContext.load()
assert result is None