diff --git a/ui/src/e2e/agent-config-save.e2e.test.ts b/ui/src/e2e/agent-config-save.e2e.test.ts index 139a5c85263d..da6769a701e1 100644 --- a/ui/src/e2e/agent-config-save.e2e.test.ts +++ b/ui/src/e2e/agent-config-save.e2e.test.ts @@ -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( { diff --git a/ui/src/e2e/avatar-initial-emoji.e2e.test.ts b/ui/src/e2e/avatar-initial-emoji.e2e.test.ts index dc0ff75c41a3..d36bca1d04c9 100644 --- a/ui/src/e2e/avatar-initial-emoji.e2e.test.ts +++ b/ui/src/e2e/avatar-initial-emoji.e2e.test.ts @@ -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"); }, ); }); diff --git a/ui/src/pages/agents/agents-page.ts b/ui/src/pages/agents/agents-page.ts index af662f454439..9aa30dd755db 100644 --- a/ui/src/pages/agents/agents-page.ts +++ b/ui/src/pages/agents/agents-page.ts @@ -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, diff --git a/ui/src/pages/agents/panels-overview.ts b/ui/src/pages/agents/panels-overview.ts index ad617d0e7532..9f29424700b6 100644 --- a/ui/src/pages/agents/panels-overview.ts +++ b/ui/src/pages/agents/panels-overview.ts @@ -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 = diff --git a/ui/src/pages/agents/view.test.ts b/ui/src/pages/agents/view.test.ts index ce5831d3fcbd..53605530b79f 100644 --- a/ui/src/pages/agents/view.test.ts +++ b/ui/src/pages/agents/view.test.ts @@ -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(".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", () => {