feat(federation): trigger DM failover on setInstanceStatus transition
When an instance transitions from 'connected' to 'disconnected' or 'error', fire failoverDmOriginsFromDisconnected for that origin. Dynamic import preserves the circular-dep-safe resolver pattern used elsewhere in instanceStore. Fire-and-forget; the failover utility reads fresh state at call time.
This commit is contained in:
@@ -0,0 +1,70 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
|
||||||
|
const mockFailover = vi.fn();
|
||||||
|
vi.mock('../utils/dmOriginFailover', () => ({
|
||||||
|
failoverDmOriginsFromDisconnected: (o: string) => mockFailover(o),
|
||||||
|
}));
|
||||||
|
vi.mock('../hooks/useWebSocket', () => ({
|
||||||
|
connectInstance: vi.fn(),
|
||||||
|
disconnectInstance: vi.fn(),
|
||||||
|
disconnectAllRemote: vi.fn(),
|
||||||
|
}));
|
||||||
|
vi.mock('../utils/federationOps', () => ({ clearPasswordSyncTimers: vi.fn() }));
|
||||||
|
// Stub AudioManager to avoid AudioWorkletNode reference error in jsdom
|
||||||
|
vi.mock('../audio/AudioManager', () => ({
|
||||||
|
AudioManager: { getInstance: vi.fn().mockReturnValue({ setOutputDevice: vi.fn(), setVolume: vi.fn() }) },
|
||||||
|
}));
|
||||||
|
// Stub authStore to avoid localStorage access during module init
|
||||||
|
vi.mock('./authStore', () => ({
|
||||||
|
useAuthStore: Object.assign(
|
||||||
|
(selector: (s: unknown) => unknown) => selector({ user: null, token: null }),
|
||||||
|
{ getState: () => ({ user: null, token: null }), setState: vi.fn(), subscribe: vi.fn() }
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { useInstanceStore } from './instanceStore';
|
||||||
|
import type { ConnectedInstance } from './instanceStore';
|
||||||
|
|
||||||
|
function inst(origin: string, status: ConnectedInstance['status']): ConnectedInstance {
|
||||||
|
return {
|
||||||
|
origin, label: origin, token: 'tok', username: 'u', status,
|
||||||
|
user: { id: 'u', username: 'u' } as any,
|
||||||
|
api: {} as any,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockFailover.mockClear();
|
||||||
|
useInstanceStore.setState({ instances: [], registry: new Map(), registryUpdatedAt: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('instanceStore failover triggers', () => {
|
||||||
|
it('fires failover on connected → disconnected transition', async () => {
|
||||||
|
useInstanceStore.setState({ instances: [inst('https://b.example', 'connected')] });
|
||||||
|
useInstanceStore.getState().setInstanceStatus('https://b.example', 'disconnected');
|
||||||
|
await vi.waitFor(() => expect(mockFailover).toHaveBeenCalledExactlyOnceWith('https://b.example'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fires failover on connected → error transition', async () => {
|
||||||
|
useInstanceStore.setState({ instances: [inst('https://b.example', 'connected')] });
|
||||||
|
useInstanceStore.getState().setInstanceStatus('https://b.example', 'error');
|
||||||
|
await vi.waitFor(() => expect(mockFailover).toHaveBeenCalledExactlyOnceWith('https://b.example'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not fire on connecting → connected', () => {
|
||||||
|
useInstanceStore.setState({ instances: [inst('https://b.example', 'connecting')] });
|
||||||
|
useInstanceStore.getState().setInstanceStatus('https://b.example', 'connected');
|
||||||
|
expect(mockFailover).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not fire on disconnected → error (no connected source)', () => {
|
||||||
|
useInstanceStore.setState({ instances: [inst('https://b.example', 'disconnected')] });
|
||||||
|
useInstanceStore.getState().setInstanceStatus('https://b.example', 'error');
|
||||||
|
expect(mockFailover).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not fire when instance is not in the list', () => {
|
||||||
|
useInstanceStore.getState().setInstanceStatus('https://unknown.example', 'disconnected');
|
||||||
|
expect(mockFailover).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -428,11 +428,19 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
setInstanceStatus: (origin, status, error) => {
|
setInstanceStatus: (origin, status, error) => {
|
||||||
|
const prev = get().instances.find(i => i.origin === origin)?.status;
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
instances: state.instances.map(i =>
|
instances: state.instances.map(i =>
|
||||||
i.origin === origin ? { ...i, status, error } : i
|
i.origin === origin ? { ...i, status, error } : i
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
if (prev === 'connected' && (status === 'disconnected' || status === 'error')) {
|
||||||
|
// Dynamic import keeps the circular-dep-safe resolver pattern used elsewhere
|
||||||
|
// in this file. Fire-and-forget: failover reads state at call time.
|
||||||
|
import('../utils/dmOriginFailover').then(({ failoverDmOriginsFromDisconnected }) => {
|
||||||
|
failoverDmOriginsFromDisconnected(origin);
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
disconnectInstance: (origin: string) => {
|
disconnectInstance: (origin: string) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user