diff --git a/__tests__/components/wallet/Disconnect.test.tsx b/__tests__/components/wallet/Disconnect.test.tsx
new file mode 100644
index 0000000..ee6020d
--- /dev/null
+++ b/__tests__/components/wallet/Disconnect.test.tsx
@@ -0,0 +1,107 @@
+import { render, screen, fireEvent, act } from '@testing-library/react';
+import { DisconnectButton } from '@/components/wallet/DisconnectButton';
+import { useWalletStore, WALLET_STORAGE_KEY } from '@/store/walletStore';
+import { walletService } from '@/services/walletService';
+import { mockWalletDisconnectResponse } from '../../../hooks/__tests__/fixtures/walletApiResponses';
+
+const mockPush = jest.fn();
+
+jest.mock('next/navigation', () => ({
+ useRouter: () => ({ push: mockPush }),
+}));
+
+// Only the network boundary is mocked, per the Component -> Hook -> Service
+// architecture; useWallet, useWalletStore and sessionService run for real so
+// this test exercises the actual session cleanup path.
+jest.mock('@/services/walletService', () => ({
+ walletService: {
+ connect: jest.fn(),
+ disconnect: jest.fn(),
+ },
+}));
+
+describe('DisconnectButton - session cleanup', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (walletService.disconnect as jest.Mock).mockResolvedValue(
+ mockWalletDisconnectResponse
+ );
+
+ act(() => {
+ useWalletStore.getState().setWallet('GABC123DEF456STELLAR', 0);
+ });
+ localStorage.setItem(
+ WALLET_STORAGE_KEY,
+ JSON.stringify({ address: 'GABC123DEF456STELLAR' })
+ );
+ });
+
+ afterEach(() => {
+ act(() => {
+ useWalletStore.getState().clearWalletState();
+ });
+ localStorage.clear();
+ });
+
+ it('renders the connected address and an enabled disconnect control', () => {
+ render();
+
+ expect(
+ screen.getByRole('button', { name: /disconnect wallet/i })
+ ).toBeEnabled();
+ expect(screen.getByText(/GABC123DEF456STELLAR/i)).toBeInTheDocument();
+ });
+
+ it('clears the wallet key from localStorage when disconnect is clicked', async () => {
+ render();
+
+ await act(async () => {
+ fireEvent.click(screen.getByRole('button', { name: /disconnect wallet/i }));
+ });
+
+ expect(walletService.disconnect).toHaveBeenCalledTimes(1);
+ expect(localStorage.getItem(WALLET_STORAGE_KEY)).toBeNull();
+ });
+
+ it('redirects to /login after disconnect completes', async () => {
+ render();
+
+ await act(async () => {
+ fireEvent.click(screen.getByRole('button', { name: /disconnect wallet/i }));
+ });
+
+ expect(mockPush).toHaveBeenCalledWith('/login');
+ });
+
+ it('still clears localStorage and redirects when the backend call fails', async () => {
+ (walletService.disconnect as jest.Mock).mockRejectedValueOnce(
+ new Error('Network error')
+ );
+
+ render();
+
+ await act(async () => {
+ fireEvent.click(screen.getByRole('button', { name: /disconnect wallet/i }));
+ });
+
+ expect(localStorage.getItem(WALLET_STORAGE_KEY)).toBeNull();
+ expect(mockPush).toHaveBeenCalledWith('/login');
+ });
+
+ it('renders the disabled empty state and clicking it performs no cleanup', () => {
+ act(() => {
+ useWalletStore.getState().clearWalletState();
+ });
+ localStorage.removeItem(WALLET_STORAGE_KEY);
+
+ render();
+
+ const button = screen.getByRole('button', { name: /disconnect wallet/i });
+ expect(button).toBeDisabled();
+
+ fireEvent.click(button);
+
+ expect(walletService.disconnect).not.toHaveBeenCalled();
+ expect(mockPush).not.toHaveBeenCalled();
+ });
+});