import { randomUUID } from "node:crypto"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { bootstrapUserSpace } from "./bootstrap-user.js"; import { createDb, type PrismaClient } from "./client.js"; import { createGroupRepos } from "./groups.js"; import { addMainConversationMembers, ensureMainBot, ensureMainConversation, findMainBotId, joinMainConversation, loadUserContext, MAIN_BOT_SPAWN_KEY, PROJECT_MAIN_BOT_DEFAULTS, } from "./main-agent.js"; import { createRepos } from "./repos.js"; import { requireMembership } from "./scope.js"; import { createSpaceForMember } from "./spaces.js"; const databaseUrl = process.env.DATABASE_URL; const describePostgres = process.env.VERIFY_DATABASE && databaseUrl ? describe.sequential : describe.skip; const signupEnv = { signupsEnabled: "true", signupAllowlist: "" }; describePostgres("main agent and main conversation (PostgreSQL)", () => { let prisma: PrismaClient; let close: () => Promise; const userIds: string[] = []; async function newUser(name = "Ada") { // bootstrapUserSpace derives the organization slug from the first 12 characters. const id = `ma${randomUUID().replaceAll("-", "")}`; userIds.push(id); await prisma.user.create({ data: { id, name, email: `${id}@rakazo.test` } }); const { spaceId } = await bootstrapUserSpace(prisma, { id }, signupEnv, { claimDeploymentOwner: false, }); return { userId: id, homeSpaceId: spaceId }; } beforeAll(() => { const db = createDb(databaseUrl!); prisma = db.prisma; close = async () => { await db.prisma.$disconnect(); await db.pool.end(); }; }); afterAll(async () => { if (!prisma) return; const memberships = await prisma.member.findMany({ where: { userId: { in: userIds } }, select: { organizationId: true }, }); await prisma.organization.deleteMany({ where: { id: { in: memberships.map((member) => member.organizationId) } }, }); await prisma.user.deleteMany({ where: { id: { in: userIds } } }); await close(); }); it("creates one main bot in the home space, even under concurrent calls", async () => { const { userId, homeSpaceId } = await newUser(); const results = await Promise.all([ ensureMainBot(prisma, userId), ensureMainBot(prisma, userId), ensureMainBot(prisma, userId), ]); expect(new Set(results.map((bot) => bot.id)).size).toBe(1); const bots = await prisma.bot.findMany({ where: { userId } }); expect(bots).toHaveLength(1); expect(bots[0]).toMatchObject({ kind: "main", spaceId: homeSpaceId, spawnKey: MAIN_BOT_SPAWN_KEY, name: "Chief", }); expect(await findMainBotId(prisma, userId)).toBe(bots[0]?.id); }); it("gives every project its own main bot and main conversation next to the home one", async () => { const { userId, homeSpaceId } = await newUser(); const home = await ensureMainConversation(prisma, userId); const project = await createSpaceForMember(prisma, { currentSpaceId: homeSpaceId, userId, name: "Launch", }); const [a, b] = await Promise.all([ ensureMainConversation(prisma, userId, project.id), ensureMainConversation(prisma, userId, project.id), ]); expect(a.groupId).toBe(b.groupId); expect(a).toMatchObject({ spaceId: project.id }); expect(a.groupId).not.toBe(home.groupId); expect(a.leadBotId).not.toBe(home.leadBotId); const lead = await prisma.bot.findUniqueOrThrow({ where: { id: a.leadBotId } }); expect(lead).toMatchObject({ kind: "main", spaceId: project.id, spawnKey: null, name: PROJECT_MAIN_BOT_DEFAULTS.name, title: PROJECT_MAIN_BOT_DEFAULTS.title, }); expect(await findMainBotId(prisma, userId)).toBe(home.leadBotId); expect(await findMainBotId(prisma, userId, project.id)).toBe(a.leadBotId); // The home conversation keeps its own main bot. expect(await ensureMainConversation(prisma, userId)).toEqual(home); }); it("creates the main bot without the spawn key when another user holds it in a shared space", async () => { const owner = await newUser(); const ownerMain = await ensureMainBot(prisma, owner.userId); const guestId = `ma${randomUUID().replaceAll("-", "")}`; userIds.push(guestId); await prisma.user.create({ data: { id: guestId, name: "Guest", email: `${guestId}@rakazo.test` }, }); const organization = await prisma.space.findUniqueOrThrow({ where: { id: owner.homeSpaceId }, select: { organizationId: true }, }); // The organization's default-space trigger makes the guest a member of the owner's space. await prisma.member.create({ data: { id: `member-${guestId}`, organizationId: organization.organizationId, userId: guestId, role: "member", createdAt: new Date(), }, }); const guestMain = await ensureMainBot(prisma, guestId); expect(guestMain.id).not.toBe(ownerMain.id); const guestBot = await prisma.bot.findUniqueOrThrow({ where: { id: guestMain.id } }); expect(guestBot).toMatchObject({ kind: "main", userId: guestId, spawnKey: null }); }); it("promotes the first bot onboarding already created instead of adding another", async () => { const { userId } = await newUser(); const actor = await requireMembership(prisma, userId); const first = await createRepos(prisma).createBot(actor, { name: "Nova", title: "Chief of Staff", description: "", instructions: "", notifyOnFinish: true, spawnKey: MAIN_BOT_SPAWN_KEY, }); expect(first.kind).toBe("specialist"); const main = await ensureMainBot(prisma, userId); expect(main).toMatchObject({ id: first.id, name: "Nova" }); expect(await prisma.bot.count({ where: { userId } })).toBe(1); }); it("restores an archived main bot rather than retiring it", async () => { const { userId } = await newUser(); const main = await ensureMainBot(prisma, userId); await prisma.bot.update({ where: { id: main.id }, data: { archivedAt: new Date() } }); const again = await ensureMainBot(prisma, userId); expect(again.id).toBe(main.id); const row = await prisma.bot.findUniqueOrThrow({ where: { id: main.id } }); expect(row.archivedAt).toBeNull(); }); it("creates one main conversation led by the main bot that lists with a single member", async () => { const { userId } = await newUser(); const [a, b] = await Promise.all([ ensureMainConversation(prisma, userId), ensureMainConversation(prisma, userId), ]); expect(a.groupId).toBe(b.groupId); const main = await ensureMainBot(prisma, userId); expect(a).toMatchObject({ leadBotId: main.id, spaceId: main.spaceId }); const actor = await requireMembership(prisma, userId); const groups = await createGroupRepos(prisma).listGroups(actor); expect(groups).toHaveLength(1); expect(groups[0]).toMatchObject({ id: a.groupId, kind: "main", leadBotId: main.id, members: [expect.objectContaining({ botId: main.id })], }); await expect(createGroupRepos(prisma).getGroupTarget(actor, a.groupId)).resolves.toMatchObject({ id: a.groupId, }); }); it("repairs a main conversation whose lead was lost or that was archived", async () => { const { userId } = await newUser(); const conversation = await ensureMainConversation(prisma, userId); await prisma.chatGroup.update({ where: { id: conversation.groupId }, data: { leadBotId: null, archivedAt: new Date() }, }); const repaired = await ensureMainConversation(prisma, userId); expect(repaired.groupId).toBe(conversation.groupId); const row = await prisma.chatGroup.findUniqueOrThrow({ where: { id: conversation.groupId } }); expect(row).toMatchObject({ leadBotId: conversation.leadBotId, archivedAt: null }); }); it("adds only active same-space bots of the same user, once", async () => { const { userId, homeSpaceId } = await newUser(); const other = await newUser("Bo"); const actor = await requireMembership(prisma, userId); const repos = createRepos(prisma); const specialist = await repos.createBot(actor, { name: "Researcher", title: "", description: "", instructions: "", notifyOnFinish: true, }); const project = await createSpaceForMember(prisma, { currentSpaceId: homeSpaceId, userId, name: "Project", }); const projectActor = await requireMembership(prisma, userId, project.id); const elsewhere = await repos.createBot(projectActor, { name: "Designer", title: "", description: "", instructions: "", notifyOnFinish: true, }); const foreignActor = await requireMembership(prisma, other.userId); const foreign = await repos.createBot(foreignActor, { name: "Intruder", title: "", description: "", instructions: "", notifyOnFinish: true, }); const added = await addMainConversationMembers(prisma, { userId, botIds: [specialist.id, elsewhere.id, foreign.id, specialist.id], }); expect(added).toEqual([specialist.id]); expect(await addMainConversationMembers(prisma, { userId, botIds: [specialist.id] })).toEqual( [], ); const conversation = await ensureMainConversation(prisma, userId); const members = await prisma.chatGroupMember.findMany({ where: { groupId: conversation.groupId }, select: { botId: true }, }); expect(members.map((member) => member.botId).sort()).toEqual( [conversation.leadBotId, specialist.id].sort(), ); }); it("announces joined members on the main conversation thread", async () => { const { userId } = await newUser(); const actor = await requireMembership(prisma, userId); const specialist = await createRepos(prisma).createBot(actor, { name: "Analyst", title: "", description: "", instructions: "", notifyOnFinish: true, }); const published: string[] = []; const realtime = { publish: async (topic: string) => { published.push(topic); }, subscribe: async () => () => undefined, }; expect( await joinMainConversation(prisma, { userId, botIds: [specialist.id] }, realtime as never), ).toEqual([specialist.id]); expect( await joinMainConversation(prisma, { userId, botIds: [specialist.id] }, realtime as never), ).toEqual([]); const conversation = await ensureMainConversation(prisma, userId); const events = await prisma.event.findMany({ where: { threadId: conversation.threadId, type: "group.updated" }, }); expect(events).toHaveLength(1); expect(events[0]?.payload).toEqual({ groupId: conversation.groupId, addedBotIds: [specialist.id], }); expect(published).toHaveLength(1); }); it("refuses to replace the main conversation's members through a group update", async () => { const { userId } = await newUser(); const actor = await requireMembership(prisma, userId); const repos = createRepos(prisma); const one = await repos.createBot(actor, { name: "One", title: "", description: "", instructions: "", notifyOnFinish: true, }); const two = await repos.createBot(actor, { name: "Two", title: "", description: "", instructions: "", notifyOnFinish: true, }); const conversation = await ensureMainConversation(prisma, userId); await expect( createGroupRepos(prisma).updateGroup(actor, { groupId: conversation.groupId, botIds: [one.id, two.id], }), ).rejects.toThrow(); await expect( createGroupRepos(prisma).updateGroup(actor, { groupId: conversation.groupId, pinned: true }), ).resolves.toMatchObject({ group: { kind: "main", pinned: true } }); }); it("hands runs only active facts visible in their space", async () => { const { userId, homeSpaceId } = await newUser(); const project = await createSpaceForMember(prisma, { currentSpaceId: homeSpaceId, userId, name: "Project", }); await prisma.userProfile.create({ data: { userId, timeZone: "Europe/Berlin", locale: "de-DE", communicationStyle: "direct" }, }); await prisma.userFact.create({ data: { userId, category: "work", value: "Builds evOo", source: "onboarding" }, }); await prisma.userFact.create({ data: { userId, category: "health", value: "Pending sensitive fact", source: "conversation", status: "pending", sensitive: true, }, }); await prisma.userFact.create({ data: { userId, category: "goals", value: "Ship the project", source: "user", allSpaces: false, visibleSpaces: { create: [{ spaceId: project.id }] }, }, }); const home = await loadUserContext(prisma, { userId, spaceId: homeSpaceId }); expect(home).toMatchObject({ name: "Ada", timeZone: "Europe/Berlin", locale: "de-DE", communicationStyle: "direct", }); expect(home.facts.map((fact) => fact.value)).toEqual(["Builds evOo"]); const inProject = await loadUserContext(prisma, { userId, spaceId: project.id }); expect(inProject.facts.map((fact) => fact.value).sort()).toEqual( ["Builds evOo", "Ship the project"].sort(), ); }); });