Skip to content

test: strengthen focused unit coverage for core behavior#2397

Open
PeterDaveHello wants to merge 1 commit into
The-PR-Agent:mainfrom
PeterDaveHello:harden-tests
Open

test: strengthen focused unit coverage for core behavior#2397
PeterDaveHello wants to merge 1 commit into
The-PR-Agent:mainfrom
PeterDaveHello:harden-tests

Conversation

@PeterDaveHello
Copy link
Copy Markdown
Contributor

Cover PR processing, provider helpers, review output rendering, configuration behavior, retry handling, ticket extraction, code suggestions, and GitHub app workflows.

These tests target behavior that is important and likely to change, while keeping production code untouched. They also isolate shared test state so the new coverage remains order-independent.

This work was developed through multiple rounds of human-led AI collaboration using codex-cli v0.130.0 with GPT-5.5, GitHub Copilot CLI v1.0.48 with GPT-5.5, and Claude Opus 4.7.

cc #2373

GitHub Copilot Pull Request Summary

This pull request adds comprehensive security-focused tests for configuration handling in the repository, specifically targeting Dynaconf settings management, repository settings application, and CLI argument validation. The new tests ensure that forbidden or malformed configuration changes do not leak into global settings and that all temporary resources are properly cleaned up. The most important changes are as follows:

Security and Robustness Tests for Settings Management:

  • Added a test helper module _settings_helpers.py that provides reliable snapshot/restore utilities for Dynaconf settings, ensuring that test state is isolated and settings are properly removed or restored, including for dotted keys.
  • Introduced a suite of tests in test_apply_repo_settings_security.py to verify that repository settings are only applied when enabled, that valid overrides are merged correctly, and that malformed or forbidden TOML does not pollute global settings. The tests also ensure that temporary files are always cleaned up, even in error cases.

Security Tests for CLI Argument Validation:

  • Added test_cli_args_security.py to verify that forbidden CLI arguments (such as those that could override secrets, URLs, or critical toggles) are correctly rejected by the CliArgs.validate_user_args method, while allowing safe configuration changes. The tests also include integration coverage to ensure forbidden arguments block further processing.

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Comprehensive unit test coverage for core PR-Agent behavior and security hardening

🧪 Tests

Grey Divider

Walkthroughs

Description
• Adds comprehensive unit test coverage for core PR-Agent functionality across 15 new test modules
• **Markdown and Output Rendering**: Tests for convert_to_markdown_v2, ticket extraction, and PR
  description generation with structural marker validation
• **Diff Processing Pipeline**: Tests for hunk processing, file deletion handling, token budgeting,
  and pagination logic
• **GitHub App Integration**: Tests for timeout dict behavior, GitHub app events, push trigger
  deduplication, and webhook signature verification
• **Code Suggestions**: Tests for filtering, quality guards, rendering, and output formatting with
  truncation and score thresholds
• **Ticket Extraction**: Async tests for GitHub and Azure DevOps provider integration with
  deduplication and caching
• **Provider Helpers**: Tests for GitHub provider comment publishing, URL parsing, diff handling,
  and inline code suggestions
• **Configuration Security**: Tests for repository settings application, CLI argument validation,
  and custom merge loader with forbidden directive detection
• **Retry Logic**: Tests for async retry with fallback model chains and deployment ID updates
• **Test Infrastructure**: Adds _settings_helpers.py module providing snapshot_settings and
  restore_settings utilities for reliable test isolation with Dynaconf settings
• All tests use structural marker validation and fake providers to avoid network calls and LLM
  dependencies
• Tests maintain order-independence through proper settings state management and cleanup
Diagram
flowchart LR
  A["Test Infrastructure<br/>_settings_helpers.py"] --> B["Configuration Security<br/>Settings/CLI/Loader"]
  A --> C["Output Rendering<br/>Markdown/PR Description"]
  A --> D["Code Suggestions<br/>Filtering/Rendering"]
  B --> E["Integration Tests<br/>Settings Application"]
  C --> F["Diff Pipeline<br/>Hunk Processing"]
  D --> G["Provider Helpers<br/>Comments/Suggestions"]
  F --> H["Ticket Extraction<br/>Async Providers"]
  G --> I["GitHub App<br/>Events/Timeouts"]
  H --> J["Retry Logic<br/>Fallback Models"]
  I --> K["Webhook Verification<br/>Signature Checks"]
Loading

Grey Divider

File Changes

1. tests/unittest/test_markdown_ticket_output_core.py 🧪 Tests +682/-0

Comprehensive unit tests for Markdown output and ticket rendering

• Adds 682 lines of focused unit tests for Markdown rendering, ticket extraction, and output helpers
• Tests cover convert_to_markdown_v2, emphasize_header, ticket_markdown_logic,
 parse_code_suggestion, and related functions
• Validates structural markers (emoji, headers, links) rather than full golden strings for
 robustness
• Tests both GFM and non-GFM rendering paths, edge cases, and error conditions

tests/unittest/test_markdown_ticket_output_core.py


2. tests/unittest/test_diff_pipeline_core.py 🧪 Tests +475/-0

Unit tests for diff processing and hunk pipeline behavior

• Adds 475 lines of tests for diff/hunk pipeline processing functions
• Covers decouple_and_convert_to_hunks_with_lines_numbers, extract_hunk_lines_from_patch,
 generate_full_patch, and pr_generate_compressed_diff
• Tests hunk numbering, file deletion handling, token budgeting, and pagination logic
• Validates structural markers (hunk headers, line numbers) rather than full golden strings

tests/unittest/test_diff_pipeline_core.py


3. tests/unittest/test_github_app_timeout_core.py 🧪 Tests +512/-0

Tests for timeout dict, GitHub app events, and push trigger handling

• Adds 512 lines of tests for DefaultDictWithTimeout and GitHub app helper functions
• Tests TTL expiration, key refresh behavior, and timeout handling with monkeypatched time
• Covers handle_line_comments, _check_pull_request_event, and push trigger deduplication logic
• Includes settings snapshot/restore helpers to prevent test state leakage

tests/unittest/test_github_app_timeout_core.py


View more (14)
4. tests/unittest/test_pr_description_output_core.py 🧪 Tests +479/-0

Unit tests for PR description generation and output formatting

• Adds 479 lines of focused tests for /describe output behavior and PRDescription helpers
• Tests _prepare_data, _prepare_labels, _prepare_pr_answer_with_markers, and
 process_pr_files_prediction methods
• Validates key reordering, diagram sanitization, marker replacement, GFM vs non-GFM rendering, and
 file table generation
• Includes round-trip test verifying structured file recovery from rendered walkthrough

tests/unittest/test_pr_description_output_core.py


5. tests/unittest/test_ticket_extraction_async.py 🧪 Tests +485/-0

Tests for async ticket extraction and provider integration

• Adds 485 lines of deterministic tests for async ticket extraction and caching
• Tests GitHub and Azure DevOps provider ticket extraction with fake providers and no network access
• Covers deduplication, body truncation, sub-issue fetching, label extraction, and error isolation
• Includes settings snapshot/restore helpers to prevent test state leakage across tests

tests/unittest/test_ticket_extraction_async.py


6. tests/unittest/test_github_provider_comments.py 🧪 Tests +376/-0

Tests for GitHub provider comment publishing and code suggestions

• Adds 376 lines of tests for GitHub provider inline comment creation and publishing
• Tests create_inline_comment, publish_inline_comments, and publish_code_suggestions with fake
 PR objects
• Validates payload shape for single-line vs multi-line suggestions, fallback behavior on 422
 errors, and body truncation
• Tests backtick handling asymmetry and position resolution edge cases

tests/unittest/test_github_provider_comments.py


7. tests/unittest/_settings_helpers.py 🧪 Tests +55/-0

Settings snapshot/restore helpers for test isolation

• Adds helper module providing snapshot_settings and restore_settings utilities for Dynaconf
 settings management
• Enables reliable test isolation by capturing and restoring settings state, including dotted keys
• Distinguishes between absent keys and keys with None values using sentinel objects
• Prevents test state leakage into other tests sharing the settings singleton

tests/unittest/_settings_helpers.py


8. tests/unittest/test_pr_code_suggestions_filtering.py 🧪 Tests +450/-0

Unit tests for code suggestions filtering and quality guards

• Added 450 lines of focused unit tests for PRCodeSuggestions helper methods without LLM or
 network calls
• Tests cover _truncate_if_needed, validate_one_liner_suggestion_not_repeating_code,
 remove_line_numbers, analyze_self_reflection_response, and _prepare_pr_code_suggestions
 filtering
• Uses snapshot_settings and restore_settings helpers to isolate test state for settings
 mutations
• Includes parametrized tests for truncation behavior, stale suggestion detection, and reflection
 response validation

tests/unittest/test_pr_code_suggestions_filtering.py


9. tests/unittest/test_apply_repo_settings_security.py 🧪 Tests +335/-0

Security tests for repository settings application

• Added 335 lines of security-focused tests for apply_repo_settings function
• Tests verify repo settings are only applied when enabled, valid overrides merge correctly, and
 malformed/forbidden TOML does not pollute global settings
• Includes tests for temporary file cleanup on both success and failure paths
• Uses settings_snapshot fixture to isolate and restore Dynaconf settings sections and environment
 variables

tests/unittest/test_apply_repo_settings_security.py


10. tests/unittest/test_pr_code_suggestions_rendering.py 🧪 Tests +348/-0

Tests for code suggestions rendering and output formatting

• Added 348 lines of tests for PRCodeSuggestions rendering and output generation
• Tests cover truncation, inline code suggestion rendering, summarized suggestion generation with
 table formatting, and stale one-liner validation
• Includes tests for score mechanism thresholds and HTML escaping of angle brackets in summaries
• Uses settings snapshot/restore helpers to manage configuration state across tests

tests/unittest/test_pr_code_suggestions_rendering.py


11. tests/unittest/test_pr_questions_helpers.py 🧪 Tests +342/-0

Unit tests for PR questions and line questions helpers

• Added 342 lines of focused unit tests for PRQuestions and PR_LineQuestions helper methods
• Tests cover argument parsing, image URL extraction, answer preparation with slash-command
 sanitization, and GitLab quick-action protections
• Includes tests for conversation history loading with settings validation and provider exception
 handling
• Uses snapshot_settings with SENTINEL-based restoration for dotted keys

tests/unittest/test_pr_questions_helpers.py


12. tests/unittest/test_github_provider_urls_and_diffs.py 🧪 Tests +306/-0

Tests for GitHub provider URL parsing and diff handling

• Added 306 lines of tests documenting GithubProvider URL parsing and diff file edit-type mapping
 behavior
• Tests cover PR/issue URL parsing for github.com, GitHub API, and GHES endpoints with query string
 and trailing slash handling
• Includes tests for get_diff_files edit-type mapping (added, deleted, renamed, modified) and
 large diff loading
• Uses bare provider instantiation via __new__ to avoid network calls

tests/unittest/test_github_provider_urls_and_diffs.py


13. tests/unittest/test_custom_merge_loader_security.py 🧪 Tests +247/-0

Security tests for custom merge loader configuration

• Added 247 lines of security and behavior tests for custom_merge_loader module
• Tests verify forbidden directives (dynaconf_include, loaders, etc.) are rejected at all nesting
 levels and mixed-case variants
• Includes tests for excessive nesting guard, safe PR-Agent config validation, and file-skipping
 behavior
• Tests load() function with fake Dynaconf-like objects for security enforcement and single-key
 loading semantics

tests/unittest/test_custom_merge_loader_security.py


14. tests/unittest/test_retry_with_fallback_models.py 🧪 Tests +200/-0

Tests for retry logic with fallback model chains

• Added 200 lines of tests for retry_with_fallback_models async retry logic with model fallback
 chains
• Tests cover primary model success, fallback on failure, all-models-failure aggregation, and
 deployment ID updates per attempt
• Includes tests for weak and reasoning model types using alternate settings
• Uses snapshot_settings with SENTINEL-based restoration to handle dotted keys like
 openai.fallback_deployments

tests/unittest/test_retry_with_fallback_models.py


15. tests/unittest/test_cli_args_security.py 🧪 Tests +145/-0

Security tests for CLI argument validation

• Added 145 lines of security tests for CLI argument validation in CliArgs.validate_user_args
• Tests verify forbidden arguments (secrets, URLs, auth tokens, approval toggles) are rejected while
 safe configuration changes are allowed
• Includes parametrized tests for forbidden patterns and integration test verifying forbidden args
 block settings updates and tool instantiation
• Tests both section-qualified and double-underscore argument forms

tests/unittest/test_cli_args_security.py


16. tests/unittest/test_verify_signature.py 🧪 Tests +48/-0

Tests for webhook signature verification

• Added 48 lines of tests for verify_signature function in server utilities
• Tests cover valid signature acceptance, missing/invalid signature rejection with 403 status, and
 payload/secret mismatch detection
• Uses parametrized tests for missing signature variants (None and empty string)

tests/unittest/test_verify_signature.py


17. tests/unittest/_settings_helpers.py 🧪 Tests +55/-0

Settings snapshot/restore helpers for test isolation

• Added 55 lines of test-only helper module for snapshotting and restoring Dynaconf settings
• Provides snapshot_settings() and restore_settings() functions with SENTINEL-based approach for
 truly removing originally-absent keys
• Includes special handling for dotted keys (e.g., openai.deployment_id) that don't reliably unset
 via Dynaconf's standard API
• Ensures test state isolation by properly cleaning up settings mutations including nested DynaBox
 entries

tests/unittest/_settings_helpers.py


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented May 16, 2026

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (3)

Grey Divider


Action required

1. ask_diff_hunk unset can raise 📘 Rule violation ☼ Reliability
Description
_restore_ask_diff_hunk calls settings.unset("ask_diff_hunk", force=True) without handling
KeyError for keys that were never set. This can break teardown and leak process-global Dynaconf
state, creating order-dependent/flaky tests.
Code

tests/unittest/test_github_app_timeout_core.py[R42-45]

+    if original is sentinel:
+        settings.unset("ask_diff_hunk", force=True)
+    else:
+        settings.set("ask_diff_hunk", original)
Evidence
Rule 14 requires tests to explicitly and safely reset process-global state; the new cleanup helper
unsets a Dynaconf key without a KeyError guard, risking teardown failure and leaked global state.
The shared settings helper added in this PR documents that settings.unset(..., force=True) can
raise KeyError when a key was never set, making this a real risk.

tests/unittest/test_github_app_timeout_core.py[42-45]
tests/unittest/_settings_helpers.py[47-54]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A test cleanup helper unsets `ask_diff_hunk` without guarding against `KeyError` when the key was never set, which can cause teardown failures and leave process-global settings in a dirty state.
## Issue Context
Other new test helpers document that Dynaconf `settings.unset(..., force=True)` may raise `KeyError` for keys that were never set; cleanup code should treat that case as benign.
## Fix Focus Areas
- tests/unittest/test_github_app_timeout_core.py[42-45]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. super-secret-token hardcoded in test 📘 Rule violation ⛨ Security
Description
The new signature tests hardcode a secret-like value (super-secret-token) directly in the
repository. This violates the no-hardcoded-secrets requirement and can trigger secret scanning or
accidental reuse.
Code

tests/unittest/test_verify_signature.py[R14-16]

+class TestVerifySignature:
+    secret = "super-secret-token"
+    payload = b'{"action":"opened","number":1}'
Evidence
Rule 7 prohibits committed secrets/tokens; the test introduces a hardcoded secret string
(super-secret-token) as a class attribute, which is committed in source control.

AGENTS.md
tests/unittest/test_verify_signature.py[14-16]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A secret-like string is hardcoded in a committed test file.
## Issue Context
Even though this is test-only, committed secret-looking values can violate policy and may be flagged by scanners.
## Fix Focus Areas
- tests/unittest/test_verify_signature.py[14-16]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. _remove_key() ignores exceptions 📘 Rule violation ☼ Reliability
Description
The settings snapshot/restore helper in tests/unittest/_settings_helpers.py silently swallows all
exceptions when removing keys, which can mask unexpected failures and leave the process-global
Dynaconf state only partially restored. This undermines test isolation and can reintroduce
order-dependent/flaky behavior that is difficult to debug.
Code

tests/unittest/_settings_helpers.py[R34-45]

+        for stored in list(container.keys()):
+            if stored.lower() == leaf.lower():
+                try:
+                    container.pop(stored, None)
+                except Exception:
+                    pass
+                return
+        return
+    try:
+        settings.unset(key, force=True)
+    except Exception:
+        pass
Evidence
PR Compliance ID 3 requires handling errors and edge cases explicitly rather than ignoring them, yet
_remove_key() wraps both container.pop(...) (including dotted-key handling) and
settings.unset(...) in except Exception: pass, meaning any unexpected error during cleanup is
discarded with no signal. Because snapshot_settings() / restore_settings() are used to isolate
the global Dynaconf settings singleton across multiple tests (including those that mutate global
settings), suppressing these failures defeats the isolation guarantee and allows mutated state to
leak into subsequent tests while appearing to restore correctly.

Rule 3: Robust Error Handling
tests/unittest/_settings_helpers.py[34-45]
tests/unittest/_settings_helpers.py[19-55]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test settings cleanup helper `_remove_key()` currently catches `Exception` and does nothing (`pass`) when popping keys from a Dynaconf container (including dotted keys) and when calling `settings.unset(...)`. This can hide real errors, allow incomplete restoration of the process-global Dynaconf state, and cause order-dependent or flaky tests with no clear signal that restore failed.
## Issue Context
The `snapshot_settings()` / `restore_settings()` helpers are used by multiple test modules to keep tests order-independent by restoring the global Dynaconf settings singleton after mutations. Silent exception swallowing during key removal undermines this goal because cleanup failures can go unnoticed and leak mutated settings into later tests, making failures hard to reproduce and debug.
## Fix Focus Areas
- tests/unittest/_settings_helpers.py[25-55]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Tempfile fd never closed 🐞 Bug ☼ Reliability
Description
test_apply_repo_settings_security mocks mkstemp() using os.open() and asserts the temp file is
removed, but apply_repo_settings() never closes the returned fd, which can prevent deletion and
leak descriptors (notably on Windows). This makes the new tests non-portable and can destabilize
long test runs due to accumulated open fds.
Code

tests/unittest/test_apply_repo_settings_security.py[R262-307]

+    def fake_mkstemp(suffix=None, prefix=None, dir=None, text=False):
+        fd = os.open(str(known_path), os.O_RDWR | os.O_CREAT | os.O_TRUNC)
+        return fd, str(known_path)
+
+    monkeypatch.setattr(tempfile, "mkstemp", fake_mkstemp)
+
+    apply_repo_settings("https://example.com/owner/repo/pull/1")
+
+    assert not known_path.exists(), "Temp settings file must be removed after successful apply"
+
+
+def test_temp_file_is_removed_after_failed_apply(monkeypatch, tmp_path, settings_snapshot):
+    """The temp file must be removed even when the Dynaconf load step raises.
+
+    We use *valid* TOML bytes (so the failure cannot be confused with the
+    silent-swallow malformed-TOML path) and force the failure by replacing
+    the ``Dynaconf`` symbol bound inside ``pr_agent.git_providers.utils``
+    with a stub that raises *after* ``mkstemp`` has been called. We do not
+    patch the external ``dynaconf`` module — only the imported reference
+    that ``apply_repo_settings`` actually uses.
+    """
+    valid_toml = b"[pr_reviewer]\nnum_max_findings = 3\n"
+    provider = FakeGitProvider(repo_settings_bytes=valid_toml)
+    captured = _install_provider(monkeypatch, provider)
+    get_settings().set("config.use_repo_settings_file", True)
+
+    known_path = tmp_path / "repo_settings_failure.toml"
+    mkstemp_calls = {"n": 0}
+
+    def fake_mkstemp(suffix=None, prefix=None, dir=None, text=False):
+        mkstemp_calls["n"] += 1
+        fd = os.open(str(known_path), os.O_RDWR | os.O_CREAT | os.O_TRUNC)
+        return fd, str(known_path)
+
+    monkeypatch.setattr(tempfile, "mkstemp", fake_mkstemp)
+
+    def exploding_dynaconf(*args, **kwargs):
+        raise RuntimeError("boom")
+
+    monkeypatch.setattr(git_utils, "Dynaconf", exploding_dynaconf)
+
+    apply_repo_settings("https://example.com/owner/repo/pull/1")
+
+    assert mkstemp_calls["n"] == 1, "mkstemp must have run before the failure"
+    assert not known_path.exists(), "Temp settings file must be removed even after a failed apply"
+
Evidence
The test’s fake_mkstemp returns a raw fd opened with os.open(), while apply_repo_settings uses
tempfile.mkstemp and os.write but never calls os.close(fd) before attempting
os.remove(repo_settings_file). This combination can prevent deletion and leaks descriptors.

tests/unittest/test_apply_repo_settings_security.py[255-307]
pr_agent/git_providers/utils.py[14-87]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new tests patch `tempfile.mkstemp()` to return an `os.open()` fd, but production `apply_repo_settings()` calls `os.write(fd, ...)` and never closes the fd. The tests then assert the file is removed; on platforms that forbid deleting open files, removal can fail, and on all platforms this leaks file descriptors.
## Issue Context
`apply_repo_settings()` creates a temp file via `mkstemp`, writes repo settings bytes, loads Dynaconf from the path, and deletes the temp file in a `finally` block. Because the fd is left open, the delete behavior is platform-dependent.
## Fix Focus Areas
- pr_agent/git_providers/utils.py[14-45]
- tests/unittest/test_apply_repo_settings_security.py[255-315]
### Implementation guidance
Prefer fixing the production leak by adding `os.close(fd)` in a `finally` around the `os.write` (or by using `os.fdopen(fd, 'wb')` and closing it). If production code must remain unchanged, adjust the tests to monkeypatch `pr_agent.git_providers.utils.os.write` to close the fd after writing, so deletion semantics match strict platforms.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Previous review results

Review updated until commit 8a7c33e

Results up to commit 370b0b8


🐞 Bugs (1) 📘 Rule violations (3)


Action required
1. ask_diff_hunk unset can raise 📘 Rule violation ☼ Reliability ⭐ New
Description
_restore_ask_diff_hunk calls settings.unset("ask_diff_hunk", force=True) without handling
KeyError for keys that were never set. This can break teardown and leak process-global Dynaconf
state, creating order-dependent/flaky tests.
Code

tests/unittest/test_github_app_timeout_core.py[R42-45]

+    if original is sentinel:
+        settings.unset("ask_diff_hunk", force=True)
+    else:
+        settings.set("ask_diff_hunk", original)
Evidence
Rule 14 requires tests to explicitly and safely reset process-global state; the new cleanup helper
unsets a Dynaconf key without a KeyError guard, risking teardown failure and leaked global state.
The shared settings helper added in this PR documents that settings.unset(..., force=True) can
raise KeyError when a key was never set, making this a real risk.

tests/unittest/test_github_app_timeout_core.py[42-45]
tests/unittest/_settings_helpers.py[47-54]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A test cleanup helper unsets `ask_diff_hunk` without guarding against `KeyError` when the key was never set, which can cause teardown failures and leave process-global settings in a dirty state.

## Issue Context
Other new test helpers document that Dynaconf `settings.unset(..., force=True)` may raise `KeyError` for keys that were never set; cleanup code should treat that case as benign.

## Fix Focus Areas
- tests/unittest/test_github_app_timeout_core.py[42-45]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
2. super-secret-token hardcoded in test 📘 Rule violation ⛨ Security ⭐ New
Description
The new signature tests hardcode a secret-like value (super-secret-token) directly in the
repository. This violates the no-hardcoded-secrets requirement and can trigger secret scanning or
accidental reuse.
Code

tests/unittest/test_verify_signature.py[R14-16]

+class TestVerifySignature:
+    secret = "super-secret-token"
+    payload = b'{"action":"opened","number":1}'
Evidence
Rule 7 prohibits committed secrets/tokens; the test introduces a hardcoded secret string
(super-secret-token) as a class attribute, which is committed in source control.

AGENTS.md
tests/unittest/test_verify_signature.py[14-16]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A secret-like string is hardcoded in a committed test file.

## Issue Context
Even though this is test-only, committed secret-looking values can violate policy and may be flagged by scanners.

## Fix Focus Areas
- tests/unittest/test_verify_signature.py[14-16]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. _remove_key() ignores exceptions 📘 Rule violation ☼ Reliability
Description
The settings snapshot/restore helper in tests/unittest/_settings_helpers.py silently swallows all
exceptions when removing keys, which can mask unexpected failures and leave the process-global
Dynaconf state only partially restored. This undermines test isolation and can reintroduce
order-dependent/flaky behavior that is difficult to debug.
Code

tests/unittest/_settings_helpers.py[R34-45]

+        for stored in list(container.keys()):
+            if stored.lower() == leaf.lower():
+                try:
+                    container.pop(stored, None)
+                except Exception:
+                    pass
+                return
+        return
+    try:
+        settings.unset(key, force=True)
+    except Exception:
+        pass
Evidence
PR Compliance ID 3 requires handling errors and edge cases explicitly rather than ignoring them, yet
_remove_key() wraps both container.pop(...) (including dotted-key handling) and
settings.unset(...) in except Exception: pass, meaning any unexpected error during cleanup is
discarded with no signal. Because snapshot_settings() / restore_settings() are used to isolate
the global Dynaconf settings singleton across multiple tests (including those that mutate global
settings), suppressing these failures defeats the isolation guarantee and allows mutated state to
leak into subsequent tests while appearing to restore correctly.

Rule 3: Robust Error Handling
tests/unittest/_settings_helpers.py[34-45]
tests/unittest/_settings_helpers.py[19-55]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test settings cleanup helper `_remove_key()` currently catches `Exception` and does nothing (`pass`) when popping keys from a Dynaconf container (including dotted keys) and when calling `settings.unset(...)`. This can hide real errors, allow incomplete restoration of the process-global Dynaconf state, and cause order-dependent or flaky tests with no clear signal that restore failed.
## Issue Context
The `snapshot_settings()` / `restore_settings()` helpers are used by multiple test modules to keep tests order-independent by restoring the global Dynaconf settings singleton after mutations. Silent exception swallowing during key removal undermines this goal because cleanup failures can go unnoticed and leak mutated settings into later tests, making failures hard to reproduce and debug.
## Fix Focus Areas
- tests/unittest/_settings_helpers.py[25-55]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Tempfile fd never closed 🐞 Bug ☼ Reliability
Description
test_apply_repo_settings_security mocks mkstemp() using os.open() and asserts the temp file is
removed, but apply_repo_settings() never closes the returned fd, which can prevent deletion and
leak descriptors (notably on Windows). This makes the new tests non-portable and can destabilize
long test runs due to accumulated open fds.
Code

tests/unittest/test_apply_repo_settings_security.py[R262-307]

+    def fake_mkstemp(suffix=None, prefix=None, dir=None, text=False):
+        fd = os.open(str(known_path), os.O_RDWR | os.O_CREAT | os.O_TRUNC)
+        return fd, str(known_path)
+
+    monkeypatch.setattr(tempfile, "mkstemp", fake_mkstemp)
+
+    apply_repo_settings("https://example.com/owner/repo/pull/1")
+
+    assert not known_path.exists(), "Temp settings file must be removed after successful apply"
+
+
+def test_temp_file_is_removed_after_failed_apply(monkeypatch, tmp_path, settings_snapshot):
+    """The temp file must be removed even when the Dynaconf load step raises.
+
+    We use *valid* TOML bytes (so the failure cannot be confused with the
+    silent-swallow malformed-TOML path) and force the failure by replacing
+    the ``Dynaconf`` symbol bound inside ``pr_agent.git_providers.utils``
+    with a stub that raises *after* ``mkstemp`` has been called. We do not
+    patch the external ``dynaconf`` module — only the imported reference
+    that ``apply_repo_settings`` actually uses.
+    """
+    valid_toml = b"[pr_reviewer]\nnum_max_findings = 3\n"
+    provider = FakeGitProvider(repo_settings_bytes=valid_toml)
+    captured = _install_provider(monkeypatch, provider)
+    get_settings().set("config.use_repo_settings_file", True)
+
+    known_path = tmp_path / "repo_settings_failure.toml"
+    mkstemp_calls = {"n": 0}
+
+    def fake_mkstemp(suffix=None, prefix=None, dir=None, text=False):
+        mkstemp_calls["n"] += 1
+        fd = os.open(str(known_path), os.O_RDWR | os.O_CREAT | os.O_TRUNC)
+        return fd, str(known_path)
+
+    monkeypatch.setattr(tempfile, "mkstemp", fake_mkstemp)
+
+    def exploding_dynaconf(*args, **kwargs):
+        raise RuntimeError("boom")
+
+    monkeypatch.setattr(git_utils, "Dynaconf", exploding_dynaconf)
+
+    apply_repo_settings("https://example.com/owner/repo/pull/1")
+
+    assert mkstemp_calls["n"] == 1, "mkstemp must have run before the failure"
+    assert not known_path.exists(), "Temp settings file must be removed even after a failed apply"
+
Evidence
The test’s fake_mkstemp returns a raw fd opened with os.open(), while apply_repo_settings uses
tempfile.mkstemp and os.write but never calls os.close(fd) before attempting
os.remove(repo_settings_file). This combination can prevent deletion and leaks descriptors.

tests/unittest/test_apply_repo_settings_security.py[255-307]
pr_agent/git_providers/utils.py[14-87]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new tests patch `tempfile.mkstemp()` to return an `os.open()` fd, but production `apply_repo_settings()` calls `os.write(fd, ...)` and never closes the fd. The tests then assert the file is removed; on platforms that forbid deleting open files, removal can fail, and on all platforms this leaks file descriptors.
## Issue Context
`apply_repo_settings()` creates a temp file via `mkstemp`, writes repo settings bytes, loads Dynaconf from the path, and deletes the temp file in a `finally` block. Because the fd is left open, the delete behavior is platform-dependent.
## Fix Focus Areas
- pr_agent/git_providers/utils.py[14-45]
- tests/unittest/test_apply_repo_settings_security.py[255-315]
### Implementation guidance
Prefer fixing the production leak by adding `os.close(fd)` in a `finally` around the `os.write` (or by using `os.fdopen(fd, 'wb')` and closing it). If production code must remain unchanged, adjust the tests to monkeypatch `pr_agent.git_providers.utils.os.write` to close the fd after writing, so deletion semantics match strict platforms.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit 1ece758


🐞 Bugs (1) 📘 Rule violations (1)


Remediation recommended
1. _remove_key() ignores exceptions 📘 Rule violation ☼ Reliability
Description
The settings snapshot/restore helper in tests/unittest/_settings_helpers.py silently swallows all
exceptions when removing keys, which can mask unexpected failures and leave the process-global
Dynaconf state only partially restored. This undermines test isolation and can reintroduce
order-dependent/flaky behavior that is difficult to debug.
Code

tests/unittest/_settings_helpers.py[R34-45]

+        for stored in list(container.keys()):
+            if stored.lower() == leaf.lower():
+                try:
+                    container.pop(stored, None)
+                except Exception:
+                    pass
+                return
+        return
+    try:
+        settings.unset(key, force=True)
+    except Exception:
+        pass
Evidence
PR Compliance ID 3 requires handling errors and edge cases explicitly rather than ignoring them, yet
_remove_key() wraps both container.pop(...) (including dotted-key handling) and
settings.unset(...) in except Exception: pass, meaning any unexpected error during cleanup is
discarded with no signal. Because snapshot_settings() / restore_settings() are used to isolate
the global Dynaconf settings singleton across multiple tests (including those that mutate global
settings), suppressing these failures defeats the isolation guarantee and allows mutated state to
leak into subsequent tests while appearing to restore correctly.

Rule 3: Robust Error Handling
tests/unittest/_settings_helpers.py[34-45]
tests/unittest/_settings_helpers.py[19-55]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test settings cleanup helper `_remove_key()` currently catches `Exception` and does nothing (`pass`) when popping keys from a Dynaconf container (including dotted keys) and when calling `settings.unset(...)`. This can hide real errors, allow incomplete restoration of the process-global Dynaconf state, and cause order-dependent or flaky tests with no clear signal that restore failed.

## Issue Context
The `snapshot_settings()` / `restore_settings()` helpers are used by multiple test modules to keep tests order-independent by restoring the global Dynaconf settings singleton after mutations. Silent exception swallowing during key removal undermines this goal because cleanup failures can go unnoticed and leak mutated settings into later tests, making failures hard to reproduce and debug.

## Fix Focus Areas
- tests/unittest/_settings_helpers.py[25-55]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Tempfile fd never closed 🐞 Bug ☼ Reliability
Description
test_apply_repo_settings_security mocks mkstemp() using os.open() and asserts the temp file is
removed, but apply_repo_settings() never closes the returned fd, which can prevent deletion and
leak descriptors (notably on Windows). This makes the new tests non-portable and can destabilize
long test runs due to accumulated open fds.
Code

tests/unittest/test_apply_repo_settings_security.py[R262-307]

+    def fake_mkstemp(suffix=None, prefix=None, dir=None, text=False):
+        fd = os.open(str(known_path), os.O_RDWR | os.O_CREAT | os.O_TRUNC)
+        return fd, str(known_path)
+
+    monkeypatch.setattr(tempfile, "mkstemp", fake_mkstemp)
+
+    apply_repo_settings("https://example.com/owner/repo/pull/1")
+
+    assert not known_path.exists(), "Temp settings file must be removed after successful apply"
+
+
+def test_temp_file_is_removed_after_failed_apply(monkeypatch, tmp_path, settings_snapshot):
+    """The temp file must be removed even when the Dynaconf load step raises.
+
+    We use *valid* TOML bytes (so the failure cannot be confused with the
+    silent-swallow malformed-TOML path) and force the failure by replacing
+    the ``Dynaconf`` symbol bound inside ``pr_agent.git_providers.utils``
+    with a stub that raises *after* ``mkstemp`` has been called. We do not
+    patch the external ``dynaconf`` module — only the imported reference
+    that ``apply_repo_settings`` actually uses.
+    """
+    valid_toml = b"[pr_reviewer]\nnum_max_findings = 3\n"
+    provider = FakeGitProvider(repo_settings_bytes=valid_toml)
+    captured = _install_provider(monkeypatch, provider)
+    get_settings().set("config.use_repo_settings_file", True)
+
+    known_path = tmp_path / "repo_settings_failure.toml"
+    mkstemp_calls = {"n": 0}
+
+    def fake_mkstemp(suffix=None, prefix=None, dir=None, text=False):
+        mkstemp_calls["n"] += 1
+        fd = os.open(str(known_path), os.O_RDWR | os.O_CREAT | os.O_TRUNC)
+        return fd, str(known_path)
+
+    monkeypatch.setattr(tempfile, "mkstemp", fake_mkstemp)
+
+    def exploding_dynaconf(*args, **kwargs):
+        raise RuntimeError("boom")
+
+    monkeypatch.setattr(git_utils, "Dynaconf", exploding_dynaconf)
+
+    apply_repo_settings("https://example.com/owner/repo/pull/1")
+
+    assert mkstemp_calls["n"] == 1, "mkstemp must have run before the failure"
+    assert not known_path.exists(), "Temp settings file must be removed even after a failed apply"
+
Evidence
The test’s fake_mkstemp returns a raw fd opened with os.open(), while apply_repo_settings uses
tempfile.mkstemp and os.write but never calls os.close(fd) before attempting
os.remove(repo_settings_file). This combination can prevent deletion and leaks descriptors.

tests/unittest/test_apply_repo_settings_security.py[255-307]
pr_agent/git_providers/utils.py[14-87]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new tests patch `tempfile.mkstemp()` to return an `os.open()` fd, but production `apply_repo_settings()` calls `os.write(fd, ...)` and never closes the fd. The tests then assert the file is removed; on platforms that forbid deleting open files, removal can fail, and on all platforms this leaks file descriptors.

## Issue Context
`apply_repo_settings()` creates a temp file via `mkstemp`, writes repo settings bytes, loads Dynaconf from the path, and deletes the temp file in a `finally` block. Because the fd is left open, the delete behavior is platform-dependent.

## Fix Focus Areas
- pr_agent/git_providers/utils.py[14-45]
- tests/unittest/test_apply_repo_settings_security.py[255-315]

### Implementation guidance
Prefer fixing the production leak by adding `os.close(fd)` in a `finally` around the `os.write` (or by using `os.fdopen(fd, 'wb')` and closing it). If production code must remain unchanged, adjust the tests to monkeypatch `pr_agent.git_providers.utils.os.write` to close the fd after writing, so deletion semantics match strict platforms.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Qodo Logo

Comment thread tests/unittest/_settings_helpers.py Fixed
Comment thread tests/unittest/_settings_helpers.py Fixed
Comment thread tests/unittest/test_apply_repo_settings_security.py Fixed
Comment thread tests/unittest/test_custom_merge_loader_security.py Fixed
Comment thread tests/unittest/test_ticket_extraction_async.py Fixed
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented May 16, 2026

Persistent review updated to latest commit 370b0b8

Comment thread tests/unittest/_settings_helpers.py Fixed
Comment thread tests/unittest/_settings_helpers.py Fixed
Comment thread tests/unittest/test_apply_repo_settings_security.py Fixed
Comment on lines +42 to +45
if original is sentinel:
settings.unset("ask_diff_hunk", force=True)
else:
settings.set("ask_diff_hunk", original)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. ask_diff_hunk unset can raise 📘 Rule violation ☼ Reliability

_restore_ask_diff_hunk calls settings.unset("ask_diff_hunk", force=True) without handling
KeyError for keys that were never set. This can break teardown and leak process-global Dynaconf
state, creating order-dependent/flaky tests.
Agent Prompt
## Issue description
A test cleanup helper unsets `ask_diff_hunk` without guarding against `KeyError` when the key was never set, which can cause teardown failures and leave process-global settings in a dirty state.

## Issue Context
Other new test helpers document that Dynaconf `settings.unset(..., force=True)` may raise `KeyError` for keys that were never set; cleanup code should treat that case as benign.

## Fix Focus Areas
- tests/unittest/test_github_app_timeout_core.py[42-45]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified locally: missing ask_diff_hunk does not raise on unset, so no change needed. Please help confirm.

Cover PR processing, provider helpers, review output rendering,
configuration behavior, retry handling, ticket extraction, code
suggestions, and GitHub app workflows.

These tests target behavior that is important and likely to change,
while keeping production code untouched. They also isolate shared test
state so the new coverage remains order-independent.

This work was developed through multiple rounds of human-led AI
collaboration using codex-cli v0.130.0 with GPT-5.5, GitHub Copilot CLI
v1.0.48 with GPT-5.5, and Claude Opus 4.7.
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented May 16, 2026

Persistent review updated to latest commit 8a7c33e

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants