docs(05): add UAT, UI-SPEC, deferred items, debug notes; refine plans 09-11
Plan refinements: Vitest tests added to 09/10 must-haves, explicit mock_flow two-tuple pattern in 10, test_admin_api.py fixture usage in 11. New artifacts: UAT checklist, UI-SPEC, deferred-items, debug investigation for cloud-doc-operations-fail. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ files_modified:
|
||||
- frontend/src/components/documents/DocumentPreviewModal.vue
|
||||
- frontend/src/views/DocumentView.vue
|
||||
- backend/tests/test_cloud.py
|
||||
- frontend/src/api/__tests__/client.test.js
|
||||
autonomous: true
|
||||
requirements: [CLOUD-03, CLOUD-05, CLOUD-07]
|
||||
gap_closure: true
|
||||
@@ -21,6 +22,7 @@ must_haves:
|
||||
- "Re-analyzing a cloud document retrieves file bytes from the cloud backend, not MinIO"
|
||||
- "PATCH /api/documents/{id} accepts filename and folder_id and persists the change"
|
||||
- "Frontend fetch-with-blob-URL approach works in DocumentPreviewModal and DocumentView"
|
||||
- "fetchDocumentContent injects the Bearer token and returns a Blob URL (verified by Vitest)"
|
||||
artifacts:
|
||||
- path: "backend/api/documents.py"
|
||||
provides: "PATCH /{doc_id} endpoint accepting {filename, folder_id}"
|
||||
@@ -30,6 +32,8 @@ must_haves:
|
||||
provides: "Authenticated fetch + Blob URL for document preview"
|
||||
- path: "frontend/src/views/DocumentView.vue"
|
||||
provides: "Authenticated fetch + Blob URL for document open"
|
||||
- path: "frontend/src/api/__tests__/client.test.js"
|
||||
provides: "Vitest unit test for fetchDocumentContent"
|
||||
key_links:
|
||||
- from: "frontend/src/components/documents/DocumentPreviewModal.vue"
|
||||
to: "/api/documents/{id}/content"
|
||||
@@ -44,7 +48,7 @@ Fix three independent root causes that prevent cloud documents from being opened
|
||||
|
||||
Purpose: Cloud-stored documents are inaccessible after upload because (1) the preview uses an unauthenticated iframe src, (2) the Celery re-analyze task hardcodes MinIO, and (3) no PATCH endpoint exists for document metadata.
|
||||
|
||||
Output: All three flows work end-to-end for cloud documents. Three new/updated tests pass.
|
||||
Output: All three flows work end-to-end for cloud documents. Three new/updated backend tests plus one Vitest unit test pass.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@@ -68,6 +72,7 @@ From backend/api/documents.py:
|
||||
- Document ORM fields: id, user_id, filename, folder_id, storage_backend, object_key
|
||||
- Ownership check pattern: `if doc.user_id != current_user.id: raise HTTPException(404)`
|
||||
- Existing PATCH for folder move lives in backend/api/folders.py with separate router
|
||||
- Try-import fallback pattern (lines 51-54): wraps optional imports in try/except ImportError and sets module to None; callers guard with `if module is not None:`
|
||||
|
||||
From backend/tasks/document_tasks.py:
|
||||
- `_run(document_id)` calls `get_storage_backend()` unconditionally (returns MinIO backend)
|
||||
@@ -79,6 +84,10 @@ From backend/tasks/document_tasks.py:
|
||||
From backend/storage/__init__.py (inferred from documents.py usage):
|
||||
- `get_storage_backend_for_document(doc, user, session)` → returns correct backend instance
|
||||
|
||||
From backend/storage/google_drive_backend.py:
|
||||
- Exports `CloudConnectionError` — the canonical exception class for all cloud backend failures
|
||||
- Used by the cloud backend when it cannot connect to or retrieve from the remote provider
|
||||
|
||||
From frontend/src/api/client.js:
|
||||
- `request(path, options)` injects Authorization: Bearer header automatically
|
||||
- Returns parsed JSON; for binary responses a different approach is needed
|
||||
@@ -102,7 +111,18 @@ From frontend/src/api/client.js:
|
||||
|
||||
In backend/tasks/document_tasks.py, in `_run()`, replace the unconditional `backend = get_storage_backend()` block with:
|
||||
- If `doc.storage_backend` is None or `doc.storage_backend == "minio"`: use `get_storage_backend()` (existing MinIO path).
|
||||
- Else: use `get_storage_backend_for_document(doc, user, session)`. If user is None (doc.user_id is None), return `{"document_id": document_id, "status": "missing_user"}`. Import `get_storage_backend_for_document` at the top of `_run()` using a deferred import (same pattern as other imports in this file: `from storage import get_storage_backend_for_document`). Catch `CloudConnectionError` from the get_object call and return `{"document_id": document_id, "status": "extract_failed", "error": "cloud backend error"}` (do NOT include the raw provider error message).
|
||||
- Else: use `get_storage_backend_for_document(doc, user, session)`. If user is None (doc.user_id is None), return `{"document_id": document_id, "status": "missing_user"}`.
|
||||
|
||||
Import `get_storage_backend_for_document` at the top of `_run()` using a deferred import: `from storage import get_storage_backend_for_document`.
|
||||
|
||||
Import `CloudConnectionError` inside the cloud-backend branch using the try-import fallback pattern already used in `backend/api/documents.py` (lines 51-54) to avoid hard import errors if the module is absent:
|
||||
```python
|
||||
try:
|
||||
from storage.google_drive_backend import CloudConnectionError
|
||||
except ImportError:
|
||||
CloudConnectionError = Exception
|
||||
```
|
||||
Place this try-import block at the top of the cloud-backend branch (before the `get_storage_backend_for_document` call), not at module top-level. Catch `CloudConnectionError` from the get_object call and return `{"document_id": document_id, "status": "extract_failed", "error": "cloud backend error"}` (do NOT include the raw provider error message).
|
||||
|
||||
In backend/tests/test_cloud.py, add three tests:
|
||||
1. `test_patch_document_filename` — create a document, PATCH with {filename: "renamed.pdf"}, assert 200 and updated filename.
|
||||
@@ -116,8 +136,8 @@ From frontend/src/api/client.js:
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Authenticated document preview — fetch-with-Blob-URL in frontend</name>
|
||||
<files>frontend/src/api/client.js, frontend/src/components/documents/DocumentPreviewModal.vue, frontend/src/views/DocumentView.vue</files>
|
||||
<name>Task 2: Authenticated document preview — fetch-with-Blob-URL in frontend + Vitest test</name>
|
||||
<files>frontend/src/api/client.js, frontend/src/components/documents/DocumentPreviewModal.vue, frontend/src/views/DocumentView.vue, frontend/src/api/__tests__/client.test.js</files>
|
||||
<action>
|
||||
In frontend/src/api/client.js, add a new exported function `fetchDocumentContent(docId)` that calls `fetch(/api/documents/${docId}/content, { headers: { Authorization: Bearer ${authStore.accessToken} }, credentials: "include" })`. On 401, attempt one refresh via `authStore.refresh()` and retry (same pattern as `request()`). Return the raw `Response` object (not parsed JSON) so callers can call `.blob()` on it. Do NOT use the existing `request()` helper because it calls `res.json()` unconditionally.
|
||||
|
||||
@@ -136,11 +156,22 @@ From frontend/src/api/client.js:
|
||||
- Import `fetchDocumentContent` from the API client.
|
||||
|
||||
Note: the `request()` helper in client.js already handles 401 → refresh → retry. The new `fetchDocumentContent` must replicate only the auth injection + single retry, not the JSON parsing. Keep it simple: use the `useAuthStore` lazy import pattern already in `request()`.
|
||||
|
||||
In frontend/src/api/__tests__/client.test.js (create the file if it does not exist), add a Vitest unit test for `fetchDocumentContent`:
|
||||
- Mock `fetch` globally using `vi.stubGlobal('fetch', vi.fn())`.
|
||||
- Mock the auth store so `authStore.accessToken` returns a known token string (e.g. "test-token-abc").
|
||||
- Configure the mock fetch to return a Response-like object with `status: 200` and a `.blob()` method that resolves to a `new Blob(['%PDF-1.4'], { type: 'application/pdf' })`.
|
||||
- Mock `URL.createObjectURL` to return a fixed string `"blob:http://localhost/fake-uuid"`.
|
||||
- Call `fetchDocumentContent("doc-123")` and await the result.
|
||||
- Assert: the first argument to fetch was `/api/documents/doc-123/content`.
|
||||
- Assert: the `Authorization` header in the fetch call was `"Bearer test-token-abc"`.
|
||||
- Assert: the returned Response is the mock response (not a Blob URL — callers are responsible for calling `.blob()` and `URL.createObjectURL`).
|
||||
- Restore all stubs in `afterEach` using `vi.restoreAllMocks()`.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run build 2>&1 | tail -5</automated>
|
||||
<automated>cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run test -- --reporter=verbose --run src/api/__tests__/client.test.js 2>&1 | tail -20 && npm run build 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<done>Frontend build passes with zero errors. DocumentPreviewModal and DocumentView use fetchDocumentContent. No unauthenticated src= URLs remain for the /content endpoint.</done>
|
||||
<done>Vitest test for fetchDocumentContent passes. Frontend build passes with zero errors. DocumentPreviewModal and DocumentView use fetchDocumentContent. No unauthenticated src= URLs remain for the /content endpoint.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
@@ -168,6 +199,7 @@ From frontend/src/api/client.js:
|
||||
<verification>
|
||||
After both tasks complete:
|
||||
- `pytest backend/tests/test_cloud.py -v` — all three new tests pass, no regressions
|
||||
- `npm run test -- --run src/api/__tests__/client.test.js` — Vitest test for fetchDocumentContent passes
|
||||
- `npm run build` — zero errors
|
||||
- Manual: open a cloud document in the app — preview loads without 401
|
||||
- Manual: re-analyze a cloud document — task completes without NoSuchKey error
|
||||
@@ -177,9 +209,11 @@ After both tasks complete:
|
||||
<success_criteria>
|
||||
- PATCH /api/documents/{id} is callable with {filename} or {folder_id} and returns the updated document
|
||||
- Re-analyze Celery task calls the cloud backend for cloud documents (not MinIO)
|
||||
- CloudConnectionError imported via try-import fallback pattern; raw provider error not exposed in response
|
||||
- DocumentPreviewModal uses fetch + Blob URL, no unauthenticated iframe src
|
||||
- DocumentView uses fetch + Blob URL, no window.open with raw /content URL
|
||||
- All three new pytest tests pass; full suite has zero new failures
|
||||
- Vitest test for fetchDocumentContent asserts Bearer header injection and correct fetch URL
|
||||
- All three new pytest tests pass; Vitest test passes; full suite has zero new failures
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
||||
Reference in New Issue
Block a user