cicaddy-github 0.3.2__tar.gz → 0.4.0__tar.gz

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 (43) hide show
  1. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/.claude/skills/cicaddy-action/SKILL.md +1 -1
  2. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/PKG-INFO +4 -3
  3. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/README.md +3 -2
  4. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/action.yml +4 -0
  5. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/entrypoint.sh +1 -0
  6. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/pyproject.toml +1 -1
  7. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/src/cicaddy_github/config/settings.py +10 -0
  8. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/src/cicaddy_github/github_integration/agents.py +80 -2
  9. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/src/cicaddy_github/github_integration/analyzer.py +17 -0
  10. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/src/cicaddy_github/security/leak_detector.py +1 -1
  11. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/tests/unit/test_agents.py +40 -2
  12. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/tests/unit/test_analyzer.py +43 -0
  13. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/tests/unit/test_settings.py +28 -0
  14. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/.github/dependabot.yml +0 -0
  15. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/.github/workflows/changelog.yml +0 -0
  16. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/.github/workflows/ci.yml +0 -0
  17. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/.github/workflows/pr-review.yml +0 -0
  18. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/.github/workflows/release.yml +0 -0
  19. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/.gitignore +0 -0
  20. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/.pre-commit-config.yaml +0 -0
  21. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/CLAUDE.md +0 -0
  22. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/CODE_OF_CONDUCT.md +0 -0
  23. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/CONTRIBUTING.md +0 -0
  24. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/Dockerfile +0 -0
  25. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/LICENSE +0 -0
  26. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/src/cicaddy_github/__init__.py +0 -0
  27. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/src/cicaddy_github/config/__init__.py +0 -0
  28. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/src/cicaddy_github/github_integration/__init__.py +0 -0
  29. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/src/cicaddy_github/github_integration/detector.py +0 -0
  30. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/src/cicaddy_github/github_integration/tools.py +0 -0
  31. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/src/cicaddy_github/plugin.py +0 -0
  32. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/src/cicaddy_github/security/__init__.py +0 -0
  33. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/src/cicaddy_github/validation.py +0 -0
  34. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/tasks/changelog_report.yml +0 -0
  35. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/tasks/pr_review.yml +0 -0
  36. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/templates/report_template.html +0 -0
  37. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/tests/__init__.py +0 -0
  38. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/tests/conftest.py +0 -0
  39. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/tests/unit/__init__.py +0 -0
  40. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/tests/unit/test_detector.py +0 -0
  41. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/tests/unit/test_leak_detector.py +0 -0
  42. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/tests/unit/test_plugin.py +0 -0
  43. {cicaddy_github-0.3.2 → cicaddy_github-0.4.0}/tests/unit/test_tools.py +0 -0
@@ -225,7 +225,7 @@ the `safe-to-review` label. The label is auto-removed on new pushes to prevent
225
225
  TOCTOU bypasses.
226
226
 
227
227
  ```yaml
228
- - uses: redhat-community-ai-tools/cicaddy-action@v0.3.2
228
+ - uses: redhat-community-ai-tools/cicaddy-action@v0.4.0
229
229
  with:
230
230
  ai_provider: gemini
231
231
  ai_model: gemini-3-flash-preview
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cicaddy-github
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: GitHub Actions plugin for cicaddy AI agent framework
5
5
  Project-URL: Homepage, https://github.com/redhat-community-ai-tools/cicaddy-action
6
6
  Project-URL: Repository, https://github.com/redhat-community-ai-tools/cicaddy-action.git
@@ -57,7 +57,7 @@ jobs:
57
57
  with:
58
58
  fetch-depth: 0
59
59
 
60
- - uses: redhat-community-ai-tools/cicaddy-action@v0.3.2
60
+ - uses: redhat-community-ai-tools/cicaddy-action@v0.4.0
61
61
  with:
62
62
  ai_provider: gemini
63
63
  ai_model: gemini-3-flash-preview
@@ -83,7 +83,7 @@ jobs:
83
83
  with:
84
84
  fetch-depth: 0
85
85
 
86
- - uses: redhat-community-ai-tools/cicaddy-action@v0.3.2
86
+ - uses: redhat-community-ai-tools/cicaddy-action@v0.4.0
87
87
  with:
88
88
  ai_provider: gemini
89
89
  ai_model: gemini-3-flash-preview
@@ -104,6 +104,7 @@ jobs:
104
104
  | `mcp_servers_config` | No | JSON array of MCP server configs |
105
105
  | `slack_webhook_url` | No | Slack webhook URL for notifications |
106
106
  | `post_pr_comment` | No | Post results as PR comment (default: `false`) |
107
+ | `submit_review` | No | Submit formal PR review with APPROVE/REQUEST_CHANGES (default: `false`) |
107
108
  | `github_token` | No | GitHub token (default: `${{ github.token }}`) |
108
109
 
109
110
  ## Outputs
@@ -37,7 +37,7 @@ jobs:
37
37
  with:
38
38
  fetch-depth: 0
39
39
 
40
- - uses: redhat-community-ai-tools/cicaddy-action@v0.3.2
40
+ - uses: redhat-community-ai-tools/cicaddy-action@v0.4.0
41
41
  with:
42
42
  ai_provider: gemini
43
43
  ai_model: gemini-3-flash-preview
@@ -63,7 +63,7 @@ jobs:
63
63
  with:
64
64
  fetch-depth: 0
65
65
 
66
- - uses: redhat-community-ai-tools/cicaddy-action@v0.3.2
66
+ - uses: redhat-community-ai-tools/cicaddy-action@v0.4.0
67
67
  with:
68
68
  ai_provider: gemini
69
69
  ai_model: gemini-3-flash-preview
@@ -84,6 +84,7 @@ jobs:
84
84
  | `mcp_servers_config` | No | JSON array of MCP server configs |
85
85
  | `slack_webhook_url` | No | Slack webhook URL for notifications |
86
86
  | `post_pr_comment` | No | Post results as PR comment (default: `false`) |
87
+ | `submit_review` | No | Submit formal PR review with APPROVE/REQUEST_CHANGES (default: `false`) |
87
88
  | `github_token` | No | GitHub token (default: `${{ github.token }}`) |
88
89
 
89
90
  ## Outputs
@@ -34,6 +34,10 @@ inputs:
34
34
  description: 'Post analysis results as PR comment (requires github-token with pull-requests: write)'
35
35
  required: false
36
36
  default: 'false'
37
+ submit_review:
38
+ description: 'Submit a formal PR review with APPROVE or REQUEST_CHANGES based on AI analysis (requires github-token with pull-requests: write)'
39
+ required: false
40
+ default: 'false'
37
41
  github_token:
38
42
  description: 'GitHub token for API access'
39
43
  required: false
@@ -54,6 +54,7 @@ export MCP_SERVERS_CONFIG="${INPUT_MCP_SERVERS_CONFIG:-[]}"
54
54
  export SLACK_WEBHOOK_URL="${INPUT_SLACK_WEBHOOK_URL}"
55
55
  export GITHUB_TOKEN="${INPUT_GITHUB_TOKEN:-$GITHUB_TOKEN}"
56
56
  export POST_PR_COMMENT="${INPUT_POST_PR_COMMENT:-false}"
57
+ export SUBMIT_REVIEW="${INPUT_SUBMIT_REVIEW:-false}"
57
58
 
58
59
  # Extract PR number from GITHUB_REF (e.g. refs/pull/123/merge -> 123)
59
60
  if [[ "${GITHUB_REF}" =~ ^refs/pull/([0-9]+)/ ]]; then
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cicaddy-github"
7
- version = "0.3.2"
7
+ version = "0.4.0"
8
8
  description = "GitHub Actions plugin for cicaddy AI agent framework"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -61,6 +61,11 @@ class Settings(CoreSettings):
61
61
  validation_alias=AliasChoices("POST_PR_COMMENT"),
62
62
  description="Whether to post analysis results as PR comment",
63
63
  )
64
+ submit_review: bool = Field(
65
+ default=False,
66
+ validation_alias=AliasChoices("SUBMIT_REVIEW"),
67
+ description="Whether to submit a formal PR review (APPROVE or REQUEST_CHANGES)",
68
+ )
64
69
 
65
70
 
66
71
  def load_settings() -> Settings:
@@ -95,6 +100,11 @@ def load_settings() -> Settings:
95
100
  if post_pr:
96
101
  env_data["post_pr_comment"] = post_pr.lower() in ("true", "1", "yes")
97
102
 
103
+ # Submit formal PR review flag
104
+ submit_review = os.getenv("SUBMIT_REVIEW", "").strip()
105
+ if submit_review:
106
+ env_data["submit_review"] = submit_review.lower() in ("true", "1", "yes")
107
+
98
108
  # AI provider configuration
99
109
  if os.getenv("AI_PROVIDER"):
100
110
  env_data["ai_provider"] = os.getenv("AI_PROVIDER")
@@ -18,6 +18,14 @@ logger = get_logger(__name__)
18
18
 
19
19
  BOT_COMMENT_MARKER_PR_REVIEW = "<!-- cicaddy-action:pr-review -->"
20
20
 
21
+ # Pattern to detect a review verdict in the AI analysis output.
22
+ # The AI is instructed to include a line like "VERDICT: APPROVE" or
23
+ # "VERDICT: REQUEST_CHANGES" in its output.
24
+ _VERDICT_PATTERN = re.compile(
25
+ r"(?:^|\n)\s*(?:<!--\s*)?VERDICT:\s*(APPROVE|REQUEST_CHANGES)(?:\s*-->)?",
26
+ re.IGNORECASE,
27
+ )
28
+
21
29
  # Pattern matches fenced code blocks (possibly indented by list nesting).
22
30
  _FENCED_CODE_BLOCK = re.compile(
23
31
  r"^([ \t]*(?:`{3,}|~{3,})[^\n]*)\n(.*?)\n([ \t]*(?:`{3,}|~{3,}))[ \t]*$",
@@ -50,6 +58,19 @@ _MARKDOWN_WRAPPER = re.compile(
50
58
  )
51
59
 
52
60
 
61
+ def extract_review_verdict(ai_text: str) -> str:
62
+ """Extract the review verdict from AI analysis output.
63
+
64
+ Looks for a ``VERDICT: APPROVE`` or ``VERDICT: REQUEST_CHANGES`` line
65
+ (optionally wrapped in an HTML comment). Defaults to ``COMMENT`` when
66
+ no explicit verdict is found.
67
+ """
68
+ m = _VERDICT_PATTERN.search(ai_text)
69
+ if m:
70
+ return m.group(1).upper()
71
+ return "COMMENT"
72
+
73
+
53
74
  def strip_markdown_wrapper(text: str) -> str:
54
75
  """Strip a wrapping ```markdown fence from the entire AI output.
55
76
 
@@ -223,11 +244,16 @@ class GitHubPRAgent(BaseAIAgent):
223
244
  pr_context = self._prepare_dspy_context(context)
224
245
  dspy_prompt = self.build_dspy_prompt(task_file, pr_context)
225
246
  if dspy_prompt:
247
+ if getattr(self.settings, "submit_review", False):
248
+ dspy_prompt += self._verdict_instruction()
226
249
  return dspy_prompt
227
250
 
228
251
  pr_data = context["pull_request"]
229
252
  diff_content = context["diff"]
230
253
 
254
+ submit_review = getattr(self.settings, "submit_review", False)
255
+ verdict_block = self._verdict_instruction() if submit_review else ""
256
+
231
257
  return f"""You are an AI agent performing pull request code review.
232
258
 
233
259
  Repository: {context.get("repository", "Unknown")}
@@ -249,7 +275,21 @@ Instructions:
249
275
  4. Provide actionable, specific feedback
250
276
 
251
277
  Please provide your comprehensive analysis in markdown format.
252
- """
278
+ {verdict_block}"""
279
+
280
+ @staticmethod
281
+ def _verdict_instruction() -> str:
282
+ """Return the prompt snippet that asks the AI to emit a verdict."""
283
+ return (
284
+ "\n\nIMPORTANT: At the very end of your analysis, you MUST include a verdict line "
285
+ "in an HTML comment with the format:\n"
286
+ "<!-- VERDICT: APPROVE -->\n"
287
+ "or\n"
288
+ "<!-- VERDICT: REQUEST_CHANGES -->\n\n"
289
+ "Use REQUEST_CHANGES when there are bugs, security issues, or significant problems "
290
+ "that must be fixed before merging. Use APPROVE when the changes look good overall "
291
+ "(minor suggestions are OK with APPROVE)."
292
+ )
253
293
 
254
294
  def _prepare_dspy_context(self, context: dict[str, Any]) -> dict[str, Any]:
255
295
  """Prepare context with PR-specific data for DSPy prompt building."""
@@ -265,7 +305,7 @@ Please provide your comprehensive analysis in markdown format.
265
305
  return pr_context
266
306
 
267
307
  async def send_notifications(self, report: dict[str, Any], analysis_result: dict[str, Any]):
268
- """Send notifications via PR comment and Slack."""
308
+ """Send notifications via PR comment, formal review, and Slack."""
269
309
  # Sanitize outputs
270
310
  if "ai_analysis" in analysis_result:
271
311
  analysis_result["ai_analysis"] = self.leak_detector.sanitize_text(
@@ -287,9 +327,47 @@ Please provide your comprehensive analysis in markdown format.
287
327
  )
288
328
  logger.debug("PR comment post traceback:", exc_info=True)
289
329
 
330
+ # Submit formal PR review if enabled
331
+ submit_review = getattr(self.settings, "submit_review", False)
332
+ if submit_review and self.platform_analyzer and self.pr_number:
333
+ try:
334
+ ai_text = analysis_result.get("ai_analysis", "")
335
+ verdict = extract_review_verdict(ai_text)
336
+ review_body = self._format_review_body(analysis_result)
337
+ await self.platform_analyzer.submit_pr_review(
338
+ int(self.pr_number), review_body, event=verdict
339
+ )
340
+ logger.info(f"Submitted {verdict} review on PR #{self.pr_number}")
341
+ except Exception as e:
342
+ logger.error(
343
+ f"Failed to submit PR review: {self.leak_detector.sanitize_text(str(e))}"
344
+ )
345
+ logger.debug("PR review submit traceback:", exc_info=True)
346
+
290
347
  # Send Slack notification using parent class
291
348
  await super().send_notifications(report, analysis_result)
292
349
 
350
+ def _format_review_body(self, analysis_result: dict[str, Any]) -> str:
351
+ """Format analysis results as a PR review body.
352
+
353
+ Strips the VERDICT line from the output so it does not appear in
354
+ the rendered review.
355
+ """
356
+ body = ""
357
+ if "ai_analysis" in analysis_result:
358
+ cleaned = strip_markdown_wrapper(analysis_result["ai_analysis"])
359
+ cleaned = dedent_code_blocks(cleaned)
360
+ # Remove the VERDICT line from the review body
361
+ cleaned = _VERDICT_PATTERN.sub("", cleaned).strip()
362
+ body = cleaned
363
+
364
+ body += (
365
+ "\n\n---\n"
366
+ "*Generated with [cicaddy-action]"
367
+ "(https://github.com/redhat-community-ai-tools/cicaddy-action)*"
368
+ )
369
+ return body
370
+
293
371
  def _format_pr_comment(self, analysis_result: dict[str, Any]) -> str:
294
372
  """Format analysis results as a PR comment.
295
373
 
@@ -276,6 +276,23 @@ class GitHubAnalyzer:
276
276
 
277
277
  return result
278
278
 
279
+ async def submit_pr_review(self, pr_number: int, body: str, event: str = "COMMENT") -> None:
280
+ """Submit a formal pull request review.
281
+
282
+ Args:
283
+ pr_number: Pull request number.
284
+ body: Review body text.
285
+ event: Review event type — one of ``APPROVE``, ``REQUEST_CHANGES``,
286
+ or ``COMMENT``.
287
+ """
288
+ valid_events = {"APPROVE", "REQUEST_CHANGES", "COMMENT"}
289
+ if event not in valid_events:
290
+ raise ValueError(f"Invalid review event '{event}', must be one of {valid_events}")
291
+
292
+ pr = self.repo.get_pull(pr_number)
293
+ pr.create_review(body=body, event=event)
294
+ logger.info(f"Submitted {event} review on PR #{pr_number}")
295
+
279
296
  def close(self):
280
297
  """Close the GitHub API connection."""
281
298
  self.github.close()
@@ -90,7 +90,7 @@ class LeakDetector:
90
90
 
91
91
  try:
92
92
  if hasattr(secret, "secret_value"):
93
- secret_value: str = secret.secret_value # type: ignore[assignment]
93
+ secret_value = str(secret.secret_value)
94
94
  start = 0
95
95
  while True:
96
96
  pos = line.find(secret_value, start)
@@ -1,6 +1,10 @@
1
- """Tests for dedent_code_blocks and strip_markdown_wrapper in agents module."""
1
+ """Tests for dedent_code_blocks, strip_markdown_wrapper, and extract_review_verdict."""
2
2
 
3
- from cicaddy_github.github_integration.agents import dedent_code_blocks, strip_markdown_wrapper
3
+ from cicaddy_github.github_integration.agents import (
4
+ dedent_code_blocks,
5
+ extract_review_verdict,
6
+ strip_markdown_wrapper,
7
+ )
4
8
 
5
9
 
6
10
  class TestDedentCodeBlocks:
@@ -111,3 +115,37 @@ class TestStripMarkdownWrapper:
111
115
  """A ```python wrapper is NOT stripped."""
112
116
  text = "```python\nprint('hi')\n```"
113
117
  assert strip_markdown_wrapper(text) == text
118
+
119
+
120
+ class TestExtractReviewVerdict:
121
+ """Test extraction of review verdict from AI analysis output."""
122
+
123
+ def test_extracts_approve(self):
124
+ text = "Analysis looks good.\n<!-- VERDICT: APPROVE -->"
125
+ assert extract_review_verdict(text) == "APPROVE"
126
+
127
+ def test_extracts_request_changes(self):
128
+ text = "Found bugs.\n<!-- VERDICT: REQUEST_CHANGES -->"
129
+ assert extract_review_verdict(text) == "REQUEST_CHANGES"
130
+
131
+ def test_extracts_plain_verdict(self):
132
+ """Verdict without HTML comment wrapper."""
133
+ text = "Analysis.\nVERDICT: APPROVE"
134
+ assert extract_review_verdict(text) == "APPROVE"
135
+
136
+ def test_case_insensitive(self):
137
+ text = "Analysis.\n<!-- verdict: request_changes -->"
138
+ assert extract_review_verdict(text) == "REQUEST_CHANGES"
139
+
140
+ def test_defaults_to_comment(self):
141
+ """No verdict line returns COMMENT."""
142
+ text = "Just some analysis text without a verdict."
143
+ assert extract_review_verdict(text) == "COMMENT"
144
+
145
+ def test_verdict_with_leading_whitespace(self):
146
+ text = "Analysis.\n <!-- VERDICT: APPROVE -->"
147
+ assert extract_review_verdict(text) == "APPROVE"
148
+
149
+ def test_verdict_in_middle_of_text(self):
150
+ text = "Start.\n<!-- VERDICT: REQUEST_CHANGES -->\nEnd."
151
+ assert extract_review_verdict(text) == "REQUEST_CHANGES"
@@ -261,6 +261,49 @@ class TestPostPRComment:
261
261
  mock_pr.create_issue_comment.assert_called_once()
262
262
 
263
263
 
264
+ class TestSubmitPRReview:
265
+ """Test formal PR review submission."""
266
+
267
+ @pytest.mark.asyncio
268
+ async def test_submits_approve_review(self, analyzer, mock_github):
269
+ """Submits an APPROVE review via PyGithub."""
270
+ _, mock_repo = mock_github
271
+ mock_pr = MagicMock()
272
+ mock_repo.get_pull.return_value = mock_pr
273
+
274
+ await analyzer.submit_pr_review(42, "LGTM!", event="APPROVE")
275
+
276
+ mock_pr.create_review.assert_called_once_with(body="LGTM!", event="APPROVE")
277
+
278
+ @pytest.mark.asyncio
279
+ async def test_submits_request_changes_review(self, analyzer, mock_github):
280
+ """Submits a REQUEST_CHANGES review."""
281
+ _, mock_repo = mock_github
282
+ mock_pr = MagicMock()
283
+ mock_repo.get_pull.return_value = mock_pr
284
+
285
+ await analyzer.submit_pr_review(42, "Please fix.", event="REQUEST_CHANGES")
286
+
287
+ mock_pr.create_review.assert_called_once_with(body="Please fix.", event="REQUEST_CHANGES")
288
+
289
+ @pytest.mark.asyncio
290
+ async def test_submits_comment_review_by_default(self, analyzer, mock_github):
291
+ """Default event is COMMENT."""
292
+ _, mock_repo = mock_github
293
+ mock_pr = MagicMock()
294
+ mock_repo.get_pull.return_value = mock_pr
295
+
296
+ await analyzer.submit_pr_review(42, "Some feedback.")
297
+
298
+ mock_pr.create_review.assert_called_once_with(body="Some feedback.", event="COMMENT")
299
+
300
+ @pytest.mark.asyncio
301
+ async def test_rejects_invalid_event(self, analyzer):
302
+ """Invalid review event raises ValueError."""
303
+ with pytest.raises(ValueError, match="Invalid review event"):
304
+ await analyzer.submit_pr_review(42, "body", event="INVALID")
305
+
306
+
264
307
  class TestBuildUpdatedBody:
265
308
  """Test the history collapsing logic."""
266
309
 
@@ -174,3 +174,31 @@ class TestSettingsValidation:
174
174
 
175
175
  settings = load_settings()
176
176
  assert settings.post_pr_comment is True
177
+
178
+ def test_submit_review_default_false(self):
179
+ """SUBMIT_REVIEW defaults to False."""
180
+ env = {
181
+ "AI_PROVIDER": "gemini",
182
+ "AI_MODEL": "gemini-2.5-flash",
183
+ "MCP_SERVERS_CONFIG": "[]",
184
+ }
185
+ with patch.dict(os.environ, env, clear=False):
186
+ os.environ.pop("SUBMIT_REVIEW", None)
187
+ from cicaddy_github.config.settings import load_settings
188
+
189
+ settings = load_settings()
190
+ assert settings.submit_review is False
191
+
192
+ def test_submit_review_true(self):
193
+ """SUBMIT_REVIEW=true sets field to True."""
194
+ env = {
195
+ "SUBMIT_REVIEW": "true",
196
+ "AI_PROVIDER": "gemini",
197
+ "AI_MODEL": "gemini-2.5-flash",
198
+ "MCP_SERVERS_CONFIG": "[]",
199
+ }
200
+ with patch.dict(os.environ, env, clear=False):
201
+ from cicaddy_github.config.settings import load_settings
202
+
203
+ settings = load_settings()
204
+ assert settings.submit_review is True
File without changes
File without changes