diff --git a/frontend/src/composables/__tests__/useNostr.test.ts b/frontend/src/composables/__tests__/useNostr.test.ts index ca2bf69..d64b4eb 100644 --- a/frontend/src/composables/__tests__/useNostr.test.ts +++ b/frontend/src/composables/__tests__/useNostr.test.ts @@ -128,4 +128,34 @@ describe('useNostr', () => { expect(result).toBe(false) vi.useRealTimers() }) + + it('BUG-F7: autoRestoreRan survives module re-import (HMR)', async () => { + // Set up conditions that trigger auto-restore: pubkey stored, no bot, valid JWT + localStorage.setItem('bf_pubkey', JSON.stringify('a'.repeat(64))) + mockGetToken.mockReturnValue('valid-jwt') + mockIsTokenExpired.mockReturnValue(false) + mockAuthFetch.mockResolvedValue({ json: () => Promise.resolve({ exists: true, bot: { id: 'b1', name: 'Bot' } }) }) + + // Clear globalThis flag + delete (globalThis as any).__bf_autoRestoreRan + + // First import triggers auto-restore + vi.resetModules() + await import('../useNostr') + await new Promise(r => setTimeout(r, 0)) // flush microtasks + expect(mockAuthFetch).toHaveBeenCalledTimes(1) + + // globalThis flag should be set + expect((globalThis as any).__bf_autoRestoreRan).toBe(true) + + // Second import (simulating HMR) should NOT trigger auto-restore again + vi.resetModules() + mockAuthFetch.mockClear() + await import('../useNostr') + await new Promise(r => setTimeout(r, 0)) + expect(mockAuthFetch).not.toHaveBeenCalled() + + // Clean up + delete (globalThis as any).__bf_autoRestoreRan + }) }) diff --git a/frontend/src/composables/useNostr.ts b/frontend/src/composables/useNostr.ts index 167b77e..a060568 100644 --- a/frontend/src/composables/useNostr.ts +++ b/frontend/src/composables/useNostr.ts @@ -95,8 +95,8 @@ const bot = ref(loadStored('bf_bot')) const profilePicUrl = ref(loadStored('bf_pic')) const isLoading = ref(false) -// Guard: only auto-restore once across all component mounts -let autoRestoreRan = false +// Guard: only auto-restore once — survives Vite HMR module re-evaluation +let autoRestoreRan = (globalThis as any).__bf_autoRestoreRan ?? false // Flag: skip relay pic fetch for freshly generated keys (no profile exists) let freshlyGenerated = false // In-memory nsec for current session (never auto-persisted to localStorage) @@ -116,7 +116,8 @@ if (typeof document !== 'undefined') { // Auto-restore session from JWT on first load if (!autoRestoreRan && pubkey.value && !bot.value && getToken() && !isTokenExpired()) { - autoRestoreRan = true + autoRestoreRan = true; + (globalThis as any).__bf_autoRestoreRan = true authFetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -129,7 +130,8 @@ if (!autoRestoreRan && pubkey.value && !bot.value && getToken() && !isTokenExpir }).catch(err => console.warn('[Nostr] auto-restore failed:', err)) } else if (!autoRestoreRan && pubkey.value && !getToken()) { // No JWT — clear stale pubkey from before the JWT migration - autoRestoreRan = true + autoRestoreRan = true; + (globalThis as any).__bf_autoRestoreRan = true pubkey.value = null store('bf_pubkey', null) }