evOo-local/packages/adapters/src/child-bots.test.ts
Claude fa97f39570
feat(media): add the Studio specialist the main bot hands media work to
Media work (cutting for a format, subtitles, exports) needs editing judgement
and a focused tool set, so the main bot should be able to delegate it to a
visible specialist instead of doing it itself.

- ensureStudioBot creates "Studio" once per user, next to the main bot so it
  can join the main conversation and read its media, tagged media, with
  editor instructions (hook first, tight pacing, 9:16 safe areas, subtitles
  for muted playback) and a restricted builtin allowlist: every media
  operation plus files, questions, memory and web lookup. It has a thread,
  so the main bot's specialist directory lists it. The spawn key carries the
  user id so members of a shared space each get their own.
- A Studio the user archived or deleted stays gone. Deleted bots now record
  their spawn key in bot_deletions so idempotent creation can tell.
- Postgres suite for concurrency, shared spaces, archive and delete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 13:21:56 +00:00

787 lines
26 KiB
TypeScript

import type {
AdapterContext,
AgentHomeStore,
ArtifactStore,
JobPublisher,
SandboxProvider,
} from "@rakazo/adapter-kit";
import type { createRepos, PrismaClient } from "@rakazo/db";
import { describe, expect, it, vi } from "vitest";
import {
archiveBot,
archiveSpawnedBot,
confirmSpawnedBotName,
destroyBot,
spawnBot,
} from "./child-bots.js";
const context = {
operationId: "test",
traceId: "test",
spaceId: "workspace-1",
userId: "user-1",
signal: new AbortController().signal,
} satisfies AdapterContext;
function noGroupMemberships() {
return {
$queryRaw: vi.fn().mockResolvedValue([]),
chatGroup: {
findMany: vi.fn().mockResolvedValue([]),
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
},
chatGroupMember: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }) },
artifact: { findMany: vi.fn().mockResolvedValue([]) },
};
}
describe("spawned bot creation", () => {
it("returns the existing child when a spawn is retried", async () => {
const findUnique = vi.fn().mockResolvedValue({
id: "child-1",
name: "Scout",
title: "Venue researcher",
userId: "user-1",
thread: { id: "thread-1" },
});
const enqueue = vi.fn().mockResolvedValue(undefined);
const prisma = {
bot: {
count: vi.fn().mockResolvedValue(0),
findFirst: vi.fn().mockResolvedValue({ id: "parent-1" }),
findUnique,
},
deploymentSettings: { findUnique: vi.fn().mockResolvedValue(null) },
run: { findUnique: vi.fn().mockResolvedValue({ id: "child-run-1" }) },
$transaction: vi.fn().mockRejectedValue(new Error("unique spawn key")),
} as unknown as PrismaClient;
const result = await spawnBot(
{
prisma,
jobs: { enqueue } as unknown as JobPublisher,
},
{
spawnedBy: {
id: "parent-1",
name: "Chief",
spaceId: "workspace-1",
userId: "user-1",
},
runId: "run-retry",
spawnKey: "tool-call-1",
name: " Scout ",
title: "Ignored on a retry",
prompt: "Do not enqueue this twice",
},
);
expect(findUnique).toHaveBeenCalledWith({
where: {
spaceId_spawnKey: {
spaceId: "workspace-1",
spawnKey: "tool-call-1",
},
},
include: { thread: true },
});
expect(result).toEqual({
ok: true,
duplicate: true,
botId: "child-1",
name: "Scout",
title: "Venue researcher",
threadId: "thread-1",
});
expect(enqueue).toHaveBeenCalledOnce();
});
it("rejects a spawnKey that belongs to another member in the shared Space", async () => {
const conflict = new Error("unique spawn key");
const findUnique = vi.fn().mockResolvedValue({
id: "child-owner",
name: "Scout",
title: "Venue researcher",
userId: "owner-user",
thread: { id: "thread-owner" },
});
const createBot = vi.fn().mockRejectedValue(conflict);
const createReposSpy = vi.spyOn(await import("@rakazo/db"), "createRepos").mockReturnValue({
createBot,
} as unknown as ReturnType<typeof createRepos>);
await expect(
spawnBot(
{
prisma: { bot: { findUnique } } as unknown as PrismaClient,
jobs: { enqueue: vi.fn() } as unknown as JobPublisher,
},
{
spawnedBy: {
id: "parent-2",
name: "Other",
spaceId: "workspace-1",
userId: "other-user",
},
runId: "run-2",
spawnKey: "tool-call-shared",
name: "Scout",
},
),
).rejects.toThrow("unique spawn key");
expect(createBot).toHaveBeenCalledOnce();
expect(findUnique).toHaveBeenCalledOnce();
createReposSpy.mockRestore();
});
it("passes computerMode through to createBot", async () => {
const createBot = vi.fn().mockResolvedValue({
id: "child-2",
name: "Painter",
title: "",
threadId: "thread-2",
});
const createReposSpy = vi.spyOn(await import("@rakazo/db"), "createRepos").mockReturnValue({
createBot,
} as unknown as ReturnType<typeof createRepos>);
const result = await spawnBot(
{
prisma: {} as PrismaClient,
jobs: { enqueue: vi.fn() } as unknown as JobPublisher,
},
{
spawnedBy: {
id: "parent-1",
name: "Chief",
spaceId: "workspace-1",
userId: "user-1",
},
runId: "run-1",
spawnKey: "tool-call-2",
name: "Painter",
computerMode: "dedicated",
},
);
expect(createBot).toHaveBeenCalledWith(
expect.objectContaining({ userId: "user-1", spaceId: "workspace-1" }),
expect.objectContaining({
name: "Painter",
computerMode: "dedicated",
parentBotId: "parent-1",
}),
);
expect(result).toEqual({
ok: true,
botId: "child-2",
name: "Painter",
title: "",
threadId: "thread-2",
});
createReposSpy.mockRestore();
});
});
describe("spawned bot archival", () => {
it("refuses when confirm_name does not match exactly", () => {
expect(confirmSpawnedBotName("scout", "Scout")).toMatchObject({ ok: false });
expect(confirmSpawnedBotName("Scout ", "Scout")).toMatchObject({ ok: false });
});
it("accepts an exact name match", () => {
expect(confirmSpawnedBotName("Scout", "Scout")).toEqual({ ok: true });
});
it("reconciles a retry when the child is already archived", async () => {
const archivedAt = new Date("2026-08-16T12:00:00.000Z");
const prisma = {
bot: {
findMany: vi.fn().mockResolvedValue([
{
id: "child-1",
spaceId: "workspace-1",
userId: "user-1",
parentBotId: "parent-1",
name: "Scout",
archivedAt,
},
]),
},
computer: { findUnique: vi.fn().mockResolvedValue(null) },
run: { findMany: vi.fn().mockResolvedValue([]) },
routine: { findMany: vi.fn().mockResolvedValue([]) },
$transaction: vi.fn(async (callback: (tx: unknown) => Promise<void>) =>
callback({
...noGroupMemberships(),
run: { updateMany: vi.fn().mockResolvedValue({ count: 0 }) },
task: { updateMany: vi.fn().mockResolvedValue({ count: 0 }) },
routine: { updateMany: vi.fn().mockResolvedValue({ count: 0 }) },
computerExecutionLease: { updateMany: vi.fn().mockResolvedValue({ count: 0 }) },
computer: { updateMany: vi.fn().mockResolvedValue({ count: 0 }) },
bot: { update: vi.fn().mockResolvedValue({}) },
}),
),
} as unknown as PrismaClient;
await expect(
archiveSpawnedBot(
{
prisma,
sandbox: {} as SandboxProvider,
home: {} as AgentHomeStore,
jobs: { cancel: vi.fn() } as unknown as JobPublisher,
},
{
spawnedByBotId: "parent-1",
userId: "user-1",
spaceId: "workspace-1",
confirmName: "Scout",
botId: "child-1",
},
context,
),
).resolves.toMatchObject({ ok: true, botId: "child-1", name: "Scout" });
});
it("refuses to archive the main bot", async () => {
const transaction = vi.fn();
const prisma = {
bot: {
findMany: vi.fn().mockResolvedValue([
{
id: "main-1",
spaceId: "workspace-1",
userId: "user-1",
parentBotId: "parent-1",
name: "Chief",
kind: "main",
archivedAt: null,
},
]),
},
$transaction: transaction,
} as unknown as PrismaClient;
await expect(
archiveSpawnedBot(
{
prisma,
sandbox: {} as SandboxProvider,
home: {} as AgentHomeStore,
jobs: { cancel: vi.fn() } as unknown as JobPublisher,
},
{
spawnedByBotId: "parent-1",
userId: "user-1",
spaceId: "workspace-1",
confirmName: "Chief",
},
context,
),
).resolves.toEqual({ error: "The main bot can't be archived." });
expect(transaction).not.toHaveBeenCalled();
});
});
describe("destroyBot", () => {
it("moves bot memories into shared memory before the cascading delete", async () => {
const createDeletion = vi.fn().mockResolvedValue({});
const deleteBot = vi.fn().mockResolvedValue({});
const executeRaw = vi.fn().mockResolvedValue(1);
const releaseComputers = vi.fn().mockResolvedValue({ count: 1 });
const removeArtifact = vi.fn().mockResolvedValue(undefined);
const findArtifacts = vi.fn().mockResolvedValue([{ storageKey: "stored-artifact" }]);
const deleteArtifacts = vi.fn().mockResolvedValue({ count: 1 });
const transaction = vi.fn(async (callback: (tx: unknown) => Promise<void>) =>
callback({
$queryRaw: vi
.fn()
.mockResolvedValue([{ id: "bot-1", webhookSecretId: null, spawnKey: "spawn-1" }]),
chatGroup: {
findMany: vi.fn().mockResolvedValue([]),
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
},
artifact: { findMany: findArtifacts, deleteMany: deleteArtifacts },
computerExecutionLease: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }) },
computer: { updateMany: releaseComputers },
$executeRaw: executeRaw,
botDeletion: { create: createDeletion },
bot: { delete: deleteBot },
}),
);
const prisma = {
bot: {
findUnique: vi.fn(),
},
computer: { findUnique: vi.fn().mockResolvedValue(null) },
run: {
findMany: vi.fn().mockResolvedValue([]),
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
},
routine: { findMany: vi.fn().mockResolvedValue([]) },
$transaction: transaction,
} as unknown as PrismaClient;
await destroyBot(
{
prisma,
sandbox: {} as SandboxProvider,
home: {} as AgentHomeStore,
jobs: { cancel: vi.fn() } as unknown as JobPublisher,
artifacts: { remove: removeArtifact } as unknown as ArtifactStore,
},
{ id: "bot-1", spaceId: "workspace-1", name: "Researcher", archivedAt: null },
context,
{ deleteMemories: false },
);
expect(transaction).toHaveBeenCalledOnce();
expect(releaseComputers).toHaveBeenCalledWith({
where: {
OR: [{ controlBotId: "bot-1" }, { executionBotId: "bot-1" }],
},
data: expect.objectContaining({
controlHolder: "none",
controlBotId: null,
executionBotId: null,
}),
});
expect(executeRaw).toHaveBeenCalledOnce();
expect(executeRaw.mock.calls[0]?.slice(1)).toEqual([
"Archived bots/Researcher (bot-1)",
"bot-1",
]);
expect(createDeletion).toHaveBeenCalledWith({
data: {
id: "bot-1",
spaceId: "workspace-1",
name: "Researcher",
spawnKey: "spawn-1",
deletedByUserId: "user-1",
memoriesPreserved: true,
},
});
expect(deleteBot).toHaveBeenCalledWith({ where: { id: "bot-1" } });
expect(findArtifacts).toHaveBeenCalledWith({
where: { botId: "bot-1", groupId: null, spaceId: "workspace-1" },
select: { storageKey: true },
});
expect(deleteArtifacts).toHaveBeenCalledWith({
where: { botId: "bot-1", groupId: null, spaceId: "workspace-1" },
});
expect(deleteArtifacts.mock.invocationCallOrder[0]).toBeLessThan(
deleteBot.mock.invocationCallOrder[0]!,
);
expect(removeArtifact).toHaveBeenCalledWith("stored-artifact", context);
});
it("dissolves groups with fewer than two active members, never the main conversation", async () => {
const deleteGroups = vi.fn().mockResolvedValue({ count: 1 });
const deleteMemberships = vi.fn().mockResolvedValue({ count: 1 });
const cancel = vi.fn().mockResolvedValue(undefined);
const releaseScreen = vi.fn().mockResolvedValue(undefined);
const cancelRuns = vi.fn().mockResolvedValue({ count: 1 });
const cancelAttempts = vi.fn().mockResolvedValue({ count: 1 });
const cancelTasks = vi.fn().mockResolvedValue({ count: 1 });
const deleteExecutionLeases = vi.fn().mockResolvedValue({ count: 1 });
const expireExecutionLeases = vi.fn().mockResolvedValue({ count: 1 });
const clearExecution = vi.fn().mockResolvedValue({ count: 1 });
const queryRaw = vi.fn().mockResolvedValue([{ id: "group-1" }, { id: "group-2" }]);
const findRuns = vi.fn().mockResolvedValue([
{
id: "group-run",
taskId: "group-task",
botId: "bot-2",
bot: {
computer: { homeKey: "team-home", kind: "fake", providerRef: "screen-1" },
},
},
]);
const transaction = vi.fn(async (callback: (tx: unknown) => Promise<void>) =>
callback({
$queryRaw: queryRaw,
chatGroup: {
findMany: vi.fn().mockResolvedValue([
{
id: "group-1",
thread: { id: "thread-1" },
members: [
{ botId: "bot-1", bot: { archivedAt: null } },
{ botId: "bot-2", bot: { archivedAt: null } },
],
},
{
id: "group-2",
thread: { id: "thread-2" },
members: [
{ botId: "bot-1", bot: { archivedAt: null } },
{ botId: "bot-2", bot: { archivedAt: null } },
{ botId: "bot-3", bot: { archivedAt: null } },
],
},
{
id: "group-3",
thread: { id: "thread-3" },
members: [
{ botId: "bot-1", bot: { archivedAt: null } },
{ botId: "bot-2", bot: { archivedAt: new Date() } },
{ botId: "bot-3", bot: { archivedAt: null } },
],
},
{
id: "main-conversation",
kind: "main",
thread: { id: "thread-main" },
members: [
{ botId: "bot-1", bot: { archivedAt: null } },
{ botId: "main-bot", bot: { archivedAt: null } },
],
},
]),
deleteMany: deleteGroups,
},
run: {
findMany: findRuns,
updateMany: cancelRuns,
},
attempt: { updateMany: cancelAttempts },
task: { updateMany: cancelTasks },
chatGroupMember: { deleteMany: deleteMemberships },
artifact: {
findMany: vi.fn().mockResolvedValue([]),
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
},
computerExecutionLease: {
deleteMany: deleteExecutionLeases,
updateMany: expireExecutionLeases,
},
computer: { updateMany: clearExecution },
$executeRaw: vi.fn(),
botDeletion: { create: vi.fn() },
bot: { delete: vi.fn() },
}),
);
const prisma = {
computer: { findUnique: vi.fn().mockResolvedValue(null) },
run: {
findMany: vi.fn().mockResolvedValue([]),
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
},
routine: { findMany: vi.fn().mockResolvedValue([]) },
artifact: { findMany: vi.fn().mockResolvedValue([]) },
$transaction: transaction,
} as unknown as PrismaClient;
await destroyBot(
{
prisma,
sandbox: { releaseScreen } as unknown as SandboxProvider,
home: {} as AgentHomeStore,
jobs: { cancel } as unknown as JobPublisher,
},
{ id: "bot-1", spaceId: "workspace-1", name: "Researcher", archivedAt: null },
context,
{ deleteMemories: true },
);
expect(deleteGroups).toHaveBeenCalledWith({ where: { id: { in: ["group-1", "group-3"] } } });
expect(deleteMemberships).toHaveBeenCalledWith({ where: { botId: "bot-1" } });
expect(deleteMemberships.mock.invocationCallOrder[0]!).toBeLessThan(
deleteGroups.mock.invocationCallOrder[0]!,
);
expect(queryRaw.mock.calls.some(([query]) => String(query).includes("FROM chat_groups"))).toBe(
true,
);
expect(findRuns).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
threadId: { in: ["thread-1", "thread-3"] },
}),
}),
);
expect(cancelRuns).toHaveBeenCalledWith({
where: { id: { in: ["group-run"] } },
data: {
status: "cancelled",
completedAt: expect.any(Date),
leaseOwner: null,
leaseExpiresAt: null,
},
});
expect(cancelAttempts).toHaveBeenCalledWith({
where: { runId: { in: ["group-run"] }, status: "running" },
data: { status: "cancelled", finishedAt: expect.any(Date) },
});
expect(cancelTasks).toHaveBeenCalledWith({
where: { id: { in: ["group-task"] } },
data: { status: "cancelled" },
});
expect(expireExecutionLeases).toHaveBeenCalledWith({
where: { runId: { in: ["group-run"] } },
data: { expiresAt: new Date(0) },
});
expect(deleteExecutionLeases).toHaveBeenCalledWith({ where: { botId: "bot-1" } });
expect(clearExecution).toHaveBeenCalledWith({
where: { executionRunId: { in: ["group-run"] } },
data: {
executionRunId: null,
executionBotId: null,
executionLeaseExpiresAt: null,
},
});
expect(cancel).toHaveBeenCalledWith("run:group-run");
expect(releaseScreen).toHaveBeenCalledWith(
{ id: "screen-1", botId: "team-home", kind: "fake", providerRef: "screen-1" },
expect.objectContaining({
operationId: "destroy-group-run:bot-2",
botId: "bot-2",
}),
);
});
it("surfaces transaction failures instead of reporting deletion success", async () => {
const transaction = vi.fn().mockRejectedValue(new Error("delete failed"));
const prisma = {
bot: {
findUnique: vi.fn(),
},
computer: { findUnique: vi.fn().mockResolvedValue(null) },
run: {
findMany: vi.fn().mockResolvedValue([]),
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
},
routine: { findMany: vi.fn().mockResolvedValue([]) },
artifact: { findMany: vi.fn().mockResolvedValue([]) },
$transaction: transaction,
} as unknown as PrismaClient;
await expect(
destroyBot(
{
prisma,
sandbox: {} as SandboxProvider,
home: {} as AgentHomeStore,
jobs: { cancel: vi.fn() } as unknown as JobPublisher,
dataDir: "/tmp/rakazo-destroy-bot-test",
},
{
id: "bot-1",
userId: "user-1",
spaceId: "workspace-1",
name: "Researcher",
archivedAt: null,
},
context,
{ deleteMemories: true },
),
).rejects.toThrow("delete failed");
expect(transaction).toHaveBeenCalledOnce();
});
it("retries retryable transaction conflicts during deletion", async () => {
const conflict = Object.assign(new Error("deadlock"), {
code: "P2039",
meta: { driverAdapterError: { cause: { originalCode: "40P01" } } },
});
const finalError = new Error("second attempt reached");
const transaction = vi.fn().mockRejectedValueOnce(conflict).mockRejectedValueOnce(finalError);
const prisma = {
computer: { findUnique: vi.fn().mockResolvedValue(null) },
run: {
findMany: vi.fn().mockResolvedValue([]),
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
},
routine: { findMany: vi.fn().mockResolvedValue([]) },
artifact: { findMany: vi.fn().mockResolvedValue([]) },
$transaction: transaction,
} as unknown as PrismaClient;
await expect(
destroyBot(
{
prisma,
sandbox: {} as SandboxProvider,
home: {} as AgentHomeStore,
jobs: { cancel: vi.fn() } as unknown as JobPublisher,
},
{ id: "bot-1", spaceId: "workspace-1", name: "Researcher", archivedAt: null },
context,
{ deleteMemories: true },
),
).rejects.toBe(finalError);
expect(transaction).toHaveBeenCalledTimes(2);
});
});
describe("archiveBot", () => {
it("stops work and routines while preserving the bot", async () => {
const updateBot = vi.fn().mockResolvedValue({});
const disableRoutines = vi.fn().mockResolvedValue({ count: 2 });
const groupCleanup = noGroupMemberships();
const transaction = vi.fn(async (callback: (tx: unknown) => Promise<void>) =>
callback({
...groupCleanup,
run: { updateMany: vi.fn().mockResolvedValue({ count: 1 }) },
task: { updateMany: vi.fn().mockResolvedValue({ count: 1 }) },
routine: { updateMany: disableRoutines },
computerExecutionLease: { updateMany: vi.fn().mockResolvedValue({ count: 0 }) },
computer: { updateMany: vi.fn().mockResolvedValue({ count: 0 }) },
bot: { update: updateBot },
}),
);
const cancel = vi.fn().mockResolvedValue(undefined);
const prisma = {
bot: {
findUnique: vi.fn().mockResolvedValue({
id: "bot-1",
spaceId: "workspace-1",
archivedAt: null,
}),
},
computer: { findUnique: vi.fn().mockResolvedValue(null) },
run: { findMany: vi.fn().mockResolvedValue([{ id: "run-1" }]) },
routine: { findMany: vi.fn().mockResolvedValue([]) },
$transaction: transaction,
} as unknown as PrismaClient;
await archiveBot(
{
prisma,
sandbox: {} as SandboxProvider,
home: {} as AgentHomeStore,
jobs: { cancel } as unknown as JobPublisher,
},
{ id: "bot-1", spaceId: "workspace-1", name: "Researcher", archivedAt: null },
context,
);
expect(disableRoutines).toHaveBeenCalledWith({
where: { botId: "bot-1" },
data: { active: false, nextRunAt: null },
});
expect(updateBot).toHaveBeenCalledWith({
where: { id: "bot-1" },
data: { archivedAt: expect.any(Date), pinned: false },
});
expect(groupCleanup.$queryRaw).not.toHaveBeenCalled();
expect(groupCleanup.chatGroup.deleteMany).not.toHaveBeenCalled();
expect(groupCleanup.chatGroupMember.deleteMany).not.toHaveBeenCalled();
expect(cancel).toHaveBeenCalledWith("run:run-1");
});
it("surfaces computer stop failures and leaves the provider state retryable", async () => {
const archivedAt = new Date("2026-08-16T12:00:00.000Z");
const updateComputer = vi.fn().mockResolvedValue({});
const transaction = vi.fn(async (callback: (tx: unknown) => Promise<void>) =>
callback({
...noGroupMemberships(),
run: { updateMany: vi.fn().mockResolvedValue({ count: 0 }) },
task: { updateMany: vi.fn().mockResolvedValue({ count: 0 }) },
routine: { updateMany: vi.fn().mockResolvedValue({ count: 0 }) },
computerExecutionLease: { updateMany: vi.fn().mockResolvedValue({ count: 0 }) },
computer: { updateMany: vi.fn().mockResolvedValue({ count: 1 }) },
bot: { update: vi.fn().mockResolvedValue({}) },
}),
);
const dedicated = {
id: "computer-1",
homeKey: "bot-1",
kind: "cloud",
providerRef: "provider-1",
state: "running",
};
const stop = vi.fn().mockRejectedValue(new Error("stop failed"));
const prisma = {
computer: {
findUnique: vi.fn().mockResolvedValue(dedicated),
update: updateComputer,
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
},
run: { findMany: vi.fn().mockResolvedValue([]) },
routine: { findMany: vi.fn().mockResolvedValue([]) },
$transaction: transaction,
} as unknown as PrismaClient;
const sandbox = {
exportWorkspace: async function* () {},
stop,
} as unknown as SandboxProvider;
const home = {
commit: vi.fn().mockResolvedValue("revision-1"),
} as unknown as AgentHomeStore;
await expect(
archiveBot(
{
prisma,
sandbox,
home,
jobs: { cancel: vi.fn() } as unknown as JobPublisher,
},
{ id: "bot-1", spaceId: "workspace-1", name: "Researcher", archivedAt },
context,
),
).rejects.toThrow("stop failed");
expect(stop).toHaveBeenCalledOnce();
expect(updateComputer).not.toHaveBeenCalled();
});
it("stops a provider that finishes booting while the bot is archived", async () => {
const stop = vi.fn().mockResolvedValue(undefined);
const updateMany = vi.fn().mockResolvedValue({ count: 1 });
const booting = {
id: "computer-1",
homeKey: "bot-1",
kind: "cloud",
providerRef: null,
state: "booting",
};
const running = { ...booting, providerRef: "provider-1", state: "running" };
const transaction = vi.fn(async (callback: (tx: unknown) => Promise<void>) =>
callback({
...noGroupMemberships(),
run: { updateMany: vi.fn().mockResolvedValue({ count: 0 }) },
task: { updateMany: vi.fn().mockResolvedValue({ count: 0 }) },
routine: { updateMany: vi.fn().mockResolvedValue({ count: 0 }) },
computerExecutionLease: { updateMany: vi.fn().mockResolvedValue({ count: 0 }) },
computer: { updateMany: vi.fn().mockResolvedValue({ count: 0 }) },
bot: { update: vi.fn().mockResolvedValue({}) },
}),
);
const prisma = {
computer: {
findUnique: vi.fn().mockResolvedValueOnce(booting).mockResolvedValueOnce(running),
updateMany,
},
run: { findMany: vi.fn().mockResolvedValue([]) },
routine: { findMany: vi.fn().mockResolvedValue([]) },
$transaction: transaction,
} as unknown as PrismaClient;
const sandbox = {
exportWorkspace: async function* () {},
stop,
} as unknown as SandboxProvider;
const home = {
commit: vi.fn().mockResolvedValue("revision-1"),
} as unknown as AgentHomeStore;
await archiveBot(
{
prisma,
sandbox,
home,
jobs: { cancel: vi.fn() } as unknown as JobPublisher,
},
{ id: "bot-1", spaceId: "workspace-1", name: "Researcher", archivedAt: null },
context,
);
expect(stop).toHaveBeenCalledOnce();
expect(updateMany).toHaveBeenLastCalledWith({
where: { id: "computer-1", state: "running", providerRef: "provider-1" },
data: { state: "stopped" },
});
});
});