Extract dashboard service routes (#72)
* review: harden readonly git checks and lint verification * test: satisfy readonly reviewer sandbox typing * test: fix readonly reviewer sandbox assertions * test: remove impossible readonly sandbox guards * test: relax readonly sandbox assertions * test: cast readonly sandbox expectation shape * test: cast readonly sandbox expectation through unknown * Phase 0 — STEP_DONE/TASK_DONE split + owner-follow-up integration smoke * Add STEP_DONE guards, verdict storage, and stale delivery suppression * style: format paired stepdone telemetry files * Route STEP_DONE through reviewer * Add structured Discord attachments * style: format structured attachment test * Fix room thread bot output parity * Remove duplicate work item attachment migration from owner branch * Remove legacy dashboard rooms renderer * Extract dashboard parsed body renderer * Extract dashboard redaction helpers * Extract dashboard RoomCardV2 component * Include RoomCardV2 test in vitest suite * Extract dashboard RoomBoardV2 component * Extract dashboard EmptyState component * Extract dashboard InboxPanel component * Split dashboard InboxPanel card renderer * Extract dashboard TaskPanel component * Extract dashboard UsagePanel component * Fix dashboard room thread chunk rendering * Extract dashboard ServicePanel component * Render dashboard live progress markdown * Extract dashboard SettingsPanel component * Fix structured attachment rendering * Extract dashboard simple route table * Extract dashboard settings routes * Extract dashboard service routes
This commit is contained in:
172
src/web-dashboard-service-routes.test.ts
Normal file
172
src/web-dashboard-service-routes.test.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
handleServiceRoute,
|
||||
type ServiceRestartRecord,
|
||||
} from './web-dashboard-service-routes.js';
|
||||
|
||||
function jsonResponse(value: unknown, init?: ResponseInit): Response {
|
||||
return new Response(JSON.stringify(value), {
|
||||
...init,
|
||||
headers: {
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
...(init?.headers as Record<string, string> | undefined),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function request(
|
||||
pathname: string,
|
||||
method = 'POST',
|
||||
body?: Record<string, unknown>,
|
||||
): Request {
|
||||
return new Request(`http://localhost${pathname}`, {
|
||||
method,
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
headers:
|
||||
body === undefined ? undefined : { 'content-type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
async function route({
|
||||
activeServiceRestartTargets = new Set<string>(),
|
||||
body,
|
||||
method,
|
||||
now,
|
||||
pathname,
|
||||
recentServiceRestarts = [],
|
||||
restartServiceStack = () => ['ejclaw'],
|
||||
}: {
|
||||
activeServiceRestartTargets?: Set<string>;
|
||||
body?: Record<string, unknown>;
|
||||
method?: string;
|
||||
now?: () => string;
|
||||
pathname: string;
|
||||
recentServiceRestarts?: ServiceRestartRecord[];
|
||||
restartServiceStack?: () => string[];
|
||||
}): Promise<Response | null> {
|
||||
return handleServiceRoute({
|
||||
url: new URL(`http://localhost${pathname}`),
|
||||
request: request(pathname, method, body),
|
||||
jsonResponse,
|
||||
recentServiceRestarts,
|
||||
activeServiceRestartTargets,
|
||||
restartServiceStack,
|
||||
now,
|
||||
});
|
||||
}
|
||||
|
||||
describe('web dashboard service routes', () => {
|
||||
it('handles stack restarts and duplicate request ids', async () => {
|
||||
const recentServiceRestarts: ServiceRestartRecord[] = [];
|
||||
let restartCalls = 0;
|
||||
|
||||
const restart = async () =>
|
||||
route({
|
||||
pathname: '/api/services/stack/actions',
|
||||
body: { action: 'restart', requestId: 'stack-restart-1' },
|
||||
recentServiceRestarts,
|
||||
restartServiceStack: () => {
|
||||
restartCalls += 1;
|
||||
return ['ejclaw', 'reviewer'];
|
||||
},
|
||||
now: () => '2026-04-28T09:20:00.000Z',
|
||||
});
|
||||
|
||||
const response = await restart();
|
||||
expect(response?.status).toBe(200);
|
||||
await expect(response?.json()).resolves.toMatchObject({
|
||||
ok: true,
|
||||
restart: {
|
||||
id: 'web-restart-stack-restart-1',
|
||||
target: 'stack',
|
||||
requestedAt: '2026-04-28T09:20:00.000Z',
|
||||
completedAt: '2026-04-28T09:20:00.000Z',
|
||||
status: 'success',
|
||||
services: ['ejclaw', 'reviewer'],
|
||||
},
|
||||
});
|
||||
expect(recentServiceRestarts).toHaveLength(1);
|
||||
expect(restartCalls).toBe(1);
|
||||
|
||||
const duplicate = await restart();
|
||||
expect(duplicate?.status).toBe(200);
|
||||
await expect(duplicate?.json()).resolves.toMatchObject({
|
||||
ok: true,
|
||||
duplicate: true,
|
||||
restart: {
|
||||
id: 'web-restart-stack-restart-1',
|
||||
status: 'success',
|
||||
},
|
||||
});
|
||||
expect(restartCalls).toBe(1);
|
||||
|
||||
const unmatched = await route({ pathname: '/api/overview' });
|
||||
expect(unmatched).toBeNull();
|
||||
});
|
||||
|
||||
it('rejects invalid service actions without restarting', async () => {
|
||||
const recentServiceRestarts: ServiceRestartRecord[] = [
|
||||
{
|
||||
id: 'web-restart-failed-request',
|
||||
target: 'stack',
|
||||
requestedAt: '2026-04-28T09:10:00.000Z',
|
||||
completedAt: '2026-04-28T09:10:01.000Z',
|
||||
status: 'failed',
|
||||
services: [],
|
||||
error: 'systemctl failed',
|
||||
},
|
||||
];
|
||||
let restartCalls = 0;
|
||||
const restartServiceStack = () => {
|
||||
restartCalls += 1;
|
||||
return ['ejclaw'];
|
||||
};
|
||||
|
||||
const active = await route({
|
||||
pathname: '/api/services/stack/actions',
|
||||
body: { action: 'restart' },
|
||||
activeServiceRestartTargets: new Set(['stack']),
|
||||
restartServiceStack,
|
||||
});
|
||||
expect(active?.status).toBe(409);
|
||||
|
||||
const invalidAction = await route({
|
||||
pathname: '/api/services/stack/actions',
|
||||
body: { action: 'stop' },
|
||||
restartServiceStack,
|
||||
});
|
||||
expect(invalidAction?.status).toBe(400);
|
||||
|
||||
const invalidTarget = await route({
|
||||
pathname: '/api/services/ejclaw/actions',
|
||||
body: { action: 'restart' },
|
||||
restartServiceStack,
|
||||
});
|
||||
expect(invalidTarget?.status).toBe(400);
|
||||
|
||||
const wrongMethod = await route({
|
||||
pathname: '/api/services/stack/actions',
|
||||
method: 'GET',
|
||||
restartServiceStack,
|
||||
});
|
||||
expect(wrongMethod?.status).toBe(405);
|
||||
|
||||
const failedDuplicate = await route({
|
||||
pathname: '/api/services/stack/actions',
|
||||
body: { action: 'restart', requestId: 'failed-request' },
|
||||
recentServiceRestarts,
|
||||
restartServiceStack,
|
||||
});
|
||||
expect(failedDuplicate?.status).toBe(500);
|
||||
await expect(failedDuplicate?.json()).resolves.toMatchObject({
|
||||
error: 'systemctl failed',
|
||||
duplicate: true,
|
||||
restart: {
|
||||
id: 'web-restart-failed-request',
|
||||
status: 'failed',
|
||||
},
|
||||
});
|
||||
expect(restartCalls).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user