The onboarding cards, the closing line and the coach routine names were English server text for every user. The server keeps the state and an English fallback; the web now words them through Lingui, so there is one translation source. - Choice cards: the web says the question, the option labels and the placeholder from the step and option ids, and keeps the server text for anything it does not know (quiet-hour ranges are already written in the user's locale). Another bot's focus card is recognized by its option ids. Mobile and agent history keep the server text. - The "Got it. … are a good place to start." line is gone: the app cards speak for themselves, and with no cards nothing is said. - The closing line becomes an intake_note block (note "done", nextCheckInAt, English text). The web words it with the first check-in in the user's clock; mobile blockText and blocksToAgentHistoryText read its text through their generic fallback. - Routines carry coachKey. The routine list and editor show a check-in under its label in the UI language until the user renames it; saving an untouched label keeps the stored name. The English names move to contracts (COACH_ROUTINE_NAMES); prompts stay English. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
81 lines
3.0 KiB
TypeScript
81 lines
3.0 KiB
TypeScript
import type * as db from "@rakazo/db";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { chooseFocus, markAppConnected } from "./onboarding.js";
|
|
import { focusCard } from "./onboarding-intake.js";
|
|
|
|
const posted = vi.hoisted(() => [] as Array<{ blocks: unknown[] }>);
|
|
vi.mock("@rakazo/db", async (original) => ({
|
|
...(await original<typeof db>()),
|
|
createThreadMessageInTransaction: vi.fn(async (_tx, input) => {
|
|
posted.push(input);
|
|
return { id: "posted" };
|
|
}),
|
|
appendEventInTransaction: vi.fn(async () => ({ seq: 1 })),
|
|
}));
|
|
function fixture(catalog: unknown[]) {
|
|
posted.length = 0;
|
|
// A specialist's focus card, waiting for an answer.
|
|
const pending = vi.fn(async () => [{ id: "choice", blocks: [focusCard()] }]);
|
|
const tx = {
|
|
$executeRaw: vi.fn(),
|
|
message: { findMany: pending, update: vi.fn() },
|
|
};
|
|
const deps = {
|
|
prisma: {
|
|
bot: { findFirst: vi.fn(async () => ({ id: "bot", thread: { id: "thread" } })) },
|
|
message: { findMany: pending },
|
|
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(tx)),
|
|
},
|
|
events: { notify: vi.fn() },
|
|
connectors: { managedProviders: () => [{ catalog: async () => catalog }] },
|
|
} as unknown as Parameters<typeof chooseFocus>[0];
|
|
const actor = {
|
|
userId: "user",
|
|
spaceId: "space",
|
|
email: "user@rakazo.test",
|
|
isDeploymentOwner: true,
|
|
};
|
|
return { deps, actor, tx };
|
|
}
|
|
describe("onboarding connection suggestions", () => {
|
|
it("does not invent authorization cards when no connector has an app catalog", async () => {
|
|
const { deps, actor } = fixture([]);
|
|
await chooseFocus(deps, actor, "bot", { optionId: "day" });
|
|
// The answered card is all that changes; nothing is said around it.
|
|
expect(posted).toEqual([]);
|
|
expect(deps.events.notify).toHaveBeenCalled();
|
|
});
|
|
it("uses the available connector and omits unavailable apps", async () => {
|
|
const { deps, actor } = fixture([
|
|
{ connectorId: "pipedream", slug: "slack", name: "Slack", connected: false, logo: null },
|
|
]);
|
|
await chooseFocus(deps, actor, "bot", { optionId: "day" });
|
|
expect(
|
|
posted
|
|
.flatMap((message) => message.blocks)
|
|
.filter((block) => (block as { kind: string }).kind === "app_connect"),
|
|
).toEqual([
|
|
expect.objectContaining({ connectorId: "pipedream", provider: "slack", name: "Slack" }),
|
|
]);
|
|
// The cards speak for themselves.
|
|
expect(posted).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
it("marks only the authorized connector when provider slugs collide", async () => {
|
|
const { deps, actor, tx } = fixture([]);
|
|
const blocks = ["composio", "pipedream"].map((connectorId) => ({
|
|
kind: "app_connect",
|
|
connectorId,
|
|
provider: "slack",
|
|
name: "Slack",
|
|
status: "pending",
|
|
}));
|
|
deps.prisma.message = { findMany: vi.fn(async () => [{ id: "cards", blocks }]) } as never;
|
|
await markAppConnected(deps, actor, "bot", "slack", "pipedream");
|
|
expect(tx.message.update).toHaveBeenCalledWith({
|
|
where: { id: "cards" },
|
|
data: { blocks: [blocks[0], { ...blocks[1], status: "connected" }] },
|
|
});
|
|
});
|