---
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 |
