Harden dashboard realtime state and controls

This commit is contained in:
jjbrunton committed 2026-08-08 15:41:58 +01:00
1 parent 4b3f00729f
commit d108903174
21 files changed
+422 -29

No files matched your search

+18
View File
@@ -57,6 +57,24 @@ describe('dashboard realtime client', () => {
expect(client.getSnapshot()).toMatchObject({ status: 'closed', isStale: true });
});
it('requests an authoritative repair on initial connection and every reconnect', () => {
const source = new FakeEventSource();
const connected = vi.fn();
const client = createDashboardRealtimeClient({
sourceFactory: () => source,
onEvent: vi.fn(),
onConnected: connected,
});
client.start();
source.emit('open');
source.emit('error');
source.emit('open');
expect(connected).toHaveBeenNthCalledWith(1, { isReconnect: false });
expect(connected).toHaveBeenNthCalledWith(2, { isReconnect: true });
});
it('maps events to the queries whose authoritative snapshots need refresh', () => {
expect(
queryKeysForDashboardEvent({
+38
View File
@@ -0,0 +1,38 @@
import { describe, expect, it, vi } from 'vitest';
import { createVisibilityAwarePollingFallback } from '../../apps/dashboard/src/features/operations/realtime-fallback.js';
describe('visibility-aware realtime fallback', () => {
it('polls only while stale and visible, including immediately on returning visible', () => {
let visible = false;
let interval: (() => void) | undefined;
let visibilityListener: (() => void) | undefined;
const poll = vi.fn();
const fallback = createVisibilityAwarePollingFallback({
intervalMs: 5_000,
poll,
scheduler: {
setInterval(callback) { interval = callback; return 'timer'; },
clearInterval: vi.fn(),
},
visibility: {
isVisible: () => visible,
subscribe(listener) { visibilityListener = listener; return vi.fn(); },
},
});
fallback.start();
fallback.setStale(true);
interval?.();
expect(poll).not.toHaveBeenCalled();
visible = true;
visibilityListener?.();
expect(poll).toHaveBeenCalledOnce();
interval?.();
expect(poll).toHaveBeenCalledTimes(2);
fallback.setStale(false);
interval?.();
expect(poll).toHaveBeenCalledTimes(2);
});
});
+13
View File
@@ -0,0 +1,13 @@
import { describe, expect, it, vi } from 'vitest';
import { repairRealtimeSnapshots } from '../../apps/dashboard/src/app/realtime-repair.js';
describe('authoritative realtime repair', () => {
it('marks snapshots stale and explicitly refetches active observers', async () => {
const invalidateQueries = vi.fn().mockResolvedValue(undefined);
const refetchQueries = vi.fn().mockResolvedValue(undefined);
await repairRealtimeSnapshots({ invalidateQueries, refetchQueries });
expect(invalidateQueries).toHaveBeenCalled();
expect(refetchQueries).toHaveBeenCalled();
expect(refetchQueries).toHaveBeenCalledWith(expect.objectContaining({ type: 'active' }));
});
});
@@ -0,0 +1,10 @@
// @vitest-environment jsdom
import { describe, expect, it } from 'vitest';
import { createDashboardRoutes } from '../../apps/dashboard/src/app/router.js';
describe('dashboard route error boundary', () => {
it('protects the application shell from route rendering failures', () => {
const root = createDashboardRoutes()[0];
expect(root?.errorElement).toBeTruthy();
});
});
+22
View File
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest';
import { deriveShellStatus } from '../../apps/dashboard/src/app/shell-status.js';
describe('global shell status', () => {
it('derives safety-critical labels from authoritative snapshots', () => {
expect(deriveShellStatus({
stats: { isRunning: true, dryRunMode: true, invitationCooldown: { reason: 'Reddit cooldown' } },
reddit: { ready: true, loggedIn: true, status: 'authenticated' },
realtime: { status: 'reconnecting', isStale: true },
sync: { syncing: true, syncingKind: 'full' },
})).toEqual({
bot: 'Running (dry run)', reddit: 'Reddit authenticated', realtime: 'Realtime reconnecting (stale)',
cooldown: 'Reddit cooldown', sync: 'Full sync active', controlAction: 'stop', controlDisabled: false,
});
});
it('guards bot controls until the authoritative bot state is available', () => {
expect(deriveShellStatus({ realtime: { status: 'idle', isStale: true } })).toMatchObject({
bot: 'Bot state unavailable', controlDisabled: true,
});
});
});
+39
View File
@@ -0,0 +1,39 @@
import { describe, expect, it } from 'vitest';
import { RealtimeEventHub } from '../../../src/web/events/realtime-event-hub.js';
import { createDashboardEventAdapters } from '../../../src/web/events/event-adapters.js';
import { createDashboardEventPublisher } from '../../../src/web/events/event-publisher.js';
describe('dashboard event adapters', () => {
it('provides a transport-neutral typed publisher over the event hub', () => {
const hub = new RealtimeEventHub({ capacity: 2 });
const publisher = createDashboardEventPublisher(hub);
const event = publisher.publish('bot.status.changed', { status: 'running', dryRun: false });
expect(event.type).toBe('bot.status.changed');
expect(hub.recent()).toEqual([event]);
});
it('translates every route and service event family to typed dashboard events', () => {
const hub = new RealtimeEventHub({ capacity: 20 });
const adapter = createDashboardEventAdapters(hub);
adapter.bot({ running: true, dryRun: true });
adapter.messageSent({ id: 'm1', username: 'alice', conversationId: 'c1' });
adapter.messageFailed({ username: 'bob', error: new Error('blocked'), retryable: false });
adapter.conversation({ id: 'c1', state: 'awaiting_reply', unread: 2 });
adapter.reply({ conversationId: 'c1', id: 'r1', username: 'alice', body: 'hello' });
adapter.sync({ phase: 'started', id: 's1', kind: 'delta' });
adapter.sync({ phase: 'progress', id: 's1', kind: 'delta', current: 2, total: 3 });
adapter.sync({ phase: 'completed', id: 's1', kind: 'delta', processed: 3 });
adapter.sync({ phase: 'failed', id: 's2', kind: 'media', error: 'network' });
adapter.rateLimit({ blocked: true, remaining: 0, resetAt: 1_786_176_000_000 });
adapter.redditSession({ state: 'login-required' });
adapter.log({ level: 'warn', message: 'careful' });
expect(hub.recent().map((event) => event.type)).toEqual([
'bot.status.changed', 'message.sent', 'message.failed', 'conversation.updated', 'reply.received',
'sync.started', 'sync.progress', 'sync.completed', 'sync.failed', 'rate-limit.changed',
'reddit.session.changed', 'log.created',
]);
expect(hub.recent()[2]?.payload).toMatchObject({ recipient: 'bob', reason: 'blocked' });
expect(hub.recent()[3]?.payload).toMatchObject({ status: 'awaiting-reply', unreadCount: 2 });
expect(hub.recent()[10]?.payload).toEqual({ status: 'logged-out' });
});
});