mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-21 14:11:26 +00:00
Merged via squash.
Prepared head SHA: df8d953a14
Co-authored-by: hannasdev <4538260+hannasdev@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
237 lines
7.8 KiB
TypeScript
237 lines
7.8 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import type { OpenClawConfig } from "../config/config.js";
|
|
import { AVATAR_MAX_BYTES } from "../shared/avatar-policy.js";
|
|
import { resolveAgentAvatar } from "./identity-avatar.js";
|
|
|
|
async function writeFile(filePath: string, contents = "avatar") {
|
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
await fs.writeFile(filePath, contents, "utf-8");
|
|
}
|
|
|
|
async function expectLocalAvatarPath(
|
|
cfg: OpenClawConfig,
|
|
workspace: string,
|
|
expectedRelativePath: string,
|
|
opts?: Parameters<typeof resolveAgentAvatar>[2],
|
|
) {
|
|
const workspaceReal = await fs.realpath(workspace);
|
|
const resolved = resolveAgentAvatar(cfg, "main", opts);
|
|
expect(resolved.kind).toBe("local");
|
|
if (resolved.kind === "local") {
|
|
const resolvedReal = await fs.realpath(resolved.filePath);
|
|
expect(path.relative(workspaceReal, resolvedReal)).toBe(expectedRelativePath);
|
|
}
|
|
}
|
|
|
|
const tempRoots: string[] = [];
|
|
|
|
async function createTempAvatarRoot() {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-avatar-"));
|
|
tempRoots.push(root);
|
|
return root;
|
|
}
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
tempRoots
|
|
.splice(0, tempRoots.length)
|
|
.map((root) => fs.rm(root, { recursive: true, force: true })),
|
|
);
|
|
});
|
|
|
|
describe("resolveAgentAvatar", () => {
|
|
it("resolves local avatar from config when inside workspace", async () => {
|
|
const root = await createTempAvatarRoot();
|
|
const workspace = path.join(root, "work");
|
|
const avatarPath = path.join(workspace, "avatars", "main.png");
|
|
await writeFile(avatarPath);
|
|
|
|
const cfg: OpenClawConfig = {
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "main",
|
|
workspace,
|
|
identity: { avatar: "avatars/main.png" },
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
await expectLocalAvatarPath(cfg, workspace, path.join("avatars", "main.png"));
|
|
});
|
|
|
|
it("rejects avatars outside the workspace", async () => {
|
|
const root = await createTempAvatarRoot();
|
|
const workspace = path.join(root, "work");
|
|
await fs.mkdir(workspace, { recursive: true });
|
|
const outsidePath = path.join(root, "outside.png");
|
|
await writeFile(outsidePath);
|
|
|
|
const cfg: OpenClawConfig = {
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "main",
|
|
workspace,
|
|
identity: { avatar: outsidePath },
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const resolved = resolveAgentAvatar(cfg, "main");
|
|
expect(resolved.kind).toBe("none");
|
|
if (resolved.kind === "none") {
|
|
expect(resolved.reason).toBe("outside_workspace");
|
|
}
|
|
});
|
|
|
|
it("falls back to IDENTITY.md when config has no avatar", async () => {
|
|
const root = await createTempAvatarRoot();
|
|
const workspace = path.join(root, "work");
|
|
const avatarPath = path.join(workspace, "avatars", "fallback.png");
|
|
await writeFile(avatarPath);
|
|
await fs.mkdir(workspace, { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(workspace, "IDENTITY.md"),
|
|
"- Avatar: avatars/fallback.png\n",
|
|
"utf-8",
|
|
);
|
|
|
|
const cfg: OpenClawConfig = {
|
|
agents: {
|
|
list: [{ id: "main", workspace }],
|
|
},
|
|
};
|
|
|
|
await expectLocalAvatarPath(cfg, workspace, path.join("avatars", "fallback.png"));
|
|
});
|
|
|
|
it("returns missing for non-existent local avatar files", async () => {
|
|
const root = await createTempAvatarRoot();
|
|
const workspace = path.join(root, "work");
|
|
await fs.mkdir(workspace, { recursive: true });
|
|
|
|
const cfg: OpenClawConfig = {
|
|
agents: {
|
|
list: [{ id: "main", workspace, identity: { avatar: "avatars/missing.png" } }],
|
|
},
|
|
};
|
|
|
|
const resolved = resolveAgentAvatar(cfg, "main");
|
|
expect(resolved.kind).toBe("none");
|
|
if (resolved.kind === "none") {
|
|
expect(resolved.reason).toBe("missing");
|
|
}
|
|
});
|
|
|
|
it("rejects local avatars larger than max bytes", async () => {
|
|
const root = await createTempAvatarRoot();
|
|
const workspace = path.join(root, "work");
|
|
const avatarPath = path.join(workspace, "avatars", "too-big.png");
|
|
await fs.mkdir(path.dirname(avatarPath), { recursive: true });
|
|
await fs.writeFile(avatarPath, Buffer.alloc(AVATAR_MAX_BYTES + 1));
|
|
|
|
const cfg: OpenClawConfig = {
|
|
agents: {
|
|
list: [{ id: "main", workspace, identity: { avatar: "avatars/too-big.png" } }],
|
|
},
|
|
};
|
|
|
|
const resolved = resolveAgentAvatar(cfg, "main");
|
|
expect(resolved.kind).toBe("none");
|
|
if (resolved.kind === "none") {
|
|
expect(resolved.reason).toBe("too_large");
|
|
}
|
|
});
|
|
|
|
it("accepts remote and data avatars", () => {
|
|
const cfg: OpenClawConfig = {
|
|
agents: {
|
|
list: [
|
|
{ id: "main", identity: { avatar: "https://example.com/avatar.png" } },
|
|
{ id: "data", identity: { avatar: "data:image/png;base64,aaaa" } },
|
|
],
|
|
},
|
|
};
|
|
|
|
const remote = resolveAgentAvatar(cfg, "main");
|
|
expect(remote.kind).toBe("remote");
|
|
|
|
const data = resolveAgentAvatar(cfg, "data");
|
|
expect(data.kind).toBe("data");
|
|
});
|
|
|
|
it("resolves local avatar from ui.assistant.avatar when no agents.list identity is set", async () => {
|
|
const root = await createTempAvatarRoot();
|
|
const workspace = path.join(root, "work");
|
|
const avatarPath = path.join(workspace, "ui-avatar.png");
|
|
await writeFile(avatarPath);
|
|
|
|
const cfg: OpenClawConfig = {
|
|
ui: { assistant: { avatar: "ui-avatar.png" } },
|
|
agents: { list: [{ id: "main", workspace }] },
|
|
};
|
|
|
|
await expectLocalAvatarPath(cfg, workspace, "ui-avatar.png", { includeUiOverride: true });
|
|
});
|
|
|
|
it("ui.assistant.avatar ignored without includeUiOverride (outbound callers)", async () => {
|
|
const root = await createTempAvatarRoot();
|
|
const workspace = path.join(root, "work");
|
|
const uiAvatarPath = path.join(workspace, "ui-avatar.png");
|
|
const cfgAvatarPath = path.join(workspace, "cfg-avatar.png");
|
|
await writeFile(uiAvatarPath);
|
|
await writeFile(cfgAvatarPath);
|
|
|
|
const cfg: OpenClawConfig = {
|
|
ui: { assistant: { avatar: "ui-avatar.png" } },
|
|
agents: { list: [{ id: "main", workspace, identity: { avatar: "cfg-avatar.png" } }] },
|
|
};
|
|
|
|
// Without the opt-in, outbound callers get the per-agent identity avatar, not the UI override.
|
|
await expectLocalAvatarPath(cfg, workspace, "cfg-avatar.png");
|
|
});
|
|
|
|
it("ui.assistant.avatar takes priority over agents.list identity.avatar with includeUiOverride", async () => {
|
|
const root = await createTempAvatarRoot();
|
|
const workspace = path.join(root, "work");
|
|
const uiAvatarPath = path.join(workspace, "ui-avatar.png");
|
|
const cfgAvatarPath = path.join(workspace, "cfg-avatar.png");
|
|
await writeFile(uiAvatarPath);
|
|
await writeFile(cfgAvatarPath);
|
|
|
|
const cfg: OpenClawConfig = {
|
|
ui: { assistant: { avatar: "ui-avatar.png" } },
|
|
agents: { list: [{ id: "main", workspace, identity: { avatar: "cfg-avatar.png" } }] },
|
|
};
|
|
|
|
await expectLocalAvatarPath(cfg, workspace, "ui-avatar.png", { includeUiOverride: true });
|
|
});
|
|
|
|
it("ui.assistant.avatar takes priority over IDENTITY.md avatar with includeUiOverride", async () => {
|
|
const root = await createTempAvatarRoot();
|
|
const workspace = path.join(root, "work");
|
|
const uiAvatarPath = path.join(workspace, "ui-avatar.png");
|
|
const identityAvatarPath = path.join(workspace, "identity-avatar.png");
|
|
await writeFile(uiAvatarPath);
|
|
await writeFile(identityAvatarPath);
|
|
await fs.writeFile(
|
|
path.join(workspace, "IDENTITY.md"),
|
|
"- Avatar: identity-avatar.png\n",
|
|
"utf-8",
|
|
);
|
|
|
|
const cfg: OpenClawConfig = {
|
|
ui: { assistant: { avatar: "ui-avatar.png" } },
|
|
agents: { list: [{ id: "main", workspace }] },
|
|
};
|
|
|
|
await expectLocalAvatarPath(cfg, workspace, "ui-avatar.png", { includeUiOverride: true });
|
|
});
|
|
});
|