diff --git a/src/gateway/assistant-identity.test.ts b/src/gateway/assistant-identity.test.ts new file mode 100644 index 000000000..5085708e5 --- /dev/null +++ b/src/gateway/assistant-identity.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; + +import type { ClawdbotConfig } from "../config/config.js"; +import { DEFAULT_ASSISTANT_IDENTITY, resolveAssistantIdentity } from "./assistant-identity.js"; + +describe("resolveAssistantIdentity avatar normalization", () => { + it("drops sentence-like avatar placeholders", () => { + const cfg: ClawdbotConfig = { + ui: { + assistant: { + avatar: "workspace-relative path, http(s) URL, or data URI", + }, + }, + }; + + expect(resolveAssistantIdentity({ cfg }).avatar).toBe(DEFAULT_ASSISTANT_IDENTITY.avatar); + }); + + it("keeps short text avatars", () => { + const cfg: ClawdbotConfig = { + ui: { + assistant: { + avatar: "PS", + }, + }, + }; + + expect(resolveAssistantIdentity({ cfg }).avatar).toBe("PS"); + }); + + it("keeps path avatars", () => { + const cfg: ClawdbotConfig = { + ui: { + assistant: { + avatar: "avatars/clawd.png", + }, + }, + }; + + expect(resolveAssistantIdentity({ cfg }).avatar).toBe("avatars/clawd.png"); + }); +}); diff --git a/src/gateway/assistant-identity.ts b/src/gateway/assistant-identity.ts index 72f521d12..35ff43490 100644 --- a/src/gateway/assistant-identity.ts +++ b/src/gateway/assistant-identity.ts @@ -26,6 +26,25 @@ function coerceIdentityValue(value: string | undefined, maxLength: number): stri return trimmed.slice(0, maxLength); } +function isAvatarUrl(value: string): boolean { + return /^https?:\/\//i.test(value) || /^data:image\//i.test(value); +} + +function looksLikeAvatarPath(value: string): boolean { + if (/[\\/]/.test(value)) return true; + return /\.(png|jpe?g|gif|webp|svg|ico)$/i.test(value); +} + +function normalizeAvatarValue(value: string | undefined): string | undefined { + if (!value) return undefined; + const trimmed = value.trim(); + if (!trimmed) return undefined; + if (isAvatarUrl(trimmed)) return trimmed; + if (looksLikeAvatarPath(trimmed)) return trimmed; + if (!/\s/.test(trimmed) && trimmed.length <= 4) return trimmed; + return undefined; +} + export function resolveAssistantIdentity(params: { cfg: ClawdbotConfig; agentId?: string | null; @@ -43,12 +62,15 @@ export function resolveAssistantIdentity(params: { coerceIdentityValue(fileIdentity?.name, MAX_ASSISTANT_NAME) ?? DEFAULT_ASSISTANT_IDENTITY.name; + const avatarCandidates = [ + coerceIdentityValue(configAssistant?.avatar, MAX_ASSISTANT_AVATAR), + coerceIdentityValue(agentIdentity?.avatar, MAX_ASSISTANT_AVATAR), + coerceIdentityValue(agentIdentity?.emoji, MAX_ASSISTANT_AVATAR), + coerceIdentityValue(fileIdentity?.avatar, MAX_ASSISTANT_AVATAR), + coerceIdentityValue(fileIdentity?.emoji, MAX_ASSISTANT_AVATAR), + ]; const avatar = - coerceIdentityValue(configAssistant?.avatar, MAX_ASSISTANT_AVATAR) ?? - coerceIdentityValue(agentIdentity?.avatar, MAX_ASSISTANT_AVATAR) ?? - coerceIdentityValue(agentIdentity?.emoji, MAX_ASSISTANT_AVATAR) ?? - coerceIdentityValue(fileIdentity?.avatar, MAX_ASSISTANT_AVATAR) ?? - coerceIdentityValue(fileIdentity?.emoji, MAX_ASSISTANT_AVATAR) ?? + avatarCandidates.map((candidate) => normalizeAvatarValue(candidate)).find(Boolean) ?? DEFAULT_ASSISTANT_IDENTITY.avatar; return { agentId, name, avatar };