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.
Files changed (74) 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 +4 -3
  4. package/bundled/agents/prizm-dev-team-dev.md +12 -12
  5. package/bundled/agents/prizm-dev-team-reviewer.md +10 -10
  6. package/bundled/dev-pipeline/README.md +15 -19
  7. package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +16 -23
  8. package/bundled/dev-pipeline/launch-bugfix-daemon.sh +8 -0
  9. package/bundled/dev-pipeline/launch-daemon.sh +2 -0
  10. package/bundled/dev-pipeline/lib/branch.sh +76 -0
  11. package/bundled/dev-pipeline/retry-bug.sh +5 -2
  12. package/bundled/dev-pipeline/retry-feature.sh +5 -2
  13. package/bundled/dev-pipeline/run-bugfix.sh +74 -0
  14. package/bundled/dev-pipeline/run.sh +76 -2
  15. package/bundled/dev-pipeline/scripts/check-session-status.py +3 -1
  16. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +0 -8
  17. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +1 -1
  18. package/bundled/dev-pipeline/scripts/update-bug-status.py +24 -1
  19. package/bundled/dev-pipeline/scripts/update-feature-status.py +3 -2
  20. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +11 -25
  21. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +12 -26
  22. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +54 -65
  23. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +7 -7
  24. package/bundled/dev-pipeline/templates/session-status-schema.json +1 -1
  25. package/bundled/dev-pipeline/tests/conftest.py +19 -131
  26. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +207 -0
  27. package/bundled/dev-pipeline/tests/test_utils.py +51 -110
  28. package/bundled/rules/prizm/prizm-commit-workflow.md +3 -3
  29. package/bundled/skills/_metadata.json +15 -16
  30. package/bundled/skills/app-planner/SKILL.md +8 -7
  31. package/bundled/skills/bug-fix-workflow/SKILL.md +174 -0
  32. package/bundled/skills/bug-planner/SKILL.md +20 -32
  33. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +3 -5
  34. package/bundled/skills/dev-pipeline-launcher/SKILL.md +4 -6
  35. package/bundled/skills/feature-workflow/SKILL.md +25 -42
  36. package/bundled/skills/prizm-kit/SKILL.md +57 -21
  37. package/bundled/skills/prizm-kit/assets/{claude-md-template.md → project-memory-template.md} +2 -2
  38. package/bundled/skills/prizmkit-analyze/SKILL.md +41 -29
  39. package/bundled/skills/prizmkit-clarify/SKILL.md +40 -30
  40. package/bundled/skills/prizmkit-code-review/SKILL.md +48 -43
  41. package/bundled/skills/prizmkit-committer/SKILL.md +30 -68
  42. package/bundled/skills/prizmkit-implement/SKILL.md +48 -24
  43. package/bundled/skills/prizmkit-init/SKILL.md +57 -66
  44. package/bundled/skills/prizmkit-plan/SKILL.md +46 -20
  45. package/bundled/skills/prizmkit-prizm-docs/SKILL.md +60 -19
  46. package/bundled/skills/prizmkit-prizm-docs/assets/PRIZM-SPEC.md +23 -23
  47. package/bundled/skills/prizmkit-retrospective/SKILL.md +142 -65
  48. package/bundled/skills/prizmkit-retrospective/assets/retrospective-template.md +13 -0
  49. package/bundled/skills/prizmkit-specify/SKILL.md +63 -13
  50. package/bundled/skills/refactor-workflow/SKILL.md +105 -49
  51. package/bundled/team/prizm-dev-team.json +5 -19
  52. package/package.json +1 -1
  53. package/src/clean.js +0 -2
  54. package/src/manifest.js +8 -4
  55. package/src/scaffold.js +72 -6
  56. package/src/upgrade.js +32 -5
  57. package/bundled/agents/prizm-dev-team-coordinator.md +0 -141
  58. package/bundled/agents/prizm-dev-team-pm.md +0 -126
  59. package/bundled/dev-pipeline/tests/__init__.py +0 -0
  60. package/bundled/dev-pipeline/tests/test_check_session.py +0 -127
  61. package/bundled/dev-pipeline/tests/test_cleanup_logs.py +0 -119
  62. package/bundled/dev-pipeline/tests/test_detect_stuck.py +0 -207
  63. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +0 -181
  64. package/bundled/dev-pipeline/tests/test_generate_prompt.py +0 -190
  65. package/bundled/dev-pipeline/tests/test_init_bugfix_pipeline.py +0 -153
  66. package/bundled/dev-pipeline/tests/test_init_pipeline.py +0 -241
  67. package/bundled/dev-pipeline/tests/test_update_bug_status.py +0 -142
  68. package/bundled/dev-pipeline/tests/test_update_feature_status.py +0 -268
  69. package/bundled/skills/prizm-kit/assets/codebuddy-md-template.md +0 -35
  70. package/bundled/skills/prizm-kit/assets/hooks/prizm-commit-hook.json +0 -15
  71. package/bundled/skills/prizmkit-summarize/SKILL.md +0 -51
  72. package/bundled/skills/prizmkit-summarize/assets/registry-template.md +0 -18
  73. package/bundled/templates/hooks/commit-intent-claude.json +0 -26
  74. /package/bundled/templates/hooks/{commit-intent-codebuddy.json → commit-intent.json} +0 -0
@@ -1,127 +0,0 @@
1
- """Tests for check-session-status.py."""
2
-
3
- import json
4
- import os
5
- import sys
6
- import pytest
7
-
8
-
9
- def _import_check_session():
10
- import importlib.util
11
- path = os.path.join(
12
- os.path.dirname(__file__), "..", "scripts", "check-session-status.py"
13
- )
14
- spec = importlib.util.spec_from_file_location("check_session_status", path)
15
- mod = importlib.util.module_from_spec(spec)
16
- sys.modules["check_session_status"] = mod
17
- spec.loader.exec_module(mod)
18
- return mod
19
-
20
-
21
- css = _import_check_session()
22
- validate_required_fields = css.validate_required_fields
23
- determine_status = css.determine_status
24
-
25
-
26
- class TestValidateRequiredFields:
27
- def test_all_present(self):
28
- data = {
29
- "session_id": "s-001",
30
- "feature_id": "F-001",
31
- "status": "success",
32
- "timestamp": "2024-01-01T00:00:00Z",
33
- }
34
- missing = validate_required_fields(data)
35
- assert missing == []
36
-
37
- def test_missing_session_id(self):
38
- data = {
39
- "feature_id": "F-001",
40
- "status": "success",
41
- "timestamp": "2024-01-01T00:00:00Z",
42
- }
43
- missing = validate_required_fields(data)
44
- assert "session_id" in missing
45
-
46
- def test_missing_multiple(self):
47
- data = {"feature_id": "F-001"}
48
- missing = validate_required_fields(data)
49
- assert "session_id" in missing
50
- assert "status" in missing
51
- assert "timestamp" in missing
52
-
53
- def test_empty_string_field(self):
54
- data = {
55
- "session_id": "",
56
- "feature_id": "F-001",
57
- "status": "success",
58
- "timestamp": "2024-01-01T00:00:00Z",
59
- }
60
- missing = validate_required_fields(data)
61
- assert "session_id" in missing
62
-
63
- def test_whitespace_only_field(self):
64
- data = {
65
- "session_id": " ",
66
- "feature_id": "F-001",
67
- "status": "success",
68
- "timestamp": "2024-01-01T00:00:00Z",
69
- }
70
- missing = validate_required_fields(data)
71
- assert "session_id" in missing
72
-
73
- def test_non_string_field(self):
74
- data = {
75
- "session_id": 123,
76
- "feature_id": "F-001",
77
- "status": "success",
78
- "timestamp": "2024-01-01T00:00:00Z",
79
- }
80
- missing = validate_required_fields(data)
81
- assert "session_id" in missing
82
-
83
- def test_empty_data(self):
84
- missing = validate_required_fields({})
85
- assert len(missing) == 4
86
-
87
-
88
- class TestDetermineStatus:
89
- def test_success(self):
90
- data = {"status": "success"}
91
- assert determine_status(data) == "success"
92
-
93
- def test_failed(self):
94
- data = {"status": "failed"}
95
- assert determine_status(data) == "failed"
96
-
97
- def test_partial_resumable(self):
98
- data = {"status": "partial", "can_resume": True}
99
- assert determine_status(data) == "partial_resumable"
100
-
101
- def test_partial_not_resumable(self):
102
- data = {"status": "partial", "can_resume": False}
103
- assert determine_status(data) == "partial_not_resumable"
104
-
105
- def test_partial_no_resume_key(self):
106
- data = {"status": "partial"}
107
- assert determine_status(data) == "partial_not_resumable"
108
-
109
- def test_commit_missing(self):
110
- data = {"status": "commit_missing"}
111
- assert determine_status(data) == "commit_missing"
112
-
113
- def test_docs_missing(self):
114
- data = {"status": "docs_missing"}
115
- assert determine_status(data) == "docs_missing"
116
-
117
- def test_unknown_status(self):
118
- data = {"status": "something_weird"}
119
- assert determine_status(data) == "crashed"
120
-
121
- def test_empty_status(self):
122
- data = {"status": ""}
123
- assert determine_status(data) == "crashed"
124
-
125
- def test_missing_status(self):
126
- data = {}
127
- assert determine_status(data) == "crashed"
@@ -1,119 +0,0 @@
1
- """Tests for cleanup-logs.py."""
2
-
3
- import json
4
- import os
5
- import subprocess
6
- import sys
7
- import time
8
-
9
-
10
- def _script_path():
11
- return os.path.join(
12
- os.path.dirname(__file__), "..", "scripts", "cleanup-logs.py"
13
- )
14
-
15
-
16
- def _write_log_file(path, content):
17
- os.makedirs(os.path.dirname(path), exist_ok=True)
18
- with open(path, "w", encoding="utf-8") as f:
19
- f.write(content)
20
-
21
-
22
- def test_iter_log_files_only_sessions_logs(state_dir):
23
- # Targeted log path under sessions/*/logs/
24
- valid_log = os.path.join(
25
- state_dir, "features", "F-001", "sessions", "S-001", "logs", "session.log"
26
- )
27
- _write_log_file(valid_log, "ok")
28
-
29
- # logs/ not under sessions should be ignored
30
- ignored_log = os.path.join(state_dir, "features", "F-001", "logs", "misc.log")
31
- _write_log_file(ignored_log, "ignore me")
32
-
33
- # Import script module for direct function test
34
- import importlib.util
35
-
36
- path = _script_path()
37
- spec = importlib.util.spec_from_file_location("cleanup_logs", path)
38
- mod = importlib.util.module_from_spec(spec)
39
- sys.modules["cleanup_logs"] = mod
40
- spec.loader.exec_module(mod)
41
-
42
- files = list(mod.iter_log_files(state_dir))
43
- assert valid_log in files
44
- assert ignored_log not in files
45
-
46
-
47
- def test_cleanup_removes_old_logs_by_retention(state_dir):
48
- old_log = os.path.join(
49
- state_dir, "features", "F-001", "sessions", "S-001", "logs", "session.log"
50
- )
51
- new_log = os.path.join(
52
- state_dir, "features", "F-001", "sessions", "S-002", "logs", "session.log"
53
- )
54
-
55
- _write_log_file(old_log, "old")
56
- _write_log_file(new_log, "new")
57
-
58
- # Make old log 20 days old
59
- old_ts = time.time() - (20 * 86400)
60
- os.utime(old_log, (old_ts, old_ts))
61
-
62
- result = subprocess.run(
63
- [
64
- sys.executable,
65
- _script_path(),
66
- "--state-dir",
67
- state_dir,
68
- "--retention-days",
69
- "14",
70
- "--max-total-mb",
71
- "1024",
72
- ],
73
- check=True,
74
- capture_output=True,
75
- text=True,
76
- )
77
-
78
- report = json.loads(result.stdout)
79
- assert report["deleted_by_reason"]["retention"] >= 1
80
- assert not os.path.exists(old_log)
81
- assert os.path.exists(new_log)
82
-
83
-
84
- def test_cleanup_enforces_total_size_cap(state_dir):
85
- log1 = os.path.join(
86
- state_dir, "features", "F-001", "sessions", "S-001", "logs", "session.log"
87
- )
88
- log2 = os.path.join(
89
- state_dir, "features", "F-001", "sessions", "S-002", "logs", "session.log"
90
- )
91
-
92
- _write_log_file(log1, "a" * 1024)
93
- _write_log_file(log2, "b" * 1024)
94
-
95
- # Ensure deterministic ordering: log1 older than log2
96
- old_ts = time.time() - 120
97
- new_ts = time.time() - 60
98
- os.utime(log1, (old_ts, old_ts))
99
- os.utime(log2, (new_ts, new_ts))
100
-
101
- result = subprocess.run(
102
- [
103
- sys.executable,
104
- _script_path(),
105
- "--state-dir",
106
- state_dir,
107
- "--retention-days",
108
- "999",
109
- "--max-total-mb",
110
- "0",
111
- ],
112
- check=True,
113
- capture_output=True,
114
- text=True,
115
- )
116
-
117
- report = json.loads(result.stdout)
118
- assert report["deleted_by_reason"]["size"] >= 1
119
- assert report["final_total_bytes"] == 0
@@ -1,207 +0,0 @@
1
- """Tests for detect-stuck.py."""
2
-
3
- import json
4
- import os
5
- import sys
6
- import pytest
7
- from datetime import datetime, timezone, timedelta
8
-
9
-
10
- def _import_detect_stuck():
11
- import importlib.util
12
- path = os.path.join(
13
- os.path.dirname(__file__), "..", "scripts", "detect-stuck.py"
14
- )
15
- spec = importlib.util.spec_from_file_location("detect_stuck", path)
16
- mod = importlib.util.module_from_spec(spec)
17
- sys.modules["detect_stuck"] = mod
18
- spec.loader.exec_module(mod)
19
- return mod
20
-
21
-
22
- ds = _import_detect_stuck()
23
- parse_iso_timestamp = ds.parse_iso_timestamp
24
- check_max_retries = ds.check_max_retries
25
- check_stuck_checkpoint = ds.check_stuck_checkpoint
26
- check_dependency_deadlock = ds.check_dependency_deadlock
27
-
28
-
29
- class TestParseIsoTimestamp:
30
- def test_standard_z_format(self):
31
- result = parse_iso_timestamp("2024-01-15T10:30:00Z")
32
- assert result is not None
33
- assert result.year == 2024
34
- assert result.month == 1
35
- assert result.day == 15
36
- assert result.hour == 10
37
- assert result.minute == 30
38
-
39
- def test_plus_zero_offset(self):
40
- result = parse_iso_timestamp("2024-01-15T10:30:00+00:00")
41
- assert result is not None
42
- assert result.year == 2024
43
-
44
- def test_with_microseconds_z(self):
45
- result = parse_iso_timestamp("2024-01-15T10:30:00.123456Z")
46
- assert result is not None
47
-
48
- def test_with_microseconds_offset(self):
49
- result = parse_iso_timestamp("2024-01-15T10:30:00.123456+00:00")
50
- assert result is not None
51
-
52
- def test_invalid_string(self):
53
- result = parse_iso_timestamp("not a timestamp")
54
- assert result is None
55
-
56
- def test_none_input(self):
57
- result = parse_iso_timestamp(None)
58
- assert result is None
59
-
60
- def test_integer_input(self):
61
- result = parse_iso_timestamp(12345)
62
- assert result is None
63
-
64
- def test_empty_string(self):
65
- result = parse_iso_timestamp("")
66
- assert result is None
67
-
68
-
69
- class TestCheckMaxRetries:
70
- def test_below_max(self):
71
- status = {"retry_count": 1}
72
- result = check_max_retries(status, 3)
73
- assert result is None
74
-
75
- def test_at_max(self):
76
- status = {"retry_count": 3}
77
- result = check_max_retries(status, 3)
78
- assert result is not None
79
- assert result["reason"] == "max_retries_exceeded"
80
-
81
- def test_above_max(self):
82
- status = {"retry_count": 5}
83
- result = check_max_retries(status, 3)
84
- assert result is not None
85
-
86
- def test_zero_retries(self):
87
- status = {"retry_count": 0}
88
- result = check_max_retries(status, 3)
89
- assert result is None
90
-
91
- def test_missing_retry_count(self):
92
- status = {}
93
- result = check_max_retries(status, 3)
94
- assert result is None # defaults to 0
95
-
96
- def test_non_int_retry_count(self):
97
- status = {"retry_count": "not a number"}
98
- result = check_max_retries(status, 3)
99
- assert result is None
100
-
101
-
102
- class TestCheckStuckCheckpoint:
103
- def _setup_sessions(self, tmp_path, checkpoints):
104
- """Create a feature dir with sessions having given checkpoints."""
105
- feature_dir = tmp_path / "feature"
106
- sessions_dir = feature_dir / "sessions"
107
- for i, cp in enumerate(checkpoints):
108
- session_dir = sessions_dir / "session-{:03d}".format(i)
109
- session_dir.mkdir(parents=True)
110
- data = {"checkpoint_reached": cp}
111
- (session_dir / "session-status.json").write_text(
112
- json.dumps(data), encoding="utf-8"
113
- )
114
- return str(feature_dir)
115
-
116
- def test_stuck_at_same_checkpoint(self, tmp_path):
117
- feature_dir = self._setup_sessions(tmp_path, ["CP-3", "CP-3", "CP-3"])
118
- result = check_stuck_checkpoint(feature_dir)
119
- assert result is not None
120
- assert result["reason"] == "stuck_at_checkpoint"
121
- assert "CP-3" in result["details"]
122
-
123
- def test_different_checkpoints(self, tmp_path):
124
- feature_dir = self._setup_sessions(tmp_path, ["CP-1", "CP-2", "CP-3"])
125
- result = check_stuck_checkpoint(feature_dir)
126
- assert result is None
127
-
128
- def test_fewer_than_three_sessions(self, tmp_path):
129
- feature_dir = self._setup_sessions(tmp_path, ["CP-3", "CP-3"])
130
- result = check_stuck_checkpoint(feature_dir)
131
- assert result is None
132
-
133
- def test_no_sessions(self, tmp_path):
134
- feature_dir = tmp_path / "feature"
135
- feature_dir.mkdir()
136
- result = check_stuck_checkpoint(str(feature_dir))
137
- assert result is None
138
-
139
- def test_none_checkpoints(self, tmp_path):
140
- feature_dir = self._setup_sessions(tmp_path, [None, None, None])
141
- result = check_stuck_checkpoint(feature_dir)
142
- assert result is None # None checkpoints don't trigger stuck
143
-
144
- def test_mixed_with_last_three_same(self, tmp_path):
145
- feature_dir = self._setup_sessions(
146
- tmp_path, ["CP-1", "CP-2", "CP-3", "CP-3", "CP-3"]
147
- )
148
- result = check_stuck_checkpoint(feature_dir)
149
- assert result is not None
150
- assert "CP-3" in result["details"]
151
-
152
-
153
- class TestCheckDependencyDeadlock:
154
- def test_dependency_failed(self, state_dir):
155
- # Create a failed dependency
156
- dep_dir = os.path.join(state_dir, "features", "F-001")
157
- os.makedirs(dep_dir, exist_ok=True)
158
- with open(os.path.join(dep_dir, "status.json"), "w") as f:
159
- json.dump({"status": "failed"}, f)
160
-
161
- feature_list_data = {
162
- "features": [
163
- {"id": "F-001", "dependencies": []},
164
- {"id": "F-002", "dependencies": ["F-001"]},
165
- ]
166
- }
167
- result = check_dependency_deadlock("F-002", feature_list_data, state_dir)
168
- assert result is not None
169
- assert result["reason"] == "dependency_failed"
170
- assert "F-001" in result["details"]
171
-
172
- def test_dependency_completed(self, state_dir):
173
- dep_dir = os.path.join(state_dir, "features", "F-001")
174
- os.makedirs(dep_dir, exist_ok=True)
175
- with open(os.path.join(dep_dir, "status.json"), "w") as f:
176
- json.dump({"status": "completed"}, f)
177
-
178
- feature_list_data = {
179
- "features": [
180
- {"id": "F-001", "dependencies": []},
181
- {"id": "F-002", "dependencies": ["F-001"]},
182
- ]
183
- }
184
- result = check_dependency_deadlock("F-002", feature_list_data, state_dir)
185
- assert result is None
186
-
187
- def test_no_dependencies(self, state_dir):
188
- feature_list_data = {
189
- "features": [
190
- {"id": "F-001", "dependencies": []},
191
- ]
192
- }
193
- result = check_dependency_deadlock("F-001", feature_list_data, state_dir)
194
- assert result is None
195
-
196
- def test_none_feature_list(self, state_dir):
197
- result = check_dependency_deadlock("F-001", None, state_dir)
198
- assert result is None
199
-
200
- def test_feature_not_found(self, state_dir):
201
- feature_list_data = {
202
- "features": [
203
- {"id": "F-001", "dependencies": []},
204
- ]
205
- }
206
- result = check_dependency_deadlock("F-999", feature_list_data, state_dir)
207
- assert result is None
@@ -1,181 +0,0 @@
1
- """Tests for generate-bugfix-prompt.py."""
2
-
3
- import os
4
- import sys
5
- import pytest
6
-
7
-
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
18
-
19
-
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
26
-
27
-
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"
32
-
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
-
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({})
54
-
55
- def test_none(self):
56
- assert "none specified" in format_global_context(None)
57
-
58
-
59
- class TestFormatErrorSourceDetails:
60
- 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)
67
- assert "Stack Trace" in result
68
- assert "Traceback ... Error" in result
69
- assert "Error Message" in result
70
- assert "Something broke" in result
71
-
72
- 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
79
-
80
- 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
88
-
89
- 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
102
-
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
-
119
-
120
- 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
125
-
126
- def test_empty(self):
127
- result = format_environment({})
128
- assert "not specified" in result
129
-
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
137
-
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
143
-
144
- def test_all_empty_values(self):
145
- result = format_environment({"a": "", "b": None})
146
- assert "not specified" in result
147
-
148
-
149
- class TestProcessConditionalBlocks:
150
- def test_manual_verification_keeps_block(self):
151
- 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
156
-
157
- def test_hybrid_verification_keeps_block(self):
158
- 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
162
-
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):
171
- 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
176
-
177
- def test_no_conditional_blocks(self):
178
- bug = {"verification_type": "automated"}
179
- content = "plain text"
180
- result = process_conditional_blocks(content, bug)
181
- assert result == "plain text"