Files
openclaw/ui/src/e2e/memory-settings-engine.e2e.test.ts

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();
}
});
});