evOo-local/packages/core/src/suggestions.test.ts
cynarAI c69f00e891
feat(avatars): bots offer a new Oo look from chat (#145)
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>
2026-10-05 04:03:10 +02:00

83 lines
3.0 KiB
TypeScript

import { CardBlockSchema, SUGGESTION_TTL_MS } from "@rakazo/contracts";
import { describe, expect, it } from "vitest";
import { applyCardAct, cardHistoryText } from "./cards.js";
import {
buildSuggestionCard,
describeSuggestion,
suggestionStatus,
withSuggestionStatus,
} from "./suggestions.js";
const walk = {
id: "s1",
effectId: "s1",
tool: "schedule_create",
args: { name: "Morning walk", prompt: "Take a walk.", cron: "0 8 * * *" },
why: "A nudge each morning makes it a habit.",
};
describe("suggestions", () => {
it("says what the stored call does, not what the model claims", () => {
expect(describeSuggestion(walk.tool, walk.args)).toEqual({
title: "Set up a routine",
details: ["Morning walk", "Take a walk.", "Every day at 8:00 AM"],
});
expect(describeSuggestion("gmail_send_email", {}).title).toBe("Gmail send email");
});
it("shows a suggested look instead of describing it", () => {
const card = buildSuggestionCard({
id: "s2",
effectId: "s2",
tool: "update_bot",
args: { look: "oo1:c=f4a7b9;sp=cat" },
why: "Cosy for recipe nights.",
});
expect(card.sections?.[0]).toEqual({
type: "header",
title: "New look",
subtitle: "Cosy for recipe nights.",
avatar: "oo1:c=F4A7B9;sp=cat",
});
expect(CardBlockSchema.safeParse(card).success).toBe(true);
expect(describeSuggestion("update_bot", { look: "nope", name: "Mia" })).toEqual({
title: "Update profile",
details: ["Mia"],
});
});
it("takes one tap: accept runs it, then nothing else applies", () => {
const card = buildSuggestionCard(walk);
const accepted = applyCardAct(card, { type: "action", actionId: "accept" });
expect(accepted).toMatchObject({ ok: true, suggestion: "accept" });
if (!accepted.ok) throw new Error(accepted.error);
expect(accepted.block.suggestion?.status).toBe("running");
expect(accepted.reply).toBeUndefined();
expect(applyCardAct(accepted.block, { type: "action", actionId: "dismiss" })).toMatchObject({
ok: false,
});
expect(applyCardAct(card, { type: "action", actionId: "dismiss" })).toMatchObject({
ok: true,
suggestion: "dismiss",
block: { suggestion: { status: "dismissed" } },
});
});
it("reads an open suggestion past its time as expired", () => {
const card = buildSuggestionCard(walk);
const created = new Date("2026-10-01T08:00:00Z");
expect(suggestionStatus(card, created, created.getTime() + 1000)).toBe("proposed");
expect(suggestionStatus(card, created, created.getTime() + SUGGESTION_TTL_MS + 1)).toBe(
"expired",
);
const done = withSuggestionStatus(card, "done");
expect(suggestionStatus(done, created, created.getTime() + SUGGESTION_TTL_MS + 1)).toBe("done");
});
it("tells the model where the suggestion stands", () => {
const failed = withSuggestionStatus(buildSuggestionCard(walk), "failed", "No such tool.");
expect(cardHistoryText(failed)).toContain("schedule_create");
expect(cardHistoryText(failed)).toContain("No such tool.");
});
});