File size: 3,901 Bytes
5f40163
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
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 }),
  ),
];