claude-dev-env 1.38.1 → 1.40.0

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 (282) hide show
  1. package/CLAUDE.md +10 -36
  2. package/_shared/pr-loop/audit-reply-template.md +147 -0
  3. package/_shared/pr-loop/fix-protocol.md +25 -4
  4. package/_shared/pr-loop/gh-payloads.md +37 -50
  5. package/_shared/pr-loop/scripts/code_rules_gate.py +0 -60
  6. package/_shared/pr-loop/scripts/config/post_audit_thread_constants.py +199 -0
  7. package/_shared/pr-loop/scripts/config/reviews_disabled_constants.py +8 -0
  8. package/_shared/pr-loop/scripts/post_audit_thread.py +1242 -0
  9. package/_shared/pr-loop/scripts/preflight.py +129 -2
  10. package/_shared/pr-loop/scripts/reviews_disabled.py +59 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -19
  12. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +1116 -0
  13. package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +127 -0
  14. package/_shared/pr-loop/scripts/tests/test_preflight.py +41 -0
  15. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +36 -0
  16. package/_shared/pr-loop/state-schema.md +1 -1
  17. package/agents/clean-coder.md +2 -2
  18. package/agents/pr-description-writer.md +150 -52
  19. package/bin/install.mjs +6 -7
  20. package/bin/install.test.mjs +8 -0
  21. package/commands/doc-gist.md +16 -0
  22. package/commands/plan.md +0 -2
  23. package/commands/review-plan.md +1 -1
  24. package/docs/CODE_RULES.md +122 -2
  25. package/docs/PR_DESCRIPTION_GUIDE.md +127 -64
  26. package/hooks/blocking/bot_mention_comment_blocker.py +75 -0
  27. package/hooks/blocking/code_rules_enforcer.py +1143 -129
  28. package/hooks/blocking/convergence_gate_blocker.py +130 -0
  29. package/hooks/blocking/destructive_command_blocker.py +74 -0
  30. package/hooks/blocking/gh_body_arg_blocker.py +30 -0
  31. package/hooks/blocking/md_to_html_blocker.py +119 -0
  32. package/hooks/blocking/pr_description_enforcer.py +57 -22
  33. package/hooks/blocking/test_bot_mention_comment_blocker.py +131 -0
  34. package/hooks/blocking/test_code_rules_enforcer.py +21 -0
  35. package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +70 -0
  36. package/hooks/blocking/test_code_rules_enforcer_any_imports_and_cast.py +92 -0
  37. package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +143 -0
  38. package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +152 -0
  39. package/hooks/blocking/test_code_rules_enforcer_bare_except.py +120 -0
  40. package/hooks/blocking/test_code_rules_enforcer_boundary_types.py +175 -0
  41. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  42. package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +50 -0
  43. package/hooks/blocking/test_code_rules_enforcer_docstring_format.py +255 -0
  44. package/hooks/blocking/test_code_rules_enforcer_inline_tuple_string_magic.py +130 -0
  45. package/hooks/blocking/test_code_rules_enforcer_stub_implementations.py +141 -0
  46. package/hooks/blocking/test_code_rules_enforcer_test_branching.py +143 -0
  47. package/hooks/blocking/test_code_rules_enforcer_thin_wrapper_files.py +169 -0
  48. package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +99 -0
  49. package/hooks/blocking/test_code_rules_enforcer_typed_dict_pairs.py +141 -0
  50. package/hooks/blocking/test_convergence_gate_blocker.py +63 -0
  51. package/hooks/blocking/test_destructive_command_blocker.py +146 -0
  52. package/hooks/blocking/test_destructive_command_blocker_no_verify.py +102 -0
  53. package/hooks/blocking/test_gh_body_arg_blocker.py +45 -0
  54. package/hooks/blocking/test_md_to_html_blocker.py +317 -0
  55. package/hooks/blocking/test_pr_description_enforcer.py +69 -8
  56. package/hooks/config/any_type_config.py +7 -0
  57. package/hooks/config/banned_identifiers_constants.py +11 -0
  58. package/hooks/config/blocking_check_limits.py +38 -0
  59. package/hooks/config/bot_mention_comment_blocker_constants.py +20 -0
  60. package/hooks/config/code_rules_enforcer_constants.py +53 -0
  61. package/hooks/config/convergence_branch_constants.py +9 -0
  62. package/hooks/config/doc_gist_auto_publish_constants.py +18 -0
  63. package/hooks/config/html_companion_constants.py +20 -0
  64. package/hooks/config/inline_tuple_string_magic_constants.py +22 -0
  65. package/hooks/config/pr_description_enforcer_constants.py +14 -0
  66. package/hooks/config/test_banned_identifiers_constants.py +17 -0
  67. package/hooks/hooks.json +28 -20
  68. package/hooks/pyproject.toml +69 -0
  69. package/hooks/validators/mypy_integration.py +47 -1
  70. package/hooks/validators/run_all_validators.py +3 -3
  71. package/hooks/validators/test_mypy_integration.py +50 -1
  72. package/hooks/workflow/doc_gist_auto_publish.py +144 -0
  73. package/hooks/workflow/md_to_html_companion.py +365 -0
  74. package/hooks/workflow/test_doc_gist_auto_publish.py +117 -0
  75. package/hooks/workflow/test_md_to_html_companion.py +452 -0
  76. package/package.json +1 -1
  77. package/rules/gh-body-file.md +2 -0
  78. package/scripts/Install-SweepEmptyDirs.ps1 +111 -0
  79. package/scripts/check.ps1 +106 -0
  80. package/scripts/config/timing.py +11 -0
  81. package/scripts/sweep_empty_dirs.py +138 -0
  82. package/scripts/sync_to_cursor/rules.py +1 -1
  83. package/scripts/test_sweep_empty_dirs.py +183 -0
  84. package/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +323 -0
  85. package/skills/_shared/pr-loop/scripts/_cli_utils.py +22 -0
  86. package/skills/_shared/pr-loop/scripts/_path_resolver.py +165 -0
  87. package/skills/_shared/pr-loop/scripts/_xml_utils.py +20 -0
  88. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +182 -0
  89. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +185 -0
  90. package/skills/_shared/pr-loop/scripts/config/__init__.py +0 -0
  91. package/skills/_shared/pr-loop/scripts/config/path_resolver_constants.py +78 -0
  92. package/skills/_shared/pr-loop/scripts/init_loop_state.py +135 -0
  93. package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +175 -0
  94. package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +182 -0
  95. package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +206 -0
  96. package/skills/bugteam/CONSTRAINTS.md +21 -22
  97. package/skills/bugteam/EXAMPLES.md +3 -3
  98. package/skills/bugteam/PROMPTS.md +227 -67
  99. package/skills/bugteam/SKILL.md +132 -455
  100. package/skills/bugteam/reference/README.md +1 -1
  101. package/skills/bugteam/reference/audit-and-teammates.md +112 -39
  102. package/skills/bugteam/reference/audit-contract.md +4 -22
  103. package/skills/bugteam/reference/copilot-gap-analysis.md +8 -5
  104. package/skills/bugteam/reference/design-rationale.md +2 -2
  105. package/skills/bugteam/reference/github-pr-reviews.md +50 -57
  106. package/skills/bugteam/reference/obstacles/audit-assign-ids.md +13 -0
  107. package/skills/bugteam/reference/obstacles/audit-capture-excerpts.md +13 -0
  108. package/skills/bugteam/reference/obstacles/audit-walk-categories.md +13 -0
  109. package/skills/bugteam/reference/obstacles/audit-write-xml.md +13 -0
  110. package/skills/bugteam/reference/obstacles/fix-append-summary.md +13 -0
  111. package/skills/bugteam/reference/obstacles/fix-apply-fixes.md +13 -0
  112. package/skills/bugteam/reference/obstacles/fix-git-add-commit.md +13 -0
  113. package/skills/bugteam/reference/obstacles/fix-git-push.md +13 -0
  114. package/skills/bugteam/reference/obstacles/fix-post-reply.md +13 -0
  115. package/skills/bugteam/reference/obstacles/fix-publish-summary.md +13 -0
  116. package/skills/bugteam/reference/obstacles/fix-py-compile.md +13 -0
  117. package/skills/bugteam/reference/obstacles/fix-read-files.md +13 -0
  118. package/skills/bugteam/reference/obstacles/fix-resolve-thread.md +13 -0
  119. package/skills/bugteam/reference/obstacles/fix-test-suite.md +13 -0
  120. package/skills/bugteam/reference/obstacles/fix-violation-count.md +13 -0
  121. package/skills/bugteam/reference/obstacles/fix-write-xml.md +13 -0
  122. package/skills/bugteam/reference/team-setup.md +111 -9
  123. package/skills/bugteam/reference/teardown-publish-permissions.md +39 -8
  124. package/skills/bugteam/scripts/README.md +60 -0
  125. package/skills/bugteam/scripts/_claude_permissions_common.py +358 -0
  126. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +976 -0
  127. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +375 -0
  128. package/skills/bugteam/scripts/bugteam_preflight.py +328 -0
  129. package/skills/bugteam/scripts/config/bugteam_code_rules_gate_constants.py +25 -0
  130. package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +26 -0
  131. package/skills/bugteam/scripts/config/bugteam_preflight_constants.py +35 -0
  132. package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +20 -0
  133. package/skills/bugteam/scripts/config/probe_code_rules_enforcer_check_constants.py +12 -0
  134. package/skills/bugteam/scripts/config/windows_safe_rmtree_constants.py +7 -0
  135. package/skills/bugteam/scripts/grant_project_claude_permissions.py +175 -0
  136. package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +107 -0
  137. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +220 -0
  138. package/skills/bugteam/scripts/test__claude_permissions_common.py +112 -0
  139. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +400 -0
  140. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +384 -0
  141. package/skills/bugteam/scripts/test_bugteam_preflight.py +309 -0
  142. package/skills/bugteam/scripts/test_claude_permissions_common.py +195 -0
  143. package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +55 -0
  144. package/skills/bugteam/scripts/test_probe_code_rules_enforcer_check.py +76 -0
  145. package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +55 -0
  146. package/skills/bugteam/scripts/test_windows_safe_rmtree.py +108 -0
  147. package/skills/bugteam/scripts/windows_safe_rmtree.py +100 -0
  148. package/skills/bugteam/test_skill_additions.py +1 -11
  149. package/skills/code/SKILL.md +176 -0
  150. package/skills/copilot-review/SKILL.md +16 -0
  151. package/skills/doc-gist/SKILL.md +99 -0
  152. package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +453 -0
  153. package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +515 -0
  154. package/skills/doc-gist/references/examples/03-code-review-pr.html +638 -0
  155. package/skills/doc-gist/references/examples/04-code-understanding.html +491 -0
  156. package/skills/doc-gist/references/examples/05-design-system.html +629 -0
  157. package/skills/doc-gist/references/examples/06-component-variants.html +605 -0
  158. package/skills/doc-gist/references/examples/07-prototype-animation.html +455 -0
  159. package/skills/doc-gist/references/examples/08-prototype-interaction.html +396 -0
  160. package/skills/doc-gist/references/examples/09-slide-deck.html +592 -0
  161. package/skills/doc-gist/references/examples/10-svg-illustrations.html +492 -0
  162. package/skills/doc-gist/references/examples/11-status-report.html +528 -0
  163. package/skills/doc-gist/references/examples/12-incident-report.html +596 -0
  164. package/skills/doc-gist/references/examples/13-flowchart-diagram.html +395 -0
  165. package/skills/doc-gist/references/examples/14-research-feature-explainer.html +381 -0
  166. package/skills/doc-gist/references/examples/15-research-concept-explainer.html +368 -0
  167. package/skills/doc-gist/references/examples/16-implementation-plan.html +702 -0
  168. package/skills/doc-gist/references/examples/17-pr-writeup.html +595 -0
  169. package/skills/doc-gist/references/examples/18-editor-triage-board.html +573 -0
  170. package/skills/doc-gist/references/examples/19-editor-feature-flags.html +663 -0
  171. package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +722 -0
  172. package/skills/doc-gist/references/examples/README.md +5 -0
  173. package/skills/doc-gist/scripts/config/__init__.py +0 -0
  174. package/skills/doc-gist/scripts/config/gist_upload_constants.py +16 -0
  175. package/skills/doc-gist/scripts/gist_upload.py +177 -0
  176. package/skills/doc-gist/scripts/test_gist_upload.py +51 -0
  177. package/skills/findbugs/SKILL.md +96 -2
  178. package/skills/monitor-open-prs/SKILL.md +14 -32
  179. package/skills/monitor-open-prs/test_skill_contract.py +0 -11
  180. package/skills/pr-consistency-audit/SKILL.md +112 -0
  181. package/skills/pr-consistency-audit/reference/detection-rules.md +96 -0
  182. package/skills/pr-consistency-audit/reference/illustrations.md +78 -0
  183. package/skills/pr-converge/SKILL.md +229 -23
  184. package/skills/pr-converge/config/__init__.py +0 -0
  185. package/skills/pr-converge/config/constants.py +63 -0
  186. package/skills/pr-converge/reference/convergence-gates.md +138 -44
  187. package/skills/pr-converge/reference/examples.md +43 -11
  188. package/skills/pr-converge/reference/fix-protocol.md +6 -5
  189. package/skills/pr-converge/reference/ground-rules.md +5 -3
  190. package/skills/pr-converge/reference/multi-pr-orchestration.md +44 -19
  191. package/skills/pr-converge/reference/obstacles/fix-post-replies.md +13 -0
  192. package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +13 -0
  193. package/skills/pr-converge/reference/obstacles/fix-push.md +13 -0
  194. package/skills/pr-converge/reference/obstacles/fix-read-filelines.md +13 -0
  195. package/skills/pr-converge/reference/obstacles/fix-reset-state.md +13 -0
  196. package/skills/pr-converge/reference/obstacles/fix-resolve-threads.md +13 -0
  197. package/skills/pr-converge/reference/obstacles/fix-spawn-clean-coder.md +13 -0
  198. package/skills/pr-converge/reference/obstacles/fix-stage-commit.md +13 -0
  199. package/skills/pr-converge/reference/obstacles/fix-trigger-bugbot.md +13 -0
  200. package/skills/pr-converge/reference/obstacles/fix-write-test.md +13 -0
  201. package/skills/pr-converge/reference/per-tick.md +107 -31
  202. package/skills/pr-converge/reference/state-schema.md +22 -1
  203. package/skills/pr-converge/reference/stop-conditions.md +9 -7
  204. package/skills/pr-converge/scripts/README.md +34 -46
  205. package/skills/pr-converge/scripts/check_bugbot_ci.py +279 -0
  206. package/skills/pr-converge/scripts/check_convergence.py +497 -0
  207. package/skills/pr-converge/scripts/check_pending_reviews.py +154 -0
  208. package/skills/pr-converge/scripts/config/pr_converge_constants.py +118 -0
  209. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +134 -0
  210. package/skills/pr-converge/scripts/post_fix_reply.py +168 -0
  211. package/skills/pr-converge/scripts/test_check_bugbot_ci.py +312 -0
  212. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +5 -12
  213. package/skills/qbug/SKILL.md +157 -27
  214. package/skills/session-log/SKILL.md +216 -114
  215. package/skills/session-tidy/SKILL.md +1 -1
  216. package/skills/skill-builder/SKILL.md +138 -56
  217. package/skills/skill-builder/references/delegation-map.md +72 -113
  218. package/skills/skill-builder/references/progressive-disclosure.md +122 -0
  219. package/skills/skill-builder/references/self-audit-checklist.md +92 -0
  220. package/skills/skill-builder/references/skill-types.md +228 -0
  221. package/skills/skill-builder/references/thariq-x-post-skills.json +33 -0
  222. package/skills/skill-builder/templates/gap-analysis.md +15 -8
  223. package/skills/skill-builder/workflows/improve-skill.md +86 -57
  224. package/skills/skill-builder/workflows/new-skill.md +80 -168
  225. package/skills/skill-builder/workflows/polish-skill.md +78 -54
  226. package/skills/structure-prompt/SKILL.md +50 -0
  227. package/skills/structure-prompt/reference/adversarial-tuning.md +62 -0
  228. package/skills/structure-prompt/reference/block-classification.md +27 -0
  229. package/skills/structure-prompt/reference/canonical-case.md +48 -0
  230. package/skills/structure-prompt/reference/citation-depth.md +70 -0
  231. package/skills/structure-prompt/reference/cleanup.md +33 -0
  232. package/skills/structure-prompt/reference/constraints.md +33 -0
  233. package/skills/structure-prompt/reference/directives.md +37 -0
  234. package/skills/structure-prompt/reference/examples.md +72 -0
  235. package/skills/structure-prompt/reference/instantiation.md +51 -0
  236. package/skills/structure-prompt/reference/output-contract.md +72 -0
  237. package/skills/structure-prompt/reference/per-category.md +23 -0
  238. package/skills/structure-prompt/reference/persona.md +38 -0
  239. package/skills/structure-prompt/reference/research.md +33 -0
  240. package/skills/structure-prompt/reference/structure.md +28 -0
  241. package/agents/code-standards-agent.md +0 -93
  242. package/agents/groq-coder.md +0 -113
  243. package/agents/plan-executor.md +0 -226
  244. package/agents/project-docs-analyzer.md +0 -53
  245. package/agents/project-structure-organizer-agent.md +0 -72
  246. package/agents/skill-to-agent-converter.md +0 -370
  247. package/agents/skill-writer-agent.md +0 -470
  248. package/agents/user-docs-writer.md +0 -67
  249. package/agents/workflow-visual-documenter.md +0 -82
  250. package/commands/readability-review.md +0 -20
  251. package/hooks/mypy.ini +0 -2
  252. package/hooks/notification/attention_needed_notify.py +0 -71
  253. package/hooks/notification/claude_notification_handler.py +0 -67
  254. package/hooks/notification/notification_utils.py +0 -267
  255. package/hooks/notification/subagent_complete_notify.py +0 -381
  256. package/hooks/notification/test_attention_needed_notify.py +0 -47
  257. package/hooks/notification/test_claude_notification_handler.py +0 -54
  258. package/hooks/notification/test_notification_utils.py +0 -91
  259. package/hooks/notification/test_subagent_complete_notify.py +0 -79
  260. package/scripts/config/groq_bugteam_config.py +0 -230
  261. package/scripts/config/test_groq_bugteam_config.py +0 -83
  262. package/scripts/config/test_spec_implementer_prompt.py +0 -32
  263. package/scripts/groq_bugteam.README.md +0 -131
  264. package/scripts/groq_bugteam.py +0 -647
  265. package/scripts/groq_bugteam_dotenv.py +0 -40
  266. package/scripts/groq_bugteam_spec.py +0 -226
  267. package/scripts/test_groq_bugteam.py +0 -529
  268. package/scripts/test_groq_bugteam_apply_fix_from_spec.py +0 -426
  269. package/scripts/test_groq_bugteam_dotenv.py +0 -66
  270. package/scripts/test_groq_bugteam_spec.py +0 -338
  271. package/skills/bugteam/SKILL_EVALS.md +0 -309
  272. package/skills/dream/SKILL.md +0 -118
  273. package/skills/ingest/SKILL.md +0 -40
  274. package/skills/npm-creator/SKILL.md +0 -187
  275. package/skills/readability-review/SKILL.md +0 -127
  276. package/skills/resume-review/SKILL.md +0 -261
  277. package/skills/rule-audit/SKILL.md +0 -307
  278. package/skills/rule-creator/SKILL.md +0 -150
  279. package/skills/searching-obsidian-vault/SKILL.md +0 -131
  280. package/skills/skill-writer/REFERENCE.md +0 -284
  281. package/skills/skill-writer/SKILL.md +0 -222
  282. package/skills/tdd-team/SKILL.md +0 -128
@@ -1,529 +0,0 @@
1
- """Tests for groq_bugteam.py pure logic.
2
-
3
- Network calls (Groq HTTP) and filesystem/git side effects are out of scope for
4
- unit tests; they are exercised in the live end-to-end run.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import importlib.util
10
- import pathlib
11
- import re
12
- import sys
13
- import urllib.error
14
-
15
- scripts_directory = pathlib.Path(__file__).parent
16
- scripts_directory_string = str(scripts_directory)
17
- if scripts_directory_string not in sys.path:
18
- sys.path.insert(0, scripts_directory_string)
19
- for _cached in list(sys.modules):
20
- if _cached == "config" or _cached.startswith("config."):
21
- del sys.modules[_cached]
22
-
23
- import groq_bugteam_dotenv # noqa: E402
24
- import pytest # noqa: E402
25
-
26
- from config import groq_bugteam_config # noqa: E402
27
-
28
-
29
- def _load_groq_bugteam_module():
30
- scripts_directory = pathlib.Path(__file__).parent
31
- scripts_directory_string = str(scripts_directory)
32
- if scripts_directory_string not in sys.path:
33
- sys.path.insert(0, scripts_directory_string)
34
- for cached_module_name in list(sys.modules):
35
- if cached_module_name == "config" or cached_module_name.startswith("config."):
36
- del sys.modules[cached_module_name]
37
- module_path = scripts_directory / "groq_bugteam.py"
38
- module_spec = importlib.util.spec_from_file_location("groq_bugteam", module_path)
39
- loaded_module = importlib.util.module_from_spec(module_spec)
40
- sys.modules["groq_bugteam"] = loaded_module
41
- module_spec.loader.exec_module(loaded_module)
42
- return loaded_module
43
-
44
-
45
- groq_bugteam = _load_groq_bugteam_module()
46
-
47
-
48
- class TestConstantsSourcedFromConfig:
49
- def test_endpoint_is_imported_from_config(self):
50
- assert groq_bugteam.GROQ_API_ENDPOINT == groq_bugteam_config.GROQ_API_ENDPOINT
51
-
52
- def test_primary_model_is_imported_from_config(self):
53
- assert groq_bugteam.GROQ_PRIMARY_MODEL == groq_bugteam_config.GROQ_PRIMARY_MODEL
54
-
55
-
56
- class TestClampText:
57
- def test_returns_text_unchanged_when_under_limit(self):
58
- assert groq_bugteam.clamp_text("hello world", 100) == "hello world"
59
-
60
- def test_truncates_long_text_with_marker(self):
61
- long_text = "a" * 1000
62
- clamped = groq_bugteam.clamp_text(long_text, 200)
63
- assert "truncated" in clamped
64
- assert len(clamped) < len(long_text)
65
- assert clamped.startswith("a")
66
- assert clamped.endswith("a")
67
-
68
- def test_preserves_head_and_tail(self):
69
- text = "HEAD" + ("x" * 1000) + "TAIL"
70
- clamped = groq_bugteam.clamp_text(text, 100)
71
- assert clamped.startswith("HEAD")
72
- assert clamped.endswith("TAIL")
73
-
74
- @pytest.mark.parametrize("max_characters", [50, 100, 200, 500, 1000])
75
- def test_output_never_exceeds_max_characters(self, max_characters):
76
- long_text = "a" * 5000
77
- clamped = groq_bugteam.clamp_text(long_text, max_characters)
78
- assert len(clamped) <= max_characters
79
-
80
- def test_returns_plain_head_when_marker_does_not_fit(self):
81
- long_text = "a" * 1000
82
- tiny_budget = 10
83
- clamped = groq_bugteam.clamp_text(long_text, tiny_budget)
84
- assert len(clamped) <= tiny_budget
85
- assert clamped == long_text[:tiny_budget]
86
- assert "truncated" not in clamped
87
-
88
- def test_truncation_marker_count_matches_characters_actually_dropped(self):
89
- long_text = "a" * 1000
90
- max_characters = 200
91
- clamped = groq_bugteam.clamp_text(long_text, max_characters)
92
- marker_match = re.search(r"truncated (\d+) chars", clamped)
93
- assert marker_match is not None
94
- reported_truncated_count = int(marker_match.group(1))
95
- full_marker = f"\n\n... [truncated {reported_truncated_count} chars] ...\n\n"
96
- preserved_original_length = len(clamped) - len(full_marker)
97
- actually_truncated_count = len(long_text) - preserved_original_length
98
- assert reported_truncated_count == actually_truncated_count
99
-
100
-
101
- class TestParseJsonObject:
102
- def test_parses_clean_json(self):
103
- parsed = groq_bugteam.parse_json_object('{"findings": []}')
104
- assert parsed == {"findings": []}
105
-
106
- def test_extracts_json_from_surrounding_prose(self):
107
- noisy_response = 'Sure, here is the result:\n\n{"findings": [{"severity": "P1"}]}\n\nLet me know if you need more.'
108
- parsed = groq_bugteam.parse_json_object(noisy_response)
109
- assert parsed == {"findings": [{"severity": "P1"}]}
110
-
111
- def test_raises_when_no_json_present(self):
112
- with pytest.raises(ValueError):
113
- groq_bugteam.parse_json_object("no braces here")
114
-
115
-
116
- class TestNormalizeFindings:
117
- def test_drops_findings_with_unknown_files(self):
118
- raw_findings = [
119
- {
120
- "severity": "P0",
121
- "category": "H",
122
- "file": "known.py",
123
- "line": 10,
124
- "title": "t",
125
- "description": "d",
126
- },
127
- {
128
- "severity": "P1",
129
- "category": "A",
130
- "file": "unknown.py",
131
- "line": 5,
132
- "title": "t2",
133
- "description": "d2",
134
- },
135
- ]
136
- normalized = groq_bugteam.normalize_findings(raw_findings, {"known.py": ""})
137
- assert len(normalized) == 1
138
- assert normalized[0]["file"] == "known.py"
139
-
140
- def test_coerces_non_string_line_to_int(self):
141
- raw_findings = [
142
- {
143
- "severity": "P0",
144
- "category": "H",
145
- "file": "a.py",
146
- "line": "42",
147
- "title": "t",
148
- "description": "d",
149
- },
150
- ]
151
- normalized = groq_bugteam.normalize_findings(raw_findings, {"a.py": ""})
152
- assert normalized[0]["line"] == 42
153
-
154
- def test_defaults_to_zero_line_on_bad_value(self):
155
- raw_findings = [
156
- {
157
- "severity": "P1",
158
- "category": "H",
159
- "file": "a.py",
160
- "line": "not-a-number",
161
- "title": "t",
162
- "description": "d",
163
- },
164
- ]
165
- normalized = groq_bugteam.normalize_findings(raw_findings, {"a.py": ""})
166
- assert normalized[0]["line"] == 0
167
-
168
- def test_clamps_invalid_severity_to_p2(self):
169
- raw_findings = [
170
- {
171
- "severity": "CRITICAL",
172
- "category": "H",
173
- "file": "a.py",
174
- "line": 1,
175
- "title": "t",
176
- "description": "d",
177
- },
178
- ]
179
- normalized = groq_bugteam.normalize_findings(raw_findings, {"a.py": ""})
180
- assert normalized[0]["severity"] == "P2"
181
-
182
- def test_keeps_single_letter_category(self):
183
- raw_findings = [
184
- {
185
- "severity": "P0",
186
- "category": "HIJ",
187
- "file": "a.py",
188
- "line": 1,
189
- "title": "t",
190
- "description": "d",
191
- },
192
- ]
193
- normalized = groq_bugteam.normalize_findings(raw_findings, {"a.py": ""})
194
- assert normalized[0]["category"] == "H"
195
-
196
- def test_handles_empty_input(self):
197
- assert groq_bugteam.normalize_findings([], {"a.py": ""}) == []
198
-
199
-
200
- class TestGroupFindingsByFile:
201
- def test_groups_findings_and_preserves_global_indexes(self):
202
- findings = [
203
- {
204
- "file": "a.py",
205
- "severity": "P0",
206
- "category": "H",
207
- "line": 1,
208
- "title": "t1",
209
- "description": "d1",
210
- },
211
- {
212
- "file": "b.py",
213
- "severity": "P1",
214
- "category": "A",
215
- "line": 2,
216
- "title": "t2",
217
- "description": "d2",
218
- },
219
- {
220
- "file": "a.py",
221
- "severity": "P2",
222
- "category": "E",
223
- "line": 3,
224
- "title": "t3",
225
- "description": "d3",
226
- },
227
- ]
228
- grouped = groq_bugteam.group_findings_by_file(findings)
229
- assert set(grouped.keys()) == {"a.py", "b.py"}
230
- assert [index for index, _ in grouped["a.py"]] == [0, 2]
231
- assert [index for index, _ in grouped["b.py"]] == [1]
232
-
233
-
234
- class TestBuildReviewBody:
235
- def test_returns_clean_body_when_no_findings(self):
236
- body = groq_bugteam.build_review_body([], "llama-3.3-70b-versatile", "", [])
237
- assert "clean" in body
238
- assert "llama-3.3-70b-versatile" in body
239
-
240
- def test_counts_severities_and_lists_findings(self):
241
- findings = [
242
- {
243
- "severity": "P0",
244
- "category": "H",
245
- "file": "a.py",
246
- "line": 10,
247
- "title": "SQL injection",
248
- "description": "trace",
249
- },
250
- {
251
- "severity": "P1",
252
- "category": "F",
253
- "file": "b.py",
254
- "line": 5,
255
- "title": "silent except",
256
- "description": "trace2",
257
- },
258
- ]
259
- fix_outcomes = [
260
- {"finding_index": 0, "status": "fixed"},
261
- {"finding_index": 1, "status": "skipped", "reason": "too complex"},
262
- ]
263
- body = groq_bugteam.build_review_body(
264
- findings, "llama-3.3-70b-versatile", "abc1234", fix_outcomes
265
- )
266
- assert "1 P0 / 1 P1 / 0 P2" in body
267
- assert "abc1234" in body
268
- assert "SQL injection" in body
269
- assert "silent except" in body
270
- assert "fixed" in body
271
- assert "skipped: too complex" in body
272
-
273
- def test_marks_findings_without_outcome_as_not_attempted(self):
274
- findings = [
275
- {
276
- "severity": "P2",
277
- "category": "E",
278
- "file": "a.py",
279
- "line": 1,
280
- "title": "dead code",
281
- "description": "d",
282
- },
283
- ]
284
- body = groq_bugteam.build_review_body(
285
- findings, "llama-3.3-70b-versatile", "", []
286
- )
287
- assert "not attempted" in body
288
-
289
-
290
- class TestIsRecoverableHttpError:
291
- def _make_error(self, status_code: int) -> urllib.error.HTTPError:
292
- return urllib.error.HTTPError(
293
- url="x", code=status_code, msg="", hdrs=None, fp=None
294
- )
295
-
296
- @pytest.mark.parametrize("status", [408, 429, 500, 502, 503, 504])
297
- def test_recoverable_statuses(self, status):
298
- assert groq_bugteam.is_recoverable_http_error(self._make_error(status)) is True
299
-
300
- @pytest.mark.parametrize("status", [400, 401, 403, 404, 422])
301
- def test_non_recoverable_statuses(self, status):
302
- assert groq_bugteam.is_recoverable_http_error(self._make_error(status)) is False
303
-
304
- def test_413_triggers_skip_to_next_model(self):
305
- assert groq_bugteam.should_skip_to_next_model(self._make_error(413)) is True
306
-
307
- @pytest.mark.parametrize("status", [400, 401, 403, 429, 500, 503])
308
- def test_other_statuses_do_not_trigger_model_skip(self, status):
309
- assert groq_bugteam.should_skip_to_next_model(self._make_error(status)) is False
310
-
311
-
312
- class TestCallGroqWithFallback:
313
- def _install_fake_transport(self, monkeypatch, fake_post_to_groq):
314
- monkeypatch.setattr(groq_bugteam, "post_to_groq", fake_post_to_groq)
315
- monkeypatch.setattr(groq_bugteam.time, "sleep", lambda _seconds: None)
316
-
317
- def test_non_recoverable_http_error_does_not_attempt_fallback_model(self, monkeypatch):
318
- attempted_models: list[str] = []
319
-
320
- def fake_post_to_groq(api_key, model, messages, temperature, max_completion_tokens):
321
- attempted_models.append(model)
322
- raise urllib.error.HTTPError(
323
- url="x", code=401, msg="unauthorized", hdrs=None, fp=None
324
- )
325
-
326
- self._install_fake_transport(monkeypatch, fake_post_to_groq)
327
- with pytest.raises(RuntimeError):
328
- groq_bugteam.call_groq_with_fallback("k", [], 0.0, 100)
329
- assert attempted_models == [groq_bugteam.GROQ_PRIMARY_MODEL]
330
-
331
- def test_413_falls_back_to_secondary_model(self, monkeypatch):
332
- attempted_models: list[str] = []
333
-
334
- def fake_post_to_groq(api_key, model, messages, temperature, max_completion_tokens):
335
- attempted_models.append(model)
336
- if model == groq_bugteam.GROQ_PRIMARY_MODEL:
337
- raise urllib.error.HTTPError(
338
- url="x", code=413, msg="payload too large", hdrs=None, fp=None
339
- )
340
- return "ok-content"
341
-
342
- self._install_fake_transport(monkeypatch, fake_post_to_groq)
343
- result = groq_bugteam.call_groq_with_fallback("k", [], 0.0, 100)
344
- assert result.model == groq_bugteam.GROQ_FALLBACK_MODEL
345
- assert attempted_models[0] == groq_bugteam.GROQ_PRIMARY_MODEL
346
- assert groq_bugteam.GROQ_FALLBACK_MODEL in attempted_models
347
-
348
- def test_recoverable_error_retries_same_model_then_falls_back(self, monkeypatch):
349
- call_log: list[str] = []
350
-
351
- def fake_post_to_groq(api_key, model, messages, temperature, max_completion_tokens):
352
- call_log.append(model)
353
- raise urllib.error.HTTPError(
354
- url="x", code=503, msg="service unavailable", hdrs=None, fp=None
355
- )
356
-
357
- self._install_fake_transport(monkeypatch, fake_post_to_groq)
358
- with pytest.raises(RuntimeError):
359
- groq_bugteam.call_groq_with_fallback("k", [], 0.0, 100)
360
- assert call_log.count(groq_bugteam.GROQ_PRIMARY_MODEL) > 1
361
- assert groq_bugteam.GROQ_FALLBACK_MODEL in call_log
362
-
363
-
364
- class TestCoerceIndexesToIntSet:
365
- def test_coerces_string_indexes_to_ints(self):
366
- assert groq_bugteam.coerce_indexes_to_int_set(["0", "2"]) == {0, 2}
367
-
368
- def test_drops_non_numeric_entries(self):
369
- assert groq_bugteam.coerce_indexes_to_int_set(["0", "abc", None, 1]) == {0, 1}
370
-
371
- def test_handles_none_input(self):
372
- assert groq_bugteam.coerce_indexes_to_int_set(None) == set()
373
-
374
- def test_handles_empty_list(self):
375
- assert groq_bugteam.coerce_indexes_to_int_set([]) == set()
376
-
377
- def test_accepts_already_int_values(self):
378
- assert groq_bugteam.coerce_indexes_to_int_set([0, 1, 2]) == {0, 1, 2}
379
-
380
-
381
- class TestCoerceSkippedEntries:
382
- def test_coerces_string_finding_index_to_int(self):
383
- assert groq_bugteam.coerce_skipped_entries(
384
- [{"finding_index": "3", "reason": "x"}]
385
- ) == {3: "x"}
386
-
387
- def test_drops_entries_without_parseable_index(self):
388
- assert groq_bugteam.coerce_skipped_entries(
389
- [{"finding_index": "not-a-number", "reason": "x"}]
390
- ) == {}
391
-
392
- def test_drops_entries_missing_finding_index(self):
393
- assert groq_bugteam.coerce_skipped_entries([{"reason": "orphan"}]) == {}
394
-
395
- def test_defaults_reason_to_empty_string(self):
396
- assert groq_bugteam.coerce_skipped_entries([{"finding_index": 1}]) == {1: ""}
397
-
398
- def test_handles_none_input(self):
399
- assert groq_bugteam.coerce_skipped_entries(None) == {}
400
-
401
- def test_treats_none_reason_as_empty_string(self):
402
- assert groq_bugteam.coerce_skipped_entries(
403
- [{"finding_index": 1, "reason": None}]
404
- ) == {1: ""}
405
-
406
- def test_stringifies_non_string_reasons(self):
407
- assert groq_bugteam.coerce_skipped_entries(
408
- [{"finding_index": 1, "reason": 42}]
409
- ) == {1: "42"}
410
-
411
-
412
- class TestBuildFixUserMessage:
413
- def test_embeds_file_content_byte_for_byte_with_trailing_newline(self):
414
- original_content = "line1\nline2\n"
415
- message = groq_bugteam.build_fix_user_message("some.py", original_content, findings_block="[]")
416
- assert original_content in message
417
- assert "line2\n</current_file_contents>" in message
418
-
419
- def test_embeds_file_content_byte_for_byte_without_trailing_newline(self):
420
- original_content = "line1\nline2"
421
- message = groq_bugteam.build_fix_user_message("some.py", original_content, findings_block="[]")
422
- assert f"{original_content}\n</current_file_contents>" in message
423
- assert "line2\n\n</current_file_contents>" not in message
424
-
425
-
426
- class TestShouldWriteFixedFile:
427
- def test_does_not_write_when_no_finding_applied(self):
428
- assert groq_bugteam.should_write_fixed_file(
429
- applied_indexes=set(),
430
- updated_content="new",
431
- current_content="old",
432
- ) is False
433
-
434
- def test_does_not_write_when_content_unchanged(self):
435
- assert groq_bugteam.should_write_fixed_file(
436
- applied_indexes={0},
437
- updated_content="same",
438
- current_content="same",
439
- ) is False
440
-
441
- def test_writes_when_finding_applied_and_content_changed(self):
442
- assert groq_bugteam.should_write_fixed_file(
443
- applied_indexes={0},
444
- updated_content="new",
445
- current_content="old",
446
- ) is True
447
-
448
-
449
- class TestPreserveTrailingNewline:
450
- def test_adds_trailing_newline_when_original_had_one(self):
451
- preserved = groq_bugteam.preserve_trailing_newline(
452
- original="line1\nline2\n", updated="line1\nfixed2"
453
- )
454
- assert preserved == "line1\nfixed2\n"
455
-
456
- def test_strips_trailing_newline_when_original_lacked_one(self):
457
- preserved = groq_bugteam.preserve_trailing_newline(
458
- original="no newline", updated="fixed content\n"
459
- )
460
- assert preserved == "fixed content"
461
-
462
- def test_keeps_matching_form_unchanged(self):
463
- assert (
464
- groq_bugteam.preserve_trailing_newline(original="x\n", updated="y\n")
465
- == "y\n"
466
- )
467
- assert (
468
- groq_bugteam.preserve_trailing_newline(original="x", updated="y") == "y"
469
- )
470
-
471
-
472
- class TestIsSafeRelativePath:
473
- def test_rejects_absolute_posix_path(self):
474
- assert groq_bugteam.is_safe_relative_path("/etc/passwd") is False
475
-
476
- def test_rejects_parent_directory_escape(self):
477
- assert groq_bugteam.is_safe_relative_path("../../etc/passwd") is False
478
-
479
- def test_rejects_embedded_parent_reference(self):
480
- assert groq_bugteam.is_safe_relative_path("src/../../etc/passwd") is False
481
-
482
- def test_accepts_simple_relative_path(self):
483
- assert groq_bugteam.is_safe_relative_path("src/foo.py") is True
484
-
485
- def test_accepts_nested_relative_path(self):
486
- assert groq_bugteam.is_safe_relative_path("packages/mod/scripts/foo.py") is True
487
-
488
-
489
- class TestDecodeSubprocessStderr:
490
- def test_decodes_bytes_input(self):
491
- decoded = groq_bugteam.decode_subprocess_stderr(b"fatal: broken")
492
- assert decoded == "fatal: broken"
493
-
494
- def test_returns_str_input_unchanged(self):
495
- assert groq_bugteam.decode_subprocess_stderr("fatal: broken") == "fatal: broken"
496
-
497
- def test_handles_none_input(self):
498
- assert groq_bugteam.decode_subprocess_stderr(None) == ""
499
-
500
- def test_replaces_undecodable_bytes(self):
501
- decoded = groq_bugteam.decode_subprocess_stderr(b"\xff\xfe broken")
502
- assert "broken" in decoded
503
-
504
-
505
- class TestRunPipelineRefusals:
506
- def test_rejects_missing_api_key(self, monkeypatch, tmp_path):
507
- monkeypatch.delenv("GROQ_API_KEY", raising=False)
508
- monkeypatch.setattr(
509
- groq_bugteam_dotenv,
510
- "claude_dev_env_dotenv_path",
511
- lambda: tmp_path / "missing.env",
512
- )
513
- result = groq_bugteam.run_pipeline({"diff": "anything"})
514
- assert "error" in result
515
- assert "GROQ_API_KEY" in result["error"]
516
-
517
- def test_rejects_empty_diff(self, monkeypatch):
518
- monkeypatch.setenv("GROQ_API_KEY", "gsk_test_placeholder_value")
519
- result = groq_bugteam.run_pipeline({"diff": " ", "files_content": {}})
520
- assert "error" in result
521
- assert "diff is empty" in result["error"]
522
-
523
- def test_rejects_fixes_without_worktree(self, monkeypatch):
524
- monkeypatch.setenv("GROQ_API_KEY", "gsk_test_placeholder_value")
525
- result = groq_bugteam.run_pipeline(
526
- {"diff": "some diff", "files_content": {"a.py": ""}, "apply_fixes": True}
527
- )
528
- assert "error" in result
529
- assert "worktree_path" in result["error"]