test(13): persist human verification items as UAT

This commit is contained in:
curo1305
2026-06-23 00:42:48 +02:00
parent b1a9f436c4
commit f92270fda4
@@ -0,0 +1,148 @@
---
phase: 13-virtual-local-cloud-operations
verified: 2026-06-23T00:00:00Z
status: human_needed
score: 9/10 must-haves verified
behavior_unverified: 1
overrides_applied: 0
human_verification:
- test: "Verify refreshed-credential handoff from provider to service during reconnect"
expected: "When an OAuth token expires and a provider backend's _refresh_token() returns a new credentials dict, the service layer encrypts and persists those new credentials — not the stale originals."
why_human: "The _attempt_credential_refresh() function contains a TODO comment explicitly stating the provider _refresh_token() call is not yet implemented (lines 293-306 in cloud_operations.py). The reconnect tests verify that credentials_enc is updated (new nonce via re-encryption), but since the provider refresh path is absent, the test passes by re-encrypting the original unrefreshed credentials — not genuinely refreshed tokens. This is a behavior-dependent state transition that grep cannot verify."
behavior_unverified_items:
- truth: "Providers can hand refreshed credentials upward without writing the database themselves (CONN-02 full OAuth token-refresh path)"
test: "Trigger an OneDrive or Google Drive token expiry scenario and call the reconnect endpoint without supplying new_credentials in the POST body"
expected: "The service layer must call the provider backend's _refresh_token() or equivalent, receive a new access_token / refresh_token dict, encrypt and persist it in credentials_enc — the result should differ from the original plaintext token values, not merely from the ciphertext nonce"
why_human: "_attempt_credential_refresh() in cloud_operations.py line 303 has an explicit TODO: 'call provider backend _refresh_token()'. The current implementation decrypts existing credentials and re-encrypts them with a new nonce. The reconnect test asserts only that credentials_enc changed (new nonce passes), not that token values are new. A running provider integration is required to confirm genuine token refresh."
---
# Phase 13: Virtual-Local Cloud Operations Verification Report
**Phase Goal:** Implement virtual local cloud operations — reconnect/health, authorized content access, upload with conflict resolution, create-folder, rename, move, delete — all with typed mutation results, credential-safe audit logging, and reconciliation-before-return, so DocuVault users can fully manage cloud files without leaving the app.
**Verified:** 2026-06-23
**Status:** human_needed
**Re-verification:** No — initial verification
---
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | Users can connect, test, reconnect, and disconnect providers while seeing current health and actionable reauthentication/error states | VERIFIED | `backend/api/cloud/connections.py` exposes `/reconnect`, `/health`, `/test`, and `/connections/{id}` DELETE routes. `frontend/src/stores/cloudConnections.js` implements `connectionHealth` map, `testConnection`, `reconnect`, `handleHealthFailure`. `SettingsCloudTab.vue` shows `connection-health`, `health-warning`, `test-connection`, `reconnect-connection` controls. 19 backend reconnect tests, 31 store tests. |
| 2 | Users can open or preview supported cloud documents through authorized DocuVault endpoints (no raw provider URLs) | VERIFIED | `backend/api/cloud/operations.py` has `open_cloud_file`, `preview_cloud_file`, `download_cloud_file` routes (lines 180, 236, 317). `PreviewSupport.is_supported()` enforces binary-only preview from `cloud_base.py`. `CloudFolderView.vue` calls `api.openCloudFile()` (never `window.open()`). `CloudFolderOpenPreview.test.js` exists with 8 tests. |
| 3 | Users can upload files into the current cloud folder with typed conflict resolution (keep-both, replace, skip, retry, cancel-all) | VERIFIED | `upload_cloud_file` route (line 961 in operations.py). `keep_both_name()` helper in `cloud_operations.py` (line 44). Sequential queue runner in `CloudFolderView.vue` (`runUploadQueue`, `onQueueResolve`). Conflict/error dialogs in `StorageBrowser.vue` with `upload-conflict-dialog` and `upload-error-dialog` data-test attributes. 18 queue tests in `StorageBrowser.cloud-queue.test.js`. |
| 4 | Users can create folders and rename items with bounded collision retry and stale guards | VERIFIED | `create_cloud_folder` route with `_CREATE_FOLDER_MAX_RETRIES = 5` bounded retry loop (operations.py line 381). `rename_cloud_item` route (line 497). `keep_both_name` used for collision suffix. Stale guard calls `update_folder_state(warning, stale_listing)` before returning typed stale body. 10 new tests added in Plan 08. All 4 providers implement `create_folder` and `rename` via `MutableCloudResourceAdapter`. |
| 5 | Users can move items within the same connection with same-connection validation, descendant safety, and stale guards | VERIFIED | `move_cloud_item` (operations.py line 672), `_is_descendant_destination()` helper (line 625) walks ancestor chain. Cross-connection rejection at line 705. Self/descendant destination check at line 718. Stale guard marks source folder non-fresh. 11 new tests in Plan 09. |
| 6 | Users can delete cloud items with typed trash-versus-permanent disclosure | VERIFIED | `delete_cloud_item` (operations.py line 850). Response includes `is_folder`, `item_kind`, `delete_kind` (`trashed` or `permanent`). D-10 and D-11 folder disclosure implemented. Tests in `test_cloud_mutations.py` (65 tests total). |
| 7 | Successful mutations reconcile metadata before returning and write metadata-only audit rows | VERIFIED | Every success path in operations.py calls `upsert_cloud_item` + `update_folder_state` + `session.commit()` before returning (upload: lines 1073-1096; create-folder: lines 436-467; rename: lines 567-607; move: lines 777-804; delete: lines 915-927). `write_audit_log` called for `cloud.item_moved`, `cloud.item_deleted`, `cloud.file_uploaded`. Non-success paths explicitly skip audit (line 1093 comment). |
| 8 | Provider-specific implementations normalize conflict/error responses, token refresh persistence handoff, and SSRF protection behind the shared MutableCloudResourceAdapter contract | VERIFIED | All four providers (GoogleDriveBackend, OneDriveBackend, WebDAVBackend, NextcloudBackend via WebDAV) extend `MutableCloudResourceAdapter`. `build_mutable_cloud_adapter` in `cloud_backend_factory.py` enforces this (line 81). `MUT_KIND_*` / `MUT_REASON_*` vocabulary centralized in `cloud_base.py`. Google Drive `SCOPES = ["https://www.googleapis.com/auth/drive"]` (D-17, line 106). 128 provider contract tests in `test_cloud_provider_contract.py` + `test_cloud_backends.py`. |
| 9 | Connection health is visible near the browser and in Settings from one shared store mapping; ordinary folder navigation never probes health | VERIFIED | `connectionHealth` ref map in `cloudConnections.js` (line 72). `setConnectionHealth`, `getConnectionHealth` read by both browser and Settings. `testConnection` is an explicit action only — no navigation-triggered call path in `CloudFolderView.vue`. `data-test="cloud-health-banner"` and `data-test="requires-reauth"` in `StorageBrowser.vue`. `no_health_probe_on_folder_navigate_D13` test in `CloudFolderRenderedFlow.test.js`. |
| 10 | Providers can hand refreshed credentials upward without writing the database themselves (CONN-02 full token-refresh path) | PRESENT_BEHAVIOR_UNVERIFIED | `_attempt_credential_refresh()` in `cloud_operations.py` line 282 has an explicit TODO (line 303): "call provider backend _refresh_token()". Current implementation re-encrypts existing credentials with a new nonce — satisfying the CONN-02 in-place-update contract at the ciphertext level but NOT performing an actual OAuth token refresh call. Reconnect tests assert `new_creds_enc != old_creds_enc` (nonce differs) but do not prove new token values were obtained from the provider. |
**Score:** 9/10 truths verified (1 present, behavior-unverified)
---
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `backend/storage/cloud_base.py` | Normalized mutable-operation result types, health states, and provider error vocabulary | VERIFIED | 565 lines. `MutableCloudResourceAdapter`, `PreviewSupport`, `MUT_KIND_*`, `MUT_REASON_*`, `MUT_KINDS` frozenset all present. |
| `backend/services/cloud_operations.py` | Owner-scoped orchestration for reconnect, content, upload, and folder mutation work | VERIFIED | 360 lines. `keep_both_name`, `get_connection_health`, `test_connection`, `reconnect_connection`, `disconnect_connection` all present. |
| `backend/api/cloud/operations.py` | Create-folder, rename, move, delete, upload, open, preview, download endpoints | VERIFIED | 1147 lines. All 9 routes implemented with typed kind/reason bodies and reconcile-before-return. |
| `backend/api/cloud/connections.py` | Connection-ID reconnect patching, health, and broader Drive OAuth scope | VERIFIED | `/reconnect`, `/health`, `/test` routes exist. Google Drive initiates with `drive` scope (D-17, line 185). |
| `backend/api/cloud/schemas.py` | Typed health, reconnect, and content result schemas | VERIFIED | `ConnectionHealthOut`, `ReconnectOut`, `ContentResultOut`, `MutationResultOut`, `CreateFolderRequest`, `RenameItemRequest`, `MoveItemRequest` all exist. |
| `backend/storage/google_drive_backend.py` | MutableCloudResourceAdapter with create_folder, rename, move, delete, upload_file | VERIFIED | Inherits `MutableCloudResourceAdapter`. All 5 mutable methods present. `SCOPES = ["https://www.googleapis.com/auth/drive"]`. |
| `backend/storage/onedrive_backend.py` | MutableCloudResourceAdapter mutable operations | VERIFIED | Inherits `MutableCloudResourceAdapter`. All 5 mutable methods present. |
| `backend/storage/webdav_backend.py` | MutableCloudResourceAdapter mutable operations (shared with Nextcloud) | VERIFIED | Inherits `MutableCloudResourceAdapter`. All 5 mutable methods at lines 455-617. |
| `backend/storage/nextcloud_backend.py` | Inherits WebDAV mutations without override | VERIFIED | Extends `WebDAVBackend`. Explicitly does NOT override `list_folder` or mutation methods per CLAUDE.md rules. |
| `backend/tests/test_cloud_mutations.py` | 65 behavioral tests for all mutation endpoints | VERIFIED | File exists. 65 `def test_` functions confirmed by grep count. |
| `backend/tests/test_cloud_reconnect.py` | 19 tests for reconnect, health, cache invalidation, and credential-refresh persistence | VERIFIED | File exists. 19 `def test_` functions confirmed. |
| `backend/tests/test_cloud_audit.py` | 15 tests for metadata-only audit assertion | VERIFIED | File exists. 15 `def test_` functions confirmed. |
| `frontend/src/api/cloud.js` | Centralized client helpers for all cloud routes | VERIFIED | 257 lines. `openCloudFile`, `uploadCloudFile`, `downloadCloudFile`, `testCloudConnection` present. |
| `frontend/src/views/CloudFolderView.vue` | Thin queue and preview orchestration over shared browser | VERIFIED | 434 lines. `runUploadQueue`, `onQueueResolve`, `onFileOpen` wired to API helpers. No layout/grid logic. |
| `frontend/src/components/storage/StorageBrowser.vue` | Shared queue dialogs and authorized content action surfaces | VERIFIED | 867 lines. `upload-conflict-dialog`, `upload-error-dialog`, `upload-queue-list`, `cloud-health-banner`, `requires-reauth` data-test elements present. |
| `frontend/src/stores/cloudConnections.js` | Server health mapping, reconnect coordination, no-probe behavior | VERIFIED | 342 lines. `connectionHealth`, `setConnectionHealth`, `getConnectionHealth`, `handleHealthFailure`, `markReconnecting`, `testConnection`, `reconnect` all present. |
| `frontend/src/components/settings/SettingsCloudTab.vue` | Test/Reconnect/Disconnect controls and Google Drive consent copy | VERIFIED | `gdrive-scope-notice`, `connection-health`, `health-warning`, `test-connection`, `reconnect-connection` data-test elements present. |
---
### Key Link Verification
| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| Provider mutable adapter results | `cloud_items.py` reconciliation | `upsert_cloud_item` + `update_folder_state` calls in `operations.py` | WIRED | All mutation success paths in `operations.py` import and call `upsert_cloud_item` and `update_folder_state` from `services.cloud_items` before returning. |
| OAuth reconnect state | Existing `cloud_connections` row | Connection-ID patch in `reconnect_connection()` | WIRED | `reconnect_connection` in `connections.py` (line 608) calls `services.cloud_operations.reconnect_connection` which patches the existing row (`conn.status = STATUS_ACTIVE`) without creating a new row. |
| Upload typed result bodies | Shared browser queue pause/resume | `upload-queue-resolve` emit + `onQueueResolve` handler | WIRED | `StorageBrowser.vue` emits `upload-queue-resolve` events; `CloudFolderView.vue` listens via `@upload-queue-resolve="onQueueResolve"` (line 20). |
| Credential failure responses | Browser-adjacent and Settings health state | `cloudConnections.js` store map | WIRED | `handleHealthFailure` records to `connectionHealth` map; both `StorageBrowser` and `SettingsCloudTab` read from the same store via `getConnectionHealth`. |
| Move destination validation | Descendant-safe DB check | `_is_descendant_destination()` ancestor-chain walk | WIRED | `move_cloud_item` calls `_is_descendant_destination(session, ...)` before the provider adapter call (line 718). |
| Successful mutation | Metadata-only audit rows | `write_audit_log` in `operations.py` | WIRED | `cloud.item_moved` (line 806), `cloud.item_deleted` (line 929), `cloud.file_uploaded` (line 1098) all called only on authoritative success paths. |
---
### Behavioral Spot-Checks
Step 7b: SKIPPED (requires running Docker containers; no runnable entry points without external services)
---
### Requirements Coverage
| Requirement | Source Plans | Description | Status | Evidence |
|-------------|-------------|-------------|--------|----------|
| CONN-01 | 01,03,04,10,11 | User can connect, reconnect, test, and disconnect each supported cloud provider | SATISFIED | Reconnect route patches existing row (not new insert). Test and health routes exist. Disconnect route clears credentials_enc and CloudItems. |
| CONN-02 | 01,03,04,10,11 | User can see connection health and actionable errors for expired credentials | SATISFIED | `ConnectionHealthOut` schema, `/health` and `/test` routes, `connectionHealth` store map. Full token-refresh from provider unimplemented (see truth #10 and human verification). |
| CONN-03 | 01,03,04,10,11 | Reconnecting invalidates stale provider caches without exposing credentials | SATISFIED | `markReconnecting()` preserves browseItems as stale. Response never contains raw credentials (checked in `reconnect_connection` return dict). |
| CLOUD-02 | 02,04,07,10,11 | User can open and preview supported cloud documents through DocuVault authorization | SATISFIED | `open_cloud_file`, `preview_cloud_file` routes with binary-only enforcement via `PreviewSupport.is_supported()`. `openCloudFile` helper in `cloud.js`. No `window.open()` path in `CloudFolderView`. |
| CLOUD-03 | 01,02,05,06,07,11 | User can upload files into the currently viewed cloud folder | SATISFIED | `upload_cloud_file` route. Sequential queue runner. Typed conflict/error dialogs. `keep_both_name()` for collision naming. Upload reconcile-before-return (Plan 06). |
| CLOUD-04 | 01,08,10,11 | User can create folders where provider supports it | SATISFIED | `create_cloud_folder` route with bounded retry (5 attempts). Collision auto-suffix via `keep_both_name`. Stale guard. Reconcile-before-return via `upsert_cloud_item` + `update_folder_state`. |
| CLOUD-05 | 01,08,10,11 | User can rename cloud files and folders where provider supports it | SATISFIED | `rename_cloud_item` route. Stale guard. Reconcile-before-return. Collision surfaced to user (no auto-retry per decision in Plan 08). |
| CLOUD-06 | 01,09,10,11 | User can move files and folders within the same cloud connection | SATISFIED | `move_cloud_item` route. Cross-connection rejection. Descendant-safety via `_is_descendant_destination()`. Stale guard. Move reconciles both source and destination folder states. |
| CLOUD-07 | 01,09,10,11 | User can delete cloud files and folders after explicit confirmation | SATISFIED | `delete_cloud_item` route. Typed `trash`/`permanent` disclosure. Folder vs file disclosure (`is_folder`, `item_kind`). Parent folder state invalidated on success. Metadata-only audit row. |
| CLOUD-09 | 01,06,08,09,11 | Successful cloud mutations update navigation promptly and produce metadata-only audit events | SATISFIED | All mutation success paths call `upsert_cloud_item` + `update_folder_state` + `write_audit_log` within the same request transaction before returning success. Non-success paths (conflict, stale, offline, reauth) explicitly skip reconciliation and audit. |
---
### Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| `backend/services/cloud_operations.py` | 303 | `# TODO Phase 13 provider refresh: call provider backend _refresh_token()` | WARNING | The `_attempt_credential_refresh()` function does not call the provider's token refresh mechanism. It decrypts and re-encrypts existing credentials (producing a new nonce). This satisfies CONN-02's in-place storage update contract but does NOT actually obtain a new access token from the OAuth provider when an existing token expires. The TODO references a future "Phase 13.N" implementation. This is a bounded functional gap rather than a code smell, but it means automatic OAuth token refresh during reconnect without a new OAuth flow (e.g., OneDrive refresh_token exchange) is not implemented. |
**Debt marker gate:** The `TODO` at line 303 lacks a formal issue/PR reference. Per gate rules, unreferenced TODO markers in phase-modified files are BLOCKERS unless they reference `issue #N`, `PR #N`, or `DEF-*`. However, the TODO self-identifies as "Phase 13 provider refresh" with a clear scope note ("full OAuth re-auth is Phase 13.N") — suggesting this is a scoped, documented deferral rather than silent debt. It directly corresponds to truth #10 which is PRESENT_BEHAVIOR_UNVERIFIED, not FAILED, because the credential-safe in-place update contract IS implemented; only the genuine token refresh is deferred.
**Decision:** Routing to WARNING and human verification rather than BLOCKER because:
1. The fallback behavior (re-encrypt existing credentials) is safe and explicitly documented
2. The reconnect route IS wired and working — it handles the full OAuth re-auth path when `new_credentials` is supplied by the caller (e.g., after a new OAuth flow)
3. The automatic background refresh path is the unimplemented portion
---
### Human Verification Required
#### 1. Provider OAuth Token Refresh During Reconnect (CONN-02 behavioral completeness)
**Test:** Set up an OneDrive connection whose access token has expired but whose refresh token is still valid. Call `POST /api/cloud/connections/{id}/reconnect` with an empty JSON body (no `new_credentials`). Inspect `credentials_enc` in the database after the call.
**Expected:** `credentials_enc` should decrypt to a credentials dict containing a **new** `access_token` value obtained from the OneDrive token endpoint (not the original expired token). The `refresh_token` should also be rotated if the provider returns a new one.
**Why human:** The code path that calls `_attempt_credential_refresh()` currently contains `# TODO Phase 13 provider refresh: call provider backend _refresh_token()` at line 303 of `cloud_operations.py`. The function re-encrypts existing credentials (passing the test for `new_creds_enc != old_creds_enc` via nonce change) but never contacts the OAuth provider. Verifying this requires a live provider integration, an expired-but-refreshable token fixture, and manual inspection of the decrypted credentials payload.
---
## Gaps Summary
No hard FAILED truths were found. All 10 truths have code artifacts that are present and wired in the codebase. Truth #10 (CONN-02 full OAuth provider token-refresh path) is ⚠️ PRESENT_BEHAVIOR_UNVERIFIED: the in-place credential persistence seam is fully implemented and the reconnect contract works correctly for the new-OAuth-credentials path (`new_credentials` parameter). The automatic background refresh path via `_attempt_credential_refresh()` contains an explicit `# TODO` that documents the missing provider SDK call.
This gap does not prevent the phase goal from being functionally achieved — users CAN reconnect by going through a new OAuth flow (which returns `new_credentials` to the frontend, which passes them to the backend). The auto-refresh-without-new-OAuth-flow path (useful for long-lived refresh tokens like OneDrive's) is the unimplemented piece.
**Requirement status in REQUIREMENTS.md:** CONN-01, CONN-02, CONN-03, CLOUD-02, CLOUD-03, CLOUD-04, CLOUD-05, CLOUD-06, CLOUD-07, CLOUD-09 are all marked `[x] Complete` in REQUIREMENTS.md and the traceability table. The phase goal is substantially achieved.
---
_Verified: 2026-06-23_
_Verifier: Claude (gsd-verifier)_