--- phase: "13" plan: "05" subsystem: cloud-operations status: complete tags: - cloud - upload - conflict - keep-both - typed-results - tdd dependency_graph: requires: - "13-01 (RED mutable adapter contract tests)" - "13-03 (cloud operations seam + mutable provider implementations)" - "13-04 (operations.py route layer + typed result vocabulary)" provides: - "keep_both_name() helper in services/cloud_operations.py for D-03/D-05 collision naming" - "TestUploadConflictSemantics: 16 provider-level upload behavioral tests" - "TestKeepBothNaming: 7 keep-both naming format tests" - "7 typed upload route mechanics tests in test_cloud_mutations.py" - "All 4 providers verified to return typed upload results (never raw provider errors)" affects: - "13-06 through 13-11 (reconcile, audit, frontend upload queue plans)" tech_stack: added: - "keep_both_name(filename, counter) in services/cloud_operations.py" patterns: - "Counter-before-extension naming: Report.pdf → Report (1).pdf" - "Typed upload result vocabulary: MUT_KIND_UPLOADED / MUT_KIND_CONFLICT / MUT_KIND_OFFLINE / MUT_KIND_REAUTH" - "Provider-neutral upload mechanics: all 4 providers normalize errors into shared kinds" - "Queue-control semantics from backend-authored typed bodies (not Vue-side HTTP status inference)" key_files: modified: - path: "backend/services/cloud_operations.py" change: "Added keep_both_name(filename, counter) helper for D-03/D-05 collision naming" - path: "backend/tests/test_cloud_backends.py" change: "Added TestUploadConflictSemantics (16 tests) and TestKeepBothNaming (7 tests)" - path: "backend/tests/test_cloud_mutations.py" change: "Added 7 typed upload route mechanics tests (Task 2 behaviors)" decisions: - "keep_both_name lives in services/cloud_operations (not a provider) — naming is queue/service concern, not provider concern" - "Counter inserted before first dot to preserve compound extensions (archive.tar.gz → archive (1).tar.gz)" - "Providers already had upload_file implementations from Plan 03 — Plan 05 adds behavioral tests and the naming utility only" - "Upload route already had typed result handling from Plan 04 — Plan 05 confirms test coverage of all outcome paths" - "Reconcile and audit follow-through deferred to Plans 06+ per PLAN.md bounded scope" metrics: duration: "~45 minutes" completed: "2026-06-22" tasks_completed: 2 tasks_planned: 2 files_changed: 3 files_created: 0 tests_added: 30 tests_passing: 741 --- # Phase 13 Plan 05: Upload Mechanics Slice Summary **One-liner:** Provider-normalized upload conflict/retry/reauth typed mechanics with `keep_both_name()` helper for D-03/D-05 queue-level collision resolution. ## Tasks Completed | Task | Name | Commit | Key Files | |------|------|--------|-----------| | 1 (RED) | Add failing upload conflict and keep-both naming tests | f47e36d | test_cloud_backends.py | | 1 (GREEN) | Implement keep_both_name() helper | e1ce4cb | services/cloud_operations.py | | 2 | Add typed upload route mechanics tests | 3ddf4da | test_cloud_mutations.py | ## What Was Built ### Task 1: Provider-normalized upload contract tests + keep_both_name() helper **RED phase:** Added 23 tests (16 provider behavioral + 7 keep-both naming) to `test_cloud_backends.py`: **TestUploadConflictSemantics:** - `test_google_drive_upload_success_returns_typed_result` — kind='uploaded' with provider_item_id, name, size; no credentials in result - `test_google_drive_upload_transient_error_returns_offline_kind` — HTTP 503 → kind='offline' (retryable) - `test_google_drive_upload_auth_error_returns_reauth_kind` — HTTP 401 → kind='reauth_required' - `test_onedrive_upload_success_returns_typed_result` — resumable session success → kind='uploaded' - `test_onedrive_upload_conflict_returns_typed_conflict_kind` — conflictBehavior=fail 409 → kind='conflict', reason='name_collision' - `test_onedrive_upload_never_writes_cloud_items` — source scan confirms no cloud_items import - `test_webdav_upload_success_returns_typed_result` — PUT success → kind='uploaded' with correct path - `test_webdav_upload_provider_error_returns_error_kind` — connection timeout → kind='offline' - `test_upload_result_never_contains_credentials` — parametrized over 4 providers; no access_token/refresh_token in result - `test_upload_result_kind_in_known_kinds` — parametrized over 4 providers; kind must be in MUT_KINDS constant set **TestKeepBothNaming:** - `test_keep_both_naming_module_exists` — services.cloud_operations exposes keep_both_name - `test_keep_both_basic_pdf` — Report.pdf + 1 → Report (1).pdf - `test_keep_both_counter_increments` — Report.pdf + 2 → Report (2).pdf - `test_keep_both_no_extension` — README + 1 → README (1) - `test_keep_both_compound_extension` — archive.tar.gz + 1 → archive (1).tar.gz - `test_keep_both_preserves_existing_counter` — Report (1).pdf + 2 → Report (1) (2).pdf - `test_keep_both_spaces_in_name` — My Document.docx + 1 → My Document (1).docx **GREEN phase:** Implemented `keep_both_name(filename, counter)` in `services/cloud_operations.py`: ```python def keep_both_name(filename: str, counter: int) -> str: """Insert collision counter before first file extension. Report.pdf + 1 → Report (1).pdf archive.tar.gz + 1 → archive (1).tar.gz README + 1 → README (1) """ ``` The counter is inserted before the first dot (not last), so compound extensions are preserved intact. ### Task 2: Typed upload route mechanics tests Added 7 tests to `test_cloud_mutations.py` verifying Task 2 behavioral contract: 1. `test_upload_success_returns_typed_uploaded_body` — success body has kind/provider_item_id/name/size 2. `test_upload_provider_retryable_error_returns_offline_kind` — offline kind from provider error, no bare 500 3. `test_upload_provider_conflict_returns_conflict_kind` — provider-detected conflict when cache misses 4. `test_upload_reauth_result_is_typed_and_not_500` — CONN-02 token expiry surfaces as typed body 5. `test_upload_queue_control_semantics_are_backend_authored` — all 4 required queue fields present 6. `test_upload_keep_both_name_format` — service utility accessible to route layer 7. `test_upload_foreign_user_blocked` — IDOR protection on upload endpoint All 7 tests pass because the upload route in `operations.py` (Plan 04) already implements the typed result dispatch correctly. Plan 05 adds the test coverage that was missing. ## Deviations from Plan ### Auto-fixed Issues None. The plan stated providers should "implement D-03 and D-04 inside the provider layer" but providers already had complete `upload_file` implementations from Plan 03. Plan 05 correctly adds behavioral tests and the naming utility that were genuinely missing. **Note on scope:** The providers already normalized upload results into typed kinds from Plan 03. Plan 05's contribution is: 1. Behavioral tests that prove the normalization is correct 2. The `keep_both_name()` service helper that the upload queue needs for conflict resolution ## Known Stubs None. The upload mechanics are fully functional. Reconcile and audit follow-through are intentionally deferred to Plans 06+. ## Threat Flags No new security surfaces introduced. Upload route is covered by T-13-16 (typed conflict bodies forbid silent overwrite), T-13-17 (no provider URLs/tokens in response), T-13-18 (WebDAV SSRF guard preserved via __init__). ## Self-Check: PASSED - FOUND: `backend/services/cloud_operations.py` with `keep_both_name()` function - FOUND: `backend/tests/test_cloud_backends.py` (modified — 23 new tests) - FOUND: `backend/tests/test_cloud_mutations.py` (modified — 7 new tests) - FOUND commit: `f47e36d` (Task 1 RED) - FOUND commit: `e1ce4cb` (Task 1 GREEN — keep_both_name) - FOUND commit: `3ddf4da` (Task 2 — route mechanics tests) - Full suite: 741 passed, 17 skipped, 4 deselected, 13 xfailed