import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { api, resolveWsHost } from "./client"; const mockFetch = vi.fn(); beforeEach(() => { vi.stubGlobal("fetch", mockFetch); }); afterEach(() => { vi.restoreAllMocks(); }); function okResponse(body: unknown) { return new Response(JSON.stringify(body), { status: 200, headers: { "Content-Type": "application/json" }, }); } function errorResponse(status: number, text: string) { return new Response(text, { status }); } describe("api client", () => { describe("getCurrentProject", () => { it("sends GET to /project", async () => { mockFetch.mockResolvedValueOnce(okResponse("/home/user/project")); const result = await api.getCurrentProject(); expect(mockFetch).toHaveBeenCalledWith( "/api/project", expect.objectContaining({}), ); expect(result).toBe("/home/user/project"); }); it("returns null when no project open", async () => { mockFetch.mockResolvedValueOnce(okResponse(null)); const result = await api.getCurrentProject(); expect(result).toBeNull(); }); }); describe("openProject", () => { it("sends POST with path", async () => { mockFetch.mockResolvedValueOnce(okResponse("/home/user/project")); await api.openProject("/home/user/project"); expect(mockFetch).toHaveBeenCalledWith( "/api/project", expect.objectContaining({ method: "POST", body: JSON.stringify({ path: "/home/user/project" }), }), ); }); }); describe("closeProject", () => { it("sends DELETE to /project", async () => { mockFetch.mockResolvedValueOnce(okResponse(true)); await api.closeProject(); expect(mockFetch).toHaveBeenCalledWith( "/api/project", expect.objectContaining({ method: "DELETE" }), ); }); }); describe("getKnownProjects", () => { it("returns array of project paths", async () => { mockFetch.mockResolvedValueOnce(okResponse(["/a", "/b"])); const result = await api.getKnownProjects(); expect(result).toEqual(["/a", "/b"]); }); }); describe("error handling", () => { it("throws on non-ok response with body text", async () => { mockFetch.mockResolvedValueOnce(errorResponse(404, "Not found")); await expect(api.getCurrentProject()).rejects.toThrow("Not found"); }); it("throws with status code when no body", async () => { mockFetch.mockResolvedValueOnce(errorResponse(500, "")); await expect(api.getCurrentProject()).rejects.toThrow( "Request failed (500)", ); }); }); describe("searchFiles", () => { it("sends POST with query", async () => { mockFetch.mockResolvedValueOnce( okResponse([{ path: "src/main.rs", matches: 1 }]), ); const result = await api.searchFiles("hello"); expect(mockFetch).toHaveBeenCalledWith( "/api/fs/search", expect.objectContaining({ method: "POST", body: JSON.stringify({ query: "hello" }), }), ); expect(result).toHaveLength(1); }); }); describe("execShell", () => { it("sends POST with command and args", async () => { mockFetch.mockResolvedValueOnce( okResponse({ stdout: "output", stderr: "", exit_code: 0 }), ); const result = await api.execShell("ls", ["-la"]); expect(mockFetch).toHaveBeenCalledWith( "/api/shell/exec", expect.objectContaining({ method: "POST", body: JSON.stringify({ command: "ls", args: ["-la"] }), }), ); expect(result.exit_code).toBe(0); }); }); describe("resolveWsHost", () => { it("uses env port in dev mode", () => { expect(resolveWsHost(true, "4200", "example.com")).toBe("127.0.0.1:4200"); }); it("defaults to 3001 in dev mode when no env port", () => { expect(resolveWsHost(true, undefined, "example.com")).toBe( "127.0.0.1:3001", ); }); it("uses location host in production", () => { expect(resolveWsHost(false, "4200", "myapp.com:8080")).toBe( "myapp.com:8080", ); }); }); });