# Phase 12.1: Fix Nextcloud Root Listing and Sync Visibility — Validation Matrix

**Status:** PARTIAL external live gate. Three of four live checks pass using credentials from the ignored `.env`; exact root-manifest acceptance is blocked by one additional live root item pending owner reconciliation.
**Updated:** 2026-06-22

---

## Validation Audit 2026-06-22

| Metric | Count |
|--------|-------|
| Gaps found | 2 |
| Resolved | 1 |
| Escalated to manual-only | 1 |

Current live evidence: 3 tests pass and 1 exact-manifest test fails safely. The stale read-only assertion was repaired to verify that `list_folder` does not call byte or mutation operations even when later phases expose those capabilities. The only outstanding gate is owner reconciliation of one additional live root item.

---

## Nyquist Validation Matrix

| Requirement | Test type | Coverage | Evidence |
|------------|-----------|----------|---------|
| CONN-04: Connection-ID IDOR | Security-negative | All plans | `test_foreign_user_cannot_browse_cloud_item`, `test_admin_cannot_browse_cloud_connection` |
| CLOUD-01: Provider-neutral browse | Contract suite | P01 | `test_cloud_provider_contract.py` — 48 parametrized tests across all 4 providers |
| CLOUD-01: Root listing correct | Live smoke | P04 | `test_nextcloud_root_diagnostic` — sanitized counts and kind breakdown |
| CLOUD-01: Root exact acceptance | Live exact | PARTIAL / manual-only | `test_nextcloud_expected_root_manifest` — exact set and kind equality currently blocks on one additional live item |
| CLOUD-01: Connection-ID API browse | End-to-end live | P04 | `test_nextcloud_connection_id_browse_as_designated_user` |
| CACHE-01: Freshness truthful | TDD GREEN | P02 | `test_incomplete_listing_never_marks_folder_fresh`, `test_browse_complete_false_never_sets_fresh` |
| SYNC-01: Frontend normalized shape | TDD GREEN | P03 | `CloudFolderView.test.js` — kind, provider_item_id, verbatim freshness |

---

## Live Test Contract (Plan 04, Task 1)

### Nyquist audit result (2026-06-22)

The live credentials are present in the ignored repository-root `.env` and were passed only to the one-off backend test process. Connectivity, metadata listing, credential redaction, connection-ID browse, and owner/admin isolation all pass. Existing synthetic-fixture tests continue to cover parsing, normalization, trusted identity, and credential-free no-byte/no-mutation behavior.

The remaining exact-manifest gap is **manual-only/external-state-dependent**. The live root contains one additional item beyond the owner-confirmed fixture. Its name is intentionally withheld (T-12.1-20). The fixture must not be changed and exact equality must not be weakened until the project owner identifies the item in the Nextcloud UI and confirms whether it belongs in the authoritative manifest.

Actual command and result:

```text
docker compose exec -T -e NEXTCLOUD_URL -e NEXTCLOUD_USER -e NEXTCLOUD_APP_PASSWORD backend pytest -q -rs tests/test_nextcloud_live.py -m live_nextcloud
...F                                                                     [100%]
3 passed, 1 failed in 13.49s

Failure: exact-manifest gate detected 1 additional live root item; name withheld.
```

Required rerun condition: reconcile the additional item in the Nextcloud UI, update the fixture only after explicit owner confirmation if appropriate, then rerun the command above. No credential values belong in commands, logs, fixtures, or this artifact.

### Marker

```
live_nextcloud
```

Tests tagged with this marker are excluded from ordinary pytest runs via `addopts = -m "not live_nextcloud"` in `backend/pytest.ini`. Select explicitly with:

```bash
cd backend && pytest -m live_nextcloud tests/test_nextcloud_live.py
```

### Required environment variables (values in ignored `.env` — never committed)

```
NEXTCLOUD_URL         # Nextcloud server base URL
NEXTCLOUD_USER        # Nextcloud username
NEXTCLOUD_APP_PASSWORD # Nextcloud app password
```

If any variable is absent, all live tests skip with a message naming only the variable keys.

### Tests included

| Test | Purpose |
|------|---------|
| `test_nextcloud_adapter_read_only_root_metadata` | Verifies the production adapter lists root via canonical four-argument signature, returns CloudListing, items carry trusted caller identity, and browsing invokes no byte/mutation operation |
| `test_nextcloud_root_diagnostic` | Sanitized count/kind/match diagnostic — nonblocking while manifest is unconfirmed |
| `test_nextcloud_connection_id_browse_as_designated_user` | End-to-end browse via `GET /api/cloud/connections/{id}/items` as `testuser@docuvault.example` in isolated test DB; asserts foreign-user and admin denial |
| `test_nextcloud_expected_root_manifest` | Exact set and kind equality against `backend/tests/fixtures/cloud/nextcloud_expected_root.json` (owner_confirmed: true); enabled after Task 2 reconciliation |

### Read-only / no-mutation contract

The live adapter test installs fail-closed spies on adapter byte/mutation methods and the
underlying WebDAV client's download, upload, create-directory, and delete methods before
calling `list_folder`. Later phases may expose these capabilities, but metadata browse must
not invoke them. No MinIO, quota, or object-storage change is made.

### Threat coverage

| Threat ID | Mitigation | Test |
|-----------|-----------|------|
| T-12.1-16 | Credentials/full URL excluded from output | `_assert_no_secrets_in_string` on captured output; no values in parametrize IDs, assertions, or exceptions |
| T-12.1-17 | No byte download or mutation | Fail-closed adapter and WebDAV-client spies around `list_folder` |
| T-12.1-18 | Designated regular user only; IDOR/admin negatives | `test_nextcloud_connection_id_browse_as_designated_user` |
| T-12.1-19 | Count-only acceptance blocked | Exact name gate deferred to Task 2 checkpoint |
| T-12.1-20 | Unexpected names never printed | `print(f"Unexpected item count: {unexpected_count}  (names withheld)")` |

---

## Sanitized Probe Result (from 12.1-RESEARCH.md, pre-Plan-04)

| Check | Result |
|-------|--------|
| Endpoint reachable | Yes |
| Authentication accepted | Yes |
| HTTP status | 207 Multi-Status |
| Direct root children returned | 10 |
| Exact supplied-name matches | 7 of 10 |
| Supplied names not exactly matched | `Manual Nextcloud.png`, `Nextcloud Intro.mp4`, `Template credits.md` |
| Additional returned-name count | 3; names withheld |
| Byte download or mutation | None |

---

## Manifest Status: OWNER CONFIRMED — Task 2 Complete

The project owner reconciled the three fixture discrepancies (2026-06-22). The correct actual
Nextcloud names for the three previously unmatched slots are:

- Corrected name 1: kind=file (prior candidate name was wrong — owner confirmed)
- Corrected name 2: kind=file (prior candidate name was wrong — owner confirmed)
- Corrected name 3: kind=file (prior candidate name was wrong — owner confirmed)

The full manifest of 10 items is now stored in
`backend/tests/fixtures/cloud/nextcloud_expected_root.json` with `owner_confirmed: true`.
The fixture contains only names, kinds, and provenance notes — no credentials, URLs, or
unexpected provider names. Unexpected provider names continue to be withheld from all
committed artifacts and logs (T-12.1-20).

`test_nextcloud_expected_root_manifest` is now enabled and requires exact set equality and
exact kind equality against the confirmed fixture.

---

## Plans 01–03 Evidence Summary

### Plan 01 — Adapter Contract

- `test_cloud_provider_contract.py` — 48 tests: all pass
- `test_cloud_backends.py` — 67 tests: all pass
- `test_webdav_backend.py` — 29 tests: all pass
- `test_cloud_security.py` — 19 tests: all pass
- Full backend suite: 585 pass (1 pre-existing `test_extract_docx` failure, unrelated)

### Plan 02 — Freshness Gate

- `test_cloud_items.py` — 45 tests: all pass
- `test_cloud.py` — 25 tests: all pass
- `test_cloud_security.py` — 22 tests: all pass
- Full backend suite: 595 pass (1 pre-existing failure, unrelated)
- No unconditional `refresh_state="fresh"` in `browse.py` or `cloud_tasks.py`

### Plan 03 — Frontend Contract

- `CloudFolderView.test.js` — 23 tests: all pass
- `CloudProviderTreeItem.test.js` — 8 tests: all pass
- `CloudFolderTreeItem.test.js` — 16 tests: all pass
- `StorageBrowser.skeleton.test.js` — 22 tests: all pass
- `cloudConnections.test.js` — 23 tests: all pass
- `CloudBreadcrumbNavigation.test.js` — 8 tests: all pass
- Full frontend suite: 369 pass
- Production build: clean

### Plan 04 Task 1 — Live Test Scaffold

- `test_nextcloud_live.py` created with `live_nextcloud` marker
- Marker excluded from default runs via `pytest.ini`
- Three live tests: adapter metadata, sanitized root diagnostic, connection-ID end-to-end
- Task 2 (exact-name acceptance) deferred to checkpoint — fixture not yet created

### Plan 04 Task 2 — Fixture Reconciliation

- Owner confirmed corrected names for 3 previously unmatched items (2026-06-22)
- `backend/tests/fixtures/cloud/nextcloud_expected_root.json` created with `owner_confirmed: true`
- `test_nextcloud_expected_root_manifest` enabled with exact set and kind equality (T-12.1-19)
- Unexpected provider names remain withheld (T-12.1-20)

### Plan 04 Task 3 — Full Regression, Security, and Rendered-Flow Gates

| Gate | Command | Result |
|------|---------|--------|
| Bandit HIGH findings | `bandit -r backend/ -q` | 0 HIGH (10 LOW, pre-existing) |
| npm audit high/critical | `npm audit --audit-level=high` | 0 vulnerabilities |
| Git tracked .env files | `git ls-files '.env' '.env.*'` | None (only .env.example) |
| Secret scan new findings | `gitleaks detect --redact` | 3 findings, all pre-existing in old commits (May 2026), none from Phase 12.1 |
| Docker Compose config | `docker compose config --quiet` | Clean |
| Focused cloud tests | `pytest test_cloud_*.py test_webdav_backend.py` | 236 pass |
| Full backend suite | `pytest -v` | 595 pass, 1 pre-existing failure (test_extract_docx, unrelated) |
| Full frontend suite | `npm test` | 376 pass (7 new from CloudFolderRenderedFlow.test.js) |
| Frontend build | `npm run build` | Clean |
| Rendered flow test | `npm test -- --run CloudFolderRenderedFlow.test.js` | 7 pass |
