Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions src/server/computer-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ import type { ComputerService } from './computer-service.js';
import type { ComputerAction } from '../shared/computer-types.js';
export function computerRoutes(computers: ComputerService) {
const app = new Hono();
app.onError((error, c) =>
c.json(
app.onError((error, c) => {
if (error instanceof SyntaxError)
return c.json({ error: 'Invalid JSON request.' }, 400);
const isValidation = error instanceof z.ZodError;
return c.json(
{
error:
error instanceof z.ZodError
? 'Invalid computer request.'
: error.message,
error: isValidation ? 'Invalid computer request.' : error.message,
},
400,
),
);
isValidation ? 400 : 503,
);
});
app.get('/dots/:id/computer', async (c) =>
c.json(await computers.status(c.req.param('id'))),
);
Expand Down
69 changes: 69 additions & 0 deletions tests/computer-routes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { afterEach, expect, it } from 'vitest';
import { WorkspaceStore } from '../src/server/workspace.js';
import { ComputerService } from '../src/server/computer-service.js';
import { computerRoutes } from '../src/server/computer-routes.js';

const stores: WorkspaceStore[] = [];
afterEach(() => {
for (const store of stores.splice(0)) store.close();
});

function fixture() {
const workspace = new WorkspaceStore(':memory:', 'owner');
stores.push(workspace);
const id = workspace.dots()[0].id;
const config = {
baseUrl: 'https://example.com',
voiceName: 'voice',
slackUsers: [],
runtimeUrl: 'http://localhost',
};
// No computer supervisor configured: `configured` is false, so any
// action that reaches `allowed()` fails with a plain domain error
// rather than attempting a network call.
const service = new ComputerService(workspace, config, () => false);
const app = computerRoutes(service);
return { workspace, id, app };
}

it('answers malformed JSON on the actions route with 400, not a service error', async () => {
const { app, id } = fixture();
const response = await app.request(`/dots/${id}/computer/actions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{',
});
expect(response.status).toBe(400);
expect(await response.json()).toEqual({ error: 'Invalid JSON request.' });
});

it('answers an invalid action shape with 400 from the Zod schema', async () => {
const { app, id } = fixture();
const response = await app.request(`/dots/${id}/computer/actions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input: {} }),
});
expect(response.status).toBe(400);
expect(await response.json()).toEqual({ error: 'Invalid computer request.' });
});

it('answers an unconfigured computer service with 503, not 400', async () => {
const { app, id } = fixture();
const response = await app.request(`/dots/${id}/computer/start`, {
method: 'POST',
});
expect(response.status).toBe(503);
expect(await response.json()).toEqual({
error: 'Computer service is not configured.',
});
});

it('answers an unknown Dot id with 503, carrying the domain error message', async () => {
const { app } = fixture();
const response = await app.request('/dots/missing/computer/start', {
method: 'POST',
});
expect(response.status).toBe(503);
expect(await response.json()).toEqual({ error: 'Dot not found.' });
});