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); }); });