test(ui): cover operator administration flows (#118836)

This commit is contained in:
Vincent Koc
2026-08-04 08:14:58 +08:00
committed by GitHub
parent 2de88241a4
commit 67d67b5ca9
3 changed files with 445 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
title: Control UI agent-scoped exec approvals administration
scenario:
id: control-ui-exec-approvals-agents
surface: control-ui
category: control-ui.operator-console
coverage:
primary:
- control-ui.exec-approvals-agents
objective: >-
Prove an operator can select an agent's exec approval policy, edit it,
save with optimistic concurrency, and observe the applied Gateway state.
successCriteria:
- Chromium lists the configured agents and selects the reviewer approval scope.
- The operator edits security, prompt, fallback, skill CLI, and allowlist policy fields.
- Saving sends the complete reviewer policy with the loaded snapshot hash.
- The reloaded Gateway snapshot renders the applied values and a clean saved state.
docsRefs:
- docs/web/control-ui.md
- docs/help/testing.md
codeRefs:
- ui/src/e2e/operator-admin.e2e.test.ts
- ui/src/pages/nodes/view-exec-approvals.ts
- ui/src/lib/nodes/index.ts
execution:
kind: playwright
path: ui/src/e2e/operator-admin.e2e.test.ts
testNamePattern: edits, saves, and reapplies reviewer-scoped exec approvals
summary: >-
Real Chromium coverage for agent-scoped exec approval listing, editing,
hash-bound save, reload, and applied visible state.

View File

@@ -0,0 +1,31 @@
title: Control UI Skills and Nodes operator administration
scenario:
id: control-ui-skills-nodes
surface: control-ui
category: control-ui.operator-console
coverage:
primary:
- control-ui.skills-nodes
objective: >-
Prove an operator can administer agent-scoped Skills and inspect the
connected Nodes inventory through the Control UI.
successCriteria:
- Chromium switches Skills administration from the default agent to a named agent.
- A skill install action sends the named agent and installer identity through the Gateway.
- The post-install Gateway response renders a visible successful outcome.
- Chromium renders a connected node with its capabilities and commands.
docsRefs:
- docs/web/control-ui.md
- docs/help/testing.md
codeRefs:
- ui/src/e2e/operator-admin.e2e.test.ts
- ui/src/pages/skills/skills-page.ts
- ui/src/pages/nodes/nodes-page.ts
execution:
kind: playwright
path: ui/src/e2e/operator-admin.e2e.test.ts
testNamePattern: administers agent-scoped Skills and inspects the connected Nodes inventory
summary: >-
Real Chromium coverage for agent-scoped Skills requests, visible install
results, and connected Nodes inventory inspection.

View File

@@ -0,0 +1,383 @@
// Control UI E2E coverage for operator-facing Skills, Nodes, and exec approvals administration.
import { mkdir } from "node:fs/promises";
import path from "node:path";
import { chromium, type Browser, type BrowserContext, type Page } from "playwright";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import {
canRunPlaywrightChromium,
installMockGateway,
resolvePlaywrightChromiumExecutablePath,
startControlUiE2eServer,
type ControlUiE2eServer,
type MockGatewayControls,
type MockGatewayRequest,
} 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 captureUiProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
const proofDir = path.join(process.cwd(), ".artifacts", "control-ui-e2e", "operator-admin");
const viewport = { height: 960, width: 1440 };
let browser: Browser;
let server: ControlUiE2eServer;
const agentRoster = [
{ id: "main", identity: { name: "Main" }, name: "Main" },
{ id: "reviewer", identity: { name: "Reviewer" }, name: "Reviewer" },
];
const operatorConfig = {
agents: {
entries: {
main: { default: true, name: "Main" },
reviewer: { name: "Reviewer" },
},
},
};
function skillStatus(eligible: boolean) {
return {
workspaceDir: "/tmp/openclaw-e2e/workspace",
managedSkillsDir: "/tmp/openclaw-e2e/skills",
skills: [
{
name: "Deploy Helper",
description: "Prepare reviewed deployments.",
source: "openclaw-bundled",
bundled: true,
filePath: "/tmp/openclaw-e2e/skills/deploy-helper/SKILL.md",
baseDir: "/tmp/openclaw-e2e/skills/deploy-helper",
skillKey: "deploy-helper",
always: false,
disabled: false,
blockedByAllowlist: false,
blockedByAgentFilter: false,
eligible,
platformIncompatible: false,
modelVisible: eligible,
userInvocable: true,
commandVisible: eligible,
requirements: {
bins: ["deploy-helper"],
anyBins: [],
env: [],
config: [],
os: [],
},
missing: {
bins: eligible ? [] : ["deploy-helper"],
anyBins: [],
env: [],
config: [],
os: [],
},
configChecks: [],
install: [
{
id: "node-deploy-helper",
kind: "node",
label: "Install Deploy Helper",
bins: ["deploy-helper"],
},
],
},
],
};
}
function configResponse() {
const raw = JSON.stringify(operatorConfig);
return {
config: operatorConfig,
sourceConfig: operatorConfig,
hash: "config-hash-1",
issues: [],
raw,
valid: true,
};
}
function requestParams(request: MockGatewayRequest): Record<string, unknown> {
if (!request.params || typeof request.params !== "object" || Array.isArray(request.params)) {
return {};
}
return request.params as Record<string, unknown>;
}
async function waitForRequest(
gateway: MockGatewayControls,
method: string,
predicate: (params: Record<string, unknown>) => boolean,
) {
await expect
.poll(async () =>
(await gateway.getRequests(method)).some((request) => predicate(requestParams(request))),
)
.toBe(true);
}
async function createContext(): Promise<BrowserContext> {
if (captureUiProof) {
await mkdir(proofDir, { recursive: true });
}
return browser.newContext({
locale: "en-US",
serviceWorkers: "block",
viewport,
...(captureUiProof ? { recordVideo: { dir: proofDir, size: viewport } } : {}),
});
}
async function screenshot(page: Page, name: string) {
if (!captureUiProof) {
return;
}
await page.screenshot({
animations: "disabled",
fullPage: true,
path: path.join(proofDir, name),
});
}
describeControlUiE2e("Control UI operator administration", () => {
beforeAll(async () => {
if (!chromiumAvailable) {
throw new Error(`Playwright Chromium is unavailable at ${chromiumExecutablePath}`);
}
server = await startControlUiE2eServer();
browser = await chromium.launch({ executablePath: chromiumExecutablePath });
});
afterAll(async () => {
await browser?.close();
await server?.close();
});
it("administers agent-scoped Skills and inspects the connected Nodes inventory", async () => {
const context = await createContext();
const page = await context.newPage();
const gateway = await installMockGateway(page, {
methodResponses: {
"agents.list": {
agents: agentRoster,
defaultId: "main",
mainKey: "main",
scope: "agent",
},
"config.get": configResponse(),
"device.pair.list": { paired: [], pending: [] },
"exec.approvals.get": {
path: "/tmp/openclaw-e2e/exec-approvals.json",
exists: true,
hash: "approval-hash-1",
file: {
defaults: {
security: "deny",
ask: "on-miss",
askFallback: "deny",
autoAllowSkills: false,
},
agents: {},
},
},
"node.list": {
nodes: [
{
nodeId: "build-node",
displayName: "Build Node",
platform: "linux",
version: "2026.8.3",
caps: ["browser", "filesystem"],
commands: ["system.run", "system.execApprovals.get", "system.execApprovals.set"],
connected: true,
paired: true,
},
],
},
"skills.install": { message: "Installed Deploy Helper" },
"skills.status": {
cases: [
{ match: { agentId: "reviewer" }, response: skillStatus(false) },
{ response: skillStatus(true) },
],
},
},
});
try {
const response = await page.goto(`${server.baseUrl}skills`);
expect(response?.status()).toBe(200);
await gateway.waitForRequest("skills.status");
const agentSelect = page.locator('openclaw-agent-select[name="skills-agent"]');
await agentSelect.locator(".agent-select__trigger").click();
await agentSelect
.locator("wa-dropdown-item[data-agent-option]")
.filter({ hasText: "Reviewer" })
.click();
await waitForRequest(gateway, "skills.status", (params) => params.agentId === "reviewer");
await expect
.poll(async () => (await agentSelect.locator(".agent-select__label").textContent())?.trim())
.toBe("Reviewer");
await page.getByRole("button", { name: "Open Deploy Helper details" }).click();
const dialog = page.locator("openclaw-modal-dialog", { hasText: "Deploy Helper" });
await expect
.poll(() => dialog.getByRole("button", { name: "Install Deploy Helper" }).isVisible())
.toBe(true);
await gateway.setMethodResponse("skills.status", skillStatus(true));
await dialog.getByRole("button", { name: "Install Deploy Helper" }).click();
const installRequest = await gateway.waitForRequest("skills.install");
expect(installRequest.params).toMatchObject({
agentId: "reviewer",
name: "Deploy Helper",
installId: "node-deploy-helper",
dangerouslyForceUnsafeInstall: false,
});
await expect.poll(() => dialog.getByText("Installed Deploy Helper").isVisible()).toBe(true);
await screenshot(page, "01-reviewer-skill-installed.png");
await page.goto(`${server.baseUrl}nodes`);
await Promise.all([
gateway.waitForRequest("node.list"),
gateway.waitForRequest("device.pair.list"),
gateway.waitForRequest("exec.approvals.get"),
]);
await expect.poll(() => page.getByText("Build Node", { exact: true }).isVisible()).toBe(true);
await expect.poll(() => page.getByText("connected", { exact: true }).isVisible()).toBe(true);
await page.getByText("Details", { exact: true }).click();
await expect
.poll(() => page.getByText(/Capabilities: browser, filesystem/).isVisible())
.toBe(true);
await expect
.poll(() =>
page
.getByText(
/Commands: system\.run, system\.execApprovals\.get, system\.execApprovals\.set/,
)
.isVisible(),
)
.toBe(true);
await screenshot(page, "02-connected-node-inventory.png");
} finally {
await context.close();
}
});
it("edits, saves, and reapplies reviewer-scoped exec approvals", async () => {
const context = await createContext();
const page = await context.newPage();
const initialApprovals = {
path: "/tmp/openclaw-e2e/exec-approvals.json",
exists: true,
hash: "approval-hash-1",
file: {
defaults: {
security: "deny",
ask: "on-miss",
askFallback: "deny",
autoAllowSkills: false,
},
agents: {
reviewer: {
security: "allowlist",
ask: "on-miss",
askFallback: "deny",
autoAllowSkills: false,
allowlist: [{ pattern: "/usr/bin/git" }],
},
},
},
};
const appliedApprovals = {
...initialApprovals,
hash: "approval-hash-2",
file: {
...initialApprovals.file,
agents: {
reviewer: {
security: "full",
ask: "always",
askFallback: "allowlist",
autoAllowSkills: true,
allowlist: [{ pattern: "/usr/bin/gh" }],
},
},
},
};
const gateway = await installMockGateway(page, {
methodResponses: {
"config.get": configResponse(),
"device.pair.list": { paired: [], pending: [] },
"exec.approvals.get": initialApprovals,
"exec.approvals.set": { ok: true },
"node.list": { nodes: [] },
},
});
try {
const response = await page.goto(`${server.baseUrl}nodes`);
expect(response?.status()).toBe(200);
await gateway.waitForRequest("exec.approvals.get");
const scopeSelect = page.locator("openclaw-agent-select.agent-select--settings");
await scopeSelect.locator(".agent-select__trigger").click();
await scopeSelect
.locator("wa-dropdown-item[data-agent-option]")
.filter({ hasText: "Reviewer (reviewer)" })
.click();
await expect
.poll(async () => (await scopeSelect.locator(".agent-select__label").textContent())?.trim())
.toBe("Reviewer (reviewer)");
const modeSelects = page.getByRole("combobox", { name: "Mode" });
await modeSelects.nth(0).selectOption("full");
await modeSelects.nth(1).selectOption("always");
await page.getByRole("combobox", { name: "Fallback" }).selectOption("allowlist");
const autoAllowSwitch = page
.locator(".settings-row", { hasText: "Auto-allow skill CLIs" })
.locator("wa-switch");
await autoAllowSwitch.click();
await page.getByRole("textbox", { name: "Pattern" }).fill("/usr/bin/gh");
await screenshot(page, "03-reviewer-approval-edits.png");
await gateway.setMethodResponse("exec.approvals.get", appliedApprovals);
const getRequestsBeforeSave = (await gateway.getRequests("exec.approvals.get")).length;
const approvalsSection = page.locator(".settings-section", { hasText: "Exec approvals" });
const saveButton = approvalsSection.getByRole("button", { name: "Save", exact: true });
await saveButton.click();
const saveRequest = await gateway.waitForRequest("exec.approvals.set");
expect(saveRequest.params).toEqual({
baseHash: "approval-hash-1",
file: appliedApprovals.file,
});
await expect
.poll(async () => (await gateway.getRequests("exec.approvals.get")).length)
.toBeGreaterThan(getRequestsBeforeSave);
await expect.poll(() => modeSelects.nth(0).inputValue()).toBe("full");
await expect.poll(() => modeSelects.nth(1).inputValue()).toBe("always");
await expect
.poll(() => page.getByRole("combobox", { name: "Fallback" }).inputValue())
.toBe("allowlist");
await expect
.poll(() =>
autoAllowSwitch.evaluate(
(element) => (element as HTMLElement & { checked: boolean }).checked,
),
)
.toBe(true);
await expect
.poll(() => page.getByRole("textbox", { name: "Pattern" }).inputValue())
.toBe("/usr/bin/gh");
await expect.poll(() => saveButton.isDisabled()).toBe(true);
expect(await page.getByRole("alert").count()).toBe(0);
await screenshot(page, "04-reviewer-approval-applied.png");
} finally {
await context.close();
}
});
});