evOo-local/packages/adapters/src/child-bots.test.ts
cynarAI 97787a0272
Production readiness phase 1: journey audit, fixes 1–4, E2E (#175)
* Production readiness phase 1: journeys doc, onboarding persistence, teardown, chat UX

- Add docs/PRODUCTION-READINESS.md with journey matrix and defect backlog
- Persist onboarding draft in session storage across reload
- Localize onboarding discovery main-bot role; German Chief of Staff string
- Destroy team sandboxes and home folders when deleting an account
- Keep composer/transcript visible during queued runs; extend send watchdog
- Add Playwright specs: reload persistence, DE intake, queued progress, console guard

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Fix CI: sync Lingui catalogs, onboarding name draft, E2E stability

- Localize Chief of Staff in discovery shell; sync all locale PO files
- Propagate Oo name to onboarding draft while typing
- Adjust DE intake, queued-run, and console-error E2E specs

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* Format chat-queued-progress E2E spec

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(api): scope account-delete sandbox teardown to owned spaces

Only destroy team sandboxes and homes for spaces the deleting user
owns with no remaining members or bots. Guard home removal under
dataDir/homes. Add unit and postgres journey coverage.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* chore: apply biome formatting

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-07 11:25:39 +02:00

821 lines
27 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,
destroySpaceSandboxes,
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("destroySpaceSandboxes", () => {
it("destroys every provider-backed computer in the space", async () => {
const destroy = vi.fn().mockResolvedValue(undefined);
const updateMany = vi.fn().mockResolvedValue({ count: 1 });
const prisma = {
computer: {
findMany: vi.fn().mockResolvedValue([
{
id: "team-1",
homeKey: "team-space-1",
kind: "fake",
providerRef: "prov-team",
},
]),
updateMany,
},
} as unknown as PrismaClient;
await destroySpaceSandboxes(
{
prisma,
sandbox: { destroy } as unknown as SandboxProvider,
home: { resolvePath: vi.fn() } as unknown as AgentHomeStore,
jobs: { cancel: vi.fn() } as unknown as JobPublisher,
dataDir: "/tmp/rakazo-space-teardown",
},
{ spaceId: "space-1" },
context,
);
expect(destroy).toHaveBeenCalledOnce();
expect(updateMany).toHaveBeenCalledOnce();
});
});
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" },
});
});
});