diff --git a/pyproject.toml b/pyproject.toml index 951b18ae..75f53f28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ dependencies = [ "requests>=2.34.2", "psutil>=5.9", "questionary>=2.0", + "pathspec>=0.12", ] [project.urls] diff --git a/src/aipass/backup/README.md b/src/aipass/backup/README.md index c81c37b9..0952e8d0 100644 --- a/src/aipass/backup/README.md +++ b/src/aipass/backup/README.md @@ -35,7 +35,7 @@ apps/ │ ├── drive_clear.py # Drive clear (stub — DPLAN-003) │ ├── drive_stats.py # Drive stats (stub — DPLAN-003) │ ├── drive_sync.py # Drive sync (stub — DPLAN-003) -│ ├── drive_test.py # Drive test (stub — DPLAN-003) +│ ├── drive_check.py # Drive check (stub — DPLAN-003) │ ├── register.py # Project registration + @name resolution │ ├── restore.py # Version discovery + file restoration │ ├── settings.py # Settings UI (stub) diff --git a/src/aipass/backup/apps/backup.py b/src/aipass/backup/apps/backup.py index b80ed2c4..8dc2fe5f 100644 --- a/src/aipass/backup/apps/backup.py +++ b/src/aipass/backup/apps/backup.py @@ -71,7 +71,7 @@ def print_help() -> None: console.print(" [green]status[/green] Show backup info and recent history") console.print(" [green]settings[/green] View/edit backup settings") console.print(" [green]drive_sync[/green] Sync backups to the remote drive") - console.print(" [green]drive_test[/green] Test the remote drive connection") + console.print(" [green]drive_check[/green] Test the remote drive connection") console.print(" [green]drive_stats[/green] Drive usage statistics") console.print(" [green]drive_clear[/green] Clear backups from the remote drive") console.print() diff --git a/src/aipass/backup/apps/modules/drive_test.py b/src/aipass/backup/apps/modules/drive_check.py similarity index 80% rename from src/aipass/backup/apps/modules/drive_test.py rename to src/aipass/backup/apps/modules/drive_check.py index 964108d5..a13c4042 100644 --- a/src/aipass/backup/apps/modules/drive_test.py +++ b/src/aipass/backup/apps/modules/drive_check.py @@ -1,12 +1,12 @@ # =================== AIPass ==================== -# Name: drive_test.py -# Description: Drive test module — verifies Google Drive connectivity via @api +# Name: drive_check.py +# Description: Drive check module — verifies Google Drive connectivity via @api # Version: 1.0.0 # Created: 2026-04-17 # Modified: 2026-06-12 # ============================================= -"""Drive Test Module — tests Drive auth through @api gateway.""" +"""Drive Check Module — tests Drive auth through @api gateway.""" import sys @@ -16,8 +16,8 @@ from aipass.cli.apps.modules import console from aipass.backup.apps.handlers.json import json_handler -MODULE_NAME = "drive_test" -PRIMARY_COMMAND = "drive_test" +MODULE_NAME = "drive_check" +PRIMARY_COMMAND = "drive_check" def print_introspection(): @@ -33,7 +33,7 @@ def print_help(): print_introspection() -def run_drive_test() -> bool: +def run_drive_check() -> bool: """Test Drive auth through @api gateway. Creates a DriveClient, authenticates, tests folder access, and @@ -57,14 +57,14 @@ def run_drive_test() -> bool: logger.warning(f"[backup] Drive test failed: {result['error']}") json_handler.log_operation( - "drive_test_complete", + "drive_check_complete", {"success": result["success"]}, ) return result["success"] def handle_command(command: str, args: list) -> bool: - """Handle the drive-test command. Returns True if handled.""" + """Handle the drive-check command. Returns True if handled.""" if command != PRIMARY_COMMAND: return False @@ -77,11 +77,11 @@ def handle_command(command: str, args: list) -> bool: return True if args[0] == "run": - run_drive_test() + run_drive_check() return True - # Default: run the test - run_drive_test() + # Default: run the check + run_drive_check() return True @@ -91,5 +91,5 @@ if __name__ == "__main__": if len(sys.argv) == 1: print_introspection() sys.exit(0) - result = handle_command(PRIMARY_COMMAND, sys.argv[1:]) - sys.exit(0 if result else 1) + handle_command(PRIMARY_COMMAND, sys.argv[1:]) + sys.exit(0) diff --git a/src/aipass/backup/tests/test_cli_routing.py b/src/aipass/backup/tests/test_cli_routing.py index 58a06b70..d1516d29 100644 --- a/src/aipass/backup/tests/test_cli_routing.py +++ b/src/aipass/backup/tests/test_cli_routing.py @@ -72,7 +72,7 @@ def _load_module_fresh(module_path: str, extra_mocks: dict | None = None): SIMPLE_MODULES = [ "aipass.backup.apps.modules.drive_sync", - "aipass.backup.apps.modules.drive_test", + "aipass.backup.apps.modules.drive_check", "aipass.backup.apps.modules.drive_stats", "aipass.backup.apps.modules.drive_clear", "aipass.backup.apps.modules.settings", diff --git a/src/aipass/backup/tests/test_drive_mocked.py b/src/aipass/backup/tests/test_drive_mocked.py index 5f071b34..1a3db292 100644 --- a/src/aipass/backup/tests/test_drive_mocked.py +++ b/src/aipass/backup/tests/test_drive_mocked.py @@ -67,18 +67,18 @@ class TestDriveSyncModule: assert isinstance(result, bool) -class TestDriveTestModule: - """Drive test stub.""" +class TestDriveCheckModule: + """Drive check stub.""" - def test_drive_test_handle_command(self) -> None: - """drive_test handle_command returns True for primary.""" - mod = _get_drive_module("drive_test") + def test_drive_check_handle_command(self) -> None: + """drive_check handle_command returns True for primary.""" + mod = _get_drive_module("drive_check") result = mod.handle_command(mod.PRIMARY_COMMAND, []) assert result is True - def test_drive_test_invalid_mode_returns_false(self) -> None: + def test_drive_check_invalid_mode_returns_false(self) -> None: """invalid_mode / invalid_type -- unknown returns False.""" - mod = _get_drive_module("drive_test") + mod = _get_drive_module("drive_check") result = mod.handle_command("invalid_type", []) assert result is False diff --git a/src/aipass/backup/tests/test_drive_pipeline.py b/src/aipass/backup/tests/test_drive_pipeline.py index efa9c704..364ae5fa 100644 --- a/src/aipass/backup/tests/test_drive_pipeline.py +++ b/src/aipass/backup/tests/test_drive_pipeline.py @@ -110,7 +110,7 @@ class TestDriveClient: client = mod.DriveClient() mock_service = MagicMock() - mod.get_drive_service = MagicMock(return_value=mock_service) + mod.get_drive_service = MagicMock(return_value=mock_service) # type: ignore[attr-defined] result = client.authenticate() assert result is True @@ -119,7 +119,7 @@ class TestDriveClient: def test_authenticate_no_api(self) -> None: """GOOGLE_API_AVAILABLE=False, authenticate() returns False.""" mod = _fresh_import("aipass.backup.apps.handlers.drive.client") - mod.GOOGLE_API_AVAILABLE = False + mod.GOOGLE_API_AVAILABLE = False # type: ignore[attr-defined] client = mod.DriveClient() result = client.authenticate() @@ -130,7 +130,7 @@ class TestDriveClient: """get_drive_service returns None, authenticate() returns False.""" mod = _fresh_import("aipass.backup.apps.handlers.drive.client") client = mod.DriveClient() - mod.get_drive_service = MagicMock(return_value=None) + mod.get_drive_service = MagicMock(return_value=None) # type: ignore[attr-defined] result = client.authenticate() assert result is False @@ -140,7 +140,7 @@ class TestDriveClient: """get_drive_service raises, authenticate() returns False.""" mod = _fresh_import("aipass.backup.apps.handlers.drive.client") client = mod.DriveClient() - mod.get_drive_service = MagicMock(side_effect=RuntimeError("boom")) + mod.get_drive_service = MagicMock(side_effect=RuntimeError("boom")) # type: ignore[attr-defined] result = client.authenticate() assert result is False @@ -173,7 +173,7 @@ class TestDriveClient: mock_service = MagicMock() client._drive_service = mock_service - mod.api_call_with_retry = MagicMock(return_value={"files": [{"id": "folder_123", "name": "AIPass Backups"}]}) + mod.api_call_with_retry = MagicMock(return_value={"files": [{"id": "folder_123", "name": "AIPass Backups"}]}) # type: ignore[attr-defined] result = client.get_or_create_backup_folder() assert result == "folder_123" @@ -196,7 +196,7 @@ class TestDriveClient: return {"id": "new_folder_456"} return {"id": "new_folder_456", "trashed": False} - mod.api_call_with_retry = MagicMock(side_effect=_side_effect) + mod.api_call_with_retry = MagicMock(side_effect=_side_effect) # type: ignore[attr-defined] result = client.get_or_create_backup_folder() assert result == "new_folder_456" @@ -218,7 +218,7 @@ class TestDriveClient: client._drive_service = mock_service client.backup_folder_id = "root_folder" - mod.api_call_with_retry = MagicMock(return_value={"files": [{"id": "proj_folder_789", "name": "myproject"}]}) + mod.api_call_with_retry = MagicMock(return_value={"files": [{"id": "proj_folder_789", "name": "myproject"}]}) # type: ignore[attr-defined] result = client.get_or_create_project_folder("myproject") assert result == "proj_folder_789" @@ -231,7 +231,7 @@ class TestDriveClient: client._drive_service = MagicMock() client.project_folder_cache["cached_proj"] = "cached_id" - mod.api_call_with_retry = MagicMock(return_value={"id": "cached_id", "trashed": False}) + mod.api_call_with_retry = MagicMock(return_value={"id": "cached_id", "trashed": False}) # type: ignore[attr-defined] result = client.get_or_create_project_folder("cached_proj") assert result == "cached_id" @@ -251,7 +251,7 @@ class TestDriveClient: return {"files": []} # Not found return {"id": f"folder_{call_count['n']}"} # Created - mod.api_call_with_retry = MagicMock(side_effect=_side_effect) + mod.api_call_with_retry = MagicMock(side_effect=_side_effect) # type: ignore[attr-defined] result = client.get_or_create_nested_folder("parent_id", "a/b") assert result is not None @@ -263,7 +263,7 @@ class TestDriveClient: mock_service = MagicMock() client._drive_service = mock_service - mod.api_call_with_retry = MagicMock(return_value={"files": [{"id": "file_abc", "name": "test.txt"}]}) + mod.api_call_with_retry = MagicMock(return_value={"files": [{"id": "file_abc", "name": "test.txt"}]}) # type: ignore[attr-defined] result = client._find_existing_file("test.txt", "parent_folder") assert result is not None @@ -276,7 +276,7 @@ class TestDriveClient: mock_service = MagicMock() client._drive_service = mock_service - mod.api_call_with_retry = MagicMock(return_value={"files": []}) + mod.api_call_with_retry = MagicMock(return_value={"files": []}) # type: ignore[attr-defined] result = client._find_existing_file("missing.txt", "parent_folder") assert result is None @@ -288,7 +288,7 @@ class TestDriveClient: mock_service = MagicMock() client._drive_service = mock_service - mod.api_call_with_retry = MagicMock(return_value={"id": "folder_ok", "trashed": False}) + mod.api_call_with_retry = MagicMock(return_value={"id": "folder_ok", "trashed": False}) # type: ignore[attr-defined] result = client._verify_folder_id("folder_ok") assert result is True @@ -300,7 +300,7 @@ class TestDriveClient: mock_service = MagicMock() client._drive_service = mock_service - mod.api_call_with_retry = MagicMock(return_value={"id": "folder_trash", "trashed": True}) + mod.api_call_with_retry = MagicMock(return_value={"id": "folder_trash", "trashed": True}) # type: ignore[attr-defined] result = client._verify_folder_id("folder_trash") assert result is False @@ -312,7 +312,7 @@ class TestDriveClient: mock_service = MagicMock() client._drive_service = mock_service - mod.api_call_with_retry = MagicMock(return_value={"ok": True}) + mod.api_call_with_retry = MagicMock(return_value={"ok": True}) # type: ignore[attr-defined] mock_request = MagicMock() result = client._api_call(mock_request) @@ -332,8 +332,8 @@ class TestDriveClient: raise RuntimeError("transient error") return {"retried": True} - mod.api_call_with_retry = MagicMock(side_effect=_side_effect) - mod.get_drive_service = MagicMock(return_value=MagicMock()) + mod.api_call_with_retry = MagicMock(side_effect=_side_effect) # type: ignore[attr-defined] + mod.get_drive_service = MagicMock(return_value=MagicMock()) # type: ignore[attr-defined] result = client._api_call(MagicMock()) assert result == {"retried": True} @@ -512,7 +512,7 @@ class TestDriveUpload: test_file.write_text("print('hello')", encoding="utf-8") # Mock api_call_with_retry to return file id - client_mod.api_call_with_retry = MagicMock(return_value={"id": "new_file_id"}) + client_mod.api_call_with_retry = MagicMock(return_value={"id": "new_file_id"}) # type: ignore[attr-defined] result = mod.upload_single_file(client, test_file, "testproj", tmp_path) assert result is True @@ -533,7 +533,7 @@ class TestDriveUpload: test_file = tmp_path / "existing.py" test_file.write_text("updated content", encoding="utf-8") - client_mod.api_call_with_retry = MagicMock(return_value={"id": "existing_drive_id"}) + client_mod.api_call_with_retry = MagicMock(return_value={"id": "existing_drive_id"}) # type: ignore[attr-defined] result = mod.upload_single_file(client, test_file, "testproj", tmp_path) assert result is True @@ -577,8 +577,8 @@ class TestDriveUpload: f.write_text(f"content {i}", encoding="utf-8") files.append(f) - client_mod.api_call_with_retry = MagicMock(return_value={"id": "file_id"}) - client_mod.get_drive_service = MagicMock(return_value=mock_service) + client_mod.api_call_with_retry = MagicMock(return_value={"id": "file_id"}) # type: ignore[attr-defined] + client_mod.get_drive_service = MagicMock(return_value=mock_service) # type: ignore[attr-defined] progress_calls = [] @@ -834,24 +834,24 @@ class TestDriveSync: # --------------------------------------------------------------------------- -class TestDriveTestModule: - """Tests for drive_test module.""" +class TestDriveCheckModule: + """Tests for drive_check module.""" def test_handle_command_primary(self) -> None: - mod = _fresh_import("aipass.backup.apps.modules.drive_test") - assert mod.handle_command("drive_test", []) is True + mod = _fresh_import("aipass.backup.apps.modules.drive_check") + assert mod.handle_command("drive_check", []) is True def test_handle_command_help(self) -> None: - mod = _fresh_import("aipass.backup.apps.modules.drive_test") - assert mod.handle_command("drive_test", ["--help"]) is True + mod = _fresh_import("aipass.backup.apps.modules.drive_check") + assert mod.handle_command("drive_check", ["--help"]) is True def test_handle_command_wrong(self) -> None: - mod = _fresh_import("aipass.backup.apps.modules.drive_test") + mod = _fresh_import("aipass.backup.apps.modules.drive_check") assert mod.handle_command("wrong", []) is False - def test_run_drive_test_success(self) -> None: - """Run drive test with mocked success.""" - mod = _fresh_import("aipass.backup.apps.modules.drive_test") + def test_run_drive_check_success(self) -> None: + """Run drive check with mocked success.""" + mod = _fresh_import("aipass.backup.apps.modules.drive_check") mock_client_module = MagicMock() mock_client_instance = MagicMock() @@ -871,7 +871,7 @@ class TestDriveTestModule: "aipass.backup.apps.handlers.drive.test": mock_test_module, }, ): - result = mod.run_drive_test() + result = mod.run_drive_check() assert result is True @@ -978,7 +978,7 @@ class TestThreadSafety: return {"id": "proj_folder_unique"} return {"trashed": False} - mod.api_call_with_retry = MagicMock(side_effect=_side_effect) + mod.api_call_with_retry = MagicMock(side_effect=_side_effect) # type: ignore[attr-defined] results = [] @@ -1001,7 +1001,7 @@ class TestThreadSafety: client._drive_service = MagicMock() client.backup_folder_id = "already_set" - mod.api_call_with_retry = MagicMock(return_value={"id": "already_set", "trashed": False}) + mod.api_call_with_retry = MagicMock(return_value={"id": "already_set", "trashed": False}) # type: ignore[attr-defined] result = client.get_or_create_backup_folder() assert result == "already_set" @@ -1014,7 +1014,7 @@ class TestThreadSafety: client._drive_service = MagicMock() client.file_tracker = {"existing.txt": {"drive_id": "abc"}} - mod.api_call_with_retry = MagicMock(return_value={"files": [{"id": "found_folder", "name": "AIPass Backups"}]}) + mod.api_call_with_retry = MagicMock(return_value={"files": [{"id": "found_folder", "name": "AIPass Backups"}]}) # type: ignore[attr-defined] result = client.get_or_create_backup_folder() assert result == "found_folder" @@ -1037,7 +1037,7 @@ class TestThreadSafety: return {"id": "brand_new_folder"} return {"id": "brand_new_folder", "trashed": False} - mod.api_call_with_retry = MagicMock(side_effect=_side_effect) + mod.api_call_with_retry = MagicMock(side_effect=_side_effect) # type: ignore[attr-defined] result = client.get_or_create_backup_folder() assert result == "brand_new_folder" @@ -1060,7 +1060,7 @@ class TestThreadSafety: return {"id": "new_folder"} return {"id": "new_folder", "trashed": False} - mod.api_call_with_retry = MagicMock(side_effect=_side_effect) + mod.api_call_with_retry = MagicMock(side_effect=_side_effect) # type: ignore[attr-defined] result = client.get_or_create_backup_folder() assert result == "new_folder" @@ -1106,11 +1106,11 @@ class TestCommandRouting: assert mod.handle_command("drive_sync", []) is True assert mod.handle_command("drive-sync", []) is False - def test_drive_test_routes_underscore(self) -> None: - mod = _fresh_import("aipass.backup.apps.modules.drive_test") - assert mod.PRIMARY_COMMAND == "drive_test" - assert mod.handle_command("drive_test", []) is True - assert mod.handle_command("drive-test", []) is False + def test_drive_check_routes_underscore(self) -> None: + mod = _fresh_import("aipass.backup.apps.modules.drive_check") + assert mod.PRIMARY_COMMAND == "drive_check" + assert mod.handle_command("drive_check", []) is True + assert mod.handle_command("drive-check", []) is False def test_drive_stats_routes_underscore(self) -> None: mod = _fresh_import("aipass.backup.apps.modules.drive_stats")