From 8c8ff11fd7b6811f14036333ecc7f9d9dbb274da Mon Sep 17 00:00:00 2001 From: Jamie Brunton Date: Sat, 8 Aug 2026 15:51:58 +0100 Subject: [PATCH] feat(web): publish bot control transitions --- src/web/routes/control.ts | 2 ++ tests/web/control-realtime.test.ts | 35 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 tests/web/control-realtime.test.ts diff --git a/src/web/routes/control.ts b/src/web/routes/control.ts index 48511b0..af4d4c5 100644 --- a/src/web/routes/control.ts +++ b/src/web/routes/control.ts @@ -8,6 +8,7 @@ export function createControlRouter(ctx: WebContext): Router { ctx.setBotRunning(true); ctx.activity.push('sent', 'Bot started by user'); ctx.logger.info('Bot started via web UI'); + ctx.broadcaster.dashboardEvents.bot({ running: true, dryRun: ctx.config.browser.dryRun }); res.json({ success: true, running: true }); }); @@ -15,6 +16,7 @@ export function createControlRouter(ctx: WebContext): Router { ctx.setBotRunning(false); ctx.activity.push('skipped', 'Bot stopped by user'); ctx.logger.info('Bot stopped via web UI'); + ctx.broadcaster.dashboardEvents.bot({ running: false, dryRun: ctx.config.browser.dryRun }); res.json({ success: true, running: false }); }); diff --git a/tests/web/control-realtime.test.ts b/tests/web/control-realtime.test.ts new file mode 100644 index 0000000..4981abb --- /dev/null +++ b/tests/web/control-realtime.test.ts @@ -0,0 +1,35 @@ +import express from 'express'; +import type { AddressInfo } from 'node:net'; +import { describe, expect, it, vi } from 'vitest'; +import type { WebContext } from '../../src/web/context.js'; +import { createControlRouter } from '../../src/web/routes/control.js'; + +describe('control route realtime events', () => { + it('publishes the authoritative bot state after each successful transition', async () => { + let running = false; + const bot = vi.fn(); + const context = { + config: { browser: { dryRun: true } }, + setBotRunning: (next: boolean) => { running = next; }, + activity: { push: vi.fn() }, + logger: { info: vi.fn() }, + broadcaster: { dashboardEvents: { bot } }, + } as unknown as WebContext; + const app = express().use('/api', createControlRouter(context)); + const server = app.listen(0); + await new Promise((resolve) => server.once('listening', resolve)); + const port = (server.address() as AddressInfo).port; + + try { + expect((await fetch(`http://127.0.0.1:${port}/api/start`, { method: 'POST' })).status).toBe(200); + expect(running).toBe(true); + expect(bot).toHaveBeenLastCalledWith({ running: true, dryRun: true }); + + expect((await fetch(`http://127.0.0.1:${port}/api/stop`, { method: 'POST' })).status).toBe(200); + expect(running).toBe(false); + expect(bot).toHaveBeenLastCalledWith({ running: false, dryRun: true }); + } finally { + await new Promise((resolve) => server.close(() => resolve())); + } + }); +});