evOo-local/packages/db/src/main-agent.postgres.test.ts
cynarAI 8449dec6c2
feat: give every project its own main agent (#125)
Each project (space) now has a main agent and a main conversation of its
own. The user talks to it, and it delegates to the project's specialists.
Creating a project sets both up and opens the conversation, and deleting
an otherwise empty project removes them with it.


Claude-Session: https://claude.ai/code/session_01R8hSsKTB3W3e61wD6kvzur

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-04 00:38:39 +02:00

373 lines
13 KiB
TypeScript

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<void>;
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(),
);
});
});