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>
83 lines
3.0 KiB
TypeScript
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.");
|
|
});
|
|
});
|