Add dashboard health restart action (#38)

This commit is contained in:
Eyejoker
2026-04-27 03:52:24 +09:00
committed by GitHub
parent 38c3430156
commit 76b0ab754b
6 changed files with 670 additions and 2 deletions

View File

@@ -1019,6 +1019,162 @@ describe('web dashboard server handler', () => {
expect(noQueue.status).toBe(503);
});
it('restarts the service stack through the health action endpoint', async () => {
let restartCalls = 0;
const handler = createWebDashboardHandler({
readStatusSnapshots: () => [],
getTasks: () => [],
getPairedTasks: () => [],
restartServiceStack: () => {
restartCalls += 1;
return ['ejclaw'];
},
now: () => '2026-04-26T05:30:00.000Z',
});
const restart = () =>
handler(
new Request('http://localhost/api/services/stack/actions', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
action: 'restart',
requestId: 'stack-restart-1',
}),
}),
);
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-26T05:30:00.000Z',
completedAt: '2026-04-26T05:30:00.000Z',
status: 'success',
services: ['ejclaw'],
},
});
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 overview = await handler(
new Request('http://localhost/api/overview'),
);
expect(overview.status).toBe(200);
await expect(overview.json()).resolves.toMatchObject({
operations: {
serviceRestarts: [
{
id: 'web-restart-stack-restart-1',
status: 'success',
services: ['ejclaw'],
},
],
},
});
});
it('records failed service stack restarts', async () => {
const handler = createWebDashboardHandler({
readStatusSnapshots: () => [],
getTasks: () => [],
getPairedTasks: () => [],
restartServiceStack: () => {
throw new Error('systemctl failed');
},
now: () => '2026-04-26T05:35:00.000Z',
});
const response = await handler(
new Request('http://localhost/api/services/stack/actions', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
action: 'restart',
requestId: 'stack-restart-fail',
}),
}),
);
expect(response.status).toBe(500);
await expect(response.json()).resolves.toMatchObject({
error: 'systemctl failed',
restart: {
id: 'web-restart-stack-restart-fail',
target: 'stack',
status: 'failed',
error: 'systemctl failed',
},
});
const overview = await handler(
new Request('http://localhost/api/overview'),
);
await expect(overview.json()).resolves.toMatchObject({
operations: {
serviceRestarts: [
{
id: 'web-restart-stack-restart-fail',
status: 'failed',
error: 'systemctl failed',
},
],
},
});
});
it('rejects invalid service restart requests', async () => {
let restartCalls = 0;
const handler = createWebDashboardHandler({
readStatusSnapshots: () => [],
getTasks: () => [],
getPairedTasks: () => [],
restartServiceStack: () => {
restartCalls += 1;
return ['ejclaw'];
},
});
const invalidAction = await handler(
new Request('http://localhost/api/services/stack/actions', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ action: 'stop' }),
}),
);
expect(invalidAction.status).toBe(400);
const invalidTarget = await handler(
new Request('http://localhost/api/services/ejclaw/actions', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ action: 'restart' }),
}),
);
expect(invalidTarget.status).toBe(400);
const wrongMethod = await handler(
new Request('http://localhost/api/services/stack/actions', {
method: 'GET',
}),
);
expect(wrongMethod.status).toBe(405);
expect(restartCalls).toBe(0);
});
it('injects room messages and queues room work from the web dashboard', async () => {
const messages: NewMessage[] = [];
const metadata: Array<{