test(13-02): add red store, health-flow, reconnect, and no-probe tests
- SettingsCloudTab.health.test.js: Test/Reconnect/Disconnect per-connection actions, explicit confirmation before credential removal, Google Drive broader scope consent copy (D-12/D-13/D-15/D-16/D-17/T-13-09) - cloudConnections.test.js: connectionHealth state map, setConnectionHealth translation, degraded vs requires_reauth distinction, pendingHealthRetest after failure, reconnect preserves cached items, auto-test after connect, no-probe-on-navigation (D-12..D-15) - CloudFolderRenderedFlow.test.js: warning+reconnect banner alongside cached items, requires_reauth renders actionable prompt, folder-navigate never triggers health probe (D-12/D-13/D-14/CONN-02/T-13-06)
This commit is contained in:
@@ -236,3 +236,188 @@ describe('setBrowseState freshness mapping', () => {
|
||||
expect(store.browseItems[0].id).toBe('item-a')
|
||||
})
|
||||
})
|
||||
|
||||
// ── Phase 13 Plan 02: health-state, auto-test, and no-probe-on-navigation ──────
|
||||
// These tests MUST FAIL until Phase 13 store extensions are implemented.
|
||||
|
||||
describe('health_state_translation_D12', () => {
|
||||
it('store exposes connectionHealth state for settings and browser display', () => {
|
||||
/**
|
||||
* D-12: Connection health appears in both Settings and the cloud browser.
|
||||
* The store must provide a centralized health state so both surfaces
|
||||
* consume the same server-translated values without duplicating translation logic.
|
||||
*
|
||||
* RED: current store has no connectionHealth or healthState field.
|
||||
*/
|
||||
const store = useCloudConnectionsStore()
|
||||
// Store must expose a health state map or structured health field
|
||||
// Either a computed connectionHealth getter or a healthState ref
|
||||
expect(store.connectionHealth !== undefined || store.healthState !== undefined).toBe(true)
|
||||
})
|
||||
|
||||
it('store.setConnectionHealth translates server requires_reauth to a UI actionable state', () => {
|
||||
/**
|
||||
* D-12 and CONN-02: The store must translate server health states once
|
||||
* so both the browser compact status and Settings diagnostics use the same
|
||||
* actionable vocabulary. `requires_reauth` must map to a UI-actionable state
|
||||
* (not a raw HTTP status or API error string).
|
||||
*
|
||||
* RED: no setConnectionHealth or health translation exists in the current store.
|
||||
*/
|
||||
const store = useCloudConnectionsStore()
|
||||
if (typeof store.setConnectionHealth === 'function') {
|
||||
store.setConnectionHealth('conn-1', { state: 'requires_reauth', error_code: 'token_expired' })
|
||||
const health = store.connectionHealth?.['conn-1'] ?? store.getConnectionHealth?.('conn-1')
|
||||
expect(health).toBeDefined()
|
||||
expect(health.state).toBe('requires_reauth')
|
||||
} else {
|
||||
// RED: method does not exist yet
|
||||
expect(typeof store.setConnectionHealth).toBe('function')
|
||||
}
|
||||
})
|
||||
|
||||
it('store.setConnectionHealth translates server degraded to a transient warning state', () => {
|
||||
/**
|
||||
* D-15: Transient outage must be distinguished from requires_reauth.
|
||||
* The store must hold separate states for these two failure modes so
|
||||
* the UI can show different affordances (retry vs reconnect).
|
||||
*
|
||||
* RED: no health state distinction exists.
|
||||
*/
|
||||
const store = useCloudConnectionsStore()
|
||||
if (typeof store.setConnectionHealth === 'function') {
|
||||
store.setConnectionHealth('conn-1', { state: 'degraded', error_code: 'provider_timeout' })
|
||||
const health = store.connectionHealth?.['conn-1'] ?? store.getConnectionHealth?.('conn-1')
|
||||
expect(health?.state).toBe('degraded')
|
||||
// degraded must not equal requires_reauth
|
||||
expect(health?.state).not.toBe('requires_reauth')
|
||||
} else {
|
||||
expect(typeof store.setConnectionHealth).toBe('function')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('no_probe_on_navigation_D13', () => {
|
||||
it('ordinary folder navigation does NOT call testCloudConnection', async () => {
|
||||
/**
|
||||
* D-13: "Do not probe the provider on every folder navigation."
|
||||
* When getCloudFoldersByConnectionId is called for browse, the store must
|
||||
* NOT trigger a health probe (testCloudConnection) as a side effect.
|
||||
*
|
||||
* RED: no probe-suppression guard exists; getCloudFoldersByConnectionId
|
||||
* could trigger a health probe as a side effect in a naive implementation.
|
||||
*/
|
||||
api.getCloudFoldersByConnectionId.mockResolvedValue({
|
||||
items: [],
|
||||
capabilities: null,
|
||||
freshness: { refresh_state: 'fresh', last_refreshed_at: '2026-06-22T00:00:00Z' },
|
||||
})
|
||||
const store = useCloudConnectionsStore()
|
||||
store.connections = [{ id: 'conn-browse', provider: 'onedrive', display_name: 'My OneDrive' }]
|
||||
|
||||
// Simulate browse (what CloudFolderView calls on navigation)
|
||||
if (typeof store.browse === 'function') {
|
||||
await store.browse('conn-browse', '')
|
||||
} else {
|
||||
// Direct API call (current approach)
|
||||
await api.getCloudFoldersByConnectionId('conn-browse', '')
|
||||
}
|
||||
|
||||
// testCloudConnection must NOT have been called as a navigation side effect
|
||||
expect(api.testCloudConnection ?? vi.fn()).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('health probe DOES trigger after credential-related failure (requires_reauth)', async () => {
|
||||
/**
|
||||
* D-13: Health retest is automatically triggered after credential-related
|
||||
* failures. This SHOULD happen — it is the complement of the no-probe rule
|
||||
* (probe only after failures, not on normal navigation).
|
||||
*
|
||||
* RED: no auto-probe-after-failure mechanism exists in the store.
|
||||
*/
|
||||
const store = useCloudConnectionsStore()
|
||||
if (typeof store.handleHealthFailure === 'function') {
|
||||
// After a reauth failure, the store should schedule/trigger a health retest
|
||||
store.handleHealthFailure('conn-1', { state: 'requires_reauth', error_code: 'token_expired' })
|
||||
// Must set a pending retest flag or have called testConnection
|
||||
const needsRetest = store.pendingHealthRetest?.has?.('conn-1')
|
||||
?? store.healthRetestPending?.['conn-1']
|
||||
expect(needsRetest).toBe(true)
|
||||
} else {
|
||||
// RED: method does not exist yet
|
||||
expect(typeof store.handleHealthFailure).toBe('function')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('reconnect_refreshes_current_folder_D14', () => {
|
||||
it('store records that reconnect should refresh the current folder', () => {
|
||||
/**
|
||||
* D-14: A successful reconnect must immediately refresh the current folder.
|
||||
* The store must record a pending folder refresh flag so CloudFolderView
|
||||
* can detect and execute the refresh after reconnect.
|
||||
*
|
||||
* RED: no reconnect-triggered refresh flag exists in the current store.
|
||||
*/
|
||||
const store = useCloudConnectionsStore()
|
||||
if (typeof store.markReconnectRefreshPending === 'function') {
|
||||
store.markReconnectRefreshPending('conn-1')
|
||||
expect(store.reconnectRefreshPending).toBe(true)
|
||||
} else {
|
||||
// RED: no such method
|
||||
expect(typeof store.markReconnectRefreshPending).toBe('function')
|
||||
}
|
||||
})
|
||||
|
||||
it('reconnect preserves cached metadata as stale (does not clear browseItems)', () => {
|
||||
/**
|
||||
* D-14: A successful reconnect keeps cached metadata visible as stale.
|
||||
* The store must NOT clear browseItems during reconnect — it must set
|
||||
* freshness to 'stale' or 'reconnecting' without wiping the item list.
|
||||
*
|
||||
* RED: current selectConnection clears all browse state; reconnect must differ.
|
||||
*/
|
||||
const store = useCloudConnectionsStore()
|
||||
const cachedItems = [
|
||||
{ id: 'item-1', provider_item_id: 'pref-1', name: 'Old Folder', kind: 'folder' },
|
||||
]
|
||||
store.setBrowseState({ items: cachedItems, freshness: 'fresh' })
|
||||
|
||||
if (typeof store.markReconnecting === 'function') {
|
||||
store.markReconnecting('conn-1')
|
||||
// Items must remain visible during reconnect (stale but visible)
|
||||
expect(store.browseItems).toHaveLength(1)
|
||||
// Freshness changes to stale or reconnecting — but items are preserved
|
||||
expect(['stale', 'reconnecting', 'warning'].includes(store.folderFreshness)).toBe(true)
|
||||
} else {
|
||||
// RED: no markReconnecting method
|
||||
expect(typeof store.markReconnecting).toBe('function')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('auto_test_after_connect_D13', () => {
|
||||
it('store exposes a pendingHealthTest flag or similar after a connect action', async () => {
|
||||
/**
|
||||
* D-13: Automatically test health after connect/reconnect.
|
||||
* The store must have a mechanism to schedule or trigger a health test
|
||||
* immediately after a new connection is established.
|
||||
*
|
||||
* RED: no auto-test-after-connect mechanism exists in the store.
|
||||
*/
|
||||
const store = useCloudConnectionsStore()
|
||||
// After fetchConnections, newly added connections should trigger health check
|
||||
api.listCloudConnections.mockResolvedValue({
|
||||
items: [{ id: 'conn-new', provider: 'nextcloud', status: 'ACTIVE', health: null }],
|
||||
})
|
||||
await store.fetchConnections()
|
||||
// The store should mark connections with null health for testing
|
||||
const needsTest = store.connectionsNeedingHealthCheck
|
||||
?? store.connections.filter(c => c.health === null || c.health?.state === undefined)
|
||||
expect(needsTest).toBeDefined()
|
||||
// At least one connection should be marked for health testing
|
||||
if (Array.isArray(needsTest)) {
|
||||
expect(needsTest.length).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user