fix(ui): surface agent config errors and hydrated identity avatars (#129868)

This commit is contained in:
Peter Steinberger
2026-08-25 21:48:28 -07:00
committed by GitHub
parent 0dabafe9f5
commit 43fb810d89
5 changed files with 103 additions and 11 deletions
+73
View File
@@ -18,6 +18,79 @@ const proofDir = path.join(process.cwd(), ".artifacts", "control-ui-e2e", "agent
const requireRecord = createRequireRecord("record", "expected-object-value");
suite.define(() => {
it("shows rejected initial configuration loads and recovers when reloaded", async () => {
await suite.withPage(
{ locale: "en-US", serviceWorkers: "block", viewport: { height: 900, width: 1280 } },
async ({ page }) => {
const config = { agents: { entries: { main: { default: true } } } };
const gateway = await installMockGateway(page, {
assistantName: "Main agent",
defaultAgentId: "main",
methodResponses: {
"agents.list": {
agents: [{ id: "main", name: "Main agent" }],
defaultId: "main",
mainKey: "main",
scope: "agent",
},
"config.get": {
__mockError: {
code: "INTERNAL_ERROR",
message: "Agent configuration unavailable; retry Reload Config",
retryable: true,
},
},
},
});
expect(
(await page.goto(`${suite.server.baseUrl}settings/agents/main/overview`))?.status(),
).toBe(200);
await gateway.waitForRequest("agents.list");
await gateway.waitForRequest("config.get");
const agentsPage = page.locator("openclaw-agents-page");
const reload = agentsPage.getByRole("button", { name: "Reload Config" });
await reload.waitFor();
if (captureUiProof) {
await mkdir(proofDir, { recursive: true });
await page.screenshot({
animations: "disabled",
fullPage: true,
path: path.join(
proofDir,
`agent-config-load-${process.env.OPENCLAW_UI_PROOF_LABEL ?? "failed"}.png`,
),
});
}
const error = agentsPage
.getByRole("alert")
.filter({ hasText: "Agent configuration unavailable" });
await expect.poll(() => error.isVisible()).toBe(true);
await expect
.poll(() => agentsPage.locator(".model-picker__select").getAttribute("disabled"))
.not.toBeNull();
await gateway.setMethodResponse("config.get", {
config,
sourceConfig: config,
runtimeConfig: config,
hash: "recovered-agent-config",
issues: [],
raw: JSON.stringify(config),
valid: true,
});
const readsBefore = (await gateway.getRequests("config.get")).length;
await reload.click();
await gateway.waitForRequest("config.get", { after: readsBefore });
await expect.poll(() => error.count()).toBe(0);
await expect
.poll(() => agentsPage.locator(".model-picker__select").getAttribute("disabled"))
.toBeNull();
},
);
});
it("submits keyed entries and surfaces Gateway validation failures", async () => {
await suite.withPage(
{
+25 -4
View File
@@ -164,11 +164,26 @@ suite.define(() => {
},
async ({ page }) => {
const config = { agents: { list: [{ id: "main" }, { id: "emoji" }] } };
const hydratedEmojiAgent = { id: "emoji", identity: { name: "Rocket" }, name: "Rocket" };
const gateway = await installMockGateway(page, {
defaultAgentId: "main",
methodResponses: {
"agent.identity.get": agentIdentities,
"agents.list": agentsList,
"agent.identity.get": {
cases: [
{
match: { agentId: "emoji" },
response: {
agentId: "emoji",
avatar: "",
avatarStatus: "none",
emoji: emojiGrapheme,
name: "Rocket",
},
},
agentIdentities.cases[1],
],
},
"agents.list": { ...agentsList, agents: [asciiAgent, hydratedEmojiAgent] },
"config.get": {
config,
sourceConfig: config,
@@ -186,16 +201,22 @@ suite.define(() => {
await gateway.waitForRequest("config.get");
const agentSelect = page.locator("openclaw-agents-page openclaw-agent-select");
await agentSelect.locator(".agent-select__trigger").click();
await agentSelect.getByRole("menuitemradio", { name: "🚀Rocket", exact: true }).click();
await agentSelect.getByRole("menuitemradio", { name: "Rocket", exact: true }).click();
await expect.poll(() => new URL(page.url()).pathname).toBe("/settings/agents/emoji/tools");
await page.getByRole("tab", { name: "Overview", exact: true }).click();
await expect
.poll(() => new URL(page.url()).pathname)
.toBe("/settings/agents/emoji/overview");
await expect
.poll(() => page.locator(".agent-identity-editor__emoji input").inputValue())
.toBe(emojiGrapheme);
await screenshot(
page,
`03-agents-overview-${process.env.OPENCLAW_UI_PROOF_LABEL ?? "emoji"}.png`,
);
await expect
.poll(() => page.locator(".agent-identity-editor__avatar-text").textContent())
.toBe(emojiGrapheme);
await screenshot(page, "03-agents-overview-emoji.png");
},
);
});
+1 -5
View File
@@ -952,11 +952,7 @@ class AgentsPage
loading: configState.configLoading,
saving: configState.configSaving,
dirty: configState.configFormDirty,
error:
configState.configAutoSaveStatus === "error" ||
configState.configAutoSaveStatus === "conflict"
? configState.lastError
: null,
error: configState.lastError,
},
channels: {
snapshot: this.context.channels.state.channelsSnapshot,
+2 -1
View File
@@ -107,7 +107,8 @@ export function renderAgentOverview(params: {
const identityAvatarUrl =
identityDraft.avatar ?? resolveAgentAvatarUrl(agent, params.agentIdentity);
const identityAvatarText =
resolveAgentTextAvatar(agent) ?? (deriveAvatarInitial(identityName || agent.id) || "?");
resolveAgentTextAvatar(agent, params.agentIdentity) ??
(deriveAvatarInitial(identityName || agent.id) || "?");
const identityDirty =
identityDraft.name !== null || identityDraft.emoji !== null || identityDraft.avatar !== null;
const identityInvalid =
+2 -1
View File
@@ -109,7 +109,7 @@ describe("renderAgents", () => {
renderAgents(
createProps({
agentIdentityById: {
beta: { agentId: "beta", name: "Fetched Beta", avatar: "" },
beta: { agentId: "beta", name: "Fetched Beta", avatar: "", emoji: "🦊" },
},
}),
),
@@ -119,6 +119,7 @@ describe("renderAgents", () => {
expect(
container.querySelector<HTMLInputElement>(".agent-identity-editor__fields input")?.value,
).toBe("Fetched Beta");
expect(container.querySelector(".agent-identity-editor__avatar-text")?.textContent).toBe("🦊");
});
it("shows a model-catalog failure and lets the operator retry", () => {