2026-03-02 18:34:19 -08:00

169 lines
5.1 KiB
TypeScript

import { EventEmitter } from "node:events";
import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("node:child_process", () => ({
spawn: vi.fn(),
}));
vi.mock("@/lib/workspace", () => ({
discoverProfiles: vi.fn(() => []),
getEffectiveProfile: vi.fn(() => "default"),
resolveWorkspaceRoot: vi.fn(() => null),
}));
type MockSpawnChild = EventEmitter & {
stdout: EventEmitter;
stderr: EventEmitter;
stdin: {
write: ReturnType<typeof vi.fn>;
end: ReturnType<typeof vi.fn>;
};
kill: ReturnType<typeof vi.fn>;
};
function mockSpawnResult(
spawnMock: {
mockImplementation: (
implementation: (...args: unknown[]) => unknown,
) => unknown;
},
params: {
code?: number;
stdout?: string;
stderr?: string;
emitError?: Error | null;
},
): { getChild: () => MockSpawnChild | null } {
let spawnedChild: MockSpawnChild | null = null;
spawnMock.mockImplementation(() => {
const child = new EventEmitter() as MockSpawnChild;
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
child.stdin = {
write: vi.fn(),
end: vi.fn(),
};
child.kill = vi.fn();
spawnedChild = child;
queueMicrotask(() => {
if (params.stdout) {
child.stdout.emit("data", Buffer.from(params.stdout));
}
if (params.stderr) {
child.stderr.emit("data", Buffer.from(params.stderr));
}
if (params.emitError) {
child.emit("error", params.emitError);
return;
}
child.emit("close", params.code ?? 0);
});
return child as unknown as ReturnType<typeof import("node:child_process").spawn>;
});
return {
getChild: () => spawnedChild,
};
}
describe("POST /api/workspace/delete", () => {
beforeEach(() => {
vi.resetModules();
vi.restoreAllMocks();
});
async function callDelete(body: Record<string, unknown>) {
const { POST } = await import("./route.js");
const req = new Request("http://localhost/api/workspace/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
return POST(req);
}
it("returns 400 for invalid profile names", async () => {
const response = await callDelete({ profile: "../bad" });
expect(response.status).toBe(400);
});
it("returns 404 when profile does not exist", async () => {
const workspace = await import("@/lib/workspace");
vi.mocked(workspace.discoverProfiles).mockReturnValue([]);
const response = await callDelete({ profile: "work" });
expect(response.status).toBe(404);
});
it("returns 409 when profile has no workspace directory", async () => {
const workspace = await import("@/lib/workspace");
vi.mocked(workspace.discoverProfiles).mockReturnValue([
{
name: "work",
stateDir: "/home/testuser/.openclaw-work",
workspaceDir: null,
isActive: false,
hasConfig: true,
},
]);
const response = await callDelete({ profile: "work" });
expect(response.status).toBe(409);
});
it("runs openclaw workspace delete for the selected profile", async () => {
const workspace = await import("@/lib/workspace");
const { spawn } = await import("node:child_process");
vi.mocked(workspace.discoverProfiles).mockReturnValue([
{
name: "work",
stateDir: "/home/testuser/.openclaw-work",
workspaceDir: "/home/testuser/.openclaw-work/workspace",
isActive: true,
hasConfig: true,
},
]);
vi.mocked(workspace.getEffectiveProfile).mockReturnValue("work");
vi.mocked(workspace.resolveWorkspaceRoot).mockReturnValue("/home/testuser/.openclaw-work/workspace");
const spawnResult = mockSpawnResult(vi.mocked(spawn), { code: 0 });
const response = await callDelete({ profile: "work" });
expect(response.status).toBe(200);
const json = await response.json();
expect(json.deleted).toBe(true);
expect(json.profile).toBe("work");
expect(spawn).toHaveBeenCalledWith(
"openclaw",
["--profile", "work", "workspace", "delete"],
expect.objectContaining({ stdio: ["pipe", "pipe", "pipe"] }),
);
const child = spawnResult.getChild();
expect(child).toBeTruthy();
expect(child?.stdin.write).toHaveBeenCalledWith("y\n");
expect(child?.stdin.write).toHaveBeenCalledWith("yes\n");
expect(child?.stdin.end).toHaveBeenCalled();
});
it("returns 501 when workspace delete command is unavailable", async () => {
const workspace = await import("@/lib/workspace");
const { spawn } = await import("node:child_process");
vi.mocked(workspace.discoverProfiles).mockReturnValue([
{
name: "work",
stateDir: "/home/testuser/.openclaw-work",
workspaceDir: "/home/testuser/.openclaw-work/workspace",
isActive: false,
hasConfig: true,
},
]);
mockSpawnResult(vi.mocked(spawn), {
code: 0,
stdout:
"Usage: openclaw [options] [command]\nHint: commands suffixed with * have subcommands",
});
const response = await callDelete({ profile: "work" });
expect(response.status).toBe(501);
});
});