Skip to content

test: Phase 7 — test coverage (+24 tests, 64 → 88 total)#6

Merged
jyshnkr merged 1 commit intomainfrom
release
Mar 1, 2026
Merged

test: Phase 7 — test coverage (+24 tests, 64 → 88 total)#6
jyshnkr merged 1 commit intomainfrom
release

Conversation

@jyshnkr
Copy link
Copy Markdown
Owner

@jyshnkr jyshnkr commented Mar 1, 2026

test_backup.py (9 new):

  • backup_file path structure, rotation (3 keep), exact keep_count enforcement
  • list_backups newest-first ordering, empty when no dir
  • restore_backup single file, all files, unknown-id error
  • restore_backup path-traversal rejection (validates Phase 1.4)

test_cli.py (7 new, via typer CliRunner):

  • push updates manifest on success; skips on errors (validates Phase 2.3)
  • push cleans up sanitized temp even when engine raises
  • pull skips merge on empty remote .claude.json; merges when non-empty
  • remote add rejects address without @
  • diff includes per-project files (validates Phase 3.1)

test_engine.py (4 new):

  • push with project paths calls rsync per-project
  • get_remote_file_hashes raises on invalid JSON (validates Phase 2.1)
  • _rsync_project aggregates all failures (validates Phase 3.2)
  • check_connection handles TimeoutExpired (validates Phase 2.2)

test_sanitize.py (4 new):

  • strips primaryApiKey and hasCompletedOnboarding
  • merge raises ValueError on corrupted pulled JSON (validates Phase 1.2)
  • merge raises ValueError on corrupted local JSON (validates Phase 1.2)

cli.py: restore Optional[str] type hint for backup_restore (typer needs it
on Python 3.9 — get_type_hints doesn't evaluate str|None syntax at runtime)

Summary by CodeRabbit

  • Tests

    • Added comprehensive test coverage for backup management, CLI operations, sync engine functionality, and JSON sanitization.
  • Chores

    • Updated type annotations for improved code consistency.

test_backup.py (9 new):
  - backup_file path structure, rotation (3 keep), exact keep_count enforcement
  - list_backups newest-first ordering, empty when no dir
  - restore_backup single file, all files, unknown-id error
  - restore_backup path-traversal rejection (validates Phase 1.4)

test_cli.py (7 new, via typer CliRunner):
  - push updates manifest on success; skips on errors (validates Phase 2.3)
  - push cleans up sanitized temp even when engine raises
  - pull skips merge on empty remote .claude.json; merges when non-empty
  - remote add rejects address without @
  - diff includes per-project files (validates Phase 3.1)

test_engine.py (4 new):
  - push with project paths calls rsync per-project
  - get_remote_file_hashes raises on invalid JSON (validates Phase 2.1)
  - _rsync_project aggregates all failures (validates Phase 3.2)
  - check_connection handles TimeoutExpired (validates Phase 2.2)

test_sanitize.py (4 new):
  - strips primaryApiKey and hasCompletedOnboarding
  - merge raises ValueError on corrupted pulled JSON (validates Phase 1.2)
  - merge raises ValueError on corrupted local JSON (validates Phase 1.2)

cli.py: restore Optional[str] type hint for backup_restore (typer needs it
  on Python 3.9 — get_type_hints doesn't evaluate str|None syntax at runtime)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 1, 2026

📝 Walkthrough

Walkthrough

The PR updates a type annotation in the CLI module from str | None to Optional[str] and introduces comprehensive test suites covering backup management, CLI functionality, engine operations, and JSON sanitization/merging with extensive mocking and validation assertions.

Changes

Cohort / File(s) Summary
Type Annotation Update
src/claudesync/cli.py
Updated backup_restore parameter type from str | None to Optional[str] with corresponding import addition; no behavioral change.
Test Coverage Expansion
tests/test_backup.py, tests/test_cli.py, tests/test_engine.py, tests/test_sanitize.py
Added 386 lines of comprehensive test suites covering: backup creation/rotation/retention and restoration (including path traversal protection), CLI operations (push/pull/remote add/diff), engine rsync and connection handling, and JSON sanitization with sensitive field removal and corruption detection.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • #3: Modifies the same backup_restore function signature in src/claudesync/cli.py with inverse type annotation change (Optional[str]str | None).

Poem

🐰 Hops through tests with glee,
Types refined for clarity,
Backups safe, paths secure,
Code coverage more pure! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 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 main change: adding 24 tests and increasing total test coverage from 64 to 88 tests.

✏️ 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 release

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

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Add Phase 7 test coverage with 24 new tests (64 → 88 total)

🧪 Tests

Grey Divider

Walkthroughs

Description
• Add 24 comprehensive tests across backup, CLI, engine, and sanitize modules
• Test backup file rotation, restoration, and path-traversal security validation
• Test CLI commands (push, pull, remote add, diff) with mocked dependencies
• Test engine rsync per-project calls, JSON parsing, and timeout handling
• Fix type hint in backup_restore parameter for Python 3.9 compatibility
Diagram
flowchart LR
  A["Test Suite Expansion"] --> B["test_backup.py<br/>9 tests"]
  A --> C["test_cli.py<br/>7 tests"]
  A --> D["test_engine.py<br/>4 tests"]
  A --> E["test_sanitize.py<br/>4 tests"]
  A --> F["cli.py<br/>Type hint fix"]
  B --> B1["Backup rotation & keep_count"]
  B --> B2["List/restore operations"]
  B --> B3["Path traversal rejection"]
  C --> C1["Push manifest updates"]
  C --> C2["Pull merge logic"]
  C --> C3["Remote add validation"]
  C --> C4["Diff project files"]
  D --> D1["Per-project rsync calls"]
  D --> D2["JSON parsing errors"]
  D --> D3["Timeout handling"]
  E --> E1["Sensitive field stripping"]
  E --> E2["Corrupted JSON handling"]
Loading

Grey Divider

File Changes

1. tests/test_backup.py 🧪 Tests +123/-0

Backup management tests with rotation and security

• New file with 9 tests for backup file creation, rotation, and restoration
• Tests verify backup path structure and keep_count enforcement (3 backups max)
• Tests validate list_backups returns newest-first ordering and handles missing directories
• Tests confirm restore_backup works for single/all files and rejects path traversal attacks

tests/test_backup.py


2. tests/test_cli.py 🧪 Tests +165/-0

CLI command tests with manifest and merge validation

• New file with 7 tests for CLI commands using typer CliRunner
• Tests verify push updates manifest on success but skips on errors
• Tests confirm push cleans up sanitized temp files even on engine exceptions
• Tests validate pull skips merge for empty remote .claude.json but merges when non-empty
• Tests check remote add rejects addresses without @ symbol and diff includes project files

tests/test_cli.py


3. tests/test_engine.py 🧪 Tests +60/-0

Engine rsync, JSON parsing, and timeout tests

• Add 4 new tests to existing test_engine.py file
• Test push calls rsync per-project (minimum 2 calls: 1 global + 1+ project)
• Test get_remote_file_hashes raises SyncError on invalid JSON output
• Test _rsync_project aggregates all per-item failures into combined result
• Test check_connection returns False when subprocess.TimeoutExpired is raised

tests/test_engine.py


View more (2)
4. tests/test_sanitize.py 🧪 Tests +38/-0

Sanitization and merge error handling tests

• Add 4 new tests to existing test_sanitize.py file
• Test sanitize_claude_json strips primaryApiKey and hasCompletedOnboarding fields
• Test merge_pulled_claude_json raises ValueError on corrupted pulled JSON
• Test merge_pulled_claude_json raises ValueError on corrupted local JSON

tests/test_sanitize.py


5. src/claudesync/cli.py 🐞 Bug fix +2/-1

Fix type hint for Python 3.9 compatibility

• Add Optional import from typing module
• Change backup_restore parameter type hint from str | None to Optional[str]
• Fix Python 3.9 compatibility issue where get_type_hints doesn't evaluate union syntax at runtime

src/claudesync/cli.py


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Mar 1, 2026

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Remediation recommended

1. Missing CLI exit_code asserts 🐞 Bug ⛯ Reliability
Description
Several new tests/test_cli.py cases don’t assert result.exit_code, so they may pass even if the
CLI command errors before reaching the behavior under test. This can mask regressions (false
positives), especially for pull/diff where success is expected.
Code

tests/test_cli.py[R75-165]

+def test_push_cleans_up_sanitized_temp_on_engine_exception(mock_config, connected_engine):
+    tmp_file = MagicMock(spec=Path)
+    connected_engine.push.side_effect = RuntimeError("rsync crashed")
+
+    with patch("claudesync.cli.load_config", return_value=mock_config), \
+         patch("claudesync.cli.Engine", return_value=connected_engine), \
+         patch("claudesync.cli.get_global_include_paths", return_value=[]), \
+         patch("claudesync.cli.build_local_manifest", return_value={}), \
+         patch("claudesync.cli.get_remote_manifest", return_value={}), \
+         patch("claudesync.cli.write_sanitized_temp", return_value=tmp_file):
+        result = runner.invoke(app, ["push", REMOTE_NAME])
+
+    # Even on exception, unlink should have been called
+    tmp_file.unlink.assert_called_once()
+
+
+# ---------------------------------------------------------------------------
+# pull
+# ---------------------------------------------------------------------------
+
+def test_pull_skips_merge_on_empty_remote_claude_json(mock_config, connected_engine, tmp_path):
+    empty_tmp = tmp_path / "empty.json"
+    empty_tmp.write_text("")  # zero bytes — merge should be skipped
+
+    with patch("claudesync.cli.load_config", return_value=mock_config), \
+         patch("claudesync.cli.Engine", return_value=connected_engine), \
+         patch("claudesync.cli.get_global_include_paths", return_value=[]), \
+         patch("claudesync.cli.build_local_manifest", return_value={}), \
+         patch("claudesync.cli.get_remote_manifest", return_value={}), \
+         patch("claudesync.cli.update_manifest_for_remote"), \
+         patch("claudesync.cli.merge_pulled_claude_json") as mock_merge, \
+         patch("tempfile.NamedTemporaryFile") as mock_ntf:
+        mock_ntf.return_value.__enter__.return_value.name = str(empty_tmp)
+        result = runner.invoke(app, ["pull", REMOTE_NAME])
+
+    mock_merge.assert_not_called()
+
+
+def test_pull_merges_when_remote_claude_json_nonempty(mock_config, connected_engine, tmp_path):
+    nonempty_tmp = tmp_path / "nonempty.json"
+    nonempty_tmp.write_text('{"key": "value"}')
+
+    with patch("claudesync.cli.load_config", return_value=mock_config), \
+         patch("claudesync.cli.Engine", return_value=connected_engine), \
+         patch("claudesync.cli.get_global_include_paths", return_value=[]), \
+         patch("claudesync.cli.build_local_manifest", return_value={}), \
+         patch("claudesync.cli.get_remote_manifest", return_value={}), \
+         patch("claudesync.cli.update_manifest_for_remote"), \
+         patch("claudesync.cli.merge_pulled_claude_json") as mock_merge, \
+         patch("tempfile.NamedTemporaryFile") as mock_ntf:
+        mock_ntf.return_value.__enter__.return_value.name = str(nonempty_tmp)
+        result = runner.invoke(app, ["pull", REMOTE_NAME])
+
+    mock_merge.assert_called_once()
+
+
+# ---------------------------------------------------------------------------
+# remote add
+# ---------------------------------------------------------------------------
+
+def test_remote_add_rejects_missing_at_symbol(mock_config):
+    with patch("claudesync.cli.load_config", return_value=mock_config):
+        result = runner.invoke(app, ["remote", "add", "work", "nousernamehost"])
+
+    assert result.exit_code != 0
+    assert "user@host" in result.output
+
+
+# ---------------------------------------------------------------------------
+# diff
+# ---------------------------------------------------------------------------
+
+def test_diff_includes_project_files(mock_config, connected_engine, tmp_path):
+    """diff should discover per-project files, not just global ones."""
+    proj = tmp_path / "MyProject"
+    proj.mkdir()
+    claude_md = proj / "CLAUDE.md"
+    claude_md.write_text("# My project")
+
+    mock_config.projects = [str(proj)]
+
+    with patch("claudesync.cli.load_config", return_value=mock_config), \
+         patch("claudesync.cli.Engine", return_value=connected_engine), \
+         patch("claudesync.cli.get_global_include_paths", return_value=[]), \
+         patch("claudesync.cli.get_remote_manifest", return_value={}), \
+         patch("claudesync.cli.build_local_manifest", return_value={}) as mock_build:
+        result = runner.invoke(app, ["diff", REMOTE_NAME])
+
+    # build_local_manifest should have received CLAUDE.md in the file list
+    call_args = mock_build.call_args[0][0]
+    assert str(claude_md) in call_args
Evidence
These tests capture the CliRunner.invoke(...) result but never validate whether the command
succeeded/failed. In particular, push does not catch exceptions from engine.push (only ensures
temp cleanup), so an injected exception should produce a non-zero exit code; without asserting that,
the test can pass even if the command fails in unexpected ways. Similarly, pull and diff tests
should assert success (exit_code == 0) to ensure the command actually ran to completion.

tests/test_cli.py[75-129]
tests/test_cli.py[147-165]
src/claudesync/cli.py[185-191]

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

### Issue description
Several CLI tests call `runner.invoke(...)` but never assert `result.exit_code`. This can allow tests to pass even if the CLI command crashes/exits unexpectedly (false positives), reducing CI reliability.

### Issue Context
- `push` does not catch exceptions from `engine.push`; it only guarantees temp cleanup in a `finally`, so an injected exception should yield a non-zero `exit_code`.
- `pull` and `diff` tests should assert success (`exit_code == 0`) to ensure the command actually completed.

### Fix Focus Areas
- tests/test_cli.py[75-89]
- tests/test_cli.py[95-110]
- tests/test_cli.py[113-129]
- tests/test_cli.py[147-165]

### Suggested changes
- In `test_push_cleans_up_sanitized_temp_on_engine_exception`:
 - Add `assert result.exit_code != 0`
 - Optionally assert `isinstance(result.exception, RuntimeError)` or that the error text appears in output (depending on how Typer surfaces it).
- In `test_pull_skips_merge_on_empty_remote_claude_json` and `test_pull_merges_when_remote_claude_json_nonempty`:
 - Add `assert result.exit_code == 0`
- In `test_diff_includes_project_files`:
 - Add `assert result.exit_code == 0`

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


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 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/test_cli.py`:
- Around line 50-51: Replace the hardcoded Path("/tmp/sanitized.json") in
tests/test_cli.py (used in the patch for write_sanitized_temp) with a path
derived from the pytest tmp_path fixture (e.g., tmp_path / "sanitized.json");
update the patch call return_value to that tmp_path-derived Path and adjust any
assertions or cleanup expectations accordingly (the existing
patch("pathlib.Path.unlink") can remain but ensure it operates on the tmp_path
file); do the same replacement for the second occurrence around lines 67-68 so
tests are portable and do not rely on /tmp.
- Line 85: The test currently assigns the CLI invocation to result but never
asserts its exit status; after each runner.invoke(app, [...]) call (e.g., the
push invocation using REMOTE_NAME and the other invocations at the other noted
locations) add an explicit assertion on result.exit_code (typically assert
result.exit_code == 0 for successful commands) and optionally assert expected
output via result.output or include result.exception for failures; update each
unused `result = runner.invoke(app, ...)` occurrence to follow with an exit-code
assertion to ensure the command completed as expected.
- Around line 20-25: The fixture function mock_config currently accepts an
unused tmp_path parameter, causing ARG001; remove tmp_path from the mock_config
signature so it reads def mock_config(): and leave the function body unchanged,
ensuring any tests or fixtures that reference mock_config by name (e.g., uses of
mock_config in tests) continue to work without passing tmp_path; no changes are
required to Remote, Config, REMOTE_NAME, or SyncSettings.

In `@tests/test_engine.py`:
- Around line 177-198: The test test_rsync_project_aggregates_all_failures is
too weakly asserting aggregated errors; update the assertion that checks
combined.stderr so it expects the exact number of per-item failures (3) since
three rsync targets are created and forced to fail; modify the assertion on
combined.stderr.count("error") to assert == 3 (referencing the test function
name and engine._rsync_project / combined / fail_result to locate the
assertion).

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7dbb739 and 954b047.

📒 Files selected for processing (5)
  • src/claudesync/cli.py
  • tests/test_backup.py
  • tests/test_cli.py
  • tests/test_engine.py
  • tests/test_sanitize.py

Comment on lines +20 to +25
def mock_config(tmp_path):
"""A Config with one remote and no projects."""
remote = Remote(host="192.168.1.1", user="alice", ssh_key="~/.ssh/id_ed25519",
remote_home="/home/alice")
config = Config(remotes={REMOTE_NAME: remote}, projects=[], sync=SyncSettings())
return config
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove unused tmp_path fixture parameter from mock_config.

Line 20 introduces an unused argument and trips ARG001. This fixture does not need it.

Proposed fix
-@pytest.fixture
-def mock_config(tmp_path):
+@pytest.fixture
+def mock_config():
🧰 Tools
🪛 Ruff (0.15.2)

[warning] 20-20: Unused function argument: tmp_path

(ARG001)

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

In `@tests/test_cli.py` around lines 20 - 25, The fixture function mock_config
currently accepts an unused tmp_path parameter, causing ARG001; remove tmp_path
from the mock_config signature so it reads def mock_config(): and leave the
function body unchanged, ensuring any tests or fixtures that reference
mock_config by name (e.g., uses of mock_config in tests) continue to work
without passing tmp_path; no changes are required to Remote, Config,
REMOTE_NAME, or SyncSettings.

Comment on lines +50 to +51
patch("claudesync.cli.write_sanitized_temp", return_value=Path("/tmp/sanitized.json")), \
patch("pathlib.Path.unlink"):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid hardcoded /tmp paths in tests.

Lines 50 and 67 use Path("/tmp/sanitized.json"), which is non-portable and triggers S108. Prefer tmp_path per test.

Proposed fix
-def test_push_updates_manifest_after_successful_sync(mock_config, connected_engine):
+def test_push_updates_manifest_after_successful_sync(mock_config, connected_engine, tmp_path):
+    sanitized_tmp = tmp_path / "sanitized.json"
     with patch("claudesync.cli.load_config", return_value=mock_config), \
          patch("claudesync.cli.Engine", return_value=connected_engine), \
          patch("claudesync.cli.get_global_include_paths", return_value=[]), \
          patch("claudesync.cli.build_local_manifest", return_value={}), \
          patch("claudesync.cli.get_remote_manifest", return_value={}), \
          patch("claudesync.cli.update_manifest_for_remote") as mock_update, \
-         patch("claudesync.cli.write_sanitized_temp", return_value=Path("/tmp/sanitized.json")), \
-         patch("pathlib.Path.unlink"):
+         patch("claudesync.cli.write_sanitized_temp", return_value=sanitized_tmp):
         result = runner.invoke(app, ["push", REMOTE_NAME])

-def test_push_skips_manifest_update_on_errors(mock_config, connected_engine):
+def test_push_skips_manifest_update_on_errors(mock_config, connected_engine, tmp_path):
+    sanitized_tmp = tmp_path / "sanitized.json"
     connected_engine.push.return_value = SyncSummary(files_transferred=0, errors=["rsync failed"])

     with patch("claudesync.cli.load_config", return_value=mock_config), \
          patch("claudesync.cli.Engine", return_value=connected_engine), \
          patch("claudesync.cli.get_global_include_paths", return_value=[]), \
          patch("claudesync.cli.build_local_manifest", return_value={}), \
          patch("claudesync.cli.get_remote_manifest", return_value={}), \
          patch("claudesync.cli.update_manifest_for_remote") as mock_update, \
-         patch("claudesync.cli.write_sanitized_temp", return_value=Path("/tmp/sanitized.json")), \
-         patch("pathlib.Path.unlink"):
+         patch("claudesync.cli.write_sanitized_temp", return_value=sanitized_tmp):
         result = runner.invoke(app, ["push", REMOTE_NAME])

Also applies to: 67-68

🧰 Tools
🪛 Ruff (0.15.2)

[error] 50-50: Probable insecure usage of temporary file or directory: "/tmp/sanitized.json"

(S108)

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

In `@tests/test_cli.py` around lines 50 - 51, Replace the hardcoded
Path("/tmp/sanitized.json") in tests/test_cli.py (used in the patch for
write_sanitized_temp) with a path derived from the pytest tmp_path fixture
(e.g., tmp_path / "sanitized.json"); update the patch call return_value to that
tmp_path-derived Path and adjust any assertions or cleanup expectations
accordingly (the existing patch("pathlib.Path.unlink") can remain but ensure it
operates on the tmp_path file); do the same replacement for the second
occurrence around lines 67-68 so tests are portable and do not rely on /tmp.

patch("claudesync.cli.build_local_manifest", return_value={}), \
patch("claudesync.cli.get_remote_manifest", return_value={}), \
patch("claudesync.cli.write_sanitized_temp", return_value=tmp_file):
result = runner.invoke(app, ["push", REMOTE_NAME])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use result to assert command exit behavior.

These assignments are currently unused (F841). Add exit-code assertions so tests validate command completion/failure explicitly.

Proposed fix
 def test_push_cleans_up_sanitized_temp_on_engine_exception(mock_config, connected_engine):
@@
     with patch("claudesync.cli.load_config", return_value=mock_config), \
          patch("claudesync.cli.Engine", return_value=connected_engine), \
          patch("claudesync.cli.get_global_include_paths", return_value=[]), \
          patch("claudesync.cli.build_local_manifest", return_value={}), \
          patch("claudesync.cli.get_remote_manifest", return_value={}), \
          patch("claudesync.cli.write_sanitized_temp", return_value=tmp_file):
         result = runner.invoke(app, ["push", REMOTE_NAME])

+    assert result.exit_code != 0
     # Even on exception, unlink should have been called
     tmp_file.unlink.assert_called_once()

 def test_pull_skips_merge_on_empty_remote_claude_json(mock_config, connected_engine, tmp_path):
@@
         mock_ntf.return_value.__enter__.return_value.name = str(empty_tmp)
         result = runner.invoke(app, ["pull", REMOTE_NAME])

+    assert result.exit_code == 0
     mock_merge.assert_not_called()

 def test_pull_merges_when_remote_claude_json_nonempty(mock_config, connected_engine, tmp_path):
@@
         mock_ntf.return_value.__enter__.return_value.name = str(nonempty_tmp)
         result = runner.invoke(app, ["pull", REMOTE_NAME])

+    assert result.exit_code == 0
     mock_merge.assert_called_once()

 def test_diff_includes_project_files(mock_config, connected_engine, tmp_path):
@@
     with patch("claudesync.cli.load_config", return_value=mock_config), \
          patch("claudesync.cli.Engine", return_value=connected_engine), \
          patch("claudesync.cli.get_global_include_paths", return_value=[]), \
          patch("claudesync.cli.get_remote_manifest", return_value={}), \
          patch("claudesync.cli.build_local_manifest", return_value={}) as mock_build:
         result = runner.invoke(app, ["diff", REMOTE_NAME])

+    assert result.exit_code == 0
     # build_local_manifest should have received CLAUDE.md in the file list
     call_args = mock_build.call_args[0][0]
     assert str(claude_md) in call_args

Also applies to: 108-108, 126-126, 161-161

🧰 Tools
🪛 Ruff (0.15.2)

[error] 85-85: Local variable result is assigned to but never used

Remove assignment to unused variable result

(F841)

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

In `@tests/test_cli.py` at line 85, The test currently assigns the CLI invocation
to result but never asserts its exit status; after each runner.invoke(app,
[...]) call (e.g., the push invocation using REMOTE_NAME and the other
invocations at the other noted locations) add an explicit assertion on
result.exit_code (typically assert result.exit_code == 0 for successful
commands) and optionally assert expected output via result.output or include
result.exception for failures; update each unused `result = runner.invoke(app,
...)` occurrence to follow with an exit-code assertion to ensure the command
completed as expected.

Comment on lines +177 to +198
def test_rsync_project_aggregates_all_failures(engine, tmp_path):
"""All per-item rsync failures are aggregated, not just the first."""
proj = tmp_path / "Proj"
proj.mkdir()
# Create all three items so all rsync calls are attempted on push
(proj / ".claude").mkdir()
(proj / ".claude" / "settings.json").write_text("{}")
(proj / "CLAUDE.md").write_text("# x")
(proj / ".mcp.json").write_text("{}")

fail_result = MagicMock()
fail_result.returncode = 1
fail_result.stdout = ""
fail_result.stderr = "error"

with patch("claudesync.engine.subprocess.run", return_value=fail_result):
combined = engine._rsync_project(proj, direction="push", dry_run=False)

assert combined.returncode != 0
# Combined stderr should contain errors from all 3 items
assert combined.stderr.count("error") >= 2

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Strengthen the “all failures” assertion.

The current check (>= 2) is weaker than the test intent. Since all per-item rsync calls are forced to fail here, assert the exact expected number of aggregated failures.

Proposed fix
 def test_rsync_project_aggregates_all_failures(engine, tmp_path):
@@
     assert combined.returncode != 0
-    # Combined stderr should contain errors from all 3 items
-    assert combined.stderr.count("error") >= 2
+    # Combined stderr should contain one error line per attempted item
+    assert combined.stderr.count("error") == 3
📝 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
def test_rsync_project_aggregates_all_failures(engine, tmp_path):
"""All per-item rsync failures are aggregated, not just the first."""
proj = tmp_path / "Proj"
proj.mkdir()
# Create all three items so all rsync calls are attempted on push
(proj / ".claude").mkdir()
(proj / ".claude" / "settings.json").write_text("{}")
(proj / "CLAUDE.md").write_text("# x")
(proj / ".mcp.json").write_text("{}")
fail_result = MagicMock()
fail_result.returncode = 1
fail_result.stdout = ""
fail_result.stderr = "error"
with patch("claudesync.engine.subprocess.run", return_value=fail_result):
combined = engine._rsync_project(proj, direction="push", dry_run=False)
assert combined.returncode != 0
# Combined stderr should contain errors from all 3 items
assert combined.stderr.count("error") >= 2
def test_rsync_project_aggregates_all_failures(engine, tmp_path):
"""All per-item rsync failures are aggregated, not just the first."""
proj = tmp_path / "Proj"
proj.mkdir()
# Create all three items so all rsync calls are attempted on push
(proj / ".claude").mkdir()
(proj / ".claude" / "settings.json").write_text("{}")
(proj / "CLAUDE.md").write_text("# x")
(proj / ".mcp.json").write_text("{}")
fail_result = MagicMock()
fail_result.returncode = 1
fail_result.stdout = ""
fail_result.stderr = "error"
with patch("claudesync.engine.subprocess.run", return_value=fail_result):
combined = engine._rsync_project(proj, direction="push", dry_run=False)
assert combined.returncode != 0
# Combined stderr should contain one error line per attempted item
assert combined.stderr.count("error") == 3
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_engine.py` around lines 177 - 198, The test
test_rsync_project_aggregates_all_failures is too weakly asserting aggregated
errors; update the assertion that checks combined.stderr so it expects the exact
number of per-item failures (3) since three rsync targets are created and forced
to fail; modify the assertion on combined.stderr.count("error") to assert == 3
(referencing the test function name and engine._rsync_project / combined /
fail_result to locate the assertion).

@jyshnkr jyshnkr merged commit 42cb7a9 into main Mar 1, 2026
3 checks passed
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.

1 participant