import { beforeEach, describe, expect, it, vi } from "vitest"; import type { PrismaClient } from "./client.js"; import { createGroupRepos } from "./groups.js"; import { IsolationError } from "./scope.js"; describe("listSpaceGroupsForSpaces", () => { it("loads and maps compact cross-space group fields", async () => { const findMany = vi.fn(async (_query: { where: unknown; select: Record }) => [ { id: "group-1", spaceId: "workspace-2", kind: "group", name: "Support crew", pinned: true, sectionId: null, updatedAt: new Date("2026-08-20T00:00:00.000Z"), thread: { unread: true, messages: [{ blocks: [{ kind: "text", text: "Escalation pending" }] }], }, members: [ { bot: { id: "bot-1", name: "Triage", color: "#111", runs: [] } }, { bot: { id: "bot-2", name: "Responder", color: "#222", runs: [{ status: "running" }], }, }, ], }, ]); const repos = createGroupRepos({ chatGroup: { findMany } } as unknown as PrismaClient); const actor = { spaceId: "workspace-1", userId: "user-1", email: "user@example.test", isDeploymentOwner: false, }; await expect(repos.listSpaceGroupsForSpaces(actor, ["workspace-2"])).resolves.toEqual([ { id: "group-1", spaceId: "workspace-2", kind: "group", name: "Support crew", pinned: true, sectionId: null, members: [ { botId: "bot-1", name: "Triage", color: "#111", status: "idle" }, { botId: "bot-2", name: "Responder", color: "#222", status: "running" }, ], preview: "Escalation pending", unread: true, updatedAt: "2026-08-20T00:00:00.000Z", }, ]); const query = findMany.mock.calls[0]![0]; expect(query.select).not.toHaveProperty("userId"); expect(query.select).not.toHaveProperty("archivedAt"); expect(query.select).not.toHaveProperty("createdAt"); }); }); describe("archiveGroup", () => { const actor = { spaceId: "workspace-1", userId: "user-1", email: "user@example.com", isDeploymentOwner: true, }; let queryRaw: ReturnType; let findFirst: ReturnType; let findManyRuns: ReturnType; let findManyComputers: ReturnType; let runUpdateMany: ReturnType; let attemptUpdateMany: ReturnType; let taskUpdateMany: ReturnType; let leaseUpdateMany: ReturnType; let leaseFindMany: ReturnType; let computerUpdateMany: ReturnType; let eventDeleteMany: ReturnType; let groupUpdate: ReturnType; let prisma: PrismaClient; beforeEach(() => { queryRaw = vi.fn().mockResolvedValue([{ id: "group-1" }]); findFirst = vi.fn().mockResolvedValue({ thread: { id: "thread-1" } }); findManyRuns = vi.fn().mockResolvedValue([{ id: "run-1", taskId: "task-1" }]); findManyComputers = vi.fn().mockResolvedValue([ { id: "computer-1", homeKey: "home-1", kind: "fake", providerRef: "computer-1", executionBotId: "bot-1", executionRunId: "run-1", }, ]); runUpdateMany = vi.fn(); attemptUpdateMany = vi.fn(); taskUpdateMany = vi.fn(); leaseUpdateMany = vi.fn(); leaseFindMany = vi .fn() .mockResolvedValue([{ computerId: "computer-1", runId: "run-1", fence: 3 }]); computerUpdateMany = vi.fn(); eventDeleteMany = vi.fn(); groupUpdate = vi.fn(); const tx = { $queryRaw: queryRaw, chatGroup: { findFirst, update: groupUpdate }, run: { findMany: findManyRuns, updateMany: runUpdateMany }, attempt: { updateMany: attemptUpdateMany }, task: { updateMany: taskUpdateMany }, computerExecutionLease: { findMany: leaseFindMany, updateMany: leaseUpdateMany }, computer: { findMany: findManyComputers, updateMany: computerUpdateMany }, event: { deleteMany: eventDeleteMany }, }; prisma = { $transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)), } as unknown as PrismaClient; }); it("locks the group, archives it, and cancels only that thread's runs", async () => { const repos = createGroupRepos(prisma); await expect(repos.archiveGroup(actor, "group-1")).resolves.toEqual({ cancelledRunIds: ["run-1"], computers: [ { id: "computer-1", homeKey: "home-1", kind: "fake", providerRef: "computer-1", executionBotId: "bot-1", executionRunId: "run-1", executionFence: 3, }, ], }); expect(queryRaw).toHaveBeenCalled(); expect(findManyRuns).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ threadId: "thread-1" }), }), ); expect(findFirst).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ id: "group-1", archivedAt: null, }), }), ); expect(leaseUpdateMany).toHaveBeenCalledWith({ where: { runId: { in: ["run-1"] } }, data: { expiresAt: new Date(0) }, }); expect(computerUpdateMany).toHaveBeenCalledWith({ where: { executionRunId: { in: ["run-1"] } }, data: { executionRunId: null, executionBotId: null, executionLeaseExpiresAt: null, }, }); expect(groupUpdate).toHaveBeenCalledWith({ where: { id: "group-1" }, data: expect.objectContaining({ pinned: false, archivedAt: expect.any(Date) }), }); }); it("rejects when the group is already archived or missing", async () => { findFirst.mockResolvedValue(null); const repos = createGroupRepos(prisma); await expect(repos.archiveGroup(actor, "group-1")).rejects.toBeInstanceOf(IsolationError); expect(groupUpdate).not.toHaveBeenCalled(); }); });