mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-03 16:21:37 +00:00
213 lines
7.4 KiB
TypeScript
213 lines
7.4 KiB
TypeScript
// Control UI tests cover memory engine ordering and serialized config writes.
|
|
import { mkdir } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { chromium, type Browser } from "playwright";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import {
|
|
canRunPlaywrightChromium,
|
|
installMockGateway,
|
|
resolvePlaywrightChromiumExecutablePath,
|
|
startControlUiE2eServer,
|
|
type ControlUiE2eServer,
|
|
} from "../test-helpers/control-ui-e2e.ts";
|
|
|
|
const chromiumExecutablePath = resolvePlaywrightChromiumExecutablePath(chromium.executablePath());
|
|
const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath);
|
|
const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1";
|
|
const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip;
|
|
const captureUiProofEnabled = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
|
const uiProofArtifactDir = path.join(
|
|
process.cwd(),
|
|
".artifacts",
|
|
"control-ui-e2e",
|
|
"memory-settings-engine",
|
|
);
|
|
|
|
let browser: Browser;
|
|
let server: ControlUiE2eServer;
|
|
|
|
function configResponse(engineId: string, hash: string) {
|
|
const config = { plugins: { slots: { memory: engineId } } };
|
|
return {
|
|
config,
|
|
hash,
|
|
appliedConfigHash: hash,
|
|
issues: [],
|
|
raw: JSON.stringify(config),
|
|
valid: true,
|
|
};
|
|
}
|
|
|
|
const memoryPlugins = [
|
|
{
|
|
id: "memory-lancedb",
|
|
name: "Memory LanceDB",
|
|
installed: true,
|
|
enabled: true,
|
|
state: "enabled",
|
|
kind: ["memory"],
|
|
},
|
|
{
|
|
id: "memory-core",
|
|
// The UI owns this product label; catalog ids/names are implementation detail.
|
|
name: "memory-core",
|
|
installed: true,
|
|
enabled: true,
|
|
state: "enabled",
|
|
kind: ["memory"],
|
|
},
|
|
];
|
|
|
|
describeControlUiE2e("Control UI memory engine settings mocked Gateway E2E", () => {
|
|
beforeAll(async () => {
|
|
if (!chromiumAvailable) {
|
|
throw new Error(
|
|
`Playwright Chromium is not available at ${chromiumExecutablePath}. Run \`pnpm --dir ui exec playwright install --with-deps chromium\`, or set OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM=1 only when intentionally skipping this lane.`,
|
|
);
|
|
}
|
|
server = await startControlUiE2eServer();
|
|
browser = await chromium.launch({ executablePath: chromiumExecutablePath });
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await browser?.close();
|
|
await server?.close();
|
|
});
|
|
|
|
it("keeps the default engine first and drains Off before selecting it", async () => {
|
|
const context = await browser.newContext({
|
|
colorScheme: "dark",
|
|
locale: "en-US",
|
|
serviceWorkers: "block",
|
|
viewport: { height: 900, width: 1440 },
|
|
});
|
|
const page = await context.newPage();
|
|
const initialConfig = configResponse("memory-lancedb", "memory-hash-1");
|
|
const selectedConfig = configResponse("memory-core", "memory-hash-2");
|
|
const gateway = await installMockGateway(page, {
|
|
methodResponses: {
|
|
"config.get": initialConfig,
|
|
"plugins.list": { plugins: memoryPlugins, diagnostics: [], mutationAllowed: true },
|
|
},
|
|
});
|
|
|
|
try {
|
|
const response = await page.goto(`${server.baseUrl}settings/memory/settings`);
|
|
expect(response?.status()).toBe(200);
|
|
|
|
const engineGroup = page.locator("wa-radio-group.settings-segmented").first();
|
|
await engineGroup.waitFor();
|
|
await expect
|
|
.poll(async () =>
|
|
(await engineGroup.locator("wa-radio").allTextContents()).map((label) => label.trim()),
|
|
)
|
|
.toEqual(["OpenClaw Memory", "Memory LanceDB", "Off"]);
|
|
|
|
await gateway.deferNext("config.set");
|
|
await gateway.deferNext("plugins.setEnabled");
|
|
await engineGroup.getByRole("radio", { name: "Off", exact: true }).click();
|
|
// Click again immediately, before the 800 ms autosave debounce. The
|
|
// write barrier must flush Off first instead of letting the plugin RPC
|
|
// race a still-scheduled config.set.
|
|
await engineGroup.getByRole("radio", { name: "OpenClaw Memory", exact: true }).click();
|
|
expect(await gateway.getRequests("plugins.setEnabled")).toHaveLength(0);
|
|
|
|
const pendingOffSave = await gateway.waitForRequest("config.set");
|
|
expect(pendingOffSave.params).toMatchObject({ baseHash: "memory-hash-1" });
|
|
if (captureUiProofEnabled) {
|
|
await mkdir(uiProofArtifactDir, { recursive: true });
|
|
await page
|
|
.locator(".settings-page > .settings-section")
|
|
.first()
|
|
.screenshot({
|
|
animations: "disabled",
|
|
path: path.join(uiProofArtifactDir, "00-off-write-draining.png"),
|
|
});
|
|
}
|
|
await gateway.resolveDeferred("config.set", { ok: true, hash: "mock-config-hash-1" });
|
|
const enableRequest = await gateway.waitForRequest("plugins.setEnabled");
|
|
expect(enableRequest.params).toEqual({ pluginId: "memory-core", enabled: true });
|
|
|
|
await gateway.setMethodResponse("config.get", selectedConfig);
|
|
await gateway.resolveDeferred("plugins.setEnabled", {
|
|
ok: true,
|
|
plugin: memoryPlugins[1],
|
|
restartRequired: false,
|
|
});
|
|
|
|
const selected = engineGroup.getByRole("radio", {
|
|
name: "OpenClaw Memory",
|
|
exact: true,
|
|
});
|
|
await expect.poll(() => selected.getAttribute("aria-checked")).toBe("true");
|
|
await expect.poll(() => page.getByText("Could not change the memory engine").count()).toBe(0);
|
|
|
|
if (captureUiProofEnabled) {
|
|
await mkdir(uiProofArtifactDir, { recursive: true });
|
|
await page
|
|
.locator(".settings-page > .settings-section")
|
|
.first()
|
|
.screenshot({
|
|
animations: "disabled",
|
|
path: path.join(uiProofArtifactDir, "01-openclaw-memory-selected.png"),
|
|
});
|
|
}
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
|
|
it("keeps a missing default engine labelled, first, and selected as unavailable", async () => {
|
|
const context = await browser.newContext({
|
|
colorScheme: "dark",
|
|
locale: "en-US",
|
|
serviceWorkers: "block",
|
|
viewport: { height: 900, width: 1440 },
|
|
});
|
|
const page = await context.newPage();
|
|
await installMockGateway(page, {
|
|
methodResponses: {
|
|
"config.get": configResponse("memory-core", "memory-hash-missing-default"),
|
|
"plugins.list": {
|
|
plugins: memoryPlugins.filter((plugin) => plugin.id !== "memory-core"),
|
|
diagnostics: [],
|
|
mutationAllowed: true,
|
|
},
|
|
},
|
|
});
|
|
|
|
try {
|
|
const response = await page.goto(`${server.baseUrl}settings/memory/settings`);
|
|
expect(response?.status()).toBe(200);
|
|
|
|
const engineGroup = page.locator("wa-radio-group.settings-segmented").first();
|
|
await engineGroup.waitFor();
|
|
await expect
|
|
.poll(async () =>
|
|
(await engineGroup.locator("wa-radio").allTextContents()).map((label) => label.trim()),
|
|
)
|
|
.toEqual(["OpenClaw Memory (Unavailable)", "Memory LanceDB", "Off"]);
|
|
await expect
|
|
.poll(() =>
|
|
engineGroup
|
|
.getByRole("radio", { name: "OpenClaw Memory (Unavailable)", exact: true })
|
|
.getAttribute("aria-checked"),
|
|
)
|
|
.toBe("true");
|
|
|
|
if (captureUiProofEnabled) {
|
|
await mkdir(uiProofArtifactDir, { recursive: true });
|
|
await page
|
|
.locator(".settings-page > .settings-section")
|
|
.first()
|
|
.screenshot({
|
|
animations: "disabled",
|
|
path: path.join(uiProofArtifactDir, "02-configured-engine-unavailable.png"),
|
|
});
|
|
}
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
});
|