forked from hyphen-2025/cyber-pilot
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_core_structure.py
More file actions
398 lines (338 loc) · 16.6 KB
/
test_core_structure.py
File metadata and controls
398 lines (338 loc) · 16.6 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
"""
Tests for Cypilot project core structure validation.
Validates that the Cypilot project itself follows Cypilot conventions:
- Directory structure
- Base file structure (frontmatter, sections)
- Requirements file structure
- Workflow file structure
- AGENTS.md structure
"""
import re
from pathlib import Path
try:
import pytest # type: ignore
except ModuleNotFoundError: # pragma: no cover
import unittest
class _PytestShim:
@staticmethod
def skip(message: str = "") -> None:
raise unittest.SkipTest(message)
@staticmethod
def fail(message: str = "") -> None:
raise AssertionError(message)
pytest = _PytestShim() # type: ignore
PROJECT_ROOT = Path(__file__).resolve().parent.parent
class TestDirectoriesExist:
"""Validate required directories exist."""
REQUIRED_DIRS = [
"requirements",
"workflows",
"skills",
"architecture",
"tests",
]
def test_all_directories_exist(self):
for d in self.REQUIRED_DIRS:
assert (PROJECT_ROOT / d).is_dir(), f"Missing required directory: {d}"
class TestBaseStructure:
"""Validate base file structure for Cypilot specification files."""
def _get_spec_files(self):
"""Scan all .md files in requirements/ and workflows/."""
# Exclude protocol files that don't follow standard spec structure
req_exclude = {"README.md", "execution-protocol.md"}
req_files = [
f
for f in (PROJECT_ROOT / "requirements").glob("*.md")
if f.name not in req_exclude
]
wf_files = [
f
for f in (PROJECT_ROOT / "workflows").glob("*.md")
if f.name not in ("README.md", "AGENTS.md", "analyze.md", "generate.md", "plan.md", "cypilot.md", "rules.md", "adapter.md")
]
return req_files + wf_files
def _has_yaml_frontmatter(self, path: Path) -> bool:
"""Check if file has YAML frontmatter with cypilot: true."""
text = path.read_text(encoding="utf-8")
parsed = self._parse_frontmatter(text)
if parsed is None:
return False
frontmatter, _body = parsed
return str(frontmatter.get("cypilot", "")).strip().lower() == "true"
def _has_required_frontmatter_fields(self, path: Path) -> bool:
"""Check for required frontmatter fields: type, name, version, purpose."""
text = path.read_text(encoding="utf-8")
parsed = self._parse_frontmatter(text)
if parsed is None:
return False
frontmatter, _body = parsed
required = ["type", "name", "version", "purpose"]
return all(k in frontmatter and str(frontmatter[k]).strip() for k in required)
def _verify_version_format(self, path: Path) -> bool:
"""Verify version is MAJOR.MINOR format."""
text = path.read_text(encoding="utf-8")
parsed = self._parse_frontmatter(text)
if parsed is None:
return False
frontmatter, _body = parsed
return bool(re.fullmatch(r"\d+\.\d+", str(frontmatter.get("version", "")).strip()))
def _has_title_format(self, path: Path) -> bool:
"""Verify title format # Cypilot: {Title} or similar heading."""
text = path.read_text(encoding="utf-8")
parsed = self._parse_frontmatter(text)
if parsed is None:
return False
_frontmatter, body = parsed
for line in body.splitlines():
if line.strip() == "":
continue
return bool(re.match(r"^#\s+", line))
return False
def _parse_frontmatter(self, text: str):
lines = text.splitlines()
if not lines or lines[0].strip() != "---":
return None
end_idx = None
for i in range(1, len(lines)):
if lines[i].strip() == "---":
end_idx = i
break
if end_idx is None:
return None
fm_lines = lines[1:end_idx]
body_lines = lines[end_idx + 1 :]
fm = {}
for raw in fm_lines:
line = raw.strip()
if not line or line.startswith("#"):
continue
if ":" not in line:
continue
k, v = line.split(":", 1)
fm[k.strip()] = v.strip().strip('"').strip("'")
return fm, "\n".join(body_lines)
def _has_prereq_section(self, path: Path) -> bool:
"""Verify Prerequisite Checklist section exists."""
text = path.read_text(encoding="utf-8")
parsed = self._parse_frontmatter(text)
if parsed is None:
return False
_frontmatter, body = parsed
return "## Prerequisite Checklist" in body
def _has_overview_section(self, path: Path) -> bool:
"""Verify Overview section exists."""
text = path.read_text(encoding="utf-8")
parsed = self._parse_frontmatter(text)
if parsed is None:
return False
_frontmatter, body = parsed
return "## Overview" in body
def _has_validation_criteria(self, path: Path) -> bool:
"""Verify Validation Criteria section exists."""
text = path.read_text(encoding="utf-8")
parsed = self._parse_frontmatter(text)
if parsed is None:
return False
_frontmatter, body = parsed
return "## Validation Criteria" in body
def _has_validation_checklist(self, path: Path) -> bool:
"""Verify Validation Checklist section exists."""
text = path.read_text(encoding="utf-8")
parsed = self._parse_frontmatter(text)
if parsed is None:
return False
_frontmatter, body = parsed
return "## Validation Checklist" in body
def test_requirements_files_have_frontmatter(self):
"""Requirements spec files should have YAML frontmatter."""
req_dir = PROJECT_ROOT / "requirements"
spec_files = [f for f in req_dir.glob("*.md") if f.name not in ("README.md",)]
assert len(spec_files) > 0, "No requirements/*.md files found"
for f in spec_files:
assert self._has_yaml_frontmatter(f), f"{f.name} missing cypilot: true frontmatter"
def test_workflow_files_have_frontmatter(self):
"""Workflow files should have YAML frontmatter."""
wf_dir = PROJECT_ROOT / "workflows"
wf_files = [f for f in wf_dir.glob("*.md") if f.name not in ("README.md", "AGENTS.md")]
assert len(wf_files) > 0, "No workflow files found"
for f in wf_files:
assert self._has_yaml_frontmatter(f), f"{f.name} missing cypilot: true frontmatter"
class TestRequirementsStructure:
"""Validate requirements files follow structure conventions."""
def _get_req_files(self):
return list((PROJECT_ROOT / "requirements").glob("*.md"))
def test_requirements_files_have_type_requirement_or_core(self):
"""Requirements files should have type: requirement (or similar)."""
req_dir = PROJECT_ROOT / "requirements"
spec_files = [f for f in req_dir.glob("*.md") if f.name not in ("README.md",)]
for f in spec_files:
text = f.read_text(encoding="utf-8")
parsed = TestBaseStructure()._parse_frontmatter(text)
assert parsed is not None, f"{f.name} missing YAML frontmatter"
frontmatter, _body = parsed
assert frontmatter.get("type") == "requirement", f"{f.name} type is not requirement"
def test_requirements_naming_convention(self):
req_dir = PROJECT_ROOT / "requirements"
# Check that requirements files exist (kebab-case naming)
req_files = [f for f in req_dir.glob("*.md") if f.name not in ("README.md",)]
assert len(req_files) > 0, "No requirement files found"
# Verify at least one known requirement file exists
assert any(f.name.endswith(".md") for f in req_files), "No .md requirement files found"
def test_requirements_no_example_references(self):
"""Requirements should not reference examples/ directory."""
req_dir = PROJECT_ROOT / "requirements"
req_files = [f for f in req_dir.glob("*.md") if f.name not in ("README.md",)]
for f in req_files:
text = f.read_text(encoding="utf-8").lower()
assert "examples/requirements/" not in text, f"{f.name} must not reference examples"
def test_all_requirements_valid(self):
"""Assert all requirement files are valid."""
req_dir = PROJECT_ROOT / "requirements"
req_files = [f for f in req_dir.glob("*.md") if f.name not in ("README.md",)]
for f in req_files:
if not f.exists():
continue
text = f.read_text(encoding="utf-8")
parsed = TestBaseStructure()._parse_frontmatter(text)
assert parsed is not None, f"{f.name} missing frontmatter"
class TestWorkflowStructure:
"""Validate workflow file structure."""
def _get_workflow_files(self):
"""Scan workflow files."""
wf_dir = PROJECT_ROOT / "workflows"
return [f for f in wf_dir.glob("*.md") if f.name not in ("README.md", "AGENTS.md")]
def test_workflow_files_have_type_workflow(self):
"""Workflow files should have type: workflow."""
wf_dir = PROJECT_ROOT / "workflows"
wf_files = [f for f in wf_dir.glob("*.md") if f.name not in ("README.md", "AGENTS.md")]
for f in wf_files:
text = f.read_text(encoding="utf-8")
parsed = TestBaseStructure()._parse_frontmatter(text)
assert parsed is not None, f"{f.name} missing YAML frontmatter"
frontmatter, _body = parsed
assert frontmatter.get("type") == "workflow", f"{f.name} type is not workflow"
def test_workflow_prereq_checkboxes(self):
"""Workflow Prerequisite Checklist should have checkboxes."""
wf_dir = PROJECT_ROOT / "workflows"
wf_files = [f for f in wf_dir.glob("*.md") if f.name not in ("README.md", "AGENTS.md")]
for f in wf_files:
text = f.read_text(encoding="utf-8")
if "## Prerequisite Checklist" in text:
assert "- [ ]" in text or "- [x]" in text, f"{f.name} Prerequisite Checklist missing checkboxes"
def test_workflow_steps_numbered(self):
"""Workflow steps should be numbered or have phase/step structure."""
wf_dir = PROJECT_ROOT / "workflows"
# Exclude meta-workflows that embed protocols rather than having direct steps
exclude = {"README.md", "AGENTS.md", "analyze.md", "generate.md", "cypilot.md", "rules.md"}
wf_files = [f for f in wf_dir.glob("*.md") if f.name not in exclude]
for f in wf_files:
text = f.read_text(encoding="utf-8")
# Check for numbered steps, Step N pattern, or Phase N pattern
has_steps = bool(re.search(r"(^|\n)##+ (Step \d+|Phase \d+|1\.|2\.|3\.)", text))
assert has_steps, f"{f.name} missing numbered steps"
def test_workflow_next_steps(self):
"""Workflow should have Next Steps or similar conclusion."""
wf_dir = PROJECT_ROOT / "workflows"
wf_files = [f for f in wf_dir.glob("*.md") if f.name not in ("README.md", "AGENTS.md")]
for f in wf_files:
text = f.read_text(encoding="utf-8").lower()
has_conclusion = "next" in text or "after" in text or "complete" in text or "done" in text
assert has_conclusion, f"{f.name} missing next steps / conclusion"
def test_workflow_naming(self):
"""Workflow files should follow naming convention."""
wf_dir = PROJECT_ROOT / "workflows"
wf_files = [f for f in wf_dir.glob("*.md") if f.name not in ("README.md", "AGENTS.md")]
for f in wf_files:
# kebab-case naming
assert re.match(r"^[a-z][a-z0-9-]*\.md$", f.name), f"{f.name} bad naming convention"
def test_all_workflows_valid(self):
"""Assert all workflow files are valid."""
wf_dir = PROJECT_ROOT / "workflows"
wf_files = [f for f in wf_dir.glob("*.md") if f.name not in ("README.md", "AGENTS.md")]
for f in wf_files:
text = f.read_text(encoding="utf-8")
parsed = TestBaseStructure()._parse_frontmatter(text)
assert parsed is not None, f"{f.name} missing frontmatter"
class TestAgentsStructure:
"""Validate AGENTS.md file structure."""
def _load_root_agents(self):
"""Load root AGENTS.md content."""
return (PROJECT_ROOT / "AGENTS.md").read_text(encoding="utf-8")
def _verify_agents_type(self, text):
"""Verify agents file has proper structure."""
return "cypilot_path" in text or "ALWAYS" in text or "WHEN" in text
def test_root_agents_exists(self):
"""Root AGENTS.md should exist."""
assert (PROJECT_ROOT / "AGENTS.md").is_file(), "Missing root AGENTS.md"
def test_skills_agents_exists(self):
"""skills/cypilot/SKILL.md should exist as the skill definition."""
assert (PROJECT_ROOT / "skills" / "cypilot" / "SKILL.md").is_file(), "Missing skills/cypilot/SKILL.md"
def test_extract_when_clauses(self):
"""Test that WHEN clauses can be extracted from AGENTS.md."""
root_agents = PROJECT_ROOT / "AGENTS.md"
text = root_agents.read_text(encoding="utf-8")
assert "<!-- @cpt:root-agents -->" in text, "Missing root AGENTS managed block start"
assert 'cypilot_path = ".bootstrap"' in text, "Missing cypilot_path in root AGENTS.md"
assert "<!-- /@cpt:root-agents -->" in text, "Missing root AGENTS managed block end"
def test_agents_refs_exist(self):
"""AGENTS.md file references should point to existing files (excluding adapter-specific paths)."""
root_agents = PROJECT_ROOT / "AGENTS.md"
text = root_agents.read_text(encoding="utf-8")
# Extract file references like workflows/xxx.md or requirements/xxx.md
ref_pattern = re.compile(chr(96) + r"([a-zA-Z0-9_./-]+\.md)" + chr(96))
refs = ref_pattern.findall(text)
for ref in refs:
# Skip adapter-specific paths (project adapts these)
if ref.startswith(".cypilot-adapter/"):
continue
# Skip stale references that are being refactored
if ref == "workflows/AGENTS.md" or "-content.md" in ref:
continue
# Skip template references (templates are external now)
if ref.startswith("templates/"):
continue
# Skip deleted workflow requirements files
if ref.startswith("requirements/workflow-"):
continue
ref_path = PROJECT_ROOT / ref
assert ref_path.exists(), f"AGENTS.md references non-existent file: {ref}"
def test_all_agents_valid(self):
"""Assert all AGENTS.md files are valid."""
agents_files = [
f for f in PROJECT_ROOT.rglob("AGENTS.md")
if "worktrees" not in f.parts
]
assert len(agents_files) >= 2, "Expected at least 2 AGENTS.md files"
for f in agents_files:
text = f.read_text(encoding="utf-8")
if f == PROJECT_ROOT / "AGENTS.md":
assert 'cypilot_path = ".bootstrap"' in text, f"{f} missing managed cypilot_path"
else:
assert len(text) > 100, f"{f} too short"
class TestMakefileTargets:
"""Validate Makefile targets."""
def _load_makefile(self):
"""Load Makefile content."""
return (PROJECT_ROOT / "Makefile").read_text(encoding="utf-8")
def test_makefile_exists(self):
"""Makefile should exist."""
assert (PROJECT_ROOT / "Makefile").is_file(), "Missing Makefile"
def test_makefile_has_test_target(self):
"""Makefile should have test target."""
text = (PROJECT_ROOT / "Makefile").read_text(encoding="utf-8")
assert re.search(r"^test:", text, re.MULTILINE), "Makefile missing test target"
def test_makefile_has_validate_target(self):
"""Makefile should have validate or check target."""
text = (PROJECT_ROOT / "Makefile").read_text(encoding="utf-8")
has_validate = re.search(r"^(validate|check):", text, re.MULTILINE)
assert has_validate, "Makefile missing validate/check target"
def test_makefile_targets_documented(self):
"""Makefile targets should have comments."""
text = (PROJECT_ROOT / "Makefile").read_text(encoding="utf-8")
# Check for comments (lines starting with #)
has_comments = bool(re.search(r"^#", text, re.MULTILINE))
assert has_comments, "Makefile missing comments"
def test_makefile_valid(self):
"""Assert Makefile is valid."""
text = (PROJECT_ROOT / "Makefile").read_text(encoding="utf-8")
assert len(text) > 50, "Makefile too short"