openhands / src /mocks /workspaces-handlers.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
5f40163 verified
Raw History Blame Contribute Delete
3.9 kB
import { http, HttpResponse } from "msw";
import type {
WorkspaceItem,
WorkspaceParentItem,
WorkspacesListResponse,
} from "@openhands/typescript-client/clients";
/**
* MSW handlers for the agent-server's `/api/workspaces` and
* `/api/auth/workspace-session` endpoints used by `useLocalWorkspaces`,
* `WorkspacesService`, and `useWorkspaceSession`.
*
* Without these handlers, mock-mode renders of the home / sidebar /
* conversation pages let those requests fall through to the Vite proxy
* (`127.0.0.1:8000` ECONNREFUSED), which spams logs and surfaces
* global error toasts in mock-mode screens.
*
* The mock keeps an in-memory list so the install / remove flow round-trips
* cleanly within a single page load. State resets on every test reload.
*/
let workspaces: WorkspaceItem[] = [];
let workspaceParents: WorkspaceParentItem[] = [];
function snapshot(): WorkspacesListResponse {
return {
workspaces: workspaces.map((w) => ({ ...w })),
workspaceParents: workspaceParents.map((p) => ({ ...p })),
};
}
export function resetMockWorkspaces(): void {
workspaces = [];
workspaceParents = [];
}
export const WORKSPACES_HANDLERS = [
http.get("*/api/workspaces", async () => HttpResponse.json(snapshot())),
http.post("*/api/workspaces", async ({ request }) => {
// `workspaces` is the top-level key used by WorkspacesClient.addWorkspaces()
// in @openhands/typescript-client — aligns with the agent-server SDK contract.
const body = (await request.json()) as { workspaces?: WorkspaceItem[] };
for (const incoming of body.workspaces ?? []) {
const existingIndex = workspaces.findIndex(
(w) => w.path === incoming.path,
);
if (existingIndex >= 0) {
workspaces[existingIndex] = { ...incoming };
} else {
workspaces.push({ ...incoming });
}
}
return HttpResponse.json(snapshot());
}),
http.delete("*/api/workspaces", async ({ request }) => {
const url = new URL(request.url);
const path = url.searchParams.get("path");
const before = workspaces.length;
workspaces = workspaces.filter((w) => w.path !== path);
// WorkspacesService.removeWorkspace() discards the response body (returns
// void), so the shape here is mock-only and not coupled to the real contract.
return HttpResponse.json({ deleted: workspaces.length !== before });
}),
http.post("*/api/workspaces/parents", async ({ request }) => {
const body = (await request.json()) as { parents?: WorkspaceParentItem[] };
for (const incoming of body.parents ?? []) {
const existingIndex = workspaceParents.findIndex(
(p) => p.path === incoming.path,
);
if (existingIndex >= 0) {
workspaceParents[existingIndex] = { ...incoming };
} else {
workspaceParents.push({ ...incoming });
}
}
return HttpResponse.json(snapshot());
}),
http.delete("*/api/workspaces/parents", async ({ request }) => {
const url = new URL(request.url);
const path = url.searchParams.get("path");
const before = workspaceParents.length;
workspaceParents = workspaceParents.filter((p) => p.path !== path);
// WorkspacesService.removeWorkspaceParent() discards the response body
// (returns void), so the shape here is mock-only and not coupled to the
// real contract.
return HttpResponse.json({ deleted: workspaceParents.length !== before });
}),
// Workspace static-asset session: minted on conversation pages by
// `useWorkspaceSession`. The real endpoint sets an HttpOnly cookie; in
// mock mode we just acknowledge the POST/DELETE so the call doesn't fall
// through to the Vite proxy.
http.post("*/api/auth/workspace-session", async () =>
HttpResponse.json({ ok: true }),
),
http.delete(
"*/api/auth/workspace-session",
async () => new HttpResponse(null, { status: 204 }),
),
];