forked from hyphen-2025/cyber-pilot
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_language_config.py
More file actions
402 lines (315 loc) · 15.5 KB
/
test_language_config.py
File metadata and controls
402 lines (315 loc) · 15.5 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
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
"""
Test language configuration loading and dynamic regex building.
Validates that language_config module correctly:
- Loads configuration from .cypilot-config.json
- Falls back to defaults when config missing
- Builds correct regex patterns for different comment styles
"""
import unittest
import sys
import json
from pathlib import Path
from tempfile import TemporaryDirectory
sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "cypilot" / "scripts"))
from cypilot.utils import (
load_language_config,
build_cypilot_begin_regex,
build_cypilot_end_regex,
build_no_cypilot_begin_regex,
build_no_cypilot_end_regex,
LanguageConfig,
DEFAULT_FILE_EXTENSIONS,
)
from cypilot.utils.language_config import (
DEFAULT_SINGLE_LINE_COMMENTS,
DEFAULT_MULTI_LINE_COMMENTS,
DEFAULT_BLOCK_COMMENT_PREFIXES,
)
class TestLanguageConfigLoading(unittest.TestCase):
"""Test language configuration loading from project config (core.toml)."""
def test_default_config_when_no_project_config(self):
"""Verify default config is used when no .cypilot-config.json exists."""
with TemporaryDirectory() as tmpdir:
config = load_language_config(Path(tmpdir))
# Should have default extensions
self.assertEqual(config.file_extensions, DEFAULT_FILE_EXTENSIONS)
self.assertIn(".py", config.file_extensions)
self.assertIn(".js", config.file_extensions)
self.assertIn(".rs", config.file_extensions)
# Should have default comment styles
self.assertIn("#", config.single_line_comments)
self.assertIn("//", config.single_line_comments)
self.assertIn("--", config.single_line_comments)
def _write_project_config(self, root: Path, core_toml_content: str) -> None:
"""Helper to set up AGENTS.md TOML block + config/core.toml."""
(root / "AGENTS.md").write_text(
'<!-- @cpt:root-agents -->\n```toml\ncypilot_path = "adapter"\n```\n',
encoding="utf-8",
)
config_dir = root / "adapter" / "config"
config_dir.mkdir(parents=True, exist_ok=True)
(config_dir / "core.toml").write_text(core_toml_content, encoding="utf-8")
def test_custom_config_overrides_defaults(self):
"""Verify custom config from core.toml overrides defaults."""
with TemporaryDirectory() as tmpdir:
tmppath = Path(tmpdir)
# Write custom config via TOML
self._write_project_config(tmppath, '''
[code_scanning]
fileExtensions = [".php", ".rb"]
singleLineComments = ["#", "//"]
blockCommentPrefixes = ["*"]
[[code_scanning.multiLineComments]]
start = "/*"
end = "*/"
''')
config = load_language_config(tmppath)
# Should use custom extensions
self.assertEqual(config.file_extensions, {".php", ".rb"})
self.assertNotIn(".py", config.file_extensions)
# Should use custom comments
self.assertEqual(config.single_line_comments, ["#", "//"])
def test_partial_config_falls_back_to_defaults(self):
"""Verify partial config uses defaults for missing fields."""
with TemporaryDirectory() as tmpdir:
tmppath = Path(tmpdir)
# Write config with only fileExtensions
self._write_project_config(tmppath, '''
[code_scanning]
fileExtensions = [".kt", ".swift"]
''')
config = load_language_config(tmppath)
# Should use custom extensions
self.assertEqual(config.file_extensions, {".kt", ".swift"})
# Should fall back to defaults for comments
self.assertIn("#", config.single_line_comments)
self.assertIn("//", config.single_line_comments)
def test_invalid_code_scanning_type_falls_back_to_defaults(self):
"""Cover: code_scanning exists but is not a dict (string in TOML is fine, just not useful)."""
with TemporaryDirectory() as tmpdir:
tmppath = Path(tmpdir)
self._write_project_config(tmppath, 'code_scanning = "not-a-dict"\n')
config = load_language_config(tmppath)
self.assertEqual(config.file_extensions, DEFAULT_FILE_EXTENSIONS)
self.assertEqual(config.single_line_comments, DEFAULT_SINGLE_LINE_COMMENTS)
def test_invalid_scanning_field_types_fall_back_to_defaults(self):
"""Cover: wrong types inside code_scanning for list-like fields."""
with TemporaryDirectory() as tmpdir:
tmppath = Path(tmpdir)
self._write_project_config(tmppath, '''
[code_scanning]
fileExtensions = "not-a-list"
singleLineComments = "not-a-list"
blockCommentPrefixes = 123
[code_scanning.multiLineComments]
start = "/*"
end = "*/"
''')
config = load_language_config(tmppath)
self.assertEqual(config.file_extensions, DEFAULT_FILE_EXTENSIONS)
self.assertEqual(config.single_line_comments, DEFAULT_SINGLE_LINE_COMMENTS)
# Note: in TOML, multiLineComments as a table is a dict, not a list
self.assertEqual(config.multi_line_comments, DEFAULT_MULTI_LINE_COMMENTS)
self.assertEqual(config.block_comment_prefixes, DEFAULT_BLOCK_COMMENT_PREFIXES)
class TestRegexPatternBuilding(unittest.TestCase):
"""Test dynamic regex pattern building from language config."""
def test_cypilot_begin_regex_matches_python_style(self):
"""Verify cpt-begin regex matches Python # comments."""
config = LanguageConfig(
file_extensions={".py"},
single_line_comments=["#"],
multi_line_comments=[],
block_comment_prefixes=[]
)
regex = build_cypilot_begin_regex(config)
# Should match Python comment
self.assertIsNotNone(regex.match("# cpt-begin cpt-test-feature-x-flow-y:p1:inst-step"))
self.assertIsNotNone(regex.match(" # cpt-begin cpt-test-feature-x-flow-y:p1:inst-step"))
# Should extract tag
match = regex.match("# cpt-begin cpt-test-feature-x-flow-y:p1:inst-step")
self.assertEqual(match.group(1), "cpt-test-feature-x-flow-y:p1:inst-step")
def test_cypilot_begin_regex_matches_javascript_style(self):
"""Verify cpt-begin regex matches JavaScript // comments."""
config = LanguageConfig(
file_extensions={".js"},
single_line_comments=["//"],
multi_line_comments=[],
block_comment_prefixes=[]
)
regex = build_cypilot_begin_regex(config)
# Should match JS comment
self.assertIsNotNone(regex.match("// cpt-begin cpt-test-feature-x-flow-y:p1:inst-step"))
self.assertIsNotNone(regex.match(" // cpt-begin cpt-test-feature-x-flow-y:p1:inst-step"))
def test_cypilot_begin_regex_matches_sql_style(self):
"""Verify cpt-begin regex matches SQL -- comments."""
config = LanguageConfig(
file_extensions={".sql"},
single_line_comments=["--"],
multi_line_comments=[],
block_comment_prefixes=[]
)
regex = build_cypilot_begin_regex(config)
# Should match SQL comment
self.assertIsNotNone(regex.match("-- cpt-begin cpt-test-feature-x-flow-y:p1:inst-step"))
def test_cypilot_begin_regex_matches_html_comment(self):
"""Verify cpt-begin regex matches HTML <!-- comments."""
config = LanguageConfig(
file_extensions={".html"},
single_line_comments=[],
multi_line_comments=[{"start": "<!--", "end": "-->"}],
block_comment_prefixes=[]
)
regex = build_cypilot_begin_regex(config)
# Should match HTML comment
self.assertIsNotNone(regex.match("<!-- cpt-begin cpt-test-feature-x-flow-y:p1:inst-step"))
def test_cypilot_begin_regex_matches_multiple_styles(self):
"""Verify cpt-begin regex matches multiple comment styles."""
config = LanguageConfig(
file_extensions={".py", ".js", ".sql"},
single_line_comments=["#", "//", "--"],
multi_line_comments=[{"start": "/*", "end": "*/"}],
block_comment_prefixes=["*"]
)
regex = build_cypilot_begin_regex(config)
# Should match all styles
self.assertIsNotNone(regex.match("# cpt-begin cpt-test-feature-x-flow-y:p1:inst-step"))
self.assertIsNotNone(regex.match("// cpt-begin cpt-test-feature-x-flow-y:p1:inst-step"))
self.assertIsNotNone(regex.match("-- cpt-begin cpt-test-feature-x-flow-y:p1:inst-step"))
self.assertIsNotNone(regex.match("/* cpt-begin cpt-test-feature-x-flow-y:p1:inst-step"))
self.assertIsNotNone(regex.match("* cpt-begin cpt-test-feature-x-flow-y:p1:inst-step"))
def test_cypilot_end_regex_matches_same_styles_as_begin(self):
"""Verify cpt-end regex matches same styles as cpt-begin."""
config = LanguageConfig(
file_extensions={".py", ".js"},
single_line_comments=["#", "//"],
multi_line_comments=[],
block_comment_prefixes=[]
)
end_regex = build_cypilot_end_regex(config)
# Should match both styles
self.assertIsNotNone(end_regex.match("# cpt-end cpt-test-feature-x-flow-y:p1:inst-step"))
self.assertIsNotNone(end_regex.match("// cpt-end cpt-test-feature-x-flow-y:p1:inst-step"))
def test_no_cypilot_begin_regex_matches_exclusion_marker(self):
"""Verify !no-cpt-begin regex matches exclusion markers."""
config = LanguageConfig(
file_extensions={".py"},
single_line_comments=["#"],
multi_line_comments=[{"start": "<!--", "end": "-->"}],
block_comment_prefixes=[]
)
regex = build_no_cypilot_begin_regex(config)
# Should match exclusion markers
self.assertIsNotNone(regex.match("# !no-cpt-begin"))
self.assertIsNotNone(regex.match("# Some text !no-cpt-begin"))
self.assertIsNotNone(regex.match("<!-- !no-cpt-begin -->"))
def test_no_cypilot_end_regex_matches_exclusion_marker(self):
"""Verify !no-cpt-end regex matches exclusion end markers."""
config = LanguageConfig(
file_extensions={".py"},
single_line_comments=["#"],
multi_line_comments=[{"start": "<!--", "end": "-->"}],
block_comment_prefixes=[]
)
regex = build_no_cypilot_end_regex(config)
# Should match exclusion end markers
self.assertIsNotNone(regex.match("# !no-cpt-end"))
self.assertIsNotNone(regex.match("<!-- !no-cpt-end -->"))
class TestCommentPatternBuilding(unittest.TestCase):
"""Test comment pattern building for regex."""
def test_build_comment_pattern_escapes_special_chars(self):
"""Verify special regex characters are properly escaped."""
config = LanguageConfig(
file_extensions={".py"},
single_line_comments=["#", "//"],
multi_line_comments=[{"start": "/*", "end": "*/"}],
block_comment_prefixes=["*"]
)
pattern = config.build_comment_pattern()
# Should contain escaped versions
self.assertIn(r"\#", pattern)
self.assertIn(r"//", pattern)
self.assertIn(r"/\*", pattern)
self.assertIn(r"\*", pattern)
# Should be wrapped in non-capturing group
self.assertTrue(pattern.startswith("(?:"))
self.assertTrue(pattern.endswith(")"))
def test_build_comment_pattern_includes_all_prefixes(self):
"""Verify all comment prefixes are included in pattern."""
config = LanguageConfig(
file_extensions={".py"},
single_line_comments=["#", "//", "--"],
multi_line_comments=[{"start": "<!--", "end": "-->"}],
block_comment_prefixes=["*"]
)
pattern = config.build_comment_pattern()
# Should include all single-line styles (some escaped)
self.assertIn("#", pattern)
self.assertIn("//", pattern)
self.assertIn("\\-\\-", pattern) # -- gets escaped to \-\-
# Should include multi-line start markers (escaped)
self.assertIn("<!\\-\\-", pattern) # <!-- gets escaped
# Should include block prefixes
self.assertIn("*", pattern)
class TestCodebaseEntryCommentFields(unittest.TestCase):
"""Test CodebaseEntry parsing of singleLineComments / multiLineComments."""
def test_from_dict_with_comment_fields(self):
from cypilot.utils.artifacts_meta import CodebaseEntry
entry = CodebaseEntry.from_dict({
"path": "src",
"extensions": [".py"],
"singleLineComments": ["#"],
"multiLineComments": [{"start": '"""', "end": '"""'}],
})
self.assertEqual(entry.single_line_comments, ["#"])
self.assertEqual(entry.multi_line_comments, [{"start": '"""', "end": '"""'}])
def test_from_dict_without_comment_fields(self):
from cypilot.utils.artifacts_meta import CodebaseEntry
entry = CodebaseEntry.from_dict({
"path": "src",
"extensions": [".ts"],
})
self.assertIsNone(entry.single_line_comments)
self.assertIsNone(entry.multi_line_comments)
def test_from_dict_empty_comment_lists(self):
from cypilot.utils.artifacts_meta import CodebaseEntry
entry = CodebaseEntry.from_dict({
"path": "src",
"extensions": [".css"],
"singleLineComments": [],
"multiLineComments": [],
})
# Explicit empty list = "no comments of this type" (valid override, not None)
self.assertEqual(entry.single_line_comments, [])
self.assertIsNone(entry.multi_line_comments) # empty after filtering invalid items
def test_from_dict_malformed_multiline_ignored(self):
from cypilot.utils.artifacts_meta import CodebaseEntry
entry = CodebaseEntry.from_dict({
"path": "src",
"extensions": [".py"],
"multiLineComments": [{"start": "/*"}], # missing 'end'
})
self.assertIsNone(entry.multi_line_comments)
class TestCommentDefaultsForExtensions(unittest.TestCase):
"""Test comment_defaults_for_extensions() utility."""
def test_python_defaults(self):
from cypilot.utils.language_config import comment_defaults_for_extensions
slc, mlc = comment_defaults_for_extensions([".py"])
self.assertEqual(slc, ["#"])
self.assertEqual(mlc, [{"start": '"""', "end": '"""'}])
def test_js_defaults(self):
from cypilot.utils.language_config import comment_defaults_for_extensions
slc, mlc = comment_defaults_for_extensions([".js"])
self.assertEqual(slc, ["//"])
self.assertEqual(mlc, [{"start": "/*", "end": "*/"}])
def test_mixed_extensions_deduplicates(self):
from cypilot.utils.language_config import comment_defaults_for_extensions
slc, mlc = comment_defaults_for_extensions([".ts", ".tsx"])
self.assertEqual(slc, ["//"]) # deduplicated
self.assertEqual(len(mlc), 1) # deduplicated
def test_unknown_extension_returns_empty(self):
from cypilot.utils.language_config import comment_defaults_for_extensions
slc, mlc = comment_defaults_for_extensions([".xyz"])
self.assertEqual(slc, [])
self.assertEqual(mlc, [])
if __name__ == "__main__":
unittest.main()