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>
This commit is contained in:
parent
9c1137780d
commit
751967a8e1
@ -84,6 +84,7 @@ import {
|
|||||||
parsePositiveInteger,
|
parsePositiveInteger,
|
||||||
provisionMessagingIdentity,
|
provisionMessagingIdentity,
|
||||||
requireMembership,
|
requireMembership,
|
||||||
|
spaceIdsForSandboxTeardownAfterAccountDelete,
|
||||||
} from "@rakazo/db";
|
} from "@rakazo/db";
|
||||||
import type { Logger } from "@rakazo/logging";
|
import type { Logger } from "@rakazo/logging";
|
||||||
import {
|
import {
|
||||||
@ -390,7 +391,11 @@ export async function createApp(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const spaceIds = [...new Set(bots.map((bot) => bot.spaceId))];
|
const candidateSpaceIds = [...new Set(bots.map((bot) => bot.spaceId))];
|
||||||
|
const spaceIds = await spaceIdsForSandboxTeardownAfterAccountDelete(prisma, {
|
||||||
|
userId,
|
||||||
|
spaceIds: candidateSpaceIds,
|
||||||
|
});
|
||||||
for (const spaceId of spaceIds) {
|
for (const spaceId of spaceIds) {
|
||||||
await destroySpaceSandboxes(lifecycleDeps, { spaceId }, { ...deleteContext, spaceId });
|
await destroySpaceSandboxes(lifecycleDeps, { spaceId }, { ...deleteContext, spaceId });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { rm } from "node:fs/promises";
|
|
||||||
import type {
|
import type {
|
||||||
AdapterContext,
|
AdapterContext,
|
||||||
AgentHomeStore,
|
AgentHomeStore,
|
||||||
@ -22,7 +21,7 @@ import {
|
|||||||
import { getLogger } from "@rakazo/logging";
|
import { getLogger } from "@rakazo/logging";
|
||||||
import { toComputerRef } from "./computer-support.js";
|
import { toComputerRef } from "./computer-support.js";
|
||||||
import { checkpointAndRecordComputerWorkspace } from "./computer-workspace.js";
|
import { checkpointAndRecordComputerWorkspace } from "./computer-workspace.js";
|
||||||
import { resolveAgentHomePath } from "./home.js";
|
import { removeAgentHomeDirectory } from "./home.js";
|
||||||
import { removePiBotSessions } from "./pi-session.js";
|
import { removePiBotSessions } from "./pi-session.js";
|
||||||
|
|
||||||
export function confirmSpawnedBotName(confirmName: string, botName: string) {
|
export function confirmSpawnedBotName(confirmName: string, botName: string) {
|
||||||
@ -351,10 +350,11 @@ export async function destroySpaceSandboxes(
|
|||||||
if (!computer.providerRef) continue;
|
if (!computer.providerRef) continue;
|
||||||
const ref = toComputerRef(computer);
|
const ref = toComputerRef(computer);
|
||||||
await deps.sandbox.destroy(ref, { ...context, botId: computer.homeKey }).catch(() => undefined);
|
await deps.sandbox.destroy(ref, { ...context, botId: computer.homeKey }).catch(() => undefined);
|
||||||
await rm(resolveAgentHomePath(deps.home, computer.homeKey, deps.dataDir ?? "./data"), {
|
await removeAgentHomeDirectory(
|
||||||
recursive: true,
|
deps.home,
|
||||||
force: true,
|
computer.homeKey,
|
||||||
}).catch(() => undefined);
|
deps.dataDir ?? "./data",
|
||||||
|
).catch(() => undefined);
|
||||||
}
|
}
|
||||||
if (computers.length) {
|
if (computers.length) {
|
||||||
await deps.prisma.computer.updateMany({
|
await deps.prisma.computer.updateMany({
|
||||||
@ -483,10 +483,11 @@ export async function destroyBot(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if (dedicated) {
|
if (dedicated) {
|
||||||
await rm(resolveAgentHomePath(deps.home, dedicated.homeKey, deps.dataDir ?? "./data"), {
|
await removeAgentHomeDirectory(
|
||||||
recursive: true,
|
deps.home,
|
||||||
force: true,
|
dedicated.homeKey,
|
||||||
}).catch(() => undefined);
|
deps.dataDir ?? "./data",
|
||||||
|
).catch(() => undefined);
|
||||||
}
|
}
|
||||||
const artifactStore = deps.artifacts;
|
const artifactStore = deps.artifacts;
|
||||||
if (artifactStore) {
|
if (artifactStore) {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { chmod, mkdir, mkdtemp, readFile, rm, stat, symlink, writeFile } from "n
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
import { LocalAgentHomeStore } from "./home.js";
|
import { LocalAgentHomeStore, removeAgentHomeDirectory, resolveSafeAgentHomePath } from "./home.js";
|
||||||
|
|
||||||
const context = {
|
const context = {
|
||||||
operationId: "test",
|
operationId: "test",
|
||||||
@ -164,3 +164,25 @@ describe("LocalAgentHomeStore path containment", () => {
|
|||||||
expect(await readFile(path.join(store.pathFor("bot-2"), "nested/run"))).toEqual(bytes);
|
expect(await readFile(path.join(store.pathFor("bot-2"), "nested/run"))).toEqual(bytes);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("resolveSafeAgentHomePath", () => {
|
||||||
|
it("rejects empty, root-like, and traversal home keys", async () => {
|
||||||
|
const root = await mkdtemp(path.join(tmpdir(), "rakazo-home-guard-"));
|
||||||
|
dirs.push(root);
|
||||||
|
const home = new LocalAgentHomeStore(root);
|
||||||
|
for (const homeKey of ["", ".", "..", "../etc", "team/../escape"]) {
|
||||||
|
expect(() => resolveSafeAgentHomePath(home, homeKey, root)).toThrow(
|
||||||
|
/Invalid agent home key|escapes/,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes only directories under homes/", async () => {
|
||||||
|
const root = await mkdtemp(path.join(tmpdir(), "rakazo-home-guard-rm-"));
|
||||||
|
dirs.push(root);
|
||||||
|
const home = new LocalAgentHomeStore(root);
|
||||||
|
const target = resolveSafeAgentHomePath(home, "bot-safe", root);
|
||||||
|
await removeAgentHomeDirectory(home, "bot-safe", root);
|
||||||
|
expect(target.startsWith(path.join(root, "homes"))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -224,6 +224,33 @@ export function resolveAgentHomePath(home: AgentHomeStore, homeKey: string, data
|
|||||||
return path.resolve(dataDir, "homes", homeKey);
|
return path.resolve(dataDir, "homes", homeKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assertValidHomeKey(homeKey: string) {
|
||||||
|
if (!homeKey || homeKey === "." || homeKey === ".." || path.basename(homeKey) !== homeKey) {
|
||||||
|
throw new Error("Invalid agent home key");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolved path must stay under `<dataDir>/homes/`; rejects traversal and empty keys. */
|
||||||
|
export function resolveSafeAgentHomePath(
|
||||||
|
home: AgentHomeStore,
|
||||||
|
homeKey: string,
|
||||||
|
dataDir = "./data",
|
||||||
|
) {
|
||||||
|
assertValidHomeKey(homeKey);
|
||||||
|
const homesRoot = path.resolve(dataDir, "homes");
|
||||||
|
const target = path.resolve(resolveAgentHomePath(home, homeKey, dataDir));
|
||||||
|
assertContained(homesRoot, target);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeAgentHomeDirectory(
|
||||||
|
home: AgentHomeStore,
|
||||||
|
homeKey: string,
|
||||||
|
dataDir = "./data",
|
||||||
|
) {
|
||||||
|
await rm(resolveSafeAgentHomePath(home, homeKey, dataDir), { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
function safeJoin(root: string, rel: string) {
|
function safeJoin(root: string, rel: string) {
|
||||||
const resolvedRoot = path.resolve(root);
|
const resolvedRoot = path.resolve(root);
|
||||||
const resolved = path.resolve(resolvedRoot, `.${path.sep}${rel.replace(/^\/+/, "")}`);
|
const resolved = path.resolve(resolvedRoot, `.${path.sep}${rel.replace(/^\/+/, "")}`);
|
||||||
|
|||||||
36
packages/db/src/account-delete.test.ts
Normal file
36
packages/db/src/account-delete.test.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import type { PrismaClient } from "./client.js";
|
||||||
|
import { spaceIdsForSandboxTeardownAfterAccountDelete } from "./account-delete.js";
|
||||||
|
|
||||||
|
describe("spaceIdsForSandboxTeardownAfterAccountDelete", () => {
|
||||||
|
it("includes solely-owned spaces and skips shared ones", async () => {
|
||||||
|
const findUnique = vi
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(async ({ where }: { where: { spaceId_userId: { spaceId: string } } }) => {
|
||||||
|
const { spaceId } = where.spaceId_userId;
|
||||||
|
if (spaceId === "solo") return { role: "owner" };
|
||||||
|
if (spaceId === "shared") return { role: "owner" };
|
||||||
|
if (spaceId === "member-only") return { role: "member" };
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
const countMembers = vi.fn(async ({ where }: { where: { spaceId: string } }) => {
|
||||||
|
if (where.spaceId === "shared") return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
const countBots = vi.fn(async ({ where }: { where: { spaceId: string } }) => {
|
||||||
|
if (where.spaceId === "shared") return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
const prisma = {
|
||||||
|
spaceMember: { findUnique, count: countMembers },
|
||||||
|
bot: { count: countBots },
|
||||||
|
} as unknown as PrismaClient;
|
||||||
|
|
||||||
|
const result = await spaceIdsForSandboxTeardownAfterAccountDelete(prisma, {
|
||||||
|
userId: "user-1",
|
||||||
|
spaceIds: ["solo", "shared", "member-only", "solo"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(["solo"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
32
packages/db/src/account-delete.ts
Normal file
32
packages/db/src/account-delete.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import type { PrismaClient } from "./client.js";
|
||||||
|
|
||||||
|
type AccountDeleteDb = Pick<PrismaClient, "spaceMember" | "bot">;
|
||||||
|
|
||||||
|
/** Spaces whose team sandboxes and homes may be torn down when this user deletes their account. */
|
||||||
|
export async function spaceIdsForSandboxTeardownAfterAccountDelete(
|
||||||
|
prisma: AccountDeleteDb,
|
||||||
|
input: { userId: string; spaceIds: Iterable<string> },
|
||||||
|
): Promise<string[]> {
|
||||||
|
const uniqueSpaceIds = [...new Set(input.spaceIds)];
|
||||||
|
if (!uniqueSpaceIds.length) return [];
|
||||||
|
|
||||||
|
const eligible: string[] = [];
|
||||||
|
for (const spaceId of uniqueSpaceIds) {
|
||||||
|
const membership = await prisma.spaceMember.findUnique({
|
||||||
|
where: { spaceId_userId: { spaceId, userId: input.userId } },
|
||||||
|
select: { role: true },
|
||||||
|
});
|
||||||
|
if (membership?.role !== "owner") continue;
|
||||||
|
|
||||||
|
const [otherMembers, otherBots] = await Promise.all([
|
||||||
|
prisma.spaceMember.count({
|
||||||
|
where: { spaceId, userId: { not: input.userId } },
|
||||||
|
}),
|
||||||
|
prisma.bot.count({
|
||||||
|
where: { spaceId, userId: { not: input.userId } },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
if (otherMembers === 0 && otherBots === 0) eligible.push(spaceId);
|
||||||
|
}
|
||||||
|
return eligible;
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
export * from "./account-delete.js";
|
||||||
export * from "./autopilot-lines.js";
|
export * from "./autopilot-lines.js";
|
||||||
export * from "./bootstrap-user.js";
|
export * from "./bootstrap-user.js";
|
||||||
export * from "./cancel-runs.js";
|
export * from "./cancel-runs.js";
|
||||||
|
|||||||
182
packages/testkit/src/account-delete-sandbox.postgres.test.ts
Normal file
182
packages/testkit/src/account-delete-sandbox.postgres.test.ts
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import { mkdirSync, mkdtempSync, existsSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { ComposioEmulator, FakeSandboxProvider } from "@rakazo/adapters";
|
||||||
|
import type { Bot, Space } from "@rakazo/contracts";
|
||||||
|
import { computerHomeKey, ensureComputerRecord } from "@rakazo/db";
|
||||||
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||||
|
import type { createApp } from "../../../apps/api/src/app.ts";
|
||||||
|
import { sessionCookieHeader } from "./index.js";
|
||||||
|
|
||||||
|
type App = { request: (input: string, init?: RequestInit) => Promise<Response> };
|
||||||
|
type AppHandles = Awaited<ReturnType<typeof createApp>>;
|
||||||
|
|
||||||
|
const databaseAvailable = process.env.VERIFY_DATABASE === "1" && Boolean(process.env.DATABASE_URL);
|
||||||
|
const fixtureOrigin = "http://127.0.0.1:5173";
|
||||||
|
|
||||||
|
describe.skipIf(!databaseAvailable)("account delete and team sandbox teardown", () => {
|
||||||
|
let handles: AppHandles;
|
||||||
|
let app: App;
|
||||||
|
const stamp = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||||
|
const dataDir = mkdtempSync(path.join(tmpdir(), "rakazo-account-delete-sandbox-"));
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const { createApp } = await import("../../../apps/api/src/app.ts");
|
||||||
|
handles = await createApp({
|
||||||
|
databaseUrl: process.env.DATABASE_URL!,
|
||||||
|
dataDir,
|
||||||
|
sandboxProvider: "fake",
|
||||||
|
agentRuntime: "scripted",
|
||||||
|
wakeupDriver: "memory",
|
||||||
|
signupsEnabled: "true",
|
||||||
|
composio: new ComposioEmulator(),
|
||||||
|
});
|
||||||
|
app = handles.app;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await handles.stop?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function seedTeamComputer(spaceId: string, userId: string) {
|
||||||
|
const homeKey = computerHomeKey("team", spaceId);
|
||||||
|
const computer = await ensureComputerRecord(handles.prisma, {
|
||||||
|
mode: "team",
|
||||||
|
spaceId,
|
||||||
|
userId,
|
||||||
|
kind: "fake",
|
||||||
|
});
|
||||||
|
const homePath = path.join(dataDir, "homes", homeKey);
|
||||||
|
mkdirSync(path.join(homePath, "marker"), { recursive: true });
|
||||||
|
const sandbox = handles.sandbox as FakeSandboxProvider;
|
||||||
|
const ref = await sandbox.provision(
|
||||||
|
{ botId: homeKey, homePath },
|
||||||
|
{
|
||||||
|
operationId: "account-delete-seed",
|
||||||
|
traceId: "account-delete-seed",
|
||||||
|
spaceId,
|
||||||
|
userId,
|
||||||
|
signal: AbortSignal.timeout(15_000),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await handles.prisma.computer.update({
|
||||||
|
where: { id: computer.id },
|
||||||
|
data: { providerRef: ref.id, state: "running" },
|
||||||
|
});
|
||||||
|
return { homeKey, homePath, refId: ref.id, sandbox };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteAccount(cookie: string) {
|
||||||
|
const deleted = await app.request("/api/auth/delete-user", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
cookie,
|
||||||
|
origin: fixtureOrigin,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ password: "password12" }),
|
||||||
|
});
|
||||||
|
expect(deleted.status).toBe(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
it("preserves shared team sandboxes when a non-owner member deletes their account", async () => {
|
||||||
|
const owner = await signup(app, `shared-owner-${stamp}@rakazo.test`, "Shared Owner");
|
||||||
|
const member = await signup(app, `shared-member-${stamp}@rakazo.test`, "Shared Member");
|
||||||
|
const shared = await rpc<Space>(app, owner, "spaces/create", { name: "Shared teardown" });
|
||||||
|
const sharedRow = await handles.prisma.space.findUniqueOrThrow({
|
||||||
|
where: { id: shared.id },
|
||||||
|
select: { organizationId: true },
|
||||||
|
});
|
||||||
|
const memberActor = await rpc<{ userId: string }>(app, member, "me");
|
||||||
|
await handles.prisma.member.deleteMany({ where: { userId: memberActor.userId } });
|
||||||
|
await handles.prisma.member.create({
|
||||||
|
data: {
|
||||||
|
id: `shared-org-member-${stamp}`,
|
||||||
|
organizationId: sharedRow.organizationId,
|
||||||
|
userId: memberActor.userId,
|
||||||
|
role: "member",
|
||||||
|
createdAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await handles.prisma.spaceMember.create({
|
||||||
|
data: {
|
||||||
|
id: `shared-space-member-${stamp}`,
|
||||||
|
spaceId: shared.id,
|
||||||
|
organizationId: sharedRow.organizationId,
|
||||||
|
userId: memberActor.userId,
|
||||||
|
role: "member",
|
||||||
|
createdAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await rpc<Bot>(app, owner, "bots/create", botPayload("Owner Bot"), shared.id);
|
||||||
|
await rpc<Bot>(app, member, "bots/create", botPayload("Member Bot"), shared.id);
|
||||||
|
const { homePath, refId, sandbox } = await seedTeamComputer(shared.id, memberActor.userId);
|
||||||
|
|
||||||
|
await deleteAccount(member);
|
||||||
|
|
||||||
|
expect(existsSync(homePath)).toBe(true);
|
||||||
|
expect(sandbox.boxes.has(refId)).toBe(true);
|
||||||
|
const computer = await handles.prisma.computer.findFirst({
|
||||||
|
where: { spaceId: shared.id, scope: "team" },
|
||||||
|
});
|
||||||
|
expect(computer?.providerRef).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("tears down team sandboxes when the sole owner deletes their account", async () => {
|
||||||
|
const cookie = await signup(app, `solo-delete-${stamp}@rakazo.test`, "Solo Delete");
|
||||||
|
const me = await rpc<{ userId: string; spaceId: string }>(app, cookie, "me");
|
||||||
|
const bot = await rpc<Bot>(app, cookie, "bots/create", botPayload("Temporary"));
|
||||||
|
const { homePath, refId, sandbox } = await seedTeamComputer(bot.spaceId, me.userId);
|
||||||
|
|
||||||
|
await deleteAccount(cookie);
|
||||||
|
|
||||||
|
expect(existsSync(homePath)).toBe(false);
|
||||||
|
expect(sandbox.boxes.has(refId)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function botPayload(name: string) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
instructions: "",
|
||||||
|
notifyOnFinish: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signup(app: App, email: string, name: string) {
|
||||||
|
const response = await app.request("/api/auth/sign-up/email", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json", origin: fixtureOrigin },
|
||||||
|
body: JSON.stringify({ email, password: "password12", name }),
|
||||||
|
});
|
||||||
|
expect(response.status).toBeLessThan(400);
|
||||||
|
return sessionCookieHeader(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rpc<T>(
|
||||||
|
app: App,
|
||||||
|
cookie: string,
|
||||||
|
procedure: string,
|
||||||
|
input: unknown = {},
|
||||||
|
spaceId?: string,
|
||||||
|
): Promise<T> {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"content-type": "application/json",
|
||||||
|
cookie,
|
||||||
|
origin: fixtureOrigin,
|
||||||
|
};
|
||||||
|
if (spaceId) headers["x-rakazo-space-id"] = spaceId;
|
||||||
|
const response = await app.request(`/rpc/${procedure}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({ json: input }),
|
||||||
|
});
|
||||||
|
const body = (await response.json()) as { json?: T; error?: { message?: string } };
|
||||||
|
if (response.status >= 400 || body.error) {
|
||||||
|
throw new Error(`${procedure}: ${body.error?.message ?? response.status}`);
|
||||||
|
}
|
||||||
|
return body.json as T;
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { existsSync, mkdtempSync, readFileSync } from "node:fs";
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import {
|
import {
|
||||||
@ -19,6 +19,8 @@ import {
|
|||||||
createRepos,
|
createRepos,
|
||||||
createThreadEvents,
|
createThreadEvents,
|
||||||
createThreadMessage,
|
createThreadMessage,
|
||||||
|
computerHomeKey,
|
||||||
|
ensureComputerRecord,
|
||||||
ensureMainBot,
|
ensureMainBot,
|
||||||
joinMainConversation,
|
joinMainConversation,
|
||||||
RunHistoryWriteError,
|
RunHistoryWriteError,
|
||||||
@ -1798,6 +1800,30 @@ describeJourneys("required product journeys", () => {
|
|||||||
instructions: "",
|
instructions: "",
|
||||||
notifyOnFinish: true,
|
notifyOnFinish: true,
|
||||||
});
|
});
|
||||||
|
const homeKey = computerHomeKey("team", bot.spaceId);
|
||||||
|
const teamComputer = await ensureComputerRecord(prisma, {
|
||||||
|
mode: "team",
|
||||||
|
spaceId: bot.spaceId,
|
||||||
|
userId: me.userId,
|
||||||
|
kind: "fake",
|
||||||
|
});
|
||||||
|
const teamHomePath = path.join(dataDir, "homes", homeKey);
|
||||||
|
mkdirSync(path.join(teamHomePath, "account-delete-marker"), { recursive: true });
|
||||||
|
const fakeSandbox = sandbox as FakeSandboxProvider;
|
||||||
|
const teamRef = await fakeSandbox.provision(
|
||||||
|
{ botId: homeKey, homePath: teamHomePath },
|
||||||
|
{
|
||||||
|
operationId: "journey-11-seed",
|
||||||
|
traceId: "journey-11-seed",
|
||||||
|
spaceId: bot.spaceId,
|
||||||
|
userId: me.userId,
|
||||||
|
signal: AbortSignal.timeout(15_000),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await prisma.computer.update({
|
||||||
|
where: { id: teamComputer.id },
|
||||||
|
data: { providerRef: teamRef.id, state: "running" },
|
||||||
|
});
|
||||||
|
|
||||||
const deleted = await app.request("/api/auth/delete-user", {
|
const deleted = await app.request("/api/auth/delete-user", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -1814,6 +1840,8 @@ describeJourneys("required product journeys", () => {
|
|||||||
expect(await prisma.organization.findUnique({ where: { id: me.spaceId } })).toBeNull();
|
expect(await prisma.organization.findUnique({ where: { id: me.spaceId } })).toBeNull();
|
||||||
expect(await prisma.bot.findUnique({ where: { id: bot.id } })).toBeNull();
|
expect(await prisma.bot.findUnique({ where: { id: bot.id } })).toBeNull();
|
||||||
expect((await raw(app, cookie, "me")).status).toBeGreaterThanOrEqual(400);
|
expect((await raw(app, cookie, "me")).status).toBeGreaterThanOrEqual(400);
|
||||||
|
expect(existsSync(teamHomePath)).toBe(false);
|
||||||
|
expect(fakeSandbox.boxes.has(teamRef.id)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("12: a bot can spawn a regular bot and must confirm the name to delete it", async () => {
|
it("12: a bot can spawn a regular bot and must confirm the name to delete it", async () => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user