prizmkit 1.0.35 → 1.0.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundled/VERSION.json +3 -3
- package/bundled/adapters/claude/agent-adapter.js +2 -1
- package/bundled/adapters/claude/command-adapter.js +4 -3
- package/bundled/agents/prizm-dev-team-dev.md +12 -12
- package/bundled/agents/prizm-dev-team-reviewer.md +10 -10
- package/bundled/dev-pipeline/README.md +15 -19
- package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +16 -23
- package/bundled/dev-pipeline/launch-bugfix-daemon.sh +8 -0
- package/bundled/dev-pipeline/launch-daemon.sh +2 -0
- package/bundled/dev-pipeline/lib/branch.sh +76 -0
- package/bundled/dev-pipeline/retry-bug.sh +5 -2
- package/bundled/dev-pipeline/retry-feature.sh +5 -2
- package/bundled/dev-pipeline/run-bugfix.sh +74 -0
- package/bundled/dev-pipeline/run.sh +76 -2
- package/bundled/dev-pipeline/scripts/check-session-status.py +3 -1
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +0 -8
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +1 -1
- package/bundled/dev-pipeline/scripts/update-bug-status.py +24 -1
- package/bundled/dev-pipeline/scripts/update-feature-status.py +3 -2
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +11 -25
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +12 -26
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +54 -65
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +7 -7
- package/bundled/dev-pipeline/templates/session-status-schema.json +1 -1
- package/bundled/dev-pipeline/tests/conftest.py +19 -131
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +207 -0
- package/bundled/dev-pipeline/tests/test_utils.py +51 -110
- package/bundled/rules/prizm/prizm-commit-workflow.md +3 -3
- package/bundled/skills/_metadata.json +15 -16
- package/bundled/skills/app-planner/SKILL.md +8 -7
- package/bundled/skills/bug-fix-workflow/SKILL.md +174 -0
- package/bundled/skills/bug-planner/SKILL.md +20 -32
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +3 -5
- package/bundled/skills/dev-pipeline-launcher/SKILL.md +4 -6
- package/bundled/skills/feature-workflow/SKILL.md +25 -42
- package/bundled/skills/prizm-kit/SKILL.md +57 -21
- package/bundled/skills/prizm-kit/assets/{claude-md-template.md → project-memory-template.md} +2 -2
- package/bundled/skills/prizmkit-analyze/SKILL.md +41 -29
- package/bundled/skills/prizmkit-clarify/SKILL.md +40 -30
- package/bundled/skills/prizmkit-code-review/SKILL.md +48 -43
- package/bundled/skills/prizmkit-committer/SKILL.md +30 -68
- package/bundled/skills/prizmkit-implement/SKILL.md +48 -24
- package/bundled/skills/prizmkit-init/SKILL.md +57 -66
- package/bundled/skills/prizmkit-plan/SKILL.md +46 -20
- package/bundled/skills/prizmkit-prizm-docs/SKILL.md +60 -19
- package/bundled/skills/prizmkit-prizm-docs/assets/PRIZM-SPEC.md +23 -23
- package/bundled/skills/prizmkit-retrospective/SKILL.md +142 -65
- package/bundled/skills/prizmkit-retrospective/assets/retrospective-template.md +13 -0
- package/bundled/skills/prizmkit-specify/SKILL.md +63 -13
- package/bundled/skills/refactor-workflow/SKILL.md +105 -49
- package/bundled/team/prizm-dev-team.json +5 -19
- package/package.json +1 -1
- package/src/clean.js +0 -2
- package/src/manifest.js +8 -4
- package/src/scaffold.js +72 -6
- package/src/upgrade.js +32 -5
- package/bundled/agents/prizm-dev-team-coordinator.md +0 -141
- package/bundled/agents/prizm-dev-team-pm.md +0 -126
- package/bundled/dev-pipeline/tests/__init__.py +0 -0
- package/bundled/dev-pipeline/tests/test_check_session.py +0 -127
- package/bundled/dev-pipeline/tests/test_cleanup_logs.py +0 -119
- package/bundled/dev-pipeline/tests/test_detect_stuck.py +0 -207
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +0 -181
- package/bundled/dev-pipeline/tests/test_generate_prompt.py +0 -190
- package/bundled/dev-pipeline/tests/test_init_bugfix_pipeline.py +0 -153
- package/bundled/dev-pipeline/tests/test_init_pipeline.py +0 -241
- package/bundled/dev-pipeline/tests/test_update_bug_status.py +0 -142
- package/bundled/dev-pipeline/tests/test_update_feature_status.py +0 -268
- package/bundled/skills/prizm-kit/assets/codebuddy-md-template.md +0 -35
- package/bundled/skills/prizm-kit/assets/hooks/prizm-commit-hook.json +0 -15
- package/bundled/skills/prizmkit-summarize/SKILL.md +0 -51
- package/bundled/skills/prizmkit-summarize/assets/registry-template.md +0 -18
- package/bundled/templates/hooks/commit-intent-claude.json +0 -26
- /package/bundled/templates/hooks/{commit-intent-codebuddy.json → commit-intent.json} +0 -0
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
"""Tests for generate-bootstrap-prompt.py."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import sys
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def _import_generate_prompt():
|
|
9
|
-
import importlib.util
|
|
10
|
-
path = os.path.join(
|
|
11
|
-
os.path.dirname(__file__), "..", "scripts", "generate-bootstrap-prompt.py"
|
|
12
|
-
)
|
|
13
|
-
spec = importlib.util.spec_from_file_location("generate_bootstrap_prompt", path)
|
|
14
|
-
mod = importlib.util.module_from_spec(spec)
|
|
15
|
-
sys.modules["generate_bootstrap_prompt"] = mod
|
|
16
|
-
spec.loader.exec_module(mod)
|
|
17
|
-
return mod
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
gen_prompt = _import_generate_prompt()
|
|
21
|
-
compute_feature_slug = gen_prompt.compute_feature_slug
|
|
22
|
-
format_acceptance_criteria = gen_prompt.format_acceptance_criteria
|
|
23
|
-
format_global_context = gen_prompt.format_global_context
|
|
24
|
-
process_conditional_blocks = gen_prompt.process_conditional_blocks
|
|
25
|
-
process_mode_blocks = gen_prompt.process_mode_blocks
|
|
26
|
-
determine_pipeline_mode = gen_prompt.determine_pipeline_mode
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class TestComputeFeatureSlug:
|
|
30
|
-
def test_basic(self):
|
|
31
|
-
result = compute_feature_slug("F-001", "Project Infrastructure Setup")
|
|
32
|
-
assert result == "001-project-infrastructure-setup"
|
|
33
|
-
|
|
34
|
-
def test_special_characters(self):
|
|
35
|
-
result = compute_feature_slug("F-002", "User Auth (OAuth2.0)")
|
|
36
|
-
assert result == "002-user-auth-oauth20"
|
|
37
|
-
|
|
38
|
-
def test_uppercase_id(self):
|
|
39
|
-
result = compute_feature_slug("F-042", "My Feature")
|
|
40
|
-
assert result == "042-my-feature"
|
|
41
|
-
|
|
42
|
-
def test_lowercase_f(self):
|
|
43
|
-
result = compute_feature_slug("f-5", "Quick Fix")
|
|
44
|
-
assert result == "005-quick-fix"
|
|
45
|
-
|
|
46
|
-
def test_extra_spaces(self):
|
|
47
|
-
result = compute_feature_slug("F-010", " Extra Spaces ")
|
|
48
|
-
assert result == "010-extra-spaces"
|
|
49
|
-
|
|
50
|
-
def test_empty_title(self):
|
|
51
|
-
result = compute_feature_slug("F-001", "")
|
|
52
|
-
# Numeric part only, no trailing hyphen
|
|
53
|
-
assert result.startswith("001")
|
|
54
|
-
|
|
55
|
-
def test_numeric_padding(self):
|
|
56
|
-
result = compute_feature_slug("F-1", "Test")
|
|
57
|
-
assert result.startswith("001-")
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class TestFormatAcceptanceCriteria:
|
|
61
|
-
def test_single_item(self):
|
|
62
|
-
result = format_acceptance_criteria(["Must work"])
|
|
63
|
-
assert result == "- Must work"
|
|
64
|
-
|
|
65
|
-
def test_multiple_items(self):
|
|
66
|
-
result = format_acceptance_criteria(["A", "B", "C"])
|
|
67
|
-
assert result == "- A\n- B\n- C"
|
|
68
|
-
|
|
69
|
-
def test_empty_list(self):
|
|
70
|
-
result = format_acceptance_criteria([])
|
|
71
|
-
assert "none specified" in result
|
|
72
|
-
|
|
73
|
-
def test_none_input(self):
|
|
74
|
-
result = format_acceptance_criteria(None)
|
|
75
|
-
assert "none specified" in result
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
class TestFormatGlobalContext:
|
|
79
|
-
def test_basic(self):
|
|
80
|
-
result = format_global_context({"language": "Python", "framework": "FastAPI"})
|
|
81
|
-
assert "**framework**" in result
|
|
82
|
-
assert "**language**" in result
|
|
83
|
-
assert "Python" in result
|
|
84
|
-
assert "FastAPI" in result
|
|
85
|
-
|
|
86
|
-
def test_sorted_keys(self):
|
|
87
|
-
result = format_global_context({"z_key": "z", "a_key": "a"})
|
|
88
|
-
lines = result.split("\n")
|
|
89
|
-
assert "a_key" in lines[0]
|
|
90
|
-
assert "z_key" in lines[1]
|
|
91
|
-
|
|
92
|
-
def test_empty_context(self):
|
|
93
|
-
result = format_global_context({})
|
|
94
|
-
assert "none specified" in result
|
|
95
|
-
|
|
96
|
-
def test_none_context(self):
|
|
97
|
-
result = format_global_context(None)
|
|
98
|
-
assert "none specified" in result
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
class TestProcessConditionalBlocks:
|
|
102
|
-
def test_fresh_start_keeps_fresh_block(self):
|
|
103
|
-
content = "before\n{{IF_FRESH_START}}\nfresh content\n{{END_IF_FRESH_START}}\nafter"
|
|
104
|
-
result = process_conditional_blocks(content, "null")
|
|
105
|
-
assert "fresh content" in result
|
|
106
|
-
assert "{{IF_FRESH_START}}" not in result
|
|
107
|
-
assert "after" in result
|
|
108
|
-
|
|
109
|
-
def test_fresh_start_removes_resume_block(self):
|
|
110
|
-
content = "before\n{{IF_RESUME}}\nresume content\n{{END_IF_RESUME}}\nafter"
|
|
111
|
-
result = process_conditional_blocks(content, "null")
|
|
112
|
-
assert "resume content" not in result
|
|
113
|
-
assert "after" in result
|
|
114
|
-
|
|
115
|
-
def test_resume_keeps_resume_block(self):
|
|
116
|
-
content = "before\n{{IF_RESUME}}\nresume content\n{{END_IF_RESUME}}\nafter"
|
|
117
|
-
result = process_conditional_blocks(content, "3")
|
|
118
|
-
assert "resume content" in result
|
|
119
|
-
assert "{{IF_RESUME}}" not in result
|
|
120
|
-
|
|
121
|
-
def test_resume_removes_fresh_block(self):
|
|
122
|
-
content = "before\n{{IF_FRESH_START}}\nfresh content\n{{END_IF_FRESH_START}}\nafter"
|
|
123
|
-
result = process_conditional_blocks(content, "3")
|
|
124
|
-
assert "fresh content" not in result
|
|
125
|
-
assert "after" in result
|
|
126
|
-
|
|
127
|
-
def test_no_conditional_blocks(self):
|
|
128
|
-
content = "just plain text"
|
|
129
|
-
result = process_conditional_blocks(content, "null")
|
|
130
|
-
assert result == "just plain text"
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
class TestProcessModeBlocks:
|
|
134
|
-
def test_keeps_matching_mode(self):
|
|
135
|
-
content = "before\n{{IF_MODE_LITE}}\nlite content\n{{END_IF_MODE_LITE}}\nafter"
|
|
136
|
-
result = process_mode_blocks(content, "lite", False)
|
|
137
|
-
assert "lite content" in result
|
|
138
|
-
assert "{{IF_MODE_LITE}}" not in result
|
|
139
|
-
|
|
140
|
-
def test_removes_non_matching_mode(self):
|
|
141
|
-
content = "{{IF_MODE_FULL}}\nfull content\n{{END_IF_MODE_FULL}}\nrest"
|
|
142
|
-
result = process_mode_blocks(content, "lite", False)
|
|
143
|
-
assert "full content" not in result
|
|
144
|
-
assert "rest" in result
|
|
145
|
-
|
|
146
|
-
def test_init_done_keeps_init_done_block(self):
|
|
147
|
-
content = "{{IF_INIT_DONE}}\ninit done\n{{END_IF_INIT_DONE}}\n"
|
|
148
|
-
result = process_mode_blocks(content, "standard", True)
|
|
149
|
-
assert "init done" in result
|
|
150
|
-
|
|
151
|
-
def test_init_done_removes_init_needed_block(self):
|
|
152
|
-
content = "{{IF_INIT_NEEDED}}\nneed init\n{{END_IF_INIT_NEEDED}}\n"
|
|
153
|
-
result = process_mode_blocks(content, "standard", True)
|
|
154
|
-
assert "need init" not in result
|
|
155
|
-
|
|
156
|
-
def test_init_not_done_keeps_init_needed_block(self):
|
|
157
|
-
content = "{{IF_INIT_NEEDED}}\nneed init\n{{END_IF_INIT_NEEDED}}\n"
|
|
158
|
-
result = process_mode_blocks(content, "standard", False)
|
|
159
|
-
assert "need init" in result
|
|
160
|
-
|
|
161
|
-
def test_multiple_modes(self):
|
|
162
|
-
content = (
|
|
163
|
-
"{{IF_MODE_LITE}}\nlite\n{{END_IF_MODE_LITE}}\n"
|
|
164
|
-
"{{IF_MODE_STANDARD}}\nstandard\n{{END_IF_MODE_STANDARD}}\n"
|
|
165
|
-
"{{IF_MODE_FULL}}\nfull\n{{END_IF_MODE_FULL}}\n"
|
|
166
|
-
)
|
|
167
|
-
result = process_mode_blocks(content, "standard", False)
|
|
168
|
-
assert "lite" not in result
|
|
169
|
-
assert "standard" in result
|
|
170
|
-
assert "full" not in result
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
class TestDeterminePipelineMode:
|
|
174
|
-
def test_low_complexity(self):
|
|
175
|
-
assert determine_pipeline_mode("low") == "lite"
|
|
176
|
-
|
|
177
|
-
def test_medium_complexity(self):
|
|
178
|
-
assert determine_pipeline_mode("medium") == "standard"
|
|
179
|
-
|
|
180
|
-
def test_high_complexity(self):
|
|
181
|
-
assert determine_pipeline_mode("high") == "full"
|
|
182
|
-
|
|
183
|
-
def test_critical_complexity(self):
|
|
184
|
-
assert determine_pipeline_mode("critical") == "full"
|
|
185
|
-
|
|
186
|
-
def test_unknown_complexity(self):
|
|
187
|
-
assert determine_pipeline_mode("unknown") == "standard"
|
|
188
|
-
|
|
189
|
-
def test_none_complexity(self):
|
|
190
|
-
assert determine_pipeline_mode(None) == "standard"
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
"""Tests for init-bugfix-pipeline.py."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import sys
|
|
6
|
-
import pytest
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def _import_init_bugfix_pipeline():
|
|
10
|
-
import importlib.util
|
|
11
|
-
path = os.path.join(
|
|
12
|
-
os.path.dirname(__file__), "..", "scripts", "init-bugfix-pipeline.py"
|
|
13
|
-
)
|
|
14
|
-
spec = importlib.util.spec_from_file_location("init_bugfix_pipeline", path)
|
|
15
|
-
mod = importlib.util.module_from_spec(spec)
|
|
16
|
-
sys.modules["init_bugfix_pipeline"] = mod
|
|
17
|
-
spec.loader.exec_module(mod)
|
|
18
|
-
return mod
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
init_bugfix = _import_init_bugfix_pipeline()
|
|
22
|
-
validate_schema = init_bugfix.validate_schema
|
|
23
|
-
validate_bugs = init_bugfix.validate_bugs
|
|
24
|
-
create_state_directory = init_bugfix.create_state_directory
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class TestValidateSchema:
|
|
28
|
-
def test_valid_schema(self, sample_bug_list):
|
|
29
|
-
errors = validate_schema(sample_bug_list)
|
|
30
|
-
assert errors == []
|
|
31
|
-
|
|
32
|
-
def test_wrong_schema(self):
|
|
33
|
-
data = {
|
|
34
|
-
"$schema": "wrong",
|
|
35
|
-
"project_name": "Test",
|
|
36
|
-
"bugs": [{"id": "B-001"}],
|
|
37
|
-
}
|
|
38
|
-
errors = validate_schema(data)
|
|
39
|
-
assert any("$schema" in e for e in errors)
|
|
40
|
-
|
|
41
|
-
def test_missing_project_name(self):
|
|
42
|
-
data = {
|
|
43
|
-
"$schema": "dev-pipeline-bug-fix-list-v1",
|
|
44
|
-
"bugs": [{"id": "B-001"}],
|
|
45
|
-
}
|
|
46
|
-
errors = validate_schema(data)
|
|
47
|
-
assert any("project_name" in e for e in errors)
|
|
48
|
-
|
|
49
|
-
def test_empty_project_name(self):
|
|
50
|
-
data = {
|
|
51
|
-
"$schema": "dev-pipeline-bug-fix-list-v1",
|
|
52
|
-
"project_name": " ",
|
|
53
|
-
"bugs": [{"id": "B-001"}],
|
|
54
|
-
}
|
|
55
|
-
errors = validate_schema(data)
|
|
56
|
-
assert any("project_name" in e for e in errors)
|
|
57
|
-
|
|
58
|
-
def test_missing_bugs(self):
|
|
59
|
-
data = {
|
|
60
|
-
"$schema": "dev-pipeline-bug-fix-list-v1",
|
|
61
|
-
"project_name": "Test",
|
|
62
|
-
}
|
|
63
|
-
errors = validate_schema(data)
|
|
64
|
-
assert any("bugs" in e for e in errors)
|
|
65
|
-
|
|
66
|
-
def test_bugs_not_array(self):
|
|
67
|
-
data = {
|
|
68
|
-
"$schema": "dev-pipeline-bug-fix-list-v1",
|
|
69
|
-
"project_name": "Test",
|
|
70
|
-
"bugs": "not array",
|
|
71
|
-
}
|
|
72
|
-
errors = validate_schema(data)
|
|
73
|
-
assert any("bugs must be an array" in e for e in errors)
|
|
74
|
-
|
|
75
|
-
def test_empty_bugs_array(self):
|
|
76
|
-
data = {
|
|
77
|
-
"$schema": "dev-pipeline-bug-fix-list-v1",
|
|
78
|
-
"project_name": "Test",
|
|
79
|
-
"bugs": [],
|
|
80
|
-
}
|
|
81
|
-
errors = validate_schema(data)
|
|
82
|
-
assert any("at least one bug" in e for e in errors)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
class TestValidateBugs:
|
|
86
|
-
def test_valid_bugs(self, sample_bug_list):
|
|
87
|
-
errors, ids = validate_bugs(sample_bug_list["bugs"])
|
|
88
|
-
assert errors == []
|
|
89
|
-
assert ids == {"B-001", "B-002"}
|
|
90
|
-
|
|
91
|
-
def test_duplicate_ids(self):
|
|
92
|
-
bugs = [
|
|
93
|
-
_make_bug("B-001"), _make_bug("B-001", title="Other"),
|
|
94
|
-
]
|
|
95
|
-
errors, ids = validate_bugs(bugs)
|
|
96
|
-
assert any("Duplicate" in e for e in errors)
|
|
97
|
-
|
|
98
|
-
def test_invalid_id_format(self):
|
|
99
|
-
bugs = [_make_bug("X-001")]
|
|
100
|
-
errors, ids = validate_bugs(bugs)
|
|
101
|
-
assert any("invalid id" in e for e in errors)
|
|
102
|
-
|
|
103
|
-
def test_invalid_severity(self):
|
|
104
|
-
bug = _make_bug("B-001")
|
|
105
|
-
bug["severity"] = "super_critical"
|
|
106
|
-
errors, ids = validate_bugs([bug])
|
|
107
|
-
assert any("invalid severity" in e for e in errors)
|
|
108
|
-
|
|
109
|
-
def test_invalid_verification_type(self):
|
|
110
|
-
bug = _make_bug("B-001")
|
|
111
|
-
bug["verification_type"] = "magic"
|
|
112
|
-
errors, ids = validate_bugs([bug])
|
|
113
|
-
assert any("invalid verification_type" in e for e in errors)
|
|
114
|
-
|
|
115
|
-
def test_invalid_status(self):
|
|
116
|
-
bug = _make_bug("B-001")
|
|
117
|
-
bug["status"] = "nonexistent_status"
|
|
118
|
-
errors, ids = validate_bugs([bug])
|
|
119
|
-
assert any("invalid status" in e for e in errors)
|
|
120
|
-
|
|
121
|
-
def test_missing_required_fields(self):
|
|
122
|
-
bugs = [{"id": "B-001"}]
|
|
123
|
-
errors, ids = validate_bugs(bugs)
|
|
124
|
-
assert len(errors) > 0
|
|
125
|
-
|
|
126
|
-
def test_error_source_not_object(self):
|
|
127
|
-
bug = _make_bug("B-001")
|
|
128
|
-
bug["error_source"] = "a string"
|
|
129
|
-
errors, ids = validate_bugs([bug])
|
|
130
|
-
assert any("error_source must be an object" in e for e in errors)
|
|
131
|
-
|
|
132
|
-
def test_error_source_missing_type(self):
|
|
133
|
-
bug = _make_bug("B-001")
|
|
134
|
-
bug["error_source"] = {"detail": "something"}
|
|
135
|
-
errors, ids = validate_bugs([bug])
|
|
136
|
-
assert any("error_source missing required field: type" in e for e in errors)
|
|
137
|
-
|
|
138
|
-
def test_non_dict_bug(self):
|
|
139
|
-
errors, ids = validate_bugs(["not a dict"])
|
|
140
|
-
assert any("not an object" in e for e in errors)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def _make_bug(bid, title="Test Bug"):
|
|
144
|
-
return {
|
|
145
|
-
"id": bid,
|
|
146
|
-
"title": title,
|
|
147
|
-
"description": "A bug",
|
|
148
|
-
"severity": "medium",
|
|
149
|
-
"error_source": {"type": "stack_trace"},
|
|
150
|
-
"verification_type": "automated",
|
|
151
|
-
"acceptance_criteria": ["Fixed"],
|
|
152
|
-
"status": "pending",
|
|
153
|
-
}
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
"""Tests for init-pipeline.py."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import sys
|
|
6
|
-
import importlib
|
|
7
|
-
import pytest
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
# The module has a hyphen in the name, so we need importlib
|
|
11
|
-
def _import_init_pipeline():
|
|
12
|
-
import importlib.util
|
|
13
|
-
path = os.path.join(
|
|
14
|
-
os.path.dirname(__file__), "..", "scripts", "init-pipeline.py"
|
|
15
|
-
)
|
|
16
|
-
spec = importlib.util.spec_from_file_location("init_pipeline", path)
|
|
17
|
-
mod = importlib.util.module_from_spec(spec)
|
|
18
|
-
# Prevent main() from running on import
|
|
19
|
-
sys.modules["init_pipeline"] = mod
|
|
20
|
-
spec.loader.exec_module(mod)
|
|
21
|
-
return mod
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
init_pipeline = _import_init_pipeline()
|
|
25
|
-
validate_schema = init_pipeline.validate_schema
|
|
26
|
-
validate_features = init_pipeline.validate_features
|
|
27
|
-
check_dag = init_pipeline.check_dag
|
|
28
|
-
create_state_directory = init_pipeline.create_state_directory
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class TestValidateSchema:
|
|
32
|
-
def test_valid_schema(self, sample_feature_list):
|
|
33
|
-
errors = validate_schema(sample_feature_list)
|
|
34
|
-
assert errors == []
|
|
35
|
-
|
|
36
|
-
def test_wrong_schema_value(self):
|
|
37
|
-
data = {
|
|
38
|
-
"$schema": "wrong-schema",
|
|
39
|
-
"app_name": "Test",
|
|
40
|
-
"features": [],
|
|
41
|
-
}
|
|
42
|
-
errors = validate_schema(data)
|
|
43
|
-
assert len(errors) == 1
|
|
44
|
-
assert "Invalid $schema" in errors[0]
|
|
45
|
-
|
|
46
|
-
def test_missing_schema(self):
|
|
47
|
-
data = {"app_name": "Test", "features": []}
|
|
48
|
-
errors = validate_schema(data)
|
|
49
|
-
assert any("$schema" in e for e in errors)
|
|
50
|
-
|
|
51
|
-
def test_missing_app_name(self):
|
|
52
|
-
data = {
|
|
53
|
-
"$schema": "dev-pipeline-feature-list-v1",
|
|
54
|
-
"features": [],
|
|
55
|
-
}
|
|
56
|
-
errors = validate_schema(data)
|
|
57
|
-
assert any("app_name" in e for e in errors)
|
|
58
|
-
|
|
59
|
-
def test_empty_app_name(self):
|
|
60
|
-
data = {
|
|
61
|
-
"$schema": "dev-pipeline-feature-list-v1",
|
|
62
|
-
"app_name": " ",
|
|
63
|
-
"features": [],
|
|
64
|
-
}
|
|
65
|
-
errors = validate_schema(data)
|
|
66
|
-
assert any("app_name" in e for e in errors)
|
|
67
|
-
|
|
68
|
-
def test_missing_features(self):
|
|
69
|
-
data = {
|
|
70
|
-
"$schema": "dev-pipeline-feature-list-v1",
|
|
71
|
-
"app_name": "Test",
|
|
72
|
-
}
|
|
73
|
-
errors = validate_schema(data)
|
|
74
|
-
assert any("features" in e for e in errors)
|
|
75
|
-
|
|
76
|
-
def test_features_not_array(self):
|
|
77
|
-
data = {
|
|
78
|
-
"$schema": "dev-pipeline-feature-list-v1",
|
|
79
|
-
"app_name": "Test",
|
|
80
|
-
"features": "not an array",
|
|
81
|
-
}
|
|
82
|
-
errors = validate_schema(data)
|
|
83
|
-
assert any("features must be an array" in e for e in errors)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
class TestValidateFeatures:
|
|
87
|
-
def test_valid_features(self, sample_feature_list):
|
|
88
|
-
errors, ids = validate_features(sample_feature_list["features"])
|
|
89
|
-
assert errors == []
|
|
90
|
-
assert ids == {"F-001", "F-002", "F-003"}
|
|
91
|
-
|
|
92
|
-
def test_duplicate_ids(self):
|
|
93
|
-
features = [
|
|
94
|
-
{"id": "F-001", "title": "A", "description": "D", "priority": 1,
|
|
95
|
-
"dependencies": [], "acceptance_criteria": [], "status": "pending"},
|
|
96
|
-
{"id": "F-001", "title": "B", "description": "D", "priority": 2,
|
|
97
|
-
"dependencies": [], "acceptance_criteria": [], "status": "pending"},
|
|
98
|
-
]
|
|
99
|
-
errors, ids = validate_features(features)
|
|
100
|
-
assert any("Duplicate" in e for e in errors)
|
|
101
|
-
|
|
102
|
-
def test_invalid_id_format(self):
|
|
103
|
-
features = [
|
|
104
|
-
{"id": "X-001", "title": "A", "description": "D", "priority": 1,
|
|
105
|
-
"dependencies": [], "acceptance_criteria": [], "status": "pending"},
|
|
106
|
-
]
|
|
107
|
-
errors, ids = validate_features(features)
|
|
108
|
-
assert any("invalid id" in e for e in errors)
|
|
109
|
-
|
|
110
|
-
def test_missing_required_fields(self):
|
|
111
|
-
features = [{"id": "F-001"}]
|
|
112
|
-
errors, ids = validate_features(features)
|
|
113
|
-
# Should have errors for missing title, description, etc.
|
|
114
|
-
assert len(errors) > 0
|
|
115
|
-
assert any("missing required field" in e for e in errors)
|
|
116
|
-
|
|
117
|
-
def test_non_object_feature(self):
|
|
118
|
-
features = ["not a dict"]
|
|
119
|
-
errors, ids = validate_features(features)
|
|
120
|
-
assert any("not an object" in e for e in errors)
|
|
121
|
-
|
|
122
|
-
def test_unknown_dependency(self):
|
|
123
|
-
features = [
|
|
124
|
-
{"id": "F-001", "title": "A", "description": "D", "priority": 1,
|
|
125
|
-
"dependencies": ["F-999"], "acceptance_criteria": [], "status": "pending"},
|
|
126
|
-
]
|
|
127
|
-
errors, ids = validate_features(features)
|
|
128
|
-
assert any("unknown feature" in e for e in errors)
|
|
129
|
-
|
|
130
|
-
def test_dependencies_not_array(self):
|
|
131
|
-
features = [
|
|
132
|
-
{"id": "F-001", "title": "A", "description": "D", "priority": 1,
|
|
133
|
-
"dependencies": "not-a-list", "acceptance_criteria": [], "status": "pending"},
|
|
134
|
-
]
|
|
135
|
-
errors, ids = validate_features(features)
|
|
136
|
-
assert any("dependencies must be an array" in e for e in errors)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
class TestCheckDag:
|
|
140
|
-
def test_simple_chain(self):
|
|
141
|
-
features = [
|
|
142
|
-
{"id": "F-001", "dependencies": []},
|
|
143
|
-
{"id": "F-002", "dependencies": ["F-001"]},
|
|
144
|
-
{"id": "F-003", "dependencies": ["F-002"]},
|
|
145
|
-
]
|
|
146
|
-
errors = check_dag(features)
|
|
147
|
-
assert errors == []
|
|
148
|
-
|
|
149
|
-
def test_diamond_dependency(self):
|
|
150
|
-
features = [
|
|
151
|
-
{"id": "F-001", "dependencies": []},
|
|
152
|
-
{"id": "F-002", "dependencies": ["F-001"]},
|
|
153
|
-
{"id": "F-003", "dependencies": ["F-001"]},
|
|
154
|
-
{"id": "F-004", "dependencies": ["F-002", "F-003"]},
|
|
155
|
-
]
|
|
156
|
-
errors = check_dag(features)
|
|
157
|
-
assert errors == []
|
|
158
|
-
|
|
159
|
-
def test_circular_dependency(self):
|
|
160
|
-
features = [
|
|
161
|
-
{"id": "F-001", "dependencies": ["F-003"]},
|
|
162
|
-
{"id": "F-002", "dependencies": ["F-001"]},
|
|
163
|
-
{"id": "F-003", "dependencies": ["F-002"]},
|
|
164
|
-
]
|
|
165
|
-
errors = check_dag(features)
|
|
166
|
-
assert len(errors) == 1
|
|
167
|
-
assert "cycle" in errors[0].lower()
|
|
168
|
-
|
|
169
|
-
def test_self_dependency(self):
|
|
170
|
-
features = [
|
|
171
|
-
{"id": "F-001", "dependencies": ["F-001"]},
|
|
172
|
-
]
|
|
173
|
-
errors = check_dag(features)
|
|
174
|
-
assert len(errors) == 1
|
|
175
|
-
assert "cycle" in errors[0].lower()
|
|
176
|
-
|
|
177
|
-
def test_no_features(self):
|
|
178
|
-
errors = check_dag([])
|
|
179
|
-
assert errors == []
|
|
180
|
-
|
|
181
|
-
def test_independent_features(self):
|
|
182
|
-
features = [
|
|
183
|
-
{"id": "F-001", "dependencies": []},
|
|
184
|
-
{"id": "F-002", "dependencies": []},
|
|
185
|
-
]
|
|
186
|
-
errors = check_dag(features)
|
|
187
|
-
assert errors == []
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
class TestCreateStateDirectory:
|
|
191
|
-
def test_creates_expected_structure(self, tmp_path, sample_feature_list):
|
|
192
|
-
state_dir = str(tmp_path / "state")
|
|
193
|
-
fl_path = str(tmp_path / "feature-list.json")
|
|
194
|
-
with open(fl_path, "w") as f:
|
|
195
|
-
json.dump(sample_feature_list, f)
|
|
196
|
-
|
|
197
|
-
result = create_state_directory(
|
|
198
|
-
state_dir, fl_path, sample_feature_list["features"]
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
assert os.path.isdir(result)
|
|
202
|
-
assert os.path.isfile(os.path.join(result, "pipeline.json"))
|
|
203
|
-
|
|
204
|
-
# Check per-feature dirs
|
|
205
|
-
for feature in sample_feature_list["features"]:
|
|
206
|
-
fid = feature["id"]
|
|
207
|
-
fdir = os.path.join(result, "features", fid)
|
|
208
|
-
assert os.path.isdir(fdir)
|
|
209
|
-
assert os.path.isfile(os.path.join(fdir, "status.json"))
|
|
210
|
-
assert os.path.isdir(os.path.join(fdir, "sessions"))
|
|
211
|
-
|
|
212
|
-
def test_pipeline_json_contents(self, tmp_path, sample_feature_list):
|
|
213
|
-
state_dir = str(tmp_path / "state")
|
|
214
|
-
fl_path = str(tmp_path / "fl.json")
|
|
215
|
-
with open(fl_path, "w") as f:
|
|
216
|
-
json.dump(sample_feature_list, f)
|
|
217
|
-
|
|
218
|
-
create_state_directory(state_dir, fl_path, sample_feature_list["features"])
|
|
219
|
-
|
|
220
|
-
with open(os.path.join(state_dir, "pipeline.json")) as f:
|
|
221
|
-
pipeline = json.load(f)
|
|
222
|
-
|
|
223
|
-
assert pipeline["status"] == "initialized"
|
|
224
|
-
assert pipeline["total_features"] == 3
|
|
225
|
-
assert pipeline["completed_features"] == 0
|
|
226
|
-
assert pipeline["run_id"].startswith("run-")
|
|
227
|
-
|
|
228
|
-
def test_feature_status_contents(self, tmp_path, sample_feature_list):
|
|
229
|
-
state_dir = str(tmp_path / "state")
|
|
230
|
-
fl_path = str(tmp_path / "fl.json")
|
|
231
|
-
with open(fl_path, "w") as f:
|
|
232
|
-
json.dump(sample_feature_list, f)
|
|
233
|
-
|
|
234
|
-
create_state_directory(state_dir, fl_path, sample_feature_list["features"])
|
|
235
|
-
|
|
236
|
-
with open(os.path.join(state_dir, "features", "F-001", "status.json")) as f:
|
|
237
|
-
status = json.load(f)
|
|
238
|
-
|
|
239
|
-
assert status["feature_id"] == "F-001"
|
|
240
|
-
assert status["status"] == "pending"
|
|
241
|
-
assert status["retry_count"] == 0
|