Conversation
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>
📝 WalkthroughWalkthroughThe PR updates a type annotation in the CLI module from Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
Review Summary by QodoAdd Phase 7 test coverage with 24 new tests (64 → 88 total)
WalkthroughsDescription• 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 Diagramflowchart 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"]
File Changes1. tests/test_backup.py
|
Code Review by Qodo
1. Missing CLI exit_code asserts
|
There was a problem hiding this comment.
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
📒 Files selected for processing (5)
src/claudesync/cli.pytests/test_backup.pytests/test_cli.pytests/test_engine.pytests/test_sanitize.py
| 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 |
There was a problem hiding this comment.
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.
| patch("claudesync.cli.write_sanitized_temp", return_value=Path("/tmp/sanitized.json")), \ | ||
| patch("pathlib.Path.unlink"): |
There was a problem hiding this comment.
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]) |
There was a problem hiding this comment.
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_argsAlso 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.
| 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 | ||
|
|
There was a problem hiding this comment.
🧹 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.
| 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).
test_backup.py (9 new):
test_cli.py (7 new, via typer CliRunner):
test_engine.py (4 new):
test_sanitize.py (4 new):
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
Chores