update_bot takes a look (oo1: value), validated and normalized in core. A suggested look renders as a "New look" card with the avatar preview on web and mobile; accepting applies it to the bot. Claude-Session: https://claude.ai/code/session_01EKuME5CgL1ddGGN7LGvtBj Co-authored-by: Claude <noreply@anthropic.com>
135 lines
4.4 KiB
TypeScript
135 lines
4.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
DEFAULT_OO_AVATAR,
|
|
decodeOoAvatar,
|
|
encodeOoAvatar,
|
|
isValidAvatarValue,
|
|
normalizeOoLook,
|
|
OO_AVATAR_MAX_LENGTH,
|
|
OO_EYE_ASYMMETRY_MIN_RATIO,
|
|
OO_RIG_EYES,
|
|
type OoAvatar,
|
|
ooAvatarColor,
|
|
ooLensRadius,
|
|
ooLookFormat,
|
|
} from "./oo-avatar.js";
|
|
|
|
const styled: OoAvatar = {
|
|
species: "cat",
|
|
color: "#FF781C",
|
|
shape: null,
|
|
arms: "paw",
|
|
lashes: "full",
|
|
liner: "wing",
|
|
eyeshadow: "rose",
|
|
iris: "green",
|
|
sparkle: "heart",
|
|
brows: "fine",
|
|
cheeks: "freckles",
|
|
eyewear: "round",
|
|
head: "chef",
|
|
neck: "bell",
|
|
energy: 1.3,
|
|
playful: 0.8,
|
|
};
|
|
|
|
describe("oo avatar value", () => {
|
|
it("round-trips a fully styled look", () => {
|
|
const value = encodeOoAvatar(styled);
|
|
expect(value).toBe(
|
|
"oo1:c=FF781C;sp=cat;ar=paw;la=full;li=wing;es=rose;ir=green;sk=heart;br=fine;ck=freckles;ew=round;hd=chef;nk=bell;en=1.30;pl=0.80",
|
|
);
|
|
expect(decodeOoAvatar(value)).toEqual(styled);
|
|
});
|
|
|
|
it("writes only the colour for the default look", () => {
|
|
expect(encodeOoAvatar({ ...DEFAULT_OO_AVATAR, color: "#a97efe" })).toBe("oo1:c=A97EFE");
|
|
expect(encodeOoAvatar({ ...DEFAULT_OO_AVATAR, shape: 3 })).toBe("oo1:c=8B5CF6;sh=3");
|
|
});
|
|
|
|
it("clamps temperament and falls back to the default colour when encoding", () => {
|
|
const value = encodeOoAvatar({ ...DEFAULT_OO_AVATAR, color: "violet", energy: 9, playful: -1 });
|
|
expect(value).toBe("oo1:c=8B5CF6;en=1.60;pl=0.00");
|
|
expect(decodeOoAvatar(value)).toMatchObject({ energy: 1.6, playful: 0 });
|
|
});
|
|
|
|
it("reads legacy colour and shape values unchanged", () => {
|
|
expect(decodeOoAvatar("#8b5cf6::shape_3")).toEqual({
|
|
...DEFAULT_OO_AVATAR,
|
|
color: "#8B5CF6",
|
|
shape: 3,
|
|
});
|
|
expect(decodeOoAvatar("#10B981::shape_11")?.shape).toBe(3);
|
|
expect(decodeOoAvatar("#10B981::shape_x")?.shape).toBe(0);
|
|
expect(decodeOoAvatar("#fff")).toMatchObject({ color: "#FFFFFF", shape: null });
|
|
expect(decodeOoAvatar("violet")).toMatchObject({ color: "violet", shape: null });
|
|
});
|
|
|
|
it("treats uploaded pictures as no look", () => {
|
|
expect(decodeOoAvatar("data:image/webp;base64,AAAA")).toBeNull();
|
|
expect(ooAvatarColor("data:image/webp;base64,AAAA")).toBe("");
|
|
});
|
|
|
|
it("rejects malformed oo1 values", () => {
|
|
for (const value of [
|
|
"oo1:",
|
|
"oo1:sp=cat",
|
|
"oo1:c=XYZXYZ",
|
|
"oo1:c=FF781C;sp=dragon",
|
|
"oo1:c=FF781C;sh=8",
|
|
"oo1:c=FF781C;en=3",
|
|
"oo1:c=FF781C;pl=1.5",
|
|
"oo1:c=FF781C;broken",
|
|
`oo1:c=FF781C;${"x=1;".repeat(OO_AVATAR_MAX_LENGTH)}`,
|
|
]) {
|
|
expect(decodeOoAvatar(value), value).toBeNull();
|
|
expect(isValidAvatarValue(value), value).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("ignores keys from newer clients", () => {
|
|
expect(decodeOoAvatar("oo1:c=FF781C;sp=dog;zz=new")).toMatchObject({
|
|
species: "dog",
|
|
color: "#FF781C",
|
|
});
|
|
});
|
|
|
|
it("accepts every legacy value for persistence", () => {
|
|
expect(isValidAvatarValue("#8B5CF6::shape_0")).toBe(true);
|
|
expect(isValidAvatarValue("data:image/png;base64,AAAA")).toBe(true);
|
|
});
|
|
|
|
it("exposes a plain colour for CSS consumers", () => {
|
|
expect(ooAvatarColor("oo1:c=FF781C;sp=cat")).toBe("#FF781C");
|
|
expect(ooAvatarColor("#8B5CF6::shape_2")).toBe("#8B5CF6");
|
|
});
|
|
|
|
it("takes a model-written look only as a valid oo1 value", () => {
|
|
expect(normalizeOoLook(" oo1:c=#f4a7b9;sp=cat;ew=round ")).toBe("oo1:c=F4A7B9;sp=cat;ew=round");
|
|
expect(normalizeOoLook("oo1:sp=cat")).toBeNull();
|
|
expect(normalizeOoLook("oo1:c=F4A7B9;sp=dragon")).toBeNull();
|
|
expect(normalizeOoLook("#8B5CF6")).toBeNull();
|
|
expect(normalizeOoLook(42)).toBeNull();
|
|
});
|
|
|
|
it("documents the format with the example it gives", () => {
|
|
const example = /e\.g\. (oo1:\S+)\./.exec(ooLookFormat())?.[1];
|
|
expect(example && normalizeOoLook(example)).toBe(example);
|
|
expect(ooLookFormat()).toContain("penguin");
|
|
});
|
|
});
|
|
|
|
describe("oo rig brand lock", () => {
|
|
it("keeps the big eye φ times the small eye", () => {
|
|
const ratio = OO_RIG_EYES.big.r / OO_RIG_EYES.small.r;
|
|
expect(ratio).toBeGreaterThanOrEqual(OO_EYE_ASYMMETRY_MIN_RATIO);
|
|
expect(ratio).toBeCloseTo(1.618, 3);
|
|
});
|
|
|
|
it("keeps glasses as asymmetric as the eyes", () => {
|
|
const ratio = ooLensRadius("big") / ooLensRadius("small");
|
|
expect(ratio).toBeGreaterThanOrEqual(OO_EYE_ASYMMETRY_MIN_RATIO);
|
|
expect(ooLensRadius("big")).toBeGreaterThan(OO_RIG_EYES.big.r);
|
|
});
|
|
});
|