test(web): update profiles API tests for gateway metadata and switch validation
This commit is contained in:
parent
d4a2e445ca
commit
d8e2d455b8
@ -1,4 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { join } from "node:path";
|
||||
import type { Dirent } from "node:fs";
|
||||
|
||||
vi.mock("node:fs", () => ({
|
||||
@ -7,6 +8,7 @@ vi.mock("node:fs", () => ({
|
||||
readdirSync: vi.fn(() => []),
|
||||
writeFileSync: vi.fn(),
|
||||
mkdirSync: vi.fn(),
|
||||
renameSync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("node:child_process", () => ({
|
||||
@ -26,26 +28,26 @@ vi.mock("node:os", () => ({
|
||||
homedir: vi.fn(() => "/home/testuser"),
|
||||
}));
|
||||
|
||||
import { join } from "node:path";
|
||||
|
||||
function makeDirent(name: string, isDir: boolean): Dirent {
|
||||
return {
|
||||
name,
|
||||
isDirectory: () => isDir,
|
||||
isFile: () => !isDir,
|
||||
isBlockDevice: () => false,
|
||||
isCharacterDevice: () => false,
|
||||
isFIFO: () => false,
|
||||
isSocket: () => false,
|
||||
isSymbolicLink: () => false,
|
||||
path: "",
|
||||
parentPath: "",
|
||||
} as Dirent;
|
||||
}
|
||||
|
||||
describe("profiles API", () => {
|
||||
const originalEnv = { ...process.env };
|
||||
const STATE_DIR = join("/home/testuser", ".openclaw");
|
||||
const DEFAULT_STATE_DIR = join("/home/testuser", ".openclaw");
|
||||
const WORK_STATE_DIR = join("/home/testuser", ".openclaw-work");
|
||||
const WORK_WORKSPACE_DIR = join(WORK_STATE_DIR, "workspace");
|
||||
|
||||
function makeDirent(name: string, isDir: boolean): Dirent {
|
||||
return {
|
||||
name,
|
||||
isDirectory: () => isDir,
|
||||
isFile: () => !isDir,
|
||||
isBlockDevice: () => false,
|
||||
isCharacterDevice: () => false,
|
||||
isFIFO: () => false,
|
||||
isSocket: () => false,
|
||||
isSymbolicLink: () => false,
|
||||
path: "",
|
||||
parentPath: "",
|
||||
} as Dirent;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
@ -55,160 +57,149 @@ describe("profiles API", () => {
|
||||
delete process.env.OPENCLAW_HOME;
|
||||
delete process.env.OPENCLAW_WORKSPACE;
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
|
||||
vi.mock("node:fs", () => ({
|
||||
existsSync: vi.fn(() => false),
|
||||
readFileSync: vi.fn(() => ""),
|
||||
readdirSync: vi.fn(() => []),
|
||||
writeFileSync: vi.fn(),
|
||||
mkdirSync: vi.fn(),
|
||||
}));
|
||||
vi.mock("node:child_process", () => ({
|
||||
execSync: vi.fn(() => ""),
|
||||
exec: vi.fn(
|
||||
(
|
||||
_cmd: string,
|
||||
_opts: unknown,
|
||||
cb: (err: Error | null, result: { stdout: string }) => void,
|
||||
) => {
|
||||
cb(null, { stdout: "" });
|
||||
},
|
||||
),
|
||||
}));
|
||||
vi.mock("node:os", () => ({
|
||||
homedir: vi.fn(() => "/home/testuser"),
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
// ─── GET /api/profiles ────────────────────────────────────────────
|
||||
it("lists discovered profiles and includes gateway metadata", async () => {
|
||||
process.env.OPENCLAW_PROFILE = "work";
|
||||
const { existsSync, readFileSync, readdirSync } = await import("node:fs");
|
||||
const mockExists = vi.mocked(existsSync);
|
||||
const mockReadFile = vi.mocked(readFileSync);
|
||||
const mockReaddir = vi.mocked(readdirSync);
|
||||
|
||||
describe("GET /api/profiles", () => {
|
||||
async function callGet() {
|
||||
const { GET } = await import("./route.js");
|
||||
return GET();
|
||||
}
|
||||
|
||||
it("returns profiles list with default profile", async () => {
|
||||
const response = await callGet();
|
||||
expect(response.status).toBe(200);
|
||||
const json = await response.json();
|
||||
expect(json.profiles).toBeDefined();
|
||||
expect(json.profiles.length).toBeGreaterThanOrEqual(1);
|
||||
expect(json.profiles[0].name).toBe("default");
|
||||
mockReaddir.mockReturnValue([
|
||||
makeDirent(".openclaw-work", true),
|
||||
makeDirent("Documents", true),
|
||||
] as unknown as Dirent[]);
|
||||
mockExists.mockImplementation((p) => {
|
||||
const s = String(p);
|
||||
return (
|
||||
s === DEFAULT_STATE_DIR ||
|
||||
s === join(DEFAULT_STATE_DIR, "openclaw.json") ||
|
||||
s === WORK_WORKSPACE_DIR ||
|
||||
s === join(WORK_STATE_DIR, "openclaw.json")
|
||||
);
|
||||
});
|
||||
mockReadFile.mockImplementation((p) => {
|
||||
const s = String(p);
|
||||
if (s === join(WORK_STATE_DIR, "openclaw.json")) {
|
||||
return JSON.stringify({ gateway: { mode: "local", port: 19001 } }) as never;
|
||||
}
|
||||
if (s === join(DEFAULT_STATE_DIR, "openclaw.json")) {
|
||||
return JSON.stringify({ gateway: { mode: "local", port: 18789 } }) as never;
|
||||
}
|
||||
return "" as never;
|
||||
});
|
||||
|
||||
it("returns activeProfile", async () => {
|
||||
const response = await callGet();
|
||||
const json = await response.json();
|
||||
expect(json.activeProfile).toBe("default");
|
||||
const { GET } = await import("./route.js");
|
||||
const response = await GET();
|
||||
expect(response.status).toBe(200);
|
||||
const json = await response.json();
|
||||
|
||||
expect(json.activeProfile).toBe("work");
|
||||
const work = json.profiles.find((p: { name: string }) => p.name === "work");
|
||||
const def = json.profiles.find((p: { name: string }) => p.name === "default");
|
||||
expect(work).toMatchObject({
|
||||
name: "work",
|
||||
stateDir: WORK_STATE_DIR,
|
||||
workspaceDir: WORK_WORKSPACE_DIR,
|
||||
isActive: true,
|
||||
hasConfig: true,
|
||||
gateway: {
|
||||
mode: "local",
|
||||
port: 19001,
|
||||
url: "ws://127.0.0.1:19001",
|
||||
},
|
||||
});
|
||||
|
||||
it("returns stateDir", async () => {
|
||||
const response = await callGet();
|
||||
const json = await response.json();
|
||||
expect(json.stateDir).toBe(STATE_DIR);
|
||||
});
|
||||
|
||||
it("discovers workspace-<name> directories", async () => {
|
||||
const { existsSync: es, readdirSync: rds } = await import("node:fs");
|
||||
const devStateDir = join("/home/testuser", ".openclaw-dev");
|
||||
const devWorkspaceDir = join(devStateDir, "workspace");
|
||||
vi.mocked(es).mockImplementation((p) => {
|
||||
const s = String(p);
|
||||
return (
|
||||
s === STATE_DIR ||
|
||||
s === devWorkspaceDir
|
||||
);
|
||||
});
|
||||
vi.mocked(rds).mockReturnValue([
|
||||
makeDirent(".openclaw-dev", true),
|
||||
] as unknown as Dirent[]);
|
||||
|
||||
const response = await callGet();
|
||||
const json = await response.json();
|
||||
const names = json.profiles.map((p: { name: string }) => p.name);
|
||||
expect(names).toContain("dev");
|
||||
expect(def).toMatchObject({
|
||||
name: "default",
|
||||
stateDir: DEFAULT_STATE_DIR,
|
||||
isActive: false,
|
||||
hasConfig: true,
|
||||
gateway: {
|
||||
mode: "local",
|
||||
port: 18789,
|
||||
url: "ws://127.0.0.1:18789",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// ─── POST /api/profiles/switch ────────────────────────────────────
|
||||
|
||||
describe("POST /api/profiles/switch", () => {
|
||||
async function callSwitch(body: Record<string, unknown>) {
|
||||
const { POST } = await import("./switch/route.js");
|
||||
const req = new Request("http://localhost/api/profiles/switch", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return POST(req);
|
||||
}
|
||||
|
||||
it("switches to named profile", async () => {
|
||||
const { writeFileSync: wfs } = await import("node:fs");
|
||||
const { existsSync: es } = await import("node:fs");
|
||||
vi.mocked(es).mockReturnValue(true);
|
||||
|
||||
const response = await callSwitch({ profile: "work" });
|
||||
expect(response.status).toBe(200);
|
||||
const json = await response.json();
|
||||
expect(json.activeProfile).toBe("work");
|
||||
|
||||
const writeCalls = vi.mocked(wfs).mock.calls;
|
||||
const stateWrite = writeCalls.find((c) =>
|
||||
(c[0] as string).includes(".ironclaw-ui-state.json"),
|
||||
);
|
||||
expect(stateWrite).toBeDefined();
|
||||
it("switches to an existing profile", async () => {
|
||||
const { existsSync, readdirSync, readFileSync } = await import("node:fs");
|
||||
const mockExists = vi.mocked(existsSync);
|
||||
const mockReaddir = vi.mocked(readdirSync);
|
||||
const mockReadFile = vi.mocked(readFileSync);
|
||||
mockReadFile.mockImplementation(() => {
|
||||
throw new Error("ENOENT");
|
||||
});
|
||||
mockReaddir.mockReturnValue([makeDirent(".openclaw-work", true)] as unknown as Dirent[]);
|
||||
mockExists.mockImplementation((p) => {
|
||||
const s = String(p);
|
||||
return s === WORK_WORKSPACE_DIR;
|
||||
});
|
||||
|
||||
it("'default' clears the override", async () => {
|
||||
const { existsSync: es } = await import("node:fs");
|
||||
vi.mocked(es).mockReturnValue(true);
|
||||
|
||||
const response = await callSwitch({ profile: "default" });
|
||||
expect(response.status).toBe(200);
|
||||
const json = await response.json();
|
||||
expect(json.activeProfile).toBe("default");
|
||||
const { POST } = await import("./switch/route.js");
|
||||
const req = new Request("http://localhost/api/profiles/switch", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ profile: "work" }),
|
||||
});
|
||||
|
||||
it("rejects missing profile name", async () => {
|
||||
const response = await callSwitch({});
|
||||
expect(response.status).toBe(400);
|
||||
const json = await response.json();
|
||||
expect(json.error).toContain("Missing profile name");
|
||||
const response = await POST(req);
|
||||
expect(response.status).toBe(200);
|
||||
const json = await response.json();
|
||||
expect(json.activeProfile).toBe("work");
|
||||
expect(json.stateDir).toBe(WORK_STATE_DIR);
|
||||
expect(json.workspaceRoot).toBe(WORK_WORKSPACE_DIR);
|
||||
expect(json.profile.name).toBe("work");
|
||||
});
|
||||
|
||||
it("rejects invalid switch profile names", async () => {
|
||||
const { POST } = await import("./switch/route.js");
|
||||
const req = new Request("http://localhost/api/profiles/switch", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ profile: "../bad" }),
|
||||
});
|
||||
const response = await POST(req);
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it("rejects invalid profile name characters", async () => {
|
||||
const response = await callSwitch({ profile: "bad name!" });
|
||||
expect(response.status).toBe(400);
|
||||
const json = await response.json();
|
||||
expect(json.error).toContain("Invalid profile name");
|
||||
it("returns 404 when switching to an unknown profile", async () => {
|
||||
const { readdirSync, readFileSync } = await import("node:fs");
|
||||
const mockReaddir = vi.mocked(readdirSync);
|
||||
const mockReadFile = vi.mocked(readFileSync);
|
||||
mockReadFile.mockImplementation(() => {
|
||||
throw new Error("ENOENT");
|
||||
});
|
||||
mockReaddir.mockReturnValue([] as unknown as Dirent[]);
|
||||
|
||||
it("returns workspace root after switching", async () => {
|
||||
const { existsSync: es } = await import("node:fs");
|
||||
const wsDir = join(STATE_DIR, "workspace-dev");
|
||||
vi.mocked(es).mockImplementation((p) => {
|
||||
const s = String(p);
|
||||
return s === wsDir || s.includes(".openclaw");
|
||||
});
|
||||
|
||||
const response = await callSwitch({ profile: "dev" });
|
||||
const json = await response.json();
|
||||
expect(json.workspaceRoot).toBeDefined();
|
||||
const { POST } = await import("./switch/route.js");
|
||||
const req = new Request("http://localhost/api/profiles/switch", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ profile: "work" }),
|
||||
});
|
||||
const response = await POST(req);
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
it("returns stateDir in response", async () => {
|
||||
const { existsSync: es } = await import("node:fs");
|
||||
vi.mocked(es).mockReturnValue(true);
|
||||
it("returns 409 when OPENCLAW_PROFILE forces a different profile", async () => {
|
||||
process.env.OPENCLAW_PROFILE = "ironclaw";
|
||||
const { readdirSync } = await import("node:fs");
|
||||
const mockReaddir = vi.mocked(readdirSync);
|
||||
mockReaddir.mockReturnValue([makeDirent(".openclaw-work", true)] as unknown as Dirent[]);
|
||||
|
||||
const response = await callSwitch({ profile: "test" });
|
||||
const json = await response.json();
|
||||
expect(json.stateDir).toBe(join("/home/testuser", ".openclaw-test"));
|
||||
const { POST } = await import("./switch/route.js");
|
||||
const req = new Request("http://localhost/api/profiles/switch", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ profile: "work" }),
|
||||
});
|
||||
const response = await POST(req);
|
||||
expect(response.status).toBe(409);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user