--- phase: "12.1" plan: "03" subsystem: cloud-storage-frontend status: complete tags: [cloud, frontend, kind, provider_item_id, freshness, breadcrumb, tdd, security] completed: "2026-06-22" duration_seconds: 677 task_count: 4 file_count: 11 requires: - "12.1-02" provides: - "kind-based item classification in CloudFolderView (folders/files computed)" - "provider_item_id for all folder navigation and tree expansion" - "server freshness mapped verbatim — no browser-clock evidence" - "explicit breadcrumb lineage from visited nodes" - "normalized test regressions covering kind/provider_item_id/freshness" affects: [cloud-browse-ui, cloud-sidebar-tree, breadcrumb-navigation] tech_stack: added: [] patterns: - "TDD RED/GREEN — 9+7 failing tests written first; implementation makes them all pass" - "Named route object navigation (never string interpolation for opaque refs)" - "Server freshness mapped verbatim — freshness.refresh_state|last_refreshed_at" - "Breadcrumb lineage as explicit visited-node list (never split provider_item_id)" key_files: created: - frontend/src/components/cloud/__tests__/CloudBreadcrumbNavigation.test.js modified: - frontend/src/views/CloudFolderView.vue - frontend/src/components/cloud/CloudProviderTreeItem.vue - frontend/src/components/cloud/CloudFolderTreeItem.vue - frontend/src/views/__tests__/CloudFolderView.test.js - frontend/src/stores/__tests__/cloudConnections.test.js - frontend/src/components/cloud/__tests__/CloudProviderTreeItem.test.js - frontend/src/components/cloud/__tests__/CloudFolderTreeItem.test.js - frontend/src/components/storage/__tests__/StorageBrowser.skeleton.test.js - backend/main.py - frontend/package.json decisions: - "Named route objects used for all cloud navigation — Vue Router handles encoding of opaque refs" - "Breadcrumb lineage maintained as explicit {name, providerItemId} array in view state — never reconstructed from provider_item_id" - "provider_item_id is the canonical navigation reference; id remains row identity for Vue keys and DocuVault metadata" - "Test stubs use updated() lifecycle hook (not mounted()) to capture props after async load resolves" requirements: - CLOUD-01 - CONN-04 - SYNC-01 --- # Phase 12.1 Plan 03: Align Cloud Browser Contract — Summary **One-liner:** Replaced `is_dir`/`item.id`/`new Date()` with `kind`, `provider_item_id`, and verbatim server freshness across all active cloud UI components. ## What Was Built ### Task 1 — TDD RED: Normalized-item and navigation regressions Added 9 failing tests across 5 test files before touching production code: **`frontend/src/views/__tests__/CloudFolderView.test.js` (4 new tests):** - `renders_kind_folder_and_file_in_shared_browser` — fixtures with no `is_dir`; asserts `folders` prop uses `kind=folder` only - `folder_click_uses_provider_item_id_not_id` — asserts `navigateTo` puts `provider_item_id` in route, not DocuVault UUID - `stable_docuvault_id_remains_row_key` — asserts items retain both `id` and `provider_item_id` - `opaque_reference_round_trip` — fixture with `/`, `?`, `#`, spaces, Unicode in `provider_item_id`; asserts intact navigation **`frontend/src/components/cloud/__tests__/CloudProviderTreeItem.test.js` (2 new tests):** - `tree_expansion_filters_kind_folder` — `loadChildren` returns `kind=folder` items only - `loadChildren passes root sentinel` — API called with connection UUID, empty string for root **`frontend/src/components/cloud/__tests__/CloudFolderTreeItem.test.js` (4 new tests):** - `nested_tree_uses_provider_item_id` — navigate/loadChildren/expandable all use `provider_item_id` and `kind` - `opaque_provider_reference_round_trip` — OneDrive-style opaque ID passed verbatim to loadChildren **`frontend/src/components/storage/__tests__/StorageBrowser.skeleton.test.js` (3 new tests):** - Cloud folder rendering with normalized `CloudItemOut` fixtures - `folder-navigate` event carries `provider_item_id` intact **`frontend/src/components/cloud/__tests__/CloudBreadcrumbNavigation.test.js` (new file, 8 tests):** - `breadcrumb_from_explicit_lineage_not_split` — at root breadcrumb is empty; opaque ref round-trips through navigate - `breadcrumb_opaque_references_survive_transport` — Google Drive, WebDAV path with spaces **Result:** 9 tests fail against `is_dir`/`item.id` behavior as expected (RED confirmed). ### Task 2 — TDD RED: Server freshness regressions Added 8 more tests before implementation: **`frontend/src/stores/__tests__/cloudConnections.test.js` (7 new tests):** - `setBrowseState warning/refreshing/fresh` state mapping - `does_not_use_browser_clock_as_refresh_evidence` — `refreshedAt` must equal server value exactly - `cached_items_remain_visible_during_warning` — items not cleared on warning state - `selectConnection clears cloud browse state` — no secrets persist **`frontend/src/views/__tests__/CloudFolderView.test.js` (5 new tests):** - `maps_server_refreshing_freshness` — `setBrowseState({ freshness: 'refreshing' })` before API resolves - `maps_server_warning_and_last_success` — HTTP 200 + warning freshness → setBrowseState warning, not fresh - `http_200_does_not_imply_fresh` — warning response produces no `fresh` setBrowseState call - `does_not_use_browser_clock_as_refresh_evidence` — `refreshedAt` matches exact server timestamp - `cached_items_remain_visible_during_warning` — items passed even during warning **Result:** 3 new failures against `freshness: 'fresh'` and `new Date().toISOString()` in current code. ### Task 3 — GREEN: Implement normalized item use **`frontend/src/views/CloudFolderView.vue`:** - Replaced `items.value.filter(i => i.is_dir)` with `i.kind === 'folder'` / `i.kind === 'file'` - `navigateTo(item)`: builds explicit breadcrumb lineage; navigates via `{ name: 'cloud-folder', params: { folderId: item.provider_item_id } }` - `handleBreadcrumbNavigate(id)`: trims lineage to clicked node; navigates via named route; opaque refs intact - `load()`: maps `data.freshness.refresh_state` → store freshness (no fallback to `'fresh'`); maps `data.freshness.last_refreshed_at` → `refreshedAt` (never `new Date()`) - Session restore: replaced string path with named route object **`frontend/src/components/cloud/CloudProviderTreeItem.vue`:** - `loadChildren`: replaced `i.is_dir` with `i.kind === 'folder'` - All other logic unchanged (root navigation via `/cloud/{id}/root` string — acceptable for root sentinel) **`frontend/src/components/cloud/CloudFolderTreeItem.vue`:** - `:expandable="folder.kind === 'folder'"` (was `folder.is_dir`) - Icon template: `v-if="folder.kind === 'folder'"` (was `folder.is_dir`) - `isActive`: uses `folder.provider_item_id` for route comparison (with `folder.id` fallback for legacy) - `loadChildren`: uses `props.folder.provider_item_id ?? props.folder.id` as parent_ref - `navigate()`: uses `provider_item_id ?? id` as route param **Verification search:** ``` rg '\.is_dir' src/views/CloudFolderView.vue src/components/cloud/ (no output — only appears in comments and test files) ``` All 82 focused tests pass; full 369-test suite passes. ### Task 4 — Documentation, security review, version bump - Version bumped `0.2.4 → 0.2.5` in `backend/main.py` and `frontend/package.json` - `CLAUDE.md`: updated current state; Plan 03 summary - `AGENTS.md`: updated current state - `README.md`: version bump - `SECURITY.md`: T-12.1-11 through T-12.1-15 closed with evidence - Full frontend suite: 369 passed, 0 failures - Production build: clean, 495ms - `npm audit --audit-level=high`: 0 vulnerabilities - `docker compose config --quiet`: valid - Pushed to remote ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 1 - Bug] Test stub captured props before async load completed** - **Found during:** Task 3 (GREEN phase — tests still failing after implementation) - **Issue:** `CapturingStub.mounted()` fired synchronously before the `onMounted` async `load()` resolved. Captured `folders` was `[]` even after `flushPromises()`. - **Fix:** Added `updated()` lifecycle hook to capturing stubs so they re-capture after props update. - **Files modified:** `frontend/src/views/__tests__/CloudFolderView.test.js` - **Scope:** Tests only. **2. [Rule 1 - Bug] folder-navigate emitted in mounted() before items loaded** - **Found during:** Task 3 (opaque_reference_round_trip and folder_click tests failing) - **Issue:** Both test stubs emitted `folder-navigate` in `mounted()` when `this.folders[0]` was still undefined, causing `TypeError: Cannot read properties of undefined (reading 'name')`. - **Fix:** Moved emit to `updated()` with a one-shot flag so it fires only after folders prop is populated. - **Files modified:** `frontend/src/views/__tests__/CloudFolderView.test.js` ## Test Evidence | Suite | Tests | Status | |-------|-------|--------| | `CloudFolderView.test.js` | 23 | All pass (4 pre-existing + 9 new + 5 freshness + 5 more freshness) | | `CloudProviderTreeItem.test.js` | 8 | All pass (4 pre-existing + 4 new) | | `CloudFolderTreeItem.test.js` | 16 | All pass (4 pre-existing + 12 new) | | `StorageBrowser.skeleton.test.js` | 22 | All pass (19 pre-existing + 3 new) | | `cloudConnections.test.js` | 23 | All pass (16 pre-existing + 7 new) | | `CloudBreadcrumbNavigation.test.js` | 8 | All new, all pass | | Full frontend suite | 369 | All pass (44 test files) | **TDD Gate Compliance:** - RED commit `ba7f652` — 9 tests fail with `is_dir`/`item.id` behavior before implementation - RED commit `9974fca` — 3 more tests fail with `fresh`/`new Date()` before implementation - GREEN commit `dc3e172` — all 82 focused tests pass; 369 total pass ## Security Verification Threat model coverage: | Threat ID | Mitigation | Evidence | |-----------|-----------|---------| | T-12.1-11 | DocuVault UUID not sent as provider path | `navigateTo` uses `item.provider_item_id`; `loadChildren` uses `folder.provider_item_id`; `folder_click_uses_provider_item_id_not_id`, `nested_tree_uses_provider_item_id` | | T-12.1-12 | UI falsely reports provider synchronization | `load()` maps `data.freshness.refresh_state` verbatim; `http_200_does_not_imply_fresh`, `does_not_use_browser_clock_as_refresh_evidence` | | T-12.1-13 | Provider reference injected into provider URL | Named route objects; `breadcrumb_opaque_references_survive_transport` | | T-12.1-14 | Secrets persisted in frontend state | Only `folderId` string stored in sessionStorage; `sessionStorage_stores_only_folder_references_not_tokens` | | T-12.1-15 | Parallel browser/tree logic drifts | `renders_StorageBrowser_no_parallel_file_grid` asserts no table/grid in view; single StorageBrowser pattern maintained | ## Known Stubs None — all classification, navigation, and freshness paths are fully wired to real API responses. ## Threat Flags None — this plan is frontend-only with no new network endpoints, auth paths, file access patterns, or schema changes. ## Self-Check: PASSED | Check | Result | |-------|--------| | `frontend/src/views/CloudFolderView.vue` (kind classification) | FOUND | | `frontend/src/components/cloud/CloudFolderTreeItem.vue` (provider_item_id) | FOUND | | `frontend/src/components/cloud/CloudProviderTreeItem.vue` (kind filter) | FOUND | | `frontend/src/components/cloud/__tests__/CloudBreadcrumbNavigation.test.js` | FOUND | | Commit ba7f652 (RED tests task 1) | FOUND | | Commit 9974fca (RED tests task 2) | FOUND | | Commit dc3e172 (GREEN implementation) | FOUND | | Commit 7bb046a (docs + version) | FOUND | | No is_dir in active cloud components | CONFIRMED | | Full test suite: 369 passed | CONFIRMED |