Skip to content

feat(adapters): detect modified files via git diff after subprocess execution - #436

Merged
frankbria merged 2 commits into
mainfrom
feature/issue-411-modified-files-detection
Mar 13, 2026
Merged

feat(adapters): detect modified files via git diff after subprocess execution#436
frankbria merged 2 commits into
mainfrom
feature/issue-411-modified-files-detection

Conversation

@frankbria

@frankbria frankbria commented Mar 13, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the remaining acceptance criterion for #411: [Phase 4] Claude Code Engine Adapter

The bulk of #411 was already implemented by PRs for #409 (AgentAdapter protocol), #414 (engine registry), and #415 (verification wrapper). The only missing piece was modified files detection via git diffAgentResult.modified_files was never populated for external engines.

Changes

  • Added _detect_modified_files() to SubprocessAdapter base class — runs git diff --name-only HEAD + git ls-files --others --exclude-standard after subprocess completion
  • Graceful fallback: returns empty list if git unavailable, not a git repo, or git fails
  • Populates AgentResult.modified_files for ALL external engines (Claude Code, OpenCode, any future adapters)
  • Added autouse fixtures to prevent git calls in existing unit tests

Acceptance Criteria (all met)

Test Plan

  • 5 new tests for _detect_modified_files() in TestSubprocessAdapterModifiedFiles
  • All 104 adapter tests pass
  • Full v2 suite: 2147 passed, 0 failed
  • Linting clean

Closes #411

Summary by CodeRabbit

  • New Features

    • Execution now reports which files were modified during runs, exposing a modified_files list with detected changes.
  • Tests

    • Added tests covering modified-file detection across success, failure, no-change, and non-git scenarios.
    • Added test fixtures to stub out real git interactions so tests run deterministically.

…xecution (#411)

SubprocessAdapter now runs git diff + git ls-files after subprocess
completion to populate AgentResult.modified_files. This gives all
external engines (Claude Code, OpenCode) automatic file change
detection, which was the last missing acceptance criterion for #411.
@coderabbitai

coderabbitai Bot commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds detection of modified, staged, and untracked files to SubprocessAdapter by running git commands after subprocess execution and populating AgentResult.modified_files. Git errors or absence are handled gracefully by returning an empty list.

Changes

Cohort / File(s) Summary
Adapter implementation
codeframe/core/adapters/subprocess_adapter.py
Adds _detect_modified_files(self, workspace_path: Path) -> list[str] which runs git diff --name-only HEAD and git ls-files --others --exclude-standard, deduplicates results, and is invoked from _drain_stderr() to set AgentResult.modified_files. Handles git absence/errors by returning [].
Adapter tests & fixtures
tests/core/adapters/test_subprocess_adapter.py
Adds autouse fixture to stub _detect_modified_files() and a new TestSubprocessAdapterModifiedFiles class with tests covering: populated list on success, empty when no changes, detection on failure, graceful behavior when not a git repo, and graceful when git fails.
Other adapter tests
tests/core/adapters/test_claude_code.py, tests/core/adapters/test_opencode.py
Add autouse _no_git fixtures to patch respective adapters' _detect_modified_files() to return [], avoiding real git interactions during tests.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Client
participant SubprocessAdapter
participant AgentProcess as Subprocess (agent CLI)
participant Git as Git commands
participant Result as AgentResult

Client->>SubprocessAdapter: execute task (cwd=workspace)
SubprocessAdapter->>AgentProcess: spawn subprocess, stream stdout/stderr
AgentProcess-->>SubprocessAdapter: stdout/stderr, exit code
SubprocessAdapter->>Git: run `git diff --name-only HEAD`
SubprocessAdapter->>Git: run `git ls-files --others --exclude-standard`
Git-->>SubprocessAdapter: file lists (or error)
SubprocessAdapter->>Result: map output + modified_files
SubprocessAdapter-->>Client: return AgentResult

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

I nibble through the repo light, 🐰
Sniffing changes in the night,
Staged or stray, I list them true,
If git is gone, I leave no clue,
Hoppity hops — an empty view. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary change: adding modified-files detection via git diff after subprocess execution in adapters.
Linked Issues check ✅ Passed The PR implements modified-files detection via git diff [#411], fulfilling the acceptance criterion to detect file modifications and populate AgentResult.modified_files.
Out of Scope Changes check ✅ Passed All changes directly support modified-files detection: new _detect_modified_files method, result integration, autouse test fixtures, and comprehensive unit tests for the feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/issue-411-modified-files-detection
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude

claude Bot commented Mar 13, 2026

Copy link
Copy Markdown

Review: feat(adapters): detect modified files via git diff

Good, focused implementation. The graceful fallback strategy is well-designed and test coverage is solid. A few issues worth addressing.

Bug: Duplicate files in _detect_modified_files (and test)

The implementation calls subprocess.run twice and appends results without deduplication. In practice git diff --name-only HEAD (tracked changes) and git ls-files --others (untracked) should not overlap — but test_populates_modified_files_on_success reveals the problem: it patches subprocess.run with a single return_value, so both calls return the same file list. Every file ends up in result.modified_files twice. The in assertions still pass, masking the duplication.

Fix the test to use side_effect with two separate mocks:

with patch(
    "subprocess.run",
    side_effect=[
        MagicMock(returncode=0, stdout="src/main.py\ntests/test_main.py\n"),  # git diff
        MagicMock(returncode=0, stdout=""),  # git ls-files (no untracked)
    ],
):

Consider adding deduplication in the implementation as a defensive guard:

return list(dict.fromkeys(files))  # preserves order, removes duplicates

Minor: Misleading test name test_modified_files_empty_on_failure

The test asserts "src/broken.py" in result.modified_files — files are detected on failure, the list is not empty. Suggested rename: test_detects_files_even_on_failure.

Edge case worth a comment: fresh git repo (no commits)

On a repo with no commits, git diff HEAD exits non-zero (HEAD does not exist yet). The code returns [] immediately, skipping the untracked-files pass too. Acceptable behavior, but a comment helps:

if result.returncode != 0:
    # Also covers repos with no commits (HEAD does not exist yet)
    return []

Implementation is correct otherwise

  • git diff --name-only HEAD correctly captures both staged and unstaged modifications vs HEAD
  • Exception handling covers FileNotFoundError, OSError, and TimeoutExpired
  • autouse fixtures in existing test classes properly guard against real git calls
  • Post-mutation pattern (result.modified_files = modified_files) is slightly awkward but functional given _map_result does not take that parameter

Summary: The duplicate-file test bug and misleading test name should be fixed before merge. Deduplication in the implementation is a nice-to-have.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
tests/core/adapters/test_subprocess_adapter.py (1)

251-267: This test doesn’t validate the untracked-files merge path yet.

At Line 260, subprocess.run is mocked with a single return value, but _detect_modified_files() performs two git calls. This can pass even if the untracked-file branch regresses.

Suggested patch
         with (
             patch("subprocess.Popen", return_value=mock_process),
             patch(
                 "subprocess.run",
-                return_value=MagicMock(
-                    returncode=0,
-                    stdout="src/main.py\ntests/test_main.py\n",
-                ),
+                side_effect=[
+                    MagicMock(returncode=0, stdout="src/main.py\n"),
+                    MagicMock(returncode=0, stdout="tests/test_main.py\n"),
+                ],
             ),
         ):
             result = adapter.run("task-1", "fix", tmp_path)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/adapters/test_subprocess_adapter.py` around lines 251 - 267, The
test test_populates_modified_files_on_success currently mocks subprocess.run
with a single return value but _detect_modified_files() invokes two git
commands; update the patch for subprocess.run in this test to use side_effect of
two MagicMock results (both with returncode=0) where the first mock's stdout
contains the git diff output (e.g., "src/main.py\n") and the second mock's
stdout contains untracked files (e.g., "tests/test_main.py\n"), so both branches
of _detect_modified_files() are exercised.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/core/adapters/test_claude_code.py`:
- Around line 15-20: This module's v2 adapter tests lack the required v2 marker;
add a module-level marker by defining pytestmark = pytest.mark.v2 at the top of
the test module (near the existing _no_git fixture and tests that exercise
ClaudeCodeAdapter) so the entire file is marked v2; alternatively, add
`@pytest.mark.v2` to each test function, but prefer the single module-level
pytestmark to ensure all tests (including fixtures like _no_git) run under v2
semantics.

In `@tests/core/adapters/test_opencode.py`:
- Around line 15-20: This test module adds v2-specific behavior but lacks the v2
marker; add a module-level pytest mark by inserting either a module decorator
`@pytest.mark.v2` above the tests or a top-level assignment pytestmark =
pytest.mark.v2 so the fixture _no_git and all tests in the module (e.g., the
_no_git autouse fixture) are executed under the v2 test configuration; ensure
you import pytest if not already present.

In `@tests/core/adapters/test_subprocess_adapter.py`:
- Around line 231-233: The new test class TestSubprocessAdapterModifiedFiles is
missing the v2 marker; update the test to be marked with pytest v2 by adding
either the decorator `@pytest.mark.v2` above the class definition or by setting
pytestmark = pytest.mark.v2 at module scope so the class is recognized as a v2
test (ensure pytest is imported as needed).

---

Nitpick comments:
In `@tests/core/adapters/test_subprocess_adapter.py`:
- Around line 251-267: The test test_populates_modified_files_on_success
currently mocks subprocess.run with a single return value but
_detect_modified_files() invokes two git commands; update the patch for
subprocess.run in this test to use side_effect of two MagicMock results (both
with returncode=0) where the first mock's stdout contains the git diff output
(e.g., "src/main.py\n") and the second mock's stdout contains untracked files
(e.g., "tests/test_main.py\n"), so both branches of _detect_modified_files() are
exercised.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d729f0e4-72b5-48e8-9afc-77be6cc67c1e

📥 Commits

Reviewing files that changed from the base of the PR and between b2cef58 and 09520be.

📒 Files selected for processing (4)
  • codeframe/core/adapters/subprocess_adapter.py
  • tests/core/adapters/test_claude_code.py
  • tests/core/adapters/test_opencode.py
  • tests/core/adapters/test_subprocess_adapter.py

Comment on lines +15 to +20
@pytest.fixture(autouse=True)
def _no_git(self):
"""Prevent _detect_modified_files from calling real git."""
with patch.object(ClaudeCodeAdapter, "_detect_modified_files", return_value=[]):
yield

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.

⚠️ Potential issue | 🟠 Major

Add a v2 marker for this test module.

The v2 adapter test updates introduced around Line 15 are in a module without @pytest.mark.v2 (or module-level pytestmark).

Suggested patch
 import pytest
 
 from codeframe.core.adapters.agent_adapter import AgentAdapter
 from codeframe.core.adapters.claude_code import ClaudeCodeAdapter
 
+pytestmark = pytest.mark.v2
+
 
 class TestClaudeCodeAdapter:

As per coding guidelines: tests/**/*.py: Test files must use the @pytest.mark.v2 decorator or module-level pytestmark = pytest.mark.v2 for v2 functionality tests.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@pytest.fixture(autouse=True)
def _no_git(self):
"""Prevent _detect_modified_files from calling real git."""
with patch.object(ClaudeCodeAdapter, "_detect_modified_files", return_value=[]):
yield
import pytest
from codeframe.core.adapters.agent_adapter import AgentAdapter
from codeframe.core.adapters.claude_code import ClaudeCodeAdapter
pytestmark = pytest.mark.v2
class TestClaudeCodeAdapter:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/adapters/test_claude_code.py` around lines 15 - 20, This module's
v2 adapter tests lack the required v2 marker; add a module-level marker by
defining pytestmark = pytest.mark.v2 at the top of the test module (near the
existing _no_git fixture and tests that exercise ClaudeCodeAdapter) so the
entire file is marked v2; alternatively, add `@pytest.mark.v2` to each test
function, but prefer the single module-level pytestmark to ensure all tests
(including fixtures like _no_git) run under v2 semantics.

Comment on lines +15 to +20
@pytest.fixture(autouse=True)
def _no_git(self):
"""Prevent _detect_modified_files from calling real git."""
with patch.object(OpenCodeAdapter, "_detect_modified_files", return_value=[]):
yield

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.

⚠️ Potential issue | 🟠 Major

Add a v2 marker for this test module.

The new v2-related adapter test behavior added around Line 15 is in a test module that is not marked with @pytest.mark.v2 (or module-level pytestmark).

Suggested patch
 import pytest
 
 from codeframe.core.adapters.agent_adapter import AgentAdapter
 from codeframe.core.adapters.opencode import OpenCodeAdapter
 
+pytestmark = pytest.mark.v2
+
 
 class TestOpenCodeAdapter:

As per coding guidelines: tests/**/*.py: New v2 Python tests must be marked with @pytest.mark.v2 decorator or pytestmark = pytest.mark.v2.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/adapters/test_opencode.py` around lines 15 - 20, This test module
adds v2-specific behavior but lacks the v2 marker; add a module-level pytest
mark by inserting either a module decorator `@pytest.mark.v2` above the tests or a
top-level assignment pytestmark = pytest.mark.v2 so the fixture _no_git and all
tests in the module (e.g., the _no_git autouse fixture) are executed under the
v2 test configuration; ensure you import pytest if not already present.

Comment on lines +231 to +233
class TestSubprocessAdapterModifiedFiles:
"""Tests for git diff file detection after execution."""

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.

⚠️ Potential issue | 🟠 Major

Mark the newly added v2 tests with @pytest.mark.v2.

The new test class introduced at Line 231 is not marked as v2.

Suggested patch
+@pytest.mark.v2
 class TestSubprocessAdapterModifiedFiles:
     """Tests for git diff file detection after execution."""

As per coding guidelines: tests/**/*.py: New v2 Python tests must be marked with @pytest.mark.v2 decorator or pytestmark = pytest.mark.v2.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/adapters/test_subprocess_adapter.py` around lines 231 - 233, The
new test class TestSubprocessAdapterModifiedFiles is missing the v2 marker;
update the test to be marked with pytest v2 by adding either the decorator
`@pytest.mark.v2` above the class definition or by setting pytestmark =
pytest.mark.v2 at module scope so the class is recognized as a v2 test (ensure
pytest is imported as needed).

- Use side_effect for subprocess.run mocks to test git diff and
  git ls-files separately
- Add dict.fromkeys deduplication as defensive guard
- Rename misleading test name
- Add comment about fresh git repos with no commits
@claude

claude Bot commented Mar 13, 2026

Copy link
Copy Markdown

Follow-up Review

All three issues from my initial review have been addressed:

  • Test bug fixedtest_populates_modified_files_on_success now uses side_effect with two distinct mocks so both git calls are exercised independently
  • Test renamedtest_detects_files_even_on_failure accurately describes the behavior
  • Fresh-repo comment added — explains the early return on non-zero exit
  • Deduplication addedlist(dict.fromkeys(files)) guards against overlap

One remaining item flagged by CodeRabbit that's still unaddressed: the new TestSubprocessAdapterModifiedFiles class (and the fixture additions in test_claude_code.py / test_opencode.py) are missing the v2 pytest marker. Per the CLAUDE.md convention:

When adding new v2 functionality, mark tests with @pytest.mark.v2 or add pytestmark = pytest.mark.v2 at module level

All three test files should have pytestmark = pytest.mark.v2 at module scope (or it can be scoped to the class for TestSubprocessAdapterModifiedFiles). This is a minor housekeeping item but keeps the test suite consistently filterable with uv run pytest -m v2.

Otherwise the implementation is clean and ready to merge once the marker is added.

@coderabbitai coderabbitai Bot left a comment

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.

♻️ Duplicate comments (1)
tests/core/adapters/test_subprocess_adapter.py (1)

231-233: ⚠️ Potential issue | 🟡 Minor

Mark the newly added v2 tests with @pytest.mark.v2.

The new test class TestSubprocessAdapterModifiedFiles is missing the required v2 marker.

Suggested fix
+@pytest.mark.v2
 class TestSubprocessAdapterModifiedFiles:
     """Tests for git diff file detection after execution."""

As per coding guidelines: tests/**/*.py: New v2 Python tests must be marked with @pytest.mark.v2 decorator or pytestmark = pytest.mark.v2.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/adapters/test_subprocess_adapter.py` around lines 231 - 233, The
new test class TestSubprocessAdapterModifiedFiles is missing the required v2
marker; add the marker by either decorating the class with `@pytest.mark.v2` or
adding pytestmark = pytest.mark.v2 at module scope so the tests are correctly
labeled as v2 per the test guidelines.
🧹 Nitpick comments (1)
tests/core/adapters/test_subprocess_adapter.py (1)

239-249: Consider extracting duplicated helper to module level.

The _make_mock_process helper is duplicated between TestSubprocessAdapterRun and TestSubprocessAdapterModifiedFiles. You could extract it to module level or a shared fixture.

def _make_mock_process(stdout_lines=None, stderr_text="", returncode=0):
    """Create a mock Popen process for subprocess tests."""
    mock = MagicMock()
    mock.stdout = iter(stdout_lines or [])
    mock.stderr = MagicMock()
    mock.stderr.read.return_value = stderr_text
    mock.stdin = MagicMock()
    mock.returncode = returncode
    mock.wait.return_value = None
    return mock

This is a minor nit and acceptable to defer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/adapters/test_subprocess_adapter.py` around lines 239 - 249,
Extract the duplicated _make_mock_process helper from inside
TestSubprocessAdapterRun and TestSubprocessAdapterModifiedFiles to a
module-level function or a shared pytest fixture so both test classes reuse the
same implementation; move the existing implementation (creating MagicMock with
stdout iterator, stderr.read, stdin, returncode, and wait) to a top-level def
_make_mock_process(...) or convert it into a fixture (e.g., mock_process) and
update test usages to call the top-level function or request the fixture instead
of defining inline copies.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@tests/core/adapters/test_subprocess_adapter.py`:
- Around line 231-233: The new test class TestSubprocessAdapterModifiedFiles is
missing the required v2 marker; add the marker by either decorating the class
with `@pytest.mark.v2` or adding pytestmark = pytest.mark.v2 at module scope so
the tests are correctly labeled as v2 per the test guidelines.

---

Nitpick comments:
In `@tests/core/adapters/test_subprocess_adapter.py`:
- Around line 239-249: Extract the duplicated _make_mock_process helper from
inside TestSubprocessAdapterRun and TestSubprocessAdapterModifiedFiles to a
module-level function or a shared pytest fixture so both test classes reuse the
same implementation; move the existing implementation (creating MagicMock with
stdout iterator, stderr.read, stdin, returncode, and wait) to a top-level def
_make_mock_process(...) or convert it into a fixture (e.g., mock_process) and
update test usages to call the top-level function or request the fixture instead
of defining inline copies.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d8486f63-af7c-4df1-9e27-5a7c5a542884

📥 Commits

Reviewing files that changed from the base of the PR and between 09520be and 7a3c84b.

📒 Files selected for processing (2)
  • codeframe/core/adapters/subprocess_adapter.py
  • tests/core/adapters/test_subprocess_adapter.py

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Phase 4] Claude Code Engine Adapter

1 participant