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:
curo1305
2026-06-22 18:13:39 +02:00
parent 514925bd4c
commit 8923ed5b3c
3 changed files with 813 additions and 0 deletions
@@ -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)
}
})
})