fix(05): revise Phase 5 plans based on checker feedback — B1-B4, W1-W4
B1: Mark RESEARCH.md Open Questions as (RESOLVED) with decision text for all 3
B2: Backends now stateless — raise CloudConnectionError(reason=) only; API layer
in cloud.py owns token refresh + DB update via _call_cloud_op helper
B3: Add Task 3 to Plan 05 — cloud connection + object cleanup on account deletion (SEC-09)
B4: Add frontend_url setting to Plan 01 Task 1; Plan 05 uses settings.frontend_url
for OAuth callback redirects
W1: ROADMAP.md Phase 5 now correctly labels Plans 03+04 as Wave 3 (not Wave 2)
W2: Plan 06 invalid_grant test now asserts both 503 HTTP response AND DB REQUIRES_REAUTH
W3: Plan 06 Task 2 split into unit tests (4, cloud_utils.py) and integration tests (11, HTTP)
W4: Plan 07 adds Vitest tests for cloudConnections store (4 tests) and SettingsCloudTab
mount test (2 tests) per CLAUDE.md testing protocol
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ depends_on:
|
||||
- "05-06"
|
||||
files_modified:
|
||||
- frontend/src/stores/cloudConnections.js
|
||||
- frontend/src/stores/__tests__/cloudConnections.test.js
|
||||
- frontend/src/api/client.js
|
||||
- frontend/src/views/SettingsView.vue
|
||||
- frontend/src/components/settings/SettingsPreferencesTab.vue
|
||||
@@ -31,10 +32,14 @@ must_haves:
|
||||
- "OAuth redirect success/error handled in onMounted via ?cloud_connected= and ?cloud_error= query params"
|
||||
- "Success toast auto-dismisses in 5 seconds; error banner persists until dismissed"
|
||||
- "cloudConnectionsStore: connections, loading, error state; fetchConnections, disconnect, disconnectAll actions"
|
||||
- "Vitest unit tests for cloudConnections store (4 tests) and SettingsCloudTab mount test (2 tests) — per CLAUDE.md testing protocol (W4)"
|
||||
artifacts:
|
||||
- path: "frontend/src/stores/cloudConnections.js"
|
||||
provides: "Pinia store for cloud connections state"
|
||||
contains: "useCloudConnectionsStore"
|
||||
- path: "frontend/src/stores/__tests__/cloudConnections.test.js"
|
||||
provides: "Vitest unit tests for cloudConnections store (W4)"
|
||||
contains: "fetchConnections"
|
||||
- path: "frontend/src/api/client.js"
|
||||
provides: "Cloud API client functions"
|
||||
contains: "listCloudConnections"
|
||||
@@ -117,8 +122,8 @@ Save button label: "Connect {providerLabel}"
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Create cloudConnections Pinia store and API client additions</name>
|
||||
<files>frontend/src/stores/cloudConnections.js, frontend/src/api/client.js</files>
|
||||
<name>Task 1: Create cloudConnections Pinia store, API client additions, and Vitest tests</name>
|
||||
<files>frontend/src/stores/cloudConnections.js, frontend/src/stores/__tests__/cloudConnections.test.js, frontend/src/api/client.js</files>
|
||||
<read_first>
|
||||
- frontend/src/stores/folders.js — Pinia store structure (defineStore composition API pattern)
|
||||
- frontend/src/api/client.js — existing API function patterns, request() helper
|
||||
@@ -193,6 +198,55 @@ Save button label: "Connect {providerLabel}"
|
||||
body: JSON.stringify({ backend }),
|
||||
})
|
||||
}
|
||||
|
||||
Create frontend/src/stores/__tests__/cloudConnections.test.js (W4 — Vitest unit tests per CLAUDE.md):
|
||||
Tests must mock api/client.js functions (no real HTTP calls).
|
||||
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { useCloudConnectionsStore } from '../cloudConnections.js'
|
||||
import * as api from '../../api/client.js'
|
||||
|
||||
beforeEach(() => { setActivePinia(createPinia()) })
|
||||
|
||||
describe('useCloudConnectionsStore', () => {
|
||||
it('fetchConnections sets connections from API response', async () => {
|
||||
vi.spyOn(api, 'listCloudConnections').mockResolvedValue({ items: [{id:'1',provider:'google_drive',status:'ACTIVE'}] })
|
||||
const store = useCloudConnectionsStore()
|
||||
await store.fetchConnections()
|
||||
expect(store.connections).toHaveLength(1)
|
||||
expect(store.connections[0].provider).toBe('google_drive')
|
||||
expect(store.loading).toBe(false)
|
||||
})
|
||||
|
||||
it('fetchConnections sets error on API failure', async () => {
|
||||
vi.spyOn(api, 'listCloudConnections').mockRejectedValue(new Error('Network error'))
|
||||
const store = useCloudConnectionsStore()
|
||||
await store.fetchConnections()
|
||||
expect(store.error).toBeTruthy()
|
||||
expect(store.connections).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('disconnect removes connection from state after API call', async () => {
|
||||
vi.spyOn(api, 'disconnectCloud').mockResolvedValue(undefined)
|
||||
const store = useCloudConnectionsStore()
|
||||
store.connections = [{ id: 'conn-1', provider: 'google_drive', status: 'ACTIVE' }]
|
||||
await store.disconnect('conn-1')
|
||||
expect(store.connections).toHaveLength(0)
|
||||
expect(api.disconnectCloud).toHaveBeenCalledWith('conn-1')
|
||||
})
|
||||
|
||||
it('disconnectAll clears all connections', async () => {
|
||||
vi.spyOn(api, 'disconnectCloud').mockResolvedValue(undefined)
|
||||
const store = useCloudConnectionsStore()
|
||||
store.connections = [
|
||||
{ id: 'a', provider: 'google_drive', status: 'ACTIVE' },
|
||||
{ id: 'b', provider: 'onedrive', status: 'ACTIVE' },
|
||||
]
|
||||
await store.disconnectAll()
|
||||
expect(store.connections).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/nik/Documents/Progamming/document_scanner/frontend && node -e "
|
||||
@@ -207,15 +261,19 @@ const api = fs.readFileSync('src/api/client.js', 'utf8');
|
||||
if (!api.includes(name)) throw new Error('Missing from api/client.js: ' + name);
|
||||
console.log('OK api: ' + name);
|
||||
});
|
||||
"</automated>
|
||||
if (!fs.existsSync('src/stores/__tests__/cloudConnections.test.js')) throw new Error('Missing Vitest file');
|
||||
console.log('OK: Vitest test file exists');
|
||||
" && npm run test -- src/stores/__tests__/cloudConnections.test.js 2>&1 | tail -10</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- frontend/src/stores/cloudConnections.js exists with useCloudConnectionsStore
|
||||
- Store exports: connections (ref), loading (ref), error (ref), fetchConnections, disconnect, disconnectAll
|
||||
- frontend/src/api/client.js contains listCloudConnections, disconnectCloud, connectWebDav, updateDefaultStorage
|
||||
- frontend/src/stores/__tests__/cloudConnections.test.js exists with 4 Vitest tests (W4 — CLAUDE.md requirement)
|
||||
- All 4 Vitest tests pass: fetchConnections, fetchConnections error path, disconnect, disconnectAll
|
||||
- No modifications to existing API functions (folders, auth, etc.)
|
||||
</acceptance_criteria>
|
||||
<done>cloudConnections.js store created; 4 new API functions appended to client.js; existing API functions untouched</done>
|
||||
<done>cloudConnections.js store created; 4 new API functions appended to client.js; 4 Vitest unit tests passing; existing API functions untouched</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
@@ -225,6 +283,7 @@ const api = fs.readFileSync('src/api/client.js', 'utf8');
|
||||
frontend/src/components/settings/SettingsPreferencesTab.vue,
|
||||
frontend/src/components/settings/SettingsAiTab.vue,
|
||||
frontend/src/components/settings/SettingsCloudTab.vue,
|
||||
frontend/src/components/settings/__tests__/SettingsCloudTab.test.js,
|
||||
frontend/src/components/cloud/CloudCredentialModal.vue
|
||||
</files>
|
||||
<read_first>
|
||||
@@ -313,6 +372,40 @@ const api = fs.readFileSync('src/api/client.js', 'utf8');
|
||||
Preserve: p-8 max-w-3xl mx-auto wrapper, h2 heading, description paragraph.
|
||||
|
||||
Check existing components: look for ConfirmBlock in frontend/src/components/ui/ — if present, use it for disconnect confirmation dialogs. If not present, implement inline confirmation pattern.
|
||||
|
||||
6. Create frontend/src/components/settings/__tests__/SettingsCloudTab.test.js (W4 — CLAUDE.md requires tests for new components):
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import SettingsCloudTab from '../SettingsCloudTab.vue'
|
||||
|
||||
describe('SettingsCloudTab', () => {
|
||||
it('renders all 4 provider rows', () => {
|
||||
const wrapper = mount(SettingsCloudTab, {
|
||||
global: {
|
||||
plugins: [createTestingPinia({ createSpy: vi.fn })],
|
||||
},
|
||||
})
|
||||
expect(wrapper.text()).toContain('Google Drive')
|
||||
expect(wrapper.text()).toContain('OneDrive')
|
||||
expect(wrapper.text()).toContain('Nextcloud')
|
||||
expect(wrapper.text()).toContain('WebDAV')
|
||||
})
|
||||
|
||||
it('shows "Not connected" state when no connections active', () => {
|
||||
const wrapper = mount(SettingsCloudTab, {
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
initialState: { cloudConnections: { connections: [], loading: false, error: null } },
|
||||
})],
|
||||
},
|
||||
})
|
||||
// All providers have Connect buttons when no connections exist
|
||||
const connectButtons = wrapper.findAll('button')
|
||||
expect(connectButtons.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/nik/Documents/Progamming/document_scanner/frontend && node -e "
|
||||
@@ -322,6 +415,7 @@ const files = [
|
||||
'src/components/settings/SettingsPreferencesTab.vue',
|
||||
'src/components/settings/SettingsAiTab.vue',
|
||||
'src/components/settings/SettingsCloudTab.vue',
|
||||
'src/components/settings/__tests__/SettingsCloudTab.test.js',
|
||||
'src/components/cloud/CloudCredentialModal.vue',
|
||||
];
|
||||
files.forEach(f => {
|
||||
@@ -338,7 +432,7 @@ const cloud = fs.readFileSync('src/components/settings/SettingsCloudTab.vue', 'u
|
||||
if (!cloud.includes('google_drive')) throw new Error('SettingsCloudTab missing google_drive provider');
|
||||
if (!cloud.includes('CloudCredentialModal')) throw new Error('SettingsCloudTab missing CloudCredentialModal');
|
||||
console.log('SettingsCloudTab providers and modal: OK');
|
||||
" && npm --prefix /Users/nik/Documents/Progamming/document_scanner/frontend run build 2>&1 | tail -5</automated>
|
||||
" && npm --prefix /Users/nik/Documents/Progamming/document_scanner/frontend run test -- src/stores/__tests__/cloudConnections.test.js src/components/settings/__tests__/SettingsCloudTab.test.js 2>&1 | tail -10 && npm --prefix /Users/nik/Documents/Progamming/document_scanner/frontend run build 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- All 5 new/modified files exist
|
||||
@@ -349,8 +443,9 @@ console.log('SettingsCloudTab providers and modal: OK');
|
||||
- SettingsCloudTab.vue uses useCloudConnectionsStore
|
||||
- CloudCredentialModal.vue contains authMethod ref and auth method radio group
|
||||
- `npm run build` (Vite build) exits 0 without errors
|
||||
- SettingsCloudTab.test.js exists with at least one mount test confirming all 4 providers render (W4 — CLAUDE.md requirement for new components)
|
||||
</acceptance_criteria>
|
||||
<done>5 files created/modified; 3-tab SettingsView with OAuth handling; SettingsCloudTab with 4 providers; CloudCredentialModal; Vite build passes</done>
|
||||
<done>5 files created/modified; 3-tab SettingsView with OAuth handling; SettingsCloudTab with 4 providers; CloudCredentialModal; SettingsCloudTab mount test; Vite build passes</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
@@ -384,6 +479,7 @@ cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run build 2>
|
||||
- SettingsView.vue: 3-tab layout; OAuth success/error handling; tab strip matches AdminView pattern
|
||||
- SettingsCloudTab.vue: all 4 providers; status badges; action buttons per status; REQUIRES_REAUTH banner; disconnect all
|
||||
- CloudCredentialModal.vue: server URL + username + auth method toggle + password; correct cancel/save labels
|
||||
- Vitest: cloudConnections.test.js (4 tests passing) and SettingsCloudTab.test.js (2 tests passing)
|
||||
- Vite build exits 0
|
||||
</success_criteria>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user