prizmkit 1.0.45 → 1.0.66

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.
Files changed (67) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/adapters/claude/agent-adapter.js +2 -1
  3. package/bundled/adapters/claude/command-adapter.js +3 -3
  4. package/bundled/agents/prizm-dev-team-dev.md +1 -1
  5. package/bundled/dev-pipeline/README.md +6 -8
  6. package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +24 -19
  7. package/bundled/dev-pipeline/launch-bugfix-daemon.sh +2 -2
  8. package/bundled/dev-pipeline/launch-daemon.sh +2 -2
  9. package/bundled/dev-pipeline/lib/branch.sh +76 -0
  10. package/bundled/dev-pipeline/run-bugfix.sh +58 -149
  11. package/bundled/dev-pipeline/run.sh +60 -153
  12. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +17 -4
  13. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +2 -2
  14. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +16 -27
  15. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +20 -32
  16. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +32 -53
  17. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +29 -41
  18. package/bundled/dev-pipeline/templates/session-status-schema.json +1 -1
  19. package/bundled/dev-pipeline/tests/conftest.py +19 -126
  20. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +207 -0
  21. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +128 -141
  22. package/bundled/dev-pipeline/tests/test_utils.py +51 -110
  23. package/bundled/rules/prizm/prizm-commit-workflow.md +3 -3
  24. package/bundled/skills/_metadata.json +15 -16
  25. package/bundled/skills/app-planner/SKILL.md +8 -7
  26. package/bundled/skills/bug-fix-workflow/SKILL.md +171 -0
  27. package/bundled/skills/bug-planner/SKILL.md +25 -33
  28. package/bundled/skills/bug-planner/scripts/validate-bug-list.py +156 -0
  29. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +5 -7
  30. package/bundled/skills/dev-pipeline-launcher/SKILL.md +4 -6
  31. package/bundled/skills/feature-workflow/SKILL.md +25 -42
  32. package/bundled/skills/prizm-kit/SKILL.md +61 -23
  33. package/bundled/skills/prizm-kit/assets/{claude-md-template.md → project-memory-template.md} +3 -3
  34. package/bundled/skills/prizmkit-analyze/SKILL.md +44 -33
  35. package/bundled/skills/prizmkit-clarify/SKILL.md +40 -30
  36. package/bundled/skills/prizmkit-code-review/SKILL.md +58 -45
  37. package/bundled/skills/prizmkit-committer/SKILL.md +30 -68
  38. package/bundled/skills/prizmkit-implement/SKILL.md +60 -28
  39. package/bundled/skills/prizmkit-init/SKILL.md +57 -66
  40. package/bundled/skills/prizmkit-plan/SKILL.md +60 -23
  41. package/bundled/skills/prizmkit-prizm-docs/SKILL.md +74 -19
  42. package/bundled/skills/prizmkit-prizm-docs/assets/PRIZM-SPEC.md +23 -23
  43. package/bundled/skills/prizmkit-retrospective/SKILL.md +142 -65
  44. package/bundled/skills/prizmkit-retrospective/assets/retrospective-template.md +13 -0
  45. package/bundled/skills/prizmkit-specify/SKILL.md +69 -15
  46. package/bundled/skills/refactor-workflow/SKILL.md +116 -52
  47. package/bundled/team/prizm-dev-team.json +2 -2
  48. package/package.json +1 -1
  49. package/src/scaffold.js +4 -4
  50. package/bundled/dev-pipeline/lib/worktree.sh +0 -164
  51. package/bundled/dev-pipeline/tests/__init__.py +0 -0
  52. package/bundled/dev-pipeline/tests/test_check_session.py +0 -131
  53. package/bundled/dev-pipeline/tests/test_cleanup_logs.py +0 -119
  54. package/bundled/dev-pipeline/tests/test_detect_stuck.py +0 -207
  55. package/bundled/dev-pipeline/tests/test_generate_prompt.py +0 -190
  56. package/bundled/dev-pipeline/tests/test_init_bugfix_pipeline.py +0 -153
  57. package/bundled/dev-pipeline/tests/test_init_pipeline.py +0 -241
  58. package/bundled/dev-pipeline/tests/test_update_bug_status.py +0 -142
  59. package/bundled/dev-pipeline/tests/test_update_feature_status.py +0 -338
  60. package/bundled/dev-pipeline/tests/test_worktree.py +0 -236
  61. package/bundled/dev-pipeline/tests/test_worktree_integration.py +0 -796
  62. package/bundled/skills/prizm-kit/assets/codebuddy-md-template.md +0 -35
  63. package/bundled/skills/prizm-kit/assets/hooks/prizm-commit-hook.json +0 -15
  64. package/bundled/skills/prizmkit-summarize/SKILL.md +0 -51
  65. package/bundled/skills/prizmkit-summarize/assets/registry-template.md +0 -18
  66. package/bundled/templates/hooks/commit-intent-claude.json +0 -26
  67. /package/bundled/templates/hooks/{commit-intent-codebuddy.json → commit-intent.json} +0 -0
@@ -0,0 +1,207 @@
1
+ """Tests for generate-bootstrap-prompt.py core functions."""
2
+
3
+ import re
4
+
5
+ from generate_bootstrap_prompt import (
6
+ compute_feature_slug,
7
+ find_feature,
8
+ format_acceptance_criteria,
9
+ format_global_context,
10
+ get_completed_dependencies,
11
+ determine_pipeline_mode,
12
+ process_conditional_blocks,
13
+ process_mode_blocks,
14
+ )
15
+
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # compute_feature_slug
19
+ # ---------------------------------------------------------------------------
20
+
21
+ class TestComputeFeatureSlug:
22
+ def test_basic(self):
23
+ assert compute_feature_slug("F-001", "Project Infrastructure Setup") == "001-project-infrastructure-setup"
24
+
25
+ def test_strips_special_chars(self):
26
+ assert compute_feature_slug("F-012", "Add (User) Auth!") == "012-add-user-auth"
27
+
28
+ def test_pads_numeric(self):
29
+ assert compute_feature_slug("F-1", "Init") == "001-init"
30
+
31
+ def test_collapses_hyphens(self):
32
+ assert compute_feature_slug("F-003", "foo - - bar") == "003-foo---bar" or \
33
+ compute_feature_slug("F-003", "foo bar") == "003-foo-bar"
34
+
35
+ def test_empty_title(self):
36
+ result = compute_feature_slug("F-099", "")
37
+ assert result == "099-"
38
+
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # find_feature
42
+ # ---------------------------------------------------------------------------
43
+
44
+ class TestFindFeature:
45
+ def test_found(self):
46
+ features = [{"id": "F-001", "title": "A"}, {"id": "F-002", "title": "B"}]
47
+ assert find_feature(features, "F-002") == {"id": "F-002", "title": "B"}
48
+
49
+ def test_not_found(self):
50
+ assert find_feature([{"id": "F-001"}], "F-999") is None
51
+
52
+ def test_empty_list(self):
53
+ assert find_feature([], "F-001") is None
54
+
55
+ def test_non_dict_items(self):
56
+ assert find_feature(["garbage", 42, None, {"id": "F-001"}], "F-001") == {"id": "F-001"}
57
+
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # format_acceptance_criteria
61
+ # ---------------------------------------------------------------------------
62
+
63
+ class TestFormatAcceptanceCriteria:
64
+ def test_none(self):
65
+ assert format_acceptance_criteria(None) == "- (none specified)"
66
+
67
+ def test_empty(self):
68
+ assert format_acceptance_criteria([]) == "- (none specified)"
69
+
70
+ def test_items(self):
71
+ result = format_acceptance_criteria(["Users can log in", "Password reset works"])
72
+ assert "- Users can log in" in result
73
+ assert "- Password reset works" in result
74
+
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # format_global_context
78
+ # ---------------------------------------------------------------------------
79
+
80
+ class TestFormatGlobalContext:
81
+ def test_none(self):
82
+ assert format_global_context(None) == "- (none specified)"
83
+
84
+ def test_empty(self):
85
+ assert format_global_context({}) == "- (none specified)"
86
+
87
+ def test_dict(self):
88
+ result = format_global_context({"framework": "React", "lang": "TypeScript"})
89
+ assert "**framework**: React" in result
90
+ assert "**lang**: TypeScript" in result
91
+
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # get_completed_dependencies
95
+ # ---------------------------------------------------------------------------
96
+
97
+ class TestGetCompletedDependencies:
98
+ def test_no_deps(self):
99
+ feature = {"id": "F-002", "dependencies": []}
100
+ assert "no dependencies" in get_completed_dependencies([], feature)
101
+
102
+ def test_with_completed_dep(self):
103
+ features = [
104
+ {"id": "F-001", "title": "Setup", "status": "completed"},
105
+ {"id": "F-002", "title": "Auth", "dependencies": ["F-001"]},
106
+ ]
107
+ result = get_completed_dependencies(features, features[1])
108
+ assert "F-001" in result
109
+ assert "completed" in result
110
+
111
+ def test_no_completed_dep(self):
112
+ features = [
113
+ {"id": "F-001", "title": "Setup", "status": "pending"},
114
+ {"id": "F-002", "title": "Auth", "dependencies": ["F-001"]},
115
+ ]
116
+ result = get_completed_dependencies(features, features[1])
117
+ assert "no completed dependencies" in result
118
+
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # determine_pipeline_mode
122
+ # ---------------------------------------------------------------------------
123
+
124
+ class TestDeterminePipelineMode:
125
+ def test_low(self):
126
+ assert determine_pipeline_mode("low") == "lite"
127
+
128
+ def test_medium(self):
129
+ assert determine_pipeline_mode("medium") == "standard"
130
+
131
+ def test_high(self):
132
+ assert determine_pipeline_mode("high") == "full"
133
+
134
+ def test_critical(self):
135
+ assert determine_pipeline_mode("critical") == "full"
136
+
137
+ def test_unknown(self):
138
+ assert determine_pipeline_mode("banana") == "standard"
139
+
140
+
141
+ # ---------------------------------------------------------------------------
142
+ # process_conditional_blocks
143
+ # ---------------------------------------------------------------------------
144
+
145
+ class TestProcessConditionalBlocks:
146
+ def test_fresh_start_keeps_fresh_block(self):
147
+ tpl = "before\n{{IF_FRESH_START}}\nfresh content\n{{END_IF_FRESH_START}}\nafter"
148
+ result = process_conditional_blocks(tpl, "null")
149
+ assert "fresh content" in result
150
+ assert "IF_FRESH_START" not in result
151
+
152
+ def test_fresh_start_removes_resume_block(self):
153
+ tpl = "before\n{{IF_RESUME}}\nresume content\n{{END_IF_RESUME}}\nafter"
154
+ result = process_conditional_blocks(tpl, "null")
155
+ assert "resume content" not in result
156
+
157
+ def test_resume_keeps_resume_block(self):
158
+ tpl = "before\n{{IF_RESUME}}\nresume content\n{{END_IF_RESUME}}\nafter"
159
+ result = process_conditional_blocks(tpl, "3")
160
+ assert "resume content" in result
161
+ assert "IF_RESUME" not in result
162
+
163
+ def test_resume_removes_fresh_block(self):
164
+ tpl = "before\n{{IF_FRESH_START}}\nfresh content\n{{END_IF_FRESH_START}}\nafter"
165
+ result = process_conditional_blocks(tpl, "3")
166
+ assert "fresh content" not in result
167
+
168
+
169
+ # ---------------------------------------------------------------------------
170
+ # process_mode_blocks
171
+ # ---------------------------------------------------------------------------
172
+
173
+ class TestProcessModeBlocks:
174
+ def test_lite_mode_keeps_lite(self):
175
+ tpl = "{{IF_MODE_LITE}}lite content{{END_IF_MODE_LITE}}"
176
+ result = process_mode_blocks(tpl, "lite", init_done=True)
177
+ assert "lite content" in result
178
+ assert "IF_MODE" not in result
179
+
180
+ def test_lite_mode_removes_full(self):
181
+ tpl = "{{IF_MODE_FULL}}full content{{END_IF_MODE_FULL}}"
182
+ result = process_mode_blocks(tpl, "lite", init_done=True)
183
+ assert "full content" not in result
184
+
185
+ def test_init_done_keeps_init_done_block(self):
186
+ tpl = "{{IF_INIT_DONE}}\ninit done\n{{END_IF_INIT_DONE}}"
187
+ result = process_mode_blocks(tpl, "standard", init_done=True)
188
+ assert "init done" in result
189
+
190
+ def test_init_needed_when_not_done(self):
191
+ tpl = "{{IF_INIT_NEEDED}}\nneed init\n{{END_IF_INIT_NEEDED}}"
192
+ result = process_mode_blocks(tpl, "standard", init_done=False)
193
+ assert "need init" in result
194
+
195
+ def test_self_evolve_keeps_self_evolve_and_full(self):
196
+ tpl = (
197
+ "{{IF_MODE_SELF_EVOLVE}}se content{{END_IF_MODE_SELF_EVOLVE}}"
198
+ "{{IF_MODE_FULL}}full content{{END_IF_MODE_FULL}}"
199
+ )
200
+ result = process_mode_blocks(tpl, "self-evolve", init_done=True)
201
+ assert "se content" in result
202
+ assert "full content" in result
203
+
204
+ def test_self_evolve_removes_lite(self):
205
+ tpl = "{{IF_MODE_LITE}}lite content{{END_IF_MODE_LITE}}"
206
+ result = process_mode_blocks(tpl, "self-evolve", init_done=True)
207
+ assert "lite content" not in result
@@ -1,181 +1,168 @@
1
- """Tests for generate-bugfix-prompt.py."""
1
+ """Tests for generate-bugfix-prompt.py core functions."""
2
2
 
3
- import os
4
- import sys
5
- import pytest
3
+ from generate_bugfix_prompt import (
4
+ find_bug,
5
+ format_acceptance_criteria,
6
+ format_global_context,
7
+ format_error_source_details,
8
+ format_environment,
9
+ process_conditional_blocks,
10
+ render_template,
11
+ )
6
12
 
7
13
 
8
- def _import_generate_bugfix_prompt():
9
- import importlib.util
10
- path = os.path.join(
11
- os.path.dirname(__file__), "..", "scripts", "generate-bugfix-prompt.py"
12
- )
13
- spec = importlib.util.spec_from_file_location("generate_bugfix_prompt", path)
14
- mod = importlib.util.module_from_spec(spec)
15
- sys.modules["generate_bugfix_prompt"] = mod
16
- spec.loader.exec_module(mod)
17
- return mod
14
+ # ---------------------------------------------------------------------------
15
+ # find_bug
16
+ # ---------------------------------------------------------------------------
18
17
 
18
+ class TestFindBug:
19
+ def test_found(self):
20
+ bugs = [{"id": "B-001", "title": "A"}, {"id": "B-002", "title": "B"}]
21
+ assert find_bug(bugs, "B-002") == {"id": "B-002", "title": "B"}
19
22
 
20
- gen_bugfix = _import_generate_bugfix_prompt()
21
- format_acceptance_criteria = gen_bugfix.format_acceptance_criteria
22
- format_global_context = gen_bugfix.format_global_context
23
- format_error_source_details = gen_bugfix.format_error_source_details
24
- format_environment = gen_bugfix.format_environment
25
- process_conditional_blocks = gen_bugfix.process_conditional_blocks
23
+ def test_not_found(self):
24
+ assert find_bug([{"id": "B-001"}], "B-999") is None
26
25
 
26
+ def test_empty_list(self):
27
+ assert find_bug([], "B-001") is None
27
28
 
28
- class TestFormatAcceptanceCriteria:
29
- def test_normal(self):
30
- result = format_acceptance_criteria(["Fix A", "Fix B"])
31
- assert result == "- Fix A\n- Fix B"
29
+ def test_non_dict_items(self):
30
+ assert find_bug(["garbage", 42, None, {"id": "B-001"}], "B-001") == {"id": "B-001"}
32
31
 
33
- def test_empty(self):
34
- result = format_acceptance_criteria([])
35
- assert "none specified" in result
36
-
37
- def test_none(self):
38
- result = format_acceptance_criteria(None)
39
- assert "none specified" in result
40
32
 
41
- def test_single(self):
42
- result = format_acceptance_criteria(["Only one"])
43
- assert result == "- Only one"
44
-
45
-
46
- class TestFormatGlobalContext:
47
- def test_normal(self):
48
- result = format_global_context({"lang": "Python"})
49
- assert "**lang**" in result
50
- assert "Python" in result
51
-
52
- def test_empty(self):
53
- assert "none specified" in format_global_context({})
33
+ # ---------------------------------------------------------------------------
34
+ # format_error_source_details
35
+ # ---------------------------------------------------------------------------
54
36
 
37
+ class TestFormatErrorSourceDetails:
55
38
  def test_none(self):
56
- assert "none specified" in format_global_context(None)
39
+ result = format_error_source_details(None)
40
+ assert "no error source" in result
57
41
 
42
+ def test_empty_dict(self):
43
+ result = format_error_source_details({})
44
+ assert "no error source" in result
58
45
 
59
- class TestFormatErrorSourceDetails:
60
46
  def test_stack_trace(self):
61
- error_source = {
62
- "type": "stack_trace",
63
- "stack_trace": "Traceback ... Error",
64
- "error_message": "Something broke",
65
- }
66
- result = format_error_source_details(error_source)
47
+ source = {"type": "stack_trace", "stack_trace": "Error at line 42"}
48
+ result = format_error_source_details(source)
49
+ assert "Error at line 42" in result
67
50
  assert "Stack Trace" in result
68
- assert "Traceback ... Error" in result
69
- assert "Error Message" in result
70
- assert "Something broke" in result
51
+
52
+ def test_error_message(self):
53
+ source = {"type": "unknown", "error_message": "Something went wrong"}
54
+ result = format_error_source_details(source)
55
+ assert "Something went wrong" in result
71
56
 
72
57
  def test_log_pattern(self):
73
- error_source = {
74
- "type": "log_pattern",
75
- "log_snippet": "ERROR 2024-01-01 ...",
76
- }
77
- result = format_error_source_details(error_source)
78
- assert "Log Snippet" in result
58
+ source = {"type": "log_pattern", "log_snippet": "FATAL: connection refused"}
59
+ result = format_error_source_details(source)
60
+ assert "connection refused" in result
79
61
 
80
62
  def test_failed_test(self):
81
- error_source = {
82
- "type": "failed_test",
83
- "failed_test_path": "tests/test_auth.py::test_login",
84
- }
85
- result = format_error_source_details(error_source)
86
- assert "Failed Test" in result
87
- assert "test_auth.py" in result
63
+ source = {"type": "failed_test", "failed_test_path": "tests/auth.test.js"}
64
+ result = format_error_source_details(source)
65
+ assert "tests/auth.test.js" in result
88
66
 
89
67
  def test_user_report(self):
90
- error_source = {
91
- "type": "user_report",
92
- "reproduction_steps": ["Step 1", "Step 2"],
93
- }
94
- result = format_error_source_details(error_source)
95
- assert "Reproduction Steps" in result
96
- assert "Step 1" in result
97
- assert "Step 2" in result
98
-
99
- def test_none_source(self):
100
- result = format_error_source_details(None)
101
- assert "no error source details" in result
68
+ source = {"type": "user_report", "reproduction_steps": ["Click login", "Enter bad password"]}
69
+ result = format_error_source_details(source)
70
+ assert "Click login" in result
71
+ assert "Enter bad password" in result
102
72
 
103
- def test_empty_source(self):
104
- result = format_error_source_details({})
105
- assert "no error source details" in result
106
-
107
- def test_non_dict_source(self):
108
- result = format_error_source_details("not a dict")
109
- assert "no error source details" in result
110
-
111
- def test_unknown_type_with_error_message(self):
112
- error_source = {
113
- "type": "unknown",
114
- "error_message": "Something happened",
115
- }
116
- result = format_error_source_details(error_source)
117
- assert "Error Message" in result
118
73
 
74
+ # ---------------------------------------------------------------------------
75
+ # format_environment
76
+ # ---------------------------------------------------------------------------
119
77
 
120
78
  class TestFormatEnvironment:
121
- def test_normal(self):
122
- result = format_environment({"os": "macOS", "browser": "Chrome"})
123
- assert "**browser**" in result
124
- assert "**os**" in result
79
+ def test_none(self):
80
+ assert "not specified" in format_environment(None)
125
81
 
126
82
  def test_empty(self):
127
- result = format_environment({})
128
- assert "not specified" in result
83
+ assert "not specified" in format_environment({})
129
84
 
130
- def test_none(self):
131
- result = format_environment(None)
132
- assert "not specified" in result
133
-
134
- def test_non_dict(self):
135
- result = format_environment("not a dict")
136
- assert "not specified" in result
85
+ def test_with_values(self):
86
+ result = format_environment({"os": "Linux", "node": "20.1"})
87
+ assert "**node**: 20.1" in result
88
+ assert "**os**: Linux" in result
137
89
 
138
- def test_empty_values_filtered(self):
139
- result = format_environment({"os": "macOS", "empty_key": ""})
140
- assert "os" in result
141
- # empty_key should be filtered because its value is falsy
142
- assert "empty_key" not in result
90
+ def test_skips_empty_values(self):
91
+ result = format_environment({"os": "Linux", "browser": ""})
92
+ assert "**os**: Linux" in result
93
+ assert "browser" not in result
143
94
 
144
- def test_all_empty_values(self):
145
- result = format_environment({"a": "", "b": None})
146
- assert "not specified" in result
147
95
 
96
+ # ---------------------------------------------------------------------------
97
+ # process_conditional_blocks
98
+ # ---------------------------------------------------------------------------
148
99
 
149
100
  class TestProcessConditionalBlocks:
150
- def test_manual_verification_keeps_block(self):
101
+ def test_automated_removes_manual_block(self):
102
+ tpl = "before\n{{IF_VERIFICATION_MANUAL_OR_HYBRID}}\nmanual content\n{{END_IF_VERIFICATION_MANUAL_OR_HYBRID}}\nafter"
103
+ bug = {"verification_type": "automated"}
104
+ result = process_conditional_blocks(tpl, bug)
105
+ assert "manual content" not in result
106
+ assert "before" in result
107
+ assert "after" in result
108
+
109
+ def test_manual_keeps_block(self):
110
+ tpl = "before\n{{IF_VERIFICATION_MANUAL_OR_HYBRID}}\nmanual content\n{{END_IF_VERIFICATION_MANUAL_OR_HYBRID}}\nafter"
151
111
  bug = {"verification_type": "manual"}
152
- content = "before\n{{IF_VERIFICATION_MANUAL_OR_HYBRID}}\nmanual stuff\n{{END_IF_VERIFICATION_MANUAL_OR_HYBRID}}\nafter"
153
- result = process_conditional_blocks(content, bug)
154
- assert "manual stuff" in result
155
- assert "{{IF_VERIFICATION" not in result
112
+ result = process_conditional_blocks(tpl, bug)
113
+ assert "manual content" in result
114
+ assert "IF_VERIFICATION" not in result
156
115
 
157
- def test_hybrid_verification_keeps_block(self):
116
+ def test_hybrid_keeps_block(self):
117
+ tpl = "{{IF_VERIFICATION_MANUAL_OR_HYBRID}}hybrid{{END_IF_VERIFICATION_MANUAL_OR_HYBRID}}"
158
118
  bug = {"verification_type": "hybrid"}
159
- content = "{{IF_VERIFICATION_MANUAL_OR_HYBRID}}\nhybrid stuff\n{{END_IF_VERIFICATION_MANUAL_OR_HYBRID}}"
160
- result = process_conditional_blocks(content, bug)
161
- assert "hybrid stuff" in result
119
+ result = process_conditional_blocks(tpl, bug)
120
+ assert "hybrid" in result
162
121
 
163
- def test_automated_verification_removes_block(self):
164
- bug = {"verification_type": "automated"}
165
- content = "before\n{{IF_VERIFICATION_MANUAL_OR_HYBRID}}\nmanual stuff\n{{END_IF_VERIFICATION_MANUAL_OR_HYBRID}}\nafter"
166
- result = process_conditional_blocks(content, bug)
167
- assert "manual stuff" not in result
168
- assert "after" in result
169
-
170
- def test_no_verification_type_defaults_automated(self):
122
+ def test_default_is_automated(self):
123
+ tpl = "{{IF_VERIFICATION_MANUAL_OR_HYBRID}}content{{END_IF_VERIFICATION_MANUAL_OR_HYBRID}}"
171
124
  bug = {}
172
- content = "{{IF_VERIFICATION_MANUAL_OR_HYBRID}}\nstuff\n{{END_IF_VERIFICATION_MANUAL_OR_HYBRID}}\nrest"
173
- result = process_conditional_blocks(content, bug)
174
- assert "stuff" not in result
175
- assert "rest" in result
125
+ result = process_conditional_blocks(tpl, bug)
126
+ assert "content" not in result
127
+
176
128
 
177
- def test_no_conditional_blocks(self):
129
+ # ---------------------------------------------------------------------------
130
+ # render_template (integration)
131
+ # ---------------------------------------------------------------------------
132
+
133
+ class TestRenderTemplate:
134
+ def test_placeholders_replaced(self):
135
+ tpl = "Bug: {{BUG_ID}} — {{BUG_TITLE}}"
136
+ replacements = {"{{BUG_ID}}": "B-042", "{{BUG_TITLE}}": "Login crash"}
137
+ bug = {"verification_type": "automated"}
138
+ result = render_template(tpl, replacements, bug)
139
+ assert result == "Bug: B-042 — Login crash"
140
+
141
+ def test_conditional_and_replacement(self):
142
+ tpl = (
143
+ "{{IF_VERIFICATION_MANUAL_OR_HYBRID}}manual{{END_IF_VERIFICATION_MANUAL_OR_HYBRID}}"
144
+ "id={{BUG_ID}}"
145
+ )
146
+ replacements = {"{{BUG_ID}}": "B-001"}
178
147
  bug = {"verification_type": "automated"}
179
- content = "plain text"
180
- result = process_conditional_blocks(content, bug)
181
- assert result == "plain text"
148
+ result = render_template(tpl, replacements, bug)
149
+ assert "manual" not in result
150
+ assert "id=B-001" in result
151
+
152
+
153
+ # ---------------------------------------------------------------------------
154
+ # Reuse shared helpers (same as bootstrap prompt — verify they are present)
155
+ # ---------------------------------------------------------------------------
156
+
157
+ class TestSharedHelpers:
158
+ def test_format_acceptance_criteria_items(self):
159
+ result = format_acceptance_criteria(["Fix works", "No regression"])
160
+ assert "- Fix works" in result
161
+ assert "- No regression" in result
162
+
163
+ def test_format_acceptance_criteria_empty(self):
164
+ assert "none specified" in format_acceptance_criteria([])
165
+
166
+ def test_format_global_context_dict(self):
167
+ result = format_global_context({"lang": "Python"})
168
+ assert "**lang**: Python" in result