evOo-local/packages/testkit/src/journeys.test.ts
Claude 4875753d9c
feat(memory): remember appends dated notes instead of replacing memory
The remember tool rewrote the bot's whole MEMORY.md with each call, so every
new fact erased the earlier ones unless the model re-sent the full document.

- MemoryCommitRequest gains mode "append" (the store joins the note onto the
  current content inside the same serializable transaction and still writes a
  revision row) and every commit is capped at 16 KB per document.
- remember appends a dated bullet ("- YYYY-MM-DD: ...", the user's local date)
  by default and rewrites only with mode "replace"; a full document returns an
  error that tells the bot to condense it. Retries of the same tool call stay
  single appends through the existing effect record.
- Scripted runtime memory entries can append too; the scripted "remember"
  script now appends instead of overwriting the bot's memory.
- When memory does not fit the run's 32 KB budget, the prompt keeps a
  document's newest lines instead of its oldest.
- memory.update maps an oversized save to BAD_REQUEST.
- docs/PRODUCT.md records that UserFact is the cross-space user model and the
  space MEMORY.md stays space notes (no migration).

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

3122 lines
113 KiB
TypeScript

import { existsSync, mkdtempSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import {
ComposioEmulator,
cancelScheduleFromTool,
createScheduleFromTool,
DesktopSandboxProvider,
FakeSandboxProvider,
handoffToGroupBot,
listSchedulesFromTool,
ManagedSandboxEmulator,
resolveScheduleOwner,
toComputerRef,
} from "@rakazo/adapters";
import { ACTIVE_RUN_STATUSES, ONCE_ROUTINE_CRON } from "@rakazo/core";
import {
appendEvent,
createRepos,
createThreadEvents,
createThreadMessage,
ensureMainBot,
joinMainConversation,
RunHistoryWriteError,
} from "@rakazo/db";
import { afterAll, beforeAll, describe, expect, it, vi } 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> };
process.env.WAKEUP_DRIVER = "memory";
process.env.SANDBOX_PROVIDER = "fake";
process.env.AGENT_RUNTIME = "scripted";
const hasDb = process.env.VERIFY_DATABASE === "1" && Boolean(process.env.DATABASE_URL);
const describeJourneys = hasDb ? describe : describe.skip;
describeJourneys("required product journeys", () => {
let app: App;
let stop: () => Promise<void>;
let prisma: Awaited<ReturnType<typeof createApp>>["prisma"];
let connector: Awaited<ReturnType<typeof createApp>>["connector"];
let executor: Awaited<ReturnType<typeof createApp>>["executor"];
let jobs: Awaited<ReturnType<typeof createApp>>["jobs"];
let sandbox: Awaited<ReturnType<typeof createApp>>["sandbox"];
const stamp = Date.now();
const dataDir = mkdtempSync(path.join(tmpdir(), "rakazo-journey-"));
async function sendAndWait(app: App, cookie: string, botId: string, text: string) {
const { runId } = await rpc<{ runId: string }>(app, cookie, "threads/send", { botId, text });
let terminal: { status: string; error: string | null } | null = null;
await waitForDatabase(async () => {
terminal = await prisma.run.findUnique({
where: { id: runId },
select: { status: true, error: true },
});
return Boolean(terminal && ["completed", "failed", "cancelled"].includes(terminal.status));
});
if (!terminal) throw new Error(`run ${runId} was not found after completion`);
if (terminal.status !== "completed") {
throw new Error(
`run ${runId} ended ${terminal.status}: ${terminal.error ?? "unknown error"}`,
);
}
return {
...(await rpc<Snap>(app, cookie, "threads/get", { botId })),
run: { id: runId, status: terminal.status },
};
}
async function sendGroupAndWait(
app: App,
cookie: string,
groupId: string,
text: string,
waitForBotId?: string,
) {
const { runIds, runId } = await rpc<{ runId: string; runIds?: string[] }>(
app,
cookie,
"threads/send",
{
groupId,
text,
},
);
let targets = runIds ?? [runId];
if (waitForBotId) {
const runs = await prisma.run.findMany({
where: { id: { in: targets }, botId: waitForBotId },
select: { id: true },
});
targets = runs.map((run) => run.id);
if (targets.length === 0) {
throw new Error(`no run scheduled for bot ${waitForBotId}`);
}
}
for (const runId of targets) {
let terminal: { status: string; error: string | null } | null = null;
await waitForDatabase(async () => {
terminal = await prisma.run.findUnique({
where: { id: runId },
select: { status: true, error: true },
});
return Boolean(terminal && ["completed", "failed", "cancelled"].includes(terminal.status));
});
if (!terminal) throw new Error(`run ${runId} was not found after completion`);
if (terminal.status !== "completed") {
throw new Error(
`run ${runId} ended ${terminal.status}: ${terminal.error ?? "unknown error"}`,
);
}
}
return rpc<Snap>(app, cookie, "threads/get", { groupId });
}
beforeAll(async () => {
const { createApp } = await import("../../../apps/api/src/app.ts");
const handles = await createApp({
databaseUrl: process.env.DATABASE_URL!,
dataDir,
sandboxProvider: "fake",
agentRuntime: "scripted",
composio: new ComposioEmulator(),
});
app = handles.app;
stop = handles.stop;
prisma = handles.prisma;
connector = handles.connector;
executor = handles.executor;
jobs = handles.jobs;
sandbox = handles.sandbox;
});
afterAll(async () => {
await stop?.();
});
it("computer updates preserve the workspace and reserve the shared computer until completion", async () => {
const cookie = await signup(app, `maintenance-${stamp}@rakazo.test`, "Maintenance");
const outsider = await signup(app, `maintenance-other-${stamp}@rakazo.test`, "Other workspace");
const bot = await rpc<Bot>(app, cookie, "bots/create", {
name: "Writer",
title: "Writer",
description: "Writes files",
instructions: "Write files.",
});
await waitForDatabase(
async () =>
(await prisma.run.count({
where: { botId: bot.id, status: { in: [...ACTIVE_RUN_STATUSES] } },
})) === 0,
);
await rpc(app, cookie, "computer/boot", { botId: bot.id });
const original = await prisma.bot.findUniqueOrThrow({
where: { id: bot.id },
include: { computer: true },
});
const ctx = {
operationId: "fixture",
traceId: "fixture",
spaceId: original.spaceId,
userId: original.userId,
botId: bot.id,
signal: new AbortController().signal,
};
await sandbox.writeFile(
toComputerRef(original.computer!),
{ path: "notes.txt", content: new TextEncoder().encode("durable work") },
ctx,
);
let release!: () => void;
const gate = new Promise<void>((resolve) => {
release = resolve;
});
const destroyed = vi.spyOn(sandbox, "destroy");
const exportWorkspace = sandbox.exportWorkspace.bind(sandbox);
const spy = vi
.spyOn(sandbox, "exportWorkspace")
.mockImplementation(async function* (ref, context) {
await gate;
yield* exportWorkspace(ref, context);
});
try {
const update = await rpc<{ id: string }>(app, cookie, "computer/update", { botId: bot.id });
await waitForDatabase(
async () =>
(await prisma.computerUpdate.findUniqueOrThrow({ where: { id: update.id } })).stage ===
"saving",
);
expect(await rpc(app, outsider, "computer/updates")).toEqual([]);
const duplicate = await app.request("/rpc/computer/update", {
method: "POST",
headers: { "Content-Type": "application/json", Cookie: cookie },
body: JSON.stringify({ json: { botId: bot.id } }),
});
expect(duplicate.status).toBe(409);
const stopped = await app.request("/rpc/computer/stop", {
method: "POST",
headers: { "Content-Type": "application/json", Cookie: cookie },
body: JSON.stringify({ json: { botId: bot.id } }),
});
expect(stopped.status).toBe(409);
const switched = await app.request("/rpc/bots/setComputer", {
method: "POST",
headers: { "Content-Type": "application/json", Cookie: cookie },
body: JSON.stringify({ json: { botId: bot.id, mode: "dedicated" } }),
});
expect(switched.status).toBe(409);
release();
await waitForDatabase(
async () =>
(await prisma.computerUpdate.findUniqueOrThrow({ where: { id: update.id } })).status ===
"completed",
);
const replacement = await prisma.computer.findUniqueOrThrow({
where: { id: original.computerId! },
});
expect(replacement.maintenanceId).toBeNull();
expect(destroyed).toHaveBeenCalledWith(
expect.objectContaining({ providerRef: original.computer!.providerRef }),
expect.anything(),
);
expect(
new TextDecoder().decode(
await sandbox.readFile(toComputerRef(replacement), "notes.txt", ctx),
),
).toBe("durable work");
} finally {
release();
spy.mockRestore();
destroyed.mockRestore();
}
});
it("1+2: users are isolated and workspace bots share the Team Computer", async () => {
const ada = await signup(app, `ada-j-${stamp}@rakazo.test`, "Ada Journey");
const bob = await signup(app, `bob-j-${stamp}@rakazo.test`, "Bob Journey");
const adaMe = await rpc<Me>(app, ada, "me");
const bobMe = await rpc<Me>(app, bob, "me");
expect(adaMe.spaceId).not.toBe(bobMe.spaceId);
const chief = await rpc<Bot>(app, ada, "bots/create", {
name: "Chief",
title: "Chief of staff",
description: "Keeps work moving",
instructions: "",
notifyOnFinish: true,
});
const coder = await rpc<Bot>(app, ada, "bots/create", {
name: "Coder",
title: "Engineer",
description: "Writes code",
instructions: "",
notifyOnFinish: true,
});
const bobBot = await rpc<Bot>(app, bob, "bots/create", {
name: "Chief",
title: "Chief of staff",
description: "Bob's bot",
instructions: "",
notifyOnFinish: true,
});
const [chiefRecord, coderRecord, bobRecord] = await Promise.all(
[chief.id, coder.id, bobBot.id].map((id) =>
prisma.bot.findUniqueOrThrow({ where: { id }, include: { computer: true } }),
),
);
expect(chief.computerMode).toBe("team");
expect(coder.computerMode).toBe("team");
expect(chiefRecord.computerId).toBe(coderRecord.computerId);
expect(chiefRecord.computerId).not.toBe(bobRecord.computerId);
const bobList = await rpc<Bot[]>(app, bob, "bots/list");
expect(bobList.map((b) => b.id)).not.toContain(chief.id);
const forbidden = await raw(app, bob, "bots/get", { botId: chief.id });
expect(forbidden.status).toBeGreaterThanOrEqual(400);
const pinned = await rpc<Bot>(app, ada, "bots/update", { botId: coder.id, pinned: true });
expect(pinned.pinned).toBe(true);
const [section, concurrentSection] = await Promise.all([
rpc<{ id: string; name: string }>(app, ada, "botSections/create", {
botId: chief.id,
name: "Planning",
}),
rpc<{ id: string; name: string }>(app, ada, "botSections/create", {
botId: coder.id,
name: "Planning",
}),
]);
expect(section.name).toBe("Planning");
expect(concurrentSection.id).toBe(section.id);
expect(await rpc<Array<{ id: string }>>(app, ada, "botSections/list")).toEqual([
expect.objectContaining({ id: section.id }),
]);
expect(
(await rpc<Bot[]>(app, ada, "bots/list")).find((bot) => bot.id === chief.id)?.sectionId,
).toBe(section.id);
expect(
(await rpc<Bot[]>(app, ada, "bots/list")).find((bot) => bot.id === coder.id)?.sectionId,
).toBe(section.id);
const foreignSection = await raw(app, bob, "bots/update", {
botId: bobBot.id,
sectionId: section.id,
});
expect(foreignSection.status).toBeGreaterThanOrEqual(400);
expect(await rpc<unknown[]>(app, bob, "botSections/list")).toEqual([]);
const renamed = await rpc<{ id: string; name: string }>(app, ada, "botSections/update", {
sectionId: section.id,
name: "Delivery",
});
expect(renamed).toMatchObject({ id: section.id, name: "Delivery" });
expect(await rpc<Array<{ id: string; name: string }>>(app, ada, "botSections/list")).toEqual([
expect.objectContaining({ id: section.id, name: "Delivery" }),
]);
await rpc<{ id: string; name: string }>(app, ada, "botSections/create", {
botId: coder.id,
name: "Archive",
});
const renameClash = await raw(app, ada, "botSections/update", {
sectionId: section.id,
name: "Archive",
});
expect(renameClash.status).toBe(409);
const foreignRename = await raw(app, bob, "botSections/update", {
sectionId: section.id,
name: "Stolen",
});
expect(foreignRename.status).toBeGreaterThanOrEqual(400);
expect(await rpc<Array<{ id: string; name: string }>>(app, ada, "botSections/list")).toEqual(
expect.arrayContaining([expect.objectContaining({ id: section.id, name: "Delivery" })]),
);
const duplicate = await rpc<Bot>(app, ada, "bots/duplicate", { botId: chief.id });
expect(duplicate).toMatchObject({
name: "Chief copy",
title: chief.title,
description: chief.description,
instructions: chief.instructions,
notifyOnFinish: chief.notifyOnFinish,
color: chief.color,
pinned: false,
sectionId: null,
unread: false,
});
expect(duplicate.id).not.toBe(chief.id);
expect((await rpc<Bot[]>(app, ada, "bots/list"))[0]?.id).toBe(coder.id);
await sendAndWait(
app,
ada,
chief.id,
"write a file in your home called notes/result.txt that says isolation-ok",
);
expect(
(await rpc<Bot[]>(app, ada, "bots/list")).find((bot) => bot.id === chief.id)?.unread,
).toBe(true);
await rpc(app, ada, "threads/markRead", { botId: chief.id });
expect(
(await rpc<Bot[]>(app, ada, "bots/list")).find((bot) => bot.id === chief.id)?.unread,
).toBe(false);
await rpc(app, ada, "threads/markUnread", { botId: chief.id });
expect(
(await rpc<Bot[]>(app, ada, "bots/list")).find((bot) => bot.id === chief.id)?.unread,
).toBe(true);
await sendAndWait(app, ada, coder.id, "remember that coder prefers rust");
const chiefFile = await rpc<{ path: string; content: string }>(app, ada, "computer/readFile", {
botId: chief.id,
path: "notes/result.txt",
});
expect(chiefFile.content).toContain("isolation-ok");
const computer = await rpc<{ state: string }>(app, ada, "computer/status", { botId: chief.id });
expect(computer.state).toBe("running");
await rpc(app, ada, "computer/stop", { botId: chief.id });
const persisted = await rpc<{ content: string }>(app, ada, "computer/readFile", {
botId: chief.id,
path: "notes/result.txt",
});
expect(persisted.content).toContain("isolation-ok");
const coderMem = await rpc<Array<{ content: string }>>(app, ada, "memory/list", {
botId: coder.id,
});
expect(coderMem.some((m) => m.content.toLowerCase().includes("rust"))).toBe(true);
// Remembering appends a dated note below the seeded heading instead of replacing it.
expect(coderMem.map((m) => m.content)).toContainEqual(
expect.stringMatching(/^# Coder\n\n- \d{4}-\d{2}-\d{2}: remember that coder prefers rust\n$/),
);
const dedicated = await rpc<Bot>(app, ada, "bots/setComputer", {
botId: coder.id,
mode: "dedicated",
});
expect(dedicated.computerMode).toBe("dedicated");
const dedicatedRecord = await prisma.bot.findUniqueOrThrow({
where: { id: coder.id },
include: { computer: true },
});
expect(dedicatedRecord.computerId).not.toBe(chiefRecord.computerId);
await rpc<Bot>(app, ada, "bots/setComputer", { botId: coder.id, mode: "team" });
const backOnTeam = await prisma.bot.findUniqueOrThrow({ where: { id: coder.id } });
expect(backOnTeam.computerId).toBe(chiefRecord.computerId);
await rpc<Bot>(app, ada, "bots/setComputer", { botId: coder.id, mode: "dedicated" });
const reusedDedicated = await prisma.bot.findUniqueOrThrow({ where: { id: coder.id } });
expect(reusedDedicated.computerId).toBe(dedicatedRecord.computerId);
expect(bobBot.id).not.toBe(chief.id);
});
it("clears a conversation without removing the bot, computer, memory, or routines", async () => {
const cookie = await signup(app, `clear-j-${stamp}@rakazo.test`, "Clear Journey");
const bot = await rpc<Bot>(app, cookie, "bots/create", {
name: "Keeper",
title: "Keeps its setup",
description: "",
instructions: "",
notifyOnFinish: true,
});
await sendAndWait(
app,
cookie,
bot.id,
"write a file in your home called notes/result.txt that says kept-after-clear",
);
const memories = await rpc<Array<{ id: string }>>(app, cookie, "memory/list", {
botId: bot.id,
});
await rpc(app, cookie, "memory/update", {
documentId: memories[0]!.id,
content: "# Keeper\n\nRemember this after clearing.",
});
const routine = await rpc<{ id: string }>(app, cookie, "routines/create", {
botId: bot.id,
name: "Kept routine",
prompt: "Check in",
crons: ["0 9 * * 1"],
timezone: "UTC",
notify: false,
active: false,
});
const thread = await prisma.thread.findUniqueOrThrow({ where: { botId: bot.id } });
const task = await prisma.task.create({
data: {
spaceId: thread.spaceId,
userId: thread.userId,
botId: bot.id,
threadId: thread.id,
prompt: "queued before clear",
status: "queued",
},
});
const run = await prisma.run.create({
data: {
spaceId: thread.spaceId,
userId: thread.userId,
botId: bot.id,
threadId: thread.id,
taskId: task.id,
status: "queued",
trigger: "user",
},
});
await prisma.thread.update({
where: { id: thread.id },
data: {
historyCompactedUpToSeq: 0,
historyCompactionSummary: "summary that must be invalidated",
historyCompactionGeneration: 3,
},
});
await rpc(app, cookie, "threads/clear", { botId: bot.id });
const snap = await rpc<Snap>(app, cookie, "threads/get", { botId: bot.id });
expect(snap.messages).toEqual([]);
expect(snap.run).toBeNull();
expect(await prisma.message.count({ where: { threadId: thread.id } })).toBe(0);
expect(await prisma.event.findMany({ where: { threadId: thread.id } })).toMatchObject([
{ type: "thread.cleared" },
]);
// The deleted messages all count as compacted, so compaction cannot summarize them and
// recall cannot treat the fresh conversation as having uncompacted history.
const clearedThread = await prisma.thread.findUniqueOrThrow({ where: { id: thread.id } });
expect(clearedThread.historyCompactedUpToSeq).toBe(clearedThread.nextMessageSeq - 1);
expect(clearedThread.historyCompactionSummary).toBeNull();
expect(clearedThread.historyCompactionGeneration).toBe(4);
expect(await prisma.run.findUniqueOrThrow({ where: { id: run.id } })).toMatchObject({
status: "cancelled",
});
expect(await prisma.task.findUniqueOrThrow({ where: { id: task.id } })).toMatchObject({
status: "cancelled",
});
expect(
(await rpc<Bot[]>(app, cookie, "bots/list")).find((item) => item.id === bot.id),
).toMatchObject({
preview: "",
unread: false,
});
expect(
await rpc(app, cookie, "computer/readFile", { botId: bot.id, path: "notes/result.txt" }),
).toMatchObject({ content: expect.stringContaining("kept-after-clear") });
expect(await rpc(app, cookie, "memory/list", { botId: bot.id })).toEqual(
expect.arrayContaining([
expect.objectContaining({ content: expect.stringContaining("Remember this") }),
]),
);
expect(await rpc(app, cookie, "routines/list", { botId: bot.id })).toEqual(
expect.arrayContaining([expect.objectContaining({ id: routine.id })]),
);
await expect(
appendEvent(prisma, {
spaceId: thread.spaceId,
threadId: thread.id,
botId: bot.id,
type: "thread.progress",
runId: run.id,
payload: { text: "stale output after clear" },
}),
).rejects.toThrow(RunHistoryWriteError);
await expect(
createThreadMessage(prisma, {
threadId: thread.id,
role: "bot",
blocks: [{ kind: "text", text: "stale output after clear" }],
runId: run.id,
}),
).rejects.toThrow(RunHistoryWriteError);
expect(await prisma.message.count({ where: { threadId: thread.id } })).toBe(0);
expect(await prisma.event.findMany({ where: { threadId: thread.id } })).toMatchObject([
{ type: "thread.cleared" },
]);
const after = await sendAndWait(
app,
cookie,
bot.id,
"write a file in your home called notes/after-clear.txt that says hello-after-clear",
);
expect(after.messages.length).toBeGreaterThan(0);
expect(await prisma.message.count({ where: { threadId: thread.id } })).toBeGreaterThan(0);
});
it("2b: two Team bots send at once on distinct screens", async () => {
const cookie = await signup(app, `parallel-j-${stamp}@rakazo.test`, "Parallel");
const writer = await rpc<Bot>(app, cookie, "bots/create", {
name: "Writer",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const researcher = await rpc<Bot>(app, cookie, "bots/create", {
name: "Researcher",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const [writerSnap, researcherSnap] = await Promise.all([
sendAndWait(app, cookie, writer.id, "observe your screen and type writer-desk"),
sendAndWait(app, cookie, researcher.id, "observe your screen and type researcher-desk"),
]);
expect(writerSnap.run?.status).toBe("completed");
expect(researcherSnap.run?.status).toBe("completed");
expect(
(
await rpc<{ busyBotName: string | null }>(app, cookie, "computer/status", {
botId: writer.id,
})
).busyBotName,
).toBeNull();
expect(
(
await rpc<{ busyBotName: string | null }>(app, cookie, "computer/status", {
botId: researcher.id,
})
).busyBotName,
).toBeNull();
const writerScreen = await rpc<{ url: string | null }>(app, cookie, "computer/screenUrl", {
botId: writer.id,
});
const researcherScreen = await rpc<{ url: string | null }>(app, cookie, "computer/screenUrl", {
botId: researcher.id,
});
expect(writerScreen.url).toContain(writer.id);
expect(researcherScreen.url).toContain(researcher.id);
expect(writerScreen.url).not.toBe(researcherScreen.url);
});
it("3: disconnect and reconnect from a cursor reconstructs the thread", async () => {
const cookie = await signup(app, `cursor-j-${stamp}@rakazo.test`, "Cursor");
const bot = await rpc<Bot>(app, cookie, "bots/create", {
name: "Chief",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
await sendAndWait(
app,
cookie,
bot.id,
"write a file in your home called notes/result.txt that says reconnect-ok",
);
const snap = await rpc<Snap>(app, cookie, "threads/get", { botId: bot.id });
expect(snap.messages.map((m) => m.seq)).toEqual(
[...snap.messages].map((m) => m.seq).sort((a, b) => a - b),
);
expect(snap.messages.some((m) => JSON.stringify(m.blocks).includes("reconnect-ok"))).toBe(true);
const again = await rpc<Snap>(app, cookie, "threads/get", { botId: bot.id });
expect(again.messages.length).toBe(snap.messages.length);
});
it("4: takeover login then resume without exposing credentials", async () => {
const cookie = await signup(app, `takeover-j-${stamp}@rakazo.test`, "Takeover");
const bot = await rpc<Bot>(app, cookie, "bots/create", {
name: "Chief",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
await rpc(app, cookie, "threads/send", {
botId: bot.id,
text: "install the gsc cli and sign in",
});
const waiting = await waitFor(
app,
cookie,
bot.id,
(snap) => snap.run?.status === "waiting_takeover",
);
expect(JSON.stringify(waiting.messages)).not.toMatch(/password|secret|token/i);
await rpc(app, cookie, "computer/boot", { botId: bot.id });
await rpc(app, cookie, "computer/takeover", { botId: bot.id });
await rpc(app, cookie, "computer/release", { botId: bot.id });
const done = await waitFor(
app,
cookie,
bot.id,
(snap) => !snap.run || ["completed", "failed", "cancelled"].includes(snap.run.status),
);
expect(JSON.stringify(done.messages).toLowerCase()).toMatch(/signed in|session/);
expect(done.run?.status ?? "completed").not.toBe("waiting_takeover");
const releaseEvents = await prisma.event.count({
where: { botId: bot.id, type: "computer.takeover.released" },
});
await rpc(app, cookie, "computer/release", { botId: bot.id });
expect(
await prisma.event.count({
where: { botId: bot.id, type: "computer.takeover.released" },
}),
).toBe(releaseEvents);
});
it("4d: skipping takeover resumes without treating login as done", async () => {
const cookie = await signup(app, `takeover-skip-j-${stamp}@rakazo.test`, "Skip Takeover");
const bot = await rpc<Bot>(app, cookie, "bots/create", {
name: "Chief",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
await rpc(app, cookie, "threads/send", {
botId: bot.id,
text: "install the gsc cli and sign in",
});
const waiting = await waitFor(
app,
cookie,
bot.id,
(snap) => snap.run?.status === "waiting_takeover",
);
await rpc(app, cookie, "computer/boot", { botId: bot.id });
await rpc(app, cookie, "computer/takeover", { botId: bot.id });
await rpc(app, cookie, "computer/release", { botId: bot.id, reason: "skipped" });
const done = await waitFor(
app,
cookie,
bot.id,
(snap) => !snap.run || ["completed", "failed", "cancelled"].includes(snap.run.status),
);
expect(JSON.stringify(done.messages).toLowerCase()).toMatch(/skipped/);
expect(JSON.stringify(done.messages).toLowerCase()).not.toMatch(/signed in/);
expect(done.run?.status ?? "completed").not.toBe("waiting_takeover");
const released = await prisma.event.findFirst({
where: { botId: bot.id, type: "computer.takeover.released" },
orderBy: { createdAt: "desc" },
});
expect(released).toMatchObject({
runId: waiting.run?.id,
payload: { reason: "skipped" },
});
});
it("4b: an expired takeover denies input and reconciles API and database state", async () => {
const previousTakeoverTtl = process.env.COMPUTER_TAKEOVER_TTL_MS;
process.env.COMPUTER_TAKEOVER_TTL_MS = "1000";
try {
const cookie = await signup(app, `takeover-expiry-j-${stamp}@rakazo.test`, "Expiry");
const bot = await rpc<Bot>(app, cookie, "bots/create", {
name: "Chief",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
await rpc(app, cookie, "threads/send", {
botId: bot.id,
text: "install the gsc cli and sign in",
});
const waiting = await waitFor(
app,
cookie,
bot.id,
(snap) => snap.run?.status === "waiting_takeover",
);
await rpc(app, cookie, "computer/boot", { botId: bot.id });
const lease = await rpc<{ leaseId: string; expiresAt: string }>(
app,
cookie,
"computer/takeover",
{ botId: bot.id },
);
expect(new Date(lease.expiresAt).getTime() - Date.now()).toBeLessThanOrEqual(1000);
expect(
(await rpc<{ url: string | null }>(app, cookie, "computer/screenUrl", { botId: bot.id }))
.url,
).toContain("view_only=false");
await rpc(app, cookie, "computer/input", {
botId: bot.id,
kind: "key",
payload: { key: "a" },
});
await waitForDatabase(async () => {
const storedBot = await prisma.bot.findUniqueOrThrow({
where: { id: bot.id },
include: { computer: true },
});
const computer = storedBot.computer!;
return computer.controlLeaseId === null && computer.controlHolder === "none";
});
expect(
(await rpc<{ controlHolder: string }>(app, cookie, "computer/status", { botId: bot.id }))
.controlHolder,
).toBe("none");
expect(
(await rpc<{ url: string | null }>(app, cookie, "computer/screenUrl", { botId: bot.id }))
.url,
).toContain("view_only=true");
expect(
(
await raw(app, cookie, "computer/input", {
botId: bot.id,
kind: "key",
payload: { key: "b" },
})
).status,
).toBeGreaterThanOrEqual(400);
const done = await waitFor(
app,
cookie,
bot.id,
(snap) => !snap.run || ["completed", "failed", "cancelled"].includes(snap.run.status),
);
expect(JSON.stringify(done.messages).toLowerCase()).toMatch(/skipped/);
expect(JSON.stringify(done.messages).toLowerCase()).not.toMatch(/signed in/);
expect(done.run?.status ?? "completed").not.toBe("waiting_takeover");
expect(
await prisma.event.findFirst({
where: { runId: waiting.run?.id, type: "computer.takeover.released" },
orderBy: { createdAt: "desc" },
}),
).toMatchObject({ payload: { reason: "expired" } });
} finally {
if (previousTakeoverTtl === undefined) delete process.env.COMPUTER_TAKEOVER_TTL_MS;
else process.env.COMPUTER_TAKEOVER_TTL_MS = previousTakeoverTtl;
}
});
it("4c: a takeover authorizes input only on the controlled bot screen", async () => {
const cookie = await signup(app, `takeover-scope-j-${stamp}@rakazo.test`, "Takeover Scope");
const writer = await rpc<Bot>(app, cookie, "bots/create", {
name: "Writer",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const researcher = await rpc<Bot>(app, cookie, "bots/create", {
name: "Researcher",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
await rpc(app, cookie, "computer/boot", { botId: writer.id });
await rpc(app, cookie, "computer/takeover", { botId: writer.id });
expect(
(
await raw(app, cookie, "computer/input", {
botId: researcher.id,
kind: "key",
payload: { key: "A" },
})
).status,
).toBeGreaterThanOrEqual(400);
await rpc(app, cookie, "computer/release", { botId: writer.id });
});
it("4d: a stale Team release cannot clear a newer bot takeover", async () => {
const cookie = await signup(
app,
`takeover-release-fence-j-${stamp}@rakazo.test`,
"Release Fence",
);
const writer = await rpc<Bot>(app, cookie, "bots/create", {
name: "Writer",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const researcher = await rpc<Bot>(app, cookie, "bots/create", {
name: "Researcher",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
await rpc(app, cookie, "computer/boot", { botId: writer.id });
await rpc(app, cookie, "threads/send", {
botId: researcher.id,
text: "install the gsc cli and sign in",
});
await waitFor(app, cookie, researcher.id, (snap) => snap.run?.status === "waiting_takeover");
const writerLease = await rpc<{ leaseId: string; expiresAt: string }>(
app,
cookie,
"computer/takeover",
{ botId: writer.id },
);
const releaseEventsBefore = await prisma.event.count({
where: { botId: researcher.id, type: "computer.takeover.released" },
});
const originalSetScreenControl = sandbox.setScreenControl;
let signalReleaseRevocation!: () => void;
let allowReleaseRevocation!: () => void;
const releaseRevocationReached = new Promise<void>((resolve) => {
signalReleaseRevocation = resolve;
});
const releaseRevocationAllowed = new Promise<void>((resolve) => {
allowReleaseRevocation = resolve;
});
const revokedLeases: string[] = [];
sandbox.setScreenControl = async (_computer, interactive, context, controlToken) => {
expect(interactive).toBe(false);
revokedLeases.push(controlToken ?? "");
if (revokedLeases.length !== 1) return;
expect(context.botId).toBe(writer.id);
expect(controlToken).toBe(writerLease.leaseId);
signalReleaseRevocation();
await releaseRevocationAllowed;
};
let staleRelease: Promise<{ ok: true }> | undefined;
let researcherLease!: { leaseId: string; expiresAt: string };
try {
// Pause Release after it captures Writer's lease but before database finalization. The
// replacement takeover then commits first, so the resumed Release must lose its lease CAS.
staleRelease = rpc<{ ok: true }>(app, cookie, "computer/release", { botId: writer.id });
await releaseRevocationReached;
researcherLease = await rpc<{ leaseId: string; expiresAt: string }>(
app,
cookie,
"computer/takeover",
{ botId: researcher.id },
);
allowReleaseRevocation();
await staleRelease;
} finally {
allowReleaseRevocation();
await staleRelease?.catch(() => undefined);
sandbox.setScreenControl = originalSetScreenControl;
}
expect(researcherLease.leaseId).not.toBe(writerLease.leaseId);
expect(revokedLeases).toEqual([writerLease.leaseId, writerLease.leaseId]);
const writerRecord = await prisma.bot.findUniqueOrThrow({
where: { id: writer.id },
include: { computer: true },
});
const researcherRecord = await prisma.bot.findUniqueOrThrow({
where: { id: researcher.id },
include: { computer: true },
});
const computerId = writerRecord.computer!.id;
expect(researcherRecord.computer!.id).toBe(computerId);
const afterStaleRelease = await prisma.computer.findUniqueOrThrow({
where: { id: computerId },
});
expect(afterStaleRelease.controlHolder).toBe("user");
expect(afterStaleRelease.controlBotId).toBe(researcher.id);
expect(afterStaleRelease.controlLeaseId).toBe(researcherLease.leaseId);
expect(afterStaleRelease.controlLeaseExpiresAt?.toISOString()).toBe(researcherLease.expiresAt);
expect(
await prisma.event.count({
where: { botId: researcher.id, type: "computer.takeover.released" },
}),
).toBe(releaseEventsBefore);
expect(
(
await prisma.run.findFirstOrThrow({
where: { botId: researcher.id, status: "waiting_takeover" },
})
).status,
).toBe("waiting_takeover");
await rpc(app, cookie, "computer/release", { botId: researcher.id });
const afterOwnerRelease = await prisma.computer.findUniqueOrThrow({
where: { id: computerId },
});
expect(afterOwnerRelease).toMatchObject({
controlHolder: "bot",
controlBotId: null,
controlLeaseId: null,
controlLeaseExpiresAt: null,
});
await waitFor(
app,
cookie,
researcher.id,
(snap) => !snap.run || ["completed", "failed", "cancelled"].includes(snap.run.status),
);
expect(
await prisma.event.count({
where: { botId: researcher.id, type: "computer.takeover.released" },
}),
).toBe(releaseEventsBefore + 1);
const releaseEvent = await prisma.event.findFirstOrThrow({
where: { botId: researcher.id, type: "computer.takeover.released" },
orderBy: { seq: "desc" },
});
expect(releaseEvent.payload).toMatchObject({
holder: "bot",
leaseId: researcherLease.leaseId,
reason: "released",
});
await rpc(app, cookie, "computer/release", { botId: researcher.id });
expect(
await prisma.event.count({
where: { botId: researcher.id, type: "computer.takeover.released" },
}),
).toBe(releaseEventsBefore + 1);
});
it("4e: concurrent Team takeovers never return another bot's lease", async () => {
const cookie = await signup(
app,
`takeover-owner-race-j-${stamp}@rakazo.test`,
"Takeover Owner Race",
);
const writer = await rpc<Bot>(app, cookie, "bots/create", {
name: "Writer",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const researcher = await rpc<Bot>(app, cookie, "bots/create", {
name: "Researcher",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const analyst = await rpc<Bot>(app, cookie, "bots/create", {
name: "Analyst",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
await rpc(app, cookie, "computer/boot", { botId: writer.id });
const writerLease = await rpc<{ leaseId: string }>(app, cookie, "computer/takeover", {
botId: writer.id,
});
const originalSetScreenControl = sandbox.setScreenControl;
let revocations = 0;
let releaseRevocations!: () => void;
const bothRevocationsReached = new Promise<void>((resolve) => {
releaseRevocations = resolve;
});
const barrierTimeout = setTimeout(releaseRevocations, 1_000);
sandbox.setScreenControl = async (_computer, interactive, _context, controlToken) => {
expect(interactive).toBe(false);
expect(controlToken).toBe(writerLease.leaseId);
revocations += 1;
if (revocations === 2) releaseRevocations();
await bothRevocationsReached;
};
let responses: Response[] = [];
try {
responses = await Promise.all([
raw(app, cookie, "computer/takeover", { botId: researcher.id }),
raw(app, cookie, "computer/takeover", { botId: analyst.id }),
]);
} finally {
clearTimeout(barrierTimeout);
releaseRevocations();
sandbox.setScreenControl = originalSetScreenControl;
}
expect(revocations).toBe(2);
expect(responses.map((response) => response.status).sort()).toEqual([200, 409]);
const computer = await prisma.computer.findFirstOrThrow({
where: { bots: { some: { id: writer.id } } },
select: { controlBotId: true, controlLeaseId: true },
});
const winner = computer.controlBotId === researcher.id ? researcher : analyst;
const successfulResponse = responses.find((response) => response.status === 200)!;
await expect(successfulResponse.clone().json()).resolves.toMatchObject({
json: { leaseId: computer.controlLeaseId },
});
await rpc(app, cookie, "computer/release", { botId: winner.id });
});
it("5: a routine wakes the bot and posts into the existing thread", async () => {
const cookie = await signup(app, `routine-j-${stamp}@rakazo.test`, "Routine");
const bot = await rpc<Bot>(app, cookie, "bots/create", {
name: "Chief",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const routine = await rpc<{ id: string; nextRunAt: string | null }>(
app,
cookie,
"routines/create",
{
botId: bot.id,
name: "Monday briefing",
prompt: "write a file in your home called notes/result.txt that says routine-ok",
crons: ["0 9 * * 1"],
timezone: "UTC",
notify: true,
active: true,
},
);
expect(routine.nextRunAt).toBeTruthy();
const dueAt = new Date(Date.now() - 1_000);
await prisma.routine.update({
where: { id: routine.id },
data: { nextRunAt: dueAt },
});
await jobs.enqueue({
name: "routine.wakeup",
payload: { routineId: routine.id, scheduledFor: dueAt.toISOString() },
});
await jobs.enqueue({
name: "routine.wakeup",
payload: { routineId: routine.id, scheduledFor: dueAt.toISOString() },
});
const snap = await waitFor(app, cookie, bot.id, (s) =>
s.messages.some(
(m) =>
JSON.stringify(m.blocks).includes("routine-ok") ||
JSON.stringify(m.blocks).includes("writing"),
),
);
expect(snap.messages.length).toBeGreaterThan(0);
const routineRuns = await prisma.run.count({
where: { botId: bot.id, trigger: "routine" },
});
expect(routineRuns).toBe(1);
const advanced = await prisma.routine.findUniqueOrThrow({ where: { id: routine.id } });
expect(advanced.nextRunAt?.getTime()).toBeGreaterThan(dueAt.getTime());
const legacyRunsBefore = await prisma.run.count({
where: { botId: bot.id, trigger: "routine" },
});
const legacyDueAt = new Date(Date.now() - 1_000);
const legacy = await prisma.routine.create({
data: {
spaceId: advanced.spaceId,
userId: advanced.userId,
botId: bot.id,
name: "Legacy schedule",
prompt: "Run the legacy schedule once",
crons: ["0 0 9 * * *"],
timezone: "UTC",
notify: false,
active: true,
nextRunAt: legacyDueAt,
},
});
await jobs.enqueue({
name: "routine.wakeup",
payload: { routineId: legacy.id, scheduledFor: legacyDueAt.toISOString() },
});
await waitForDatabase(async () => {
const stored = await prisma.routine.findUnique({ where: { id: legacy.id } });
return stored?.active === false && stored.nextRunAt === null;
});
expect(await prisma.run.count({ where: { botId: bot.id, trigger: "routine" } })).toBe(
legacyRunsBefore + 1,
);
});
it("5b: tool-created schedules wake in the creating group or 1:1 thread", async () => {
const cookie = await signup(app, `schedule-dest-j-${stamp}@rakazo.test`, "Schedule Dest");
const me = await rpc<Me>(app, cookie, "me");
const bot = await rpc<Bot>(app, cookie, "bots/create", {
name: "Scheduler",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const peer = await rpc<Bot>(app, cookie, "bots/create", {
name: "Peer",
title: "",
description: "",
instructions: "",
notifyOnFinish: false,
});
const dmThread = await prisma.thread.findUniqueOrThrow({ where: { botId: bot.id } });
const group = await rpc<{ id: string; threadId: string }>(app, cookie, "groups/create", {
name: "Schedule room",
botIds: [bot.id, peer.id],
});
const events = createThreadEvents(prisma);
const scheduleDeps = { prisma, events, jobs };
const groupCreated = await createScheduleFromTool(scheduleDeps, {
spaceId: me.spaceId,
botId: bot.id,
userId: me.userId,
threadId: group.threadId,
name: "Group ping",
prompt: "say group-reminder-ok",
schedule: { delaySeconds: 30 },
});
expect(groupCreated).toMatchObject({ ok: true });
if (!("ok" in groupCreated) || !groupCreated.ok)
throw new Error("group schedule create failed");
const groupRoutine = await prisma.routine.findUniqueOrThrow({
where: { id: groupCreated.routineId },
});
expect(groupRoutine.threadId).toBe(group.threadId);
expect(groupRoutine.crons).toEqual([ONCE_ROUTINE_CRON]);
const groupDueAt = new Date(Date.now() - 1_000);
await prisma.routine.update({
where: { id: groupRoutine.id },
data: { nextRunAt: groupDueAt },
});
await executor.wakeRoutine(groupRoutine.id, groupDueAt.toISOString());
await waitForDatabase(async () => {
const run = await prisma.run.findFirst({
where: { routineId: groupRoutine.id, trigger: "routine" },
});
return run?.threadId === group.threadId;
});
const groupRun = await prisma.run.findFirstOrThrow({
where: { routineId: groupRoutine.id, trigger: "routine" },
});
expect(groupRun.threadId).toBe(group.threadId);
expect(groupRun.threadId).not.toBe(dmThread.id);
expect(
await prisma.event.count({
where: {
threadId: group.threadId,
type: "routine.fired",
runId: groupRun.id,
},
}),
).toBe(1);
expect(
await prisma.event.count({
where: {
threadId: dmThread.id,
type: "routine.fired",
runId: groupRun.id,
},
}),
).toBe(0);
const dmCreated = await createScheduleFromTool(scheduleDeps, {
spaceId: me.spaceId,
botId: bot.id,
userId: me.userId,
threadId: dmThread.id,
name: "DM ping",
prompt: "say dm-reminder-ok",
schedule: { delaySeconds: 30 },
});
expect(dmCreated).toMatchObject({ ok: true });
if (!("ok" in dmCreated) || !dmCreated.ok) throw new Error("dm schedule create failed");
const dmRoutine = await prisma.routine.findUniqueOrThrow({
where: { id: dmCreated.routineId },
});
expect(dmRoutine.threadId).toBe(dmThread.id);
const dmDueAt = new Date(Date.now() - 1_000);
await prisma.routine.update({
where: { id: dmRoutine.id },
data: { nextRunAt: dmDueAt },
});
await executor.wakeRoutine(dmRoutine.id, dmDueAt.toISOString());
await waitForDatabase(async () => {
const run = await prisma.run.findFirst({
where: { routineId: dmRoutine.id, trigger: "routine" },
});
return run?.threadId === dmThread.id;
});
const dmRun = await prisma.run.findFirstOrThrow({
where: { routineId: dmRoutine.id, trigger: "routine" },
});
expect(dmRun.threadId).toBe(dmThread.id);
expect(
await prisma.event.count({
where: {
threadId: dmThread.id,
type: "routine.fired",
runId: dmRun.id,
},
}),
).toBe(1);
});
it("5c: the main bot schedules for a specialist in its own space or in the main conversation", async () => {
const cookie = await signup(app, `schedule-behalf-j-${stamp}@rakazo.test`, "Schedule Behalf");
const me = await rpc<Me>(app, cookie, "me");
const main = await ensureMainBot(prisma, me.userId);
const writer = await rpc<Bot>(app, cookie, "bots/create", {
name: "Writer",
title: "",
description: "",
instructions: "",
notifyOnFinish: false,
});
const lab = await rpc<{ id: string }>(app, cookie, "spaces/create", { name: "Lab" });
const scout = await createRepos(prisma).createBot(
{ userId: me.userId, spaceId: lab.id, email: "", isDeploymentOwner: false },
{ name: "Scout", title: "", description: "", instructions: "", notifyOnFinish: false },
);
const scheduleDeps = { prisma, events: createThreadEvents(prisma), jobs };
const creator = {
botId: main.id,
kind: "main",
spaceId: main.spaceId,
userId: me.userId,
threadId: main.threadId,
};
async function scheduleAndWake(botName: string, routineName: string) {
const owner = await resolveScheduleOwner(prisma, creator, botName);
if ("error" in owner) throw new Error(owner.error);
const created = await createScheduleFromTool(scheduleDeps, {
...owner,
name: routineName,
prompt: `say ${routineName}-ok`,
schedule: { delaySeconds: 30 },
});
if (!("ok" in created)) throw new Error(created.error);
expect(created.bot).toBe(botName);
const routine = await prisma.routine.findUniqueOrThrow({ where: { id: created.routineId } });
const dueAt = new Date(Date.now() - 1_000);
await prisma.routine.update({ where: { id: routine.id }, data: { nextRunAt: dueAt } });
await executor.wakeRoutine(routine.id, dueAt.toISOString());
await waitForDatabase(async () =>
Boolean(await prisma.run.findFirst({ where: { routineId: routine.id } })),
);
const run = await prisma.run.findFirstOrThrow({ where: { routineId: routine.id } });
return { routine, run };
}
const inLab = await scheduleAndWake("Scout", "lab-digest");
expect(inLab.routine).toMatchObject({
botId: scout.id,
spaceId: lab.id,
threadId: scout.threadId,
createdByBotId: main.id,
});
expect(inLab.run).toMatchObject({
botId: scout.id,
spaceId: lab.id,
threadId: scout.threadId,
trigger: "routine",
});
await joinMainConversation(prisma, { userId: me.userId, botIds: [writer.id] });
const conversation = await prisma.chatGroup.findFirstOrThrow({
where: { userId: me.userId, kind: "main" },
include: { thread: true },
});
const inMain = await scheduleAndWake("Writer", "main-digest");
expect(inMain.routine).toMatchObject({
botId: writer.id,
threadId: conversation.thread?.id,
createdByBotId: main.id,
});
expect(inMain.run).toMatchObject({ botId: writer.id, threadId: conversation.thread?.id });
const listed = await listSchedulesFromTool(scheduleDeps, {
spaceId: main.spaceId,
botId: main.id,
userId: me.userId,
threadId: conversation.thread?.id,
});
expect(listed.routines.map((routine) => [routine.name, routine.bot])).toEqual(
expect.arrayContaining([
["lab-digest", "Scout"],
["main-digest", "Writer"],
]),
);
expect(
await listSchedulesFromTool(scheduleDeps, {
spaceId: lab.id,
botId: scout.id,
userId: me.userId,
}),
).toMatchObject({ routines: [{ name: "lab-digest" }] });
expect(
await cancelScheduleFromTool(scheduleDeps, {
spaceId: main.spaceId,
botId: main.id,
userId: me.userId,
name: "lab-digest",
}),
).toMatchObject({ ok: true, routineId: inLab.routine.id });
expect(await prisma.routine.findUnique({ where: { id: inLab.routine.id } })).toBeNull();
// Another specialist neither owns nor scheduled the Writer's routine.
expect(
await cancelScheduleFromTool(scheduleDeps, {
spaceId: lab.id,
botId: scout.id,
userId: me.userId,
name: "main-digest",
}),
).toEqual({ error: "Schedule not found." });
expect(await prisma.routine.findUnique({ where: { id: inMain.routine.id } })).not.toBeNull();
const retired = await rpc<Bot>(app, cookie, "bots/create", {
name: "Retired",
title: "",
description: "",
instructions: "",
notifyOnFinish: false,
});
await rpc(app, cookie, "bots/archive", { botId: retired.id });
expect(await resolveScheduleOwner(prisma, creator, "Retired")).toEqual({
error: "No bot named Retired.",
});
expect(
await resolveScheduleOwner(
prisma,
{ ...creator, botId: scout.id, kind: "specialist" },
"Writer",
),
).toEqual({ error: "Only the main agent can schedule for another bot." });
});
it("5d: a bot's builtin tool allowlist holds in its runs and in bots it creates", async () => {
const cookie = await signup(app, `allowlist-j-${stamp}@rakazo.test`, "Allowlist");
const bot = await rpc<Bot & { restrictBuiltinTools: boolean; allowedBuiltinTools: string[] }>(
app,
cookie,
"bots/create",
{
name: "Reader",
title: "",
description: "",
instructions: "",
notifyOnFinish: false,
tags: ["research"],
restrictBuiltinTools: true,
allowedBuiltinTools: ["read_file"],
},
);
expect(bot).toMatchObject({
tags: ["research"],
restrictBuiltinTools: true,
allowedBuiltinTools: ["read_file"],
});
expect(await rpc<string[]>(app, cookie, "bots/builtinTools", { botId: bot.id })).toEqual(
expect.arrayContaining(["read_file", "write_file", "schedule_create"]),
);
await sendAndWait(
app,
cookie,
bot.id,
"write a file in your home called notes/blocked.txt that says blocked",
);
await expect(
rpc(app, cookie, "computer/readFile", { botId: bot.id, path: "notes/blocked.txt" }),
).rejects.toThrow();
await expect(
rpc(app, cookie, "bots/update", { botId: bot.id, allowedBuiltinTools: ["write_fiel"] }),
).rejects.toThrow(/Unknown tools: write_fiel/);
await rpc(app, cookie, "bots/update", {
botId: bot.id,
allowedBuiltinTools: ["write_file", "spawn_bot"],
});
await sendAndWait(
app,
cookie,
bot.id,
"write a file in your home called notes/allowed.txt that says allowed-ok",
);
expect(
(
await rpc<{ content: string }>(app, cookie, "computer/readFile", {
botId: bot.id,
path: "notes/allowed.txt",
})
).content,
).toContain("allowed-ok");
await sendAndWait(app, cookie, bot.id, "create a bot named Junior");
const child = await prisma.bot.findFirstOrThrow({ where: { parentBotId: bot.id } });
expect(child).toMatchObject({
name: "Junior",
restrictBuiltinTools: true,
allowedBuiltinTools: ["write_file", "spawn_bot"],
});
const copy = await rpc<{ restrictBuiltinTools: boolean; allowedBuiltinTools: string[] }>(
app,
cookie,
"bots/duplicate",
{ botId: bot.id },
);
expect(copy).toMatchObject({
restrictBuiltinTools: true,
allowedBuiltinTools: ["write_file", "spawn_bot"],
});
});
it("allocates event and message cursors atomically under concurrent writes", async () => {
const cookie = await signup(app, `sequence-j-${stamp}@rakazo.test`, "Sequence");
const actor = await rpc<Me>(app, cookie, "me");
const bot = await rpc<Bot>(app, cookie, "bots/create", {
name: "Sequencer",
title: "",
description: "",
instructions: "",
notifyOnFinish: false,
});
const thread = await prisma.thread.findUniqueOrThrow({ where: { botId: bot.id } });
await Promise.all(
Array.from({ length: 40 }, (_, index) =>
appendEvent(prisma, {
spaceId: actor.spaceId,
threadId: thread.id,
botId: bot.id,
type: "thread.progress",
payload: { delta: String(index) },
}),
),
);
await Promise.all(
Array.from({ length: 40 }, (_, index) =>
createThreadMessage(prisma, {
threadId: thread.id,
role: "system",
blocks: [{ kind: "meta", text: String(index) }],
}),
),
);
const [events, messages] = await Promise.all([
prisma.event.findMany({ where: { threadId: thread.id }, orderBy: { seq: "asc" } }),
prisma.message.findMany({ where: { threadId: thread.id }, orderBy: { seq: "asc" } }),
]);
expect(events.map((row) => row.seq)).toEqual(Array.from({ length: 40 }, (_, i) => i));
expect(messages.map((row) => row.seq)).toEqual(Array.from({ length: 40 }, (_, i) => i));
});
it("6: fake, managed-sandbox emulator, and desktop executor run the same graphical task", async () => {
const ctx = {
operationId: "1",
traceId: "1",
spaceId: "w",
userId: "u",
signal: new AbortController().signal,
};
const fake = new FakeSandboxProvider();
const managed = new ManagedSandboxEmulator();
const desktop = new DesktopSandboxProvider();
const a = await fake.provision({ botId: "ja", homePath: "/tmp/ja" }, ctx);
const b = await managed.provision({ botId: "jb", homePath: "/tmp/jb" }, ctx);
const c = await desktop.provision({ botId: "jc", homePath: "/tmp/jc" }, ctx);
let out = "";
for await (const event of fake.execute(a, { argv: ["echo", "same-task"] }, ctx)) {
if (event.type === "stdout") out += event.data;
}
for await (const event of managed.execute(b, { argv: ["echo", "same-task"] }, ctx)) {
if (event.type === "stdout") out += event.data;
}
for await (const event of desktop.execute(c, { argv: ["echo", "same-task"] }, ctx)) {
if (event.type === "stdout") out += event.data;
}
expect(out.match(/same-task/g)?.length).toBe(3);
await desktop.destroy(c, ctx);
});
it("7: destination write is independently inspectable and credentials stay out of the thread", async () => {
const cookie = await signup(app, `dest-j-${stamp}@rakazo.test`, "Dest");
const bot = await rpc<Bot>(app, cookie, "bots/create", {
name: "Chief",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const before = connector.records.length;
const secret = "test-openrouter-key-not-a-real-secret";
await rpc(app, cookie, "models/connect", {
provider: "openrouter",
apiKey: secret,
label: "test",
modelId: "scripted",
});
await sendAndWait(app, cookie, bot.id, "write this to the destination crm as a note");
expect(connector.records.length).toBeGreaterThan(before);
const snap = await rpc<Snap>(app, cookie, "threads/get", { botId: bot.id });
expect(JSON.stringify(snap)).not.toContain(secret);
const inspect = await fetch(`http://127.0.0.1:${connector.port}/records`);
const records = (await inspect.json()) as unknown[];
expect(records.length).toBeGreaterThan(0);
});
it("8: retrying a completed effect does not duplicate the destination write", async () => {
const cookie = await signup(app, `crash-j-${stamp}@rakazo.test`, "Crash");
const bot = await rpc<Bot>(app, cookie, "bots/create", {
name: "Chief",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const before = connector.records.length;
const sent = await rpc<{ runId: string }>(app, cookie, "threads/send", {
botId: bot.id,
text: "write this to the destination crm as a note",
});
await waitFor(
app,
cookie,
bot.id,
(s) => !s.run || ["completed", "failed", "cancelled"].includes(s.run.status),
);
const afterFirst = connector.records.length;
expect(afterFirst).toBeGreaterThan(before);
await prisma.run.update({
where: { id: sent.runId },
data: { status: "running", completedAt: null },
});
await executor.continueRun(sent.runId, "retry");
expect(connector.records.length).toBe(afterFirst);
});
it("9: export includes memory and files but not secrets or browser sessions", async () => {
const cookie = await signup(app, `export-j-${stamp}@rakazo.test`, "Export");
const bot = await rpc<Bot>(app, cookie, "bots/create", {
name: "Chief",
title: "",
description: "",
instructions: "Be useful",
notifyOnFinish: true,
});
const secret = "test-openrouter-key-not-a-real-secret";
await rpc(app, cookie, "models/connect", {
provider: "openrouter",
apiKey: secret,
label: "hidden",
modelId: "scripted",
});
await sendAndWait(
app,
cookie,
bot.id,
"write a file in your home called notes/result.txt that says export-ok",
);
const manifest = await rpc<Record<string, unknown>>(app, cookie, "export/bot", {
botId: bot.id,
});
const rawJson = JSON.stringify(manifest);
expect(rawJson).toContain("export-ok");
expect(rawJson).toContain("Be useful");
expect(rawJson).not.toContain(secret);
expect(rawJson).not.toMatch(/browserProfile|ciphertext|sessionCookie/i);
});
it("10: bots can be archived safely and deleted with or without their memories", async () => {
const ada = await signup(app, `delete-j-${stamp}@rakazo.test`, "Delete Ada");
const bob = await signup(app, `delete-bob-j-${stamp}@rakazo.test`, "Delete Bob");
const keep = await rpc<Bot>(app, ada, "bots/create", {
name: "Keep",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const gone = await rpc<Bot>(app, ada, "bots/create", {
name: "Gone",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
computerMode: "dedicated",
});
const forget = await rpc<Bot>(app, ada, "bots/create", {
name: "Forget",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const goneMemory = await prisma.memoryDocument.findFirstOrThrow({ where: { botId: gone.id } });
const forgetMemory = await prisma.memoryDocument.findFirstOrThrow({
where: { botId: forget.id },
});
await prisma.memoryDocument.update({
where: { id: goneMemory.id },
data: { content: "important retained context" },
});
await sendAndWait(
app,
ada,
gone.id,
"write a file in your home called notes/result.txt that says delete-ok",
);
const goneArtifact = await rpc<{ id: string }>(app, ada, "artifacts/create", {
botId: gone.id,
name: "delete-me.txt",
mimeType: "text/plain",
contentBase64: Buffer.from("fake artifact content").toString("base64"),
});
const home = path.join(dataDir, "homes", gone.id);
expect(existsSync(home)).toBe(true);
// Spend that really happened. The scripted runtime emits no usage event, so the row is
// written directly, the same way this file creates runs and tasks elsewhere.
const goneBotRow = await prisma.bot.findUniqueOrThrow({
where: { id: gone.id },
select: { spaceId: true, userId: true },
});
const goneSpend = await prisma.usageRecord.create({
data: {
spaceId: goneBotRow.spaceId,
botId: gone.id,
userId: goneBotRow.userId,
provider: "anthropic",
model: "claude-sonnet-4-5",
inputTokens: 1200,
outputTokens: 340,
},
});
const stolen = await raw(app, bob, "bots/archive", { botId: gone.id });
expect(stolen.status).toBeGreaterThanOrEqual(400);
expect((await rpc<Bot[]>(app, ada, "bots/list")).map((bot) => bot.id)).toContain(gone.id);
await rpc(app, ada, "bots/archive", { botId: gone.id });
expect((await rpc<Bot[]>(app, ada, "bots/list")).map((bot) => bot.id)).not.toContain(gone.id);
expect((await rpc<Bot[]>(app, ada, "bots/listArchived")).map((bot) => bot.id)).toContain(
gone.id,
);
expect(await prisma.artifact.findUnique({ where: { id: goneArtifact.id } })).not.toBeNull();
expect(existsSync(home)).toBe(true);
await rpc(app, ada, "bots/restore", { botId: gone.id });
expect((await rpc<Bot[]>(app, ada, "bots/list")).map((bot) => bot.id)).toContain(gone.id);
await rpc(app, ada, "bots/remove", { botId: gone.id, deleteMemories: false });
const list = await rpc<Bot[]>(app, ada, "bots/list");
expect(list.map((bot) => bot.id)).toEqual(expect.arrayContaining([keep.id, forget.id]));
expect((await raw(app, ada, "bots/get", { botId: gone.id })).status).toBeGreaterThanOrEqual(
400,
);
expect(
await prisma.memoryDocument.findUniqueOrThrow({ where: { id: goneMemory.id } }),
).toMatchObject({
botId: null,
scope: "user",
content: "important retained context",
});
// The name is the half that makes a detached usage row readable: bot_deletions is keyed by
// the bot id, so a per-bot spend report can still label spend that belongs to a bot that
// no longer exists.
expect(await prisma.botDeletion.findUniqueOrThrow({ where: { id: gone.id } })).toMatchObject({
name: "Gone",
memoriesPreserved: true,
});
expect(await prisma.artifact.findUnique({ where: { id: goneArtifact.id } })).toBeNull();
expect(existsSync(home)).toBe(false);
// Deleting a bot must not erase what it cost, nor which bot cost it. botId has no foreign
// key, so it outlives the bot and stays joinable against bot_deletions for the name.
expect(
await prisma.usageRecord.findUniqueOrThrow({ where: { id: goneSpend.id } }),
).toMatchObject({ botId: gone.id, inputTokens: 1200, outputTokens: 340 });
await rpc(app, ada, "bots/remove", { botId: forget.id, deleteMemories: true });
expect(await prisma.memoryDocument.findUnique({ where: { id: forgetMemory.id } })).toBeNull();
});
it("11: deleting an account removes the user and personal workspace data", async () => {
const email = `account-delete-j-${stamp}@rakazo.test`;
const cookie = await signup(app, email, "Delete Account");
const me = await rpc<Me>(app, cookie, "me");
const bot = await rpc<Bot>(app, cookie, "bots/create", {
name: "Temporary",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const deleted = await app.request("/api/auth/delete-user", {
method: "POST",
headers: {
"content-type": "application/json",
cookie,
origin: "http://127.0.0.1:5173",
},
body: JSON.stringify({ password: "password12" }),
});
expect(deleted.status).toBe(200);
expect(await prisma.user.findUnique({ where: { id: me.userId } })).toBeNull();
expect(await prisma.organization.findUnique({ where: { id: me.spaceId } })).toBeNull();
expect(await prisma.bot.findUnique({ where: { id: bot.id } })).toBeNull();
expect((await raw(app, cookie, "me")).status).toBeGreaterThanOrEqual(400);
});
it("12: a bot can spawn a regular bot and must confirm the name to delete it", async () => {
const cookie = await signup(app, `spawn-j-${stamp}@rakazo.test`, "Spawn");
const parent = await rpc<Bot>(app, cookie, "bots/create", {
name: "Chief",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
await sendAndWait(app, cookie, parent.id, "spawn a bot named Scout to research venues");
const listed = await rpc<Bot[]>(app, cookie, "bots/list");
const scout = listed.find((bot) => bot.name === "Scout");
expect(scout).toBeTruthy();
expect(listed.map((bot) => bot.name).sort()).toEqual(["Chief", "Scout"]);
await waitFor(
app,
cookie,
scout!.id,
(s) => !s.run || ["completed", "failed", "cancelled"].includes(s.run.status),
);
const snap = await rpc<Snap>(app, cookie, "threads/get", { botId: parent.id });
expect(JSON.stringify(snap.messages)).toMatch(/child_bot|Scout/);
await sendAndWait(app, cookie, scout!.id, "spawn a bot named Nested");
const afterNested = await rpc<Bot[]>(app, cookie, "bots/list");
const nested = afterNested.find((bot) => bot.name === "Nested");
expect(nested).toBeTruthy();
expect(afterNested.map((bot) => bot.name).sort()).toEqual(["Chief", "Nested", "Scout"]);
await waitFor(
app,
cookie,
nested!.id,
(s) => !s.run || ["completed", "failed", "cancelled"].includes(s.run.status),
);
await sendAndWait(app, cookie, parent.id, "delete the bot named Nested");
expect((await rpc<Bot[]>(app, cookie, "bots/list")).some((bot) => bot.id === nested!.id)).toBe(
true,
);
await sendAndWait(app, cookie, parent.id, "delete the bot named WrongName");
expect((await rpc<Bot[]>(app, cookie, "bots/list")).some((bot) => bot.id === scout!.id)).toBe(
true,
);
await sendAndWait(app, cookie, parent.id, "delete the bot named Scout");
const afterScout = await rpc<Bot[]>(app, cookie, "bots/list");
expect(afterScout.some((bot) => bot.id === scout!.id)).toBe(false);
expect(afterScout.some((bot) => bot.id === nested!.id)).toBe(true);
await rpc(app, cookie, "bots/remove", { botId: parent.id });
expect((await rpc<Bot[]>(app, cookie, "bots/list")).map((bot) => bot.name)).toEqual(["Nested"]);
});
it("13: a subagent shows up in the parent thread without creating a bot", async () => {
const cookie = await signup(app, `subagent-j-${stamp}@rakazo.test`, "Subagent");
const bot = await rpc<Bot>(app, cookie, "bots/create", {
name: "Chief",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const before = (await rpc<Bot[]>(app, cookie, "bots/list")).length;
const snap = await sendAndWait(app, cookie, bot.id, "run a subagent to summarize the notes");
expect(JSON.stringify(snap.messages)).toMatch(/subagent|helper/);
expect(await rpc<Bot[]>(app, cookie, "bots/list")).toHaveLength(before);
});
it("11: compose backup docs and dump tooling exist", async () => {
expect(existsSync(path.resolve("docs/self-host.md"))).toBe(true);
expect(existsSync(path.resolve("infra/compose/docker-compose.yml"))).toBe(true);
expect(existsSync(path.resolve("scripts/backup.sh"))).toBe(true);
expect(existsSync(path.resolve("scripts/restore.sh"))).toBe(true);
const docs = readFileSync(path.resolve("docs/self-host.md"), "utf8");
expect(docs).toMatch(/pg_dump/);
expect(docs).toMatch(/Restore/);
});
it("14: this-mac is refused unless the sandbox is docker", async () => {
const cookie = await signup(app, `host-j-${stamp}@rakazo.test`, "Host");
const me = await rpc<Me>(app, cookie, "me");
expect(me.canChooseHostComputer).toBe(false);
await prisma.deploymentSettings.update({
where: { id: "default" },
data: { ownerUserId: me.userId },
});
const res = await raw(app, cookie, "deployment/update", { computerHost: "this-mac" });
const text = await res.text();
expect(res.status).toBeGreaterThanOrEqual(400);
expect(text).toMatch(/This Mac mode is only available/i);
});
it("15: ask, answer, stop, follow-up, and clientNonce stay consistent", async () => {
const cookie = await signup(app, `ask-j-${stamp}@rakazo.test`, "Ask");
const bot = await rpc<Bot>(app, cookie, "bots/create", {
name: "Chief",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
await rpc(app, cookie, "bots/update", { botId: bot.id, title: "Updated chief" });
expect((await rpc<Bot>(app, cookie, "bots/get", { botId: bot.id })).title).toBe(
"Updated chief",
);
const asked = await rpc<{ runId: string }>(app, cookie, "threads/send", {
botId: bot.id,
text: "ask me which city to use",
});
const waiting = await waitFor(
app,
cookie,
bot.id,
(snap) => snap.run?.status === "waiting_input",
);
expect(JSON.stringify(waiting.messages)).toMatch(/which city/i);
const askMessage = waiting.messages.find((message) =>
message.blocks.some((block) => block.kind === "ask"),
);
expect(askMessage).toBeTruthy();
await rpc(app, cookie, "threads/answer", {
botId: bot.id,
runId: asked.runId,
messageId: askMessage!.id,
answer: "Paris",
});
const answered = await waitFor(
app,
cookie,
bot.id,
(snap) => !snap.run || ["completed", "failed", "cancelled"].includes(snap.run.status),
);
expect(answered.run?.status ?? "completed").toBe("completed");
await rpc(app, cookie, "threads/send", {
botId: bot.id,
text: "keep working until I stop you",
});
await waitFor(app, cookie, bot.id, (snap) =>
["queued", "leased", "running"].includes(snap.run?.status ?? ""),
);
const hanging = await prisma.run.findFirstOrThrow({
where: { botId: bot.id },
orderBy: { createdAt: "desc" },
});
await rpc(app, cookie, "threads/stop", { botId: bot.id });
await waitFor(app, cookie, bot.id, (snap) => !snap.run);
expect((await prisma.run.findUniqueOrThrow({ where: { id: hanging.id } })).status).toBe(
"cancelled",
);
await rpc(app, cookie, "threads/followUp", {
botId: bot.id,
text: "write a file in your home called notes/result.txt that says followup-ok",
});
await waitFor(
app,
cookie,
bot.id,
(s) => !s.run || ["completed", "failed", "cancelled"].includes(s.run.status),
);
expect(
(
await rpc<{ content: string }>(app, cookie, "computer/readFile", {
botId: bot.id,
path: "notes/result.txt",
})
).content,
).toContain("followup-ok");
const first = await rpc<{ runId: string }>(app, cookie, "threads/send", {
botId: bot.id,
text: "write a file in your home called notes/result.txt that says nonce-ok",
clientNonce: `nonce-${stamp}`,
});
const second = await rpc<{ runId: string }>(app, cookie, "threads/send", {
botId: bot.id,
text: "write a file in your home called notes/result.txt that says nonce-dup",
clientNonce: `nonce-${stamp}`,
});
expect(second.runId).toBe(first.runId);
await waitFor(
app,
cookie,
bot.id,
(s) => !s.run || ["completed", "failed", "cancelled"].includes(s.run.status),
);
const file = await rpc<{ content: string }>(app, cookie, "computer/readFile", {
botId: bot.id,
path: "notes/result.txt",
});
expect(file.content).toContain("nonce-ok");
expect(file.content).not.toContain("nonce-dup");
});
it("16: routine test-run and plugin connect/revoke", async () => {
const ada = await signup(app, `plug-j-${stamp}@rakazo.test`, "Plug Ada");
const bob = await signup(app, `plug-bob-j-${stamp}@rakazo.test`, "Plug Bob");
const bot = await rpc<Bot>(app, ada, "bots/create", {
name: "Chief",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const routine = await rpc<{ id: string }>(app, ada, "routines/create", {
botId: bot.id,
name: "Test now",
prompt: "write a file in your home called notes/result.txt that says testrun-ok",
crons: ["0 9 * * 1"],
timezone: "UTC",
notify: false,
active: false,
});
const tested = await rpc<{ runId: string }>(app, ada, "routines/testRun", {
routineId: routine.id,
});
expect(tested.runId).toBeTruthy();
await waitFor(
app,
ada,
bot.id,
(s) => !s.run || ["completed", "failed", "cancelled"].includes(s.run.status),
);
const file = await rpc<{ content: string }>(app, ada, "computer/readFile", {
botId: bot.id,
path: "notes/result.txt",
});
expect(file.content).toContain("testrun-ok");
const started = await rpc<{ connectionId: string; authorizationUrl: string | null }>(
app,
ada,
"connections/begin",
{ provider: "gmail", displayName: "Gmail" },
);
expect(started.authorizationUrl).toBeNull();
const connected = await rpc<{ status: string }>(app, ada, "connections/complete", {
connectionId: started.connectionId,
});
expect(connected.status).toBe("connected");
await rpc(app, bob, "connections/revoke", { connectionId: started.connectionId });
expect(
(await rpc<Array<{ id: string; status: string }>>(app, ada, "connections/list")).find(
(row) => row.id === started.connectionId,
)?.status,
).toBe("connected");
await rpc(app, ada, "connections/revoke", { connectionId: started.connectionId });
expect(
(await rpc<Array<{ id: string; status: string }>>(app, ada, "connections/list")).find(
(row) => row.id === started.connectionId,
)?.status,
).toBe("revoked");
});
it("54: group chats share one transcript with mentions and handoffs", async () => {
const ada = await signup(app, `ada-g-${stamp}@rakazo.test`, "Ada Groups");
const adaMe = await rpc<Me>(app, ada, "me");
const botA = await rpc<Bot>(app, ada, "bots/create", {
name: "BotA",
title: "Researcher",
description: "",
instructions: "",
notifyOnFinish: true,
});
const botB = await rpc<Bot>(app, ada, "bots/create", {
name: "BotB",
title: "Writer",
description: "",
instructions: "",
notifyOnFinish: true,
});
const botC = await rpc<Bot>(app, ada, "bots/create", {
name: "Writer",
title: "Editor",
description: "",
instructions: "",
notifyOnFinish: true,
});
const botD = await rpc<Bot>(app, ada, "bots/create", {
name: "Research Writer",
title: "Analyst",
description: "",
instructions: "",
notifyOnFinish: true,
});
const group = await rpc<{ id: string; threadId: string; members: Array<{ botId: string }> }>(
app,
ada,
"groups/create",
{ name: "Research squad", botIds: [botA.id, botB.id, botC.id, botD.id] },
);
const listed = await rpc<Array<{ id: string }>>(app, ada, "groups/list");
expect(listed.some((row) => row.id === group.id)).toBe(true);
expect((await rpc<Bot[]>(app, ada, "bots/list")).map((b) => b.id)).toEqual(
expect.arrayContaining([botA.id, botB.id, botC.id, botD.id]),
);
await sendGroupAndWait(app, ada, group.id, "@BotA gather sources. @BotB summarize.");
const mentioned = await rpc<Snap & { threadId: string }>(app, ada, "threads/get", {
groupId: group.id,
});
expect(new Set(mentioned.messages.map((m) => m.seq)).size).toBeGreaterThan(0);
const runsAfterMention = await prisma.run.findMany({
where: { threadId: group.threadId, botId: { in: [botA.id, botB.id] } },
orderBy: { createdAt: "desc" },
take: 4,
});
expect(runsAfterMention.some((run) => run.botId === botA.id)).toBe(true);
expect(runsAfterMention.some((run) => run.botId === botB.id)).toBe(true);
const countUserRuns = async (botId: string) =>
prisma.run.count({
where: { threadId: group.threadId, trigger: "user", botId },
});
const botABefore = await countUserRuns(botA.id);
const botBBefore = await countUserRuns(botB.id);
const botCBefore = await countUserRuns(botC.id);
const botDBefore = await countUserRuns(botD.id);
await sendGroupAndWait(app, ada, group.id, "hello team");
expect(
(await countUserRuns(botA.id)) +
(await countUserRuns(botB.id)) +
(await countUserRuns(botC.id)) +
(await countUserRuns(botD.id)),
).toBe(botABefore + botBBefore + botCBefore + botDBefore + 1);
await sendGroupAndWait(app, ada, group.id, "@BotA hand this to Writer for the draft", botA.id);
const handoffSnap = await rpc<Snap>(app, ada, "threads/get", { groupId: group.id });
expect(
handoffSnap.messages.some((message) =>
(message.blocks as Array<{ kind?: string }>).some((block) => block.kind === "handoff"),
),
).toBe(true);
const groupAsk = await rpc<{ runId: string }>(app, ada, "threads/send", {
groupId: group.id,
text: "@Research Writer ask me which city to use",
});
await waitForDatabase(async () => {
const run = await prisma.run.findUnique({ where: { id: groupAsk.runId } });
return run?.status === "waiting_input";
});
const groupsWithActiveMember = await rpc<
Array<{ id: string; members: Array<{ botId: string; status?: string }> }>
>(app, ada, "groups/list");
expect(
groupsWithActiveMember
.find((listedGroup) => listedGroup.id === group.id)
?.members.find((member) => member.botId === botD.id)?.status,
).toBe("waiting_input");
const activeGroupSnapshot = await rpc<{
members?: Array<{ botId: string; status?: string }>;
}>(app, ada, "threads/get", { groupId: group.id });
expect(activeGroupSnapshot.members?.find((member) => member.botId === botD.id)?.status).toBe(
"waiting_input",
);
const concurrentTask = await prisma.task.create({
data: {
spaceId: botA.spaceId,
botId: botA.id,
threadId: group.threadId,
userId: adaMe.userId,
prompt: "concurrent work",
status: "running",
},
});
const concurrentRun = await prisma.run.create({
data: {
spaceId: botA.spaceId,
botId: botA.id,
threadId: group.threadId,
taskId: concurrentTask.id,
userId: adaMe.userId,
status: "running",
trigger: "user",
createdAt: new Date(Date.now() + 1_000),
},
});
const askSnapshot = await rpc<Snap>(app, ada, "threads/get", { groupId: group.id });
// Waiting asks win the headline run even when a newer busy run exists.
expect(askSnapshot.run?.id).toBe(groupAsk.runId);
expect(askSnapshot.activeRuns?.some((run) => run.id === concurrentRun.id)).toBe(true);
expect(askSnapshot.activeRuns?.some((run) => run.id === groupAsk.runId)).toBe(true);
const askMessage = askSnapshot.messages.find(
(message) =>
message.runId === groupAsk.runId &&
message.blocks.some((block) => block.kind === "ask" && block.status !== "answered"),
);
expect(askMessage).toBeTruthy();
await rpc(app, ada, "threads/answer", {
groupId: group.id,
runId: groupAsk.runId,
messageId: askMessage!.id,
answer: "Paris",
});
await waitForDatabase(async () => {
const run = await prisma.run.findUnique({ where: { id: groupAsk.runId } });
return run?.status === "completed";
});
const answerEvent = await prisma.event.findFirstOrThrow({
where: {
threadId: group.threadId,
runId: groupAsk.runId,
type: "thread.message.updated",
},
orderBy: { seq: "desc" },
});
expect(answerEvent.botId).toBe(botD.id);
await prisma.$transaction([
prisma.run.update({
where: { id: concurrentRun.id },
data: { status: "cancelled", completedAt: new Date() },
}),
prisma.task.update({ where: { id: concurrentTask.id }, data: { status: "cancelled" } }),
]);
const staleTask = await prisma.task.create({
data: {
spaceId: botB.spaceId,
botId: botB.id,
threadId: group.threadId,
userId: adaMe.userId,
prompt: "stale handoff",
status: "running",
},
});
const staleRun = await prisma.run.create({
data: {
spaceId: botB.spaceId,
botId: botB.id,
threadId: group.threadId,
taskId: staleTask.id,
userId: adaMe.userId,
status: "running",
trigger: "user",
},
});
const replayNonce = `group-replay-${stamp}`;
const firstSend = await rpc<{ runId: string; runIds?: string[] }>(app, ada, "threads/send", {
groupId: group.id,
text: "@BotA gather updates. @BotB compare them.",
clientNonce: replayNonce,
});
await rpc(app, ada, "groups/update", {
groupId: group.id,
botIds: [botA.id, botC.id, botD.id],
});
expect((await prisma.run.findUniqueOrThrow({ where: { id: staleRun.id } })).status).toBe(
"cancelled",
);
const replayedSend = await rpc<{ runId: string; runIds?: string[] }>(app, ada, "threads/send", {
groupId: group.id,
text: "this changed text must not create another message",
clientNonce: replayNonce,
});
expect(replayedSend.runId).toBe(firstSend.runId);
expect(replayedSend.runIds).toEqual(firstSend.runIds);
const replayMessage = await prisma.message.findUniqueOrThrow({
where: { threadId_clientNonce: { threadId: group.threadId, clientNonce: replayNonce } },
include: { sourceRuns: true },
});
expect(replayMessage.sourceRuns).toHaveLength(1);
await expect(
prisma.steeringMessage.findUniqueOrThrow({
where: { messageId_botId: { messageId: replayMessage.id, botId: botB.id } },
}),
).resolves.toMatchObject({ runId: staleRun.id, claimedAt: null });
expect(
await prisma.message.count({ where: { threadId: group.threadId, clientNonce: replayNonce } }),
).toBe(1);
const messageEvents = await prisma.event.findMany({
where: { threadId: group.threadId, type: "thread.message.created" },
select: { payload: true },
});
expect(
messageEvents.filter(
(event) => (event.payload as { messageId?: string } | null)?.messageId === replayMessage.id,
),
).toHaveLength(1);
const staleHandoff = await handoffToGroupBot(
{ prisma, events: createThreadEvents(prisma), jobs },
{
id: staleRun.id,
spaceId: botB.spaceId,
threadId: group.threadId,
botId: botB.id,
userId: staleTask.userId,
},
group.id,
{ bot_id: botC.id, message: "should be rejected" },
);
expect(staleHandoff).toEqual({ error: "source run is no longer active" });
const crossThread = await rpc<{ runId: string }>(app, ada, "threads/send", {
botId: botC.id,
text: "the same nonce is valid in a different thread",
clientNonce: replayNonce,
});
expect(crossThread.runId).not.toBe(firstSend.runId);
const attachmentText = "group attachment content";
const artifact = await rpc<{ id: string }>(app, ada, "artifacts/create", {
groupId: group.id,
name: "group-note.txt",
mimeType: "text/plain",
contentBase64: Buffer.from(attachmentText).toString("base64"),
});
const attached = await rpc<{ runId: string }>(app, ada, "threads/send", {
groupId: group.id,
text: "@Research Writer inspect the attachment",
artifactIds: [artifact.id],
});
await waitForDatabase(async () => {
const run = await prisma.run.findUnique({ where: { id: attached.runId } });
return Boolean(run && ["completed", "failed", "cancelled"].includes(run.status));
});
expect(await prisma.run.findUniqueOrThrow({ where: { id: attached.runId } })).toMatchObject({
botId: botD.id,
status: "completed",
error: null,
});
const downloaded = await rpc<{ contentBase64: string }>(app, ada, "artifacts/get", {
groupId: group.id,
artifactId: artifact.id,
});
expect(Buffer.from(downloaded.contentBase64, "base64").toString()).toBe(attachmentText);
const artifactOwnerId = (
await prisma.artifact.findUniqueOrThrow({ where: { id: artifact.id } })
).botId;
if (!artifactOwnerId) throw new Error("Group artifact is missing its uploader");
const remainingGroupBotIds = [botA.id, botC.id, botD.id].filter(
(botId) => botId !== artifactOwnerId,
);
expect(remainingGroupBotIds).toHaveLength(2);
await rpc(app, ada, "groups/update", {
groupId: group.id,
botIds: remainingGroupBotIds,
});
await rpc(app, ada, "bots/remove", { botId: artifactOwnerId, deleteMemories: true });
expect(await prisma.artifact.findUniqueOrThrow({ where: { id: artifact.id } })).toMatchObject({
botId: null,
groupId: group.id,
});
const downloadedAfterUploaderRemoval = await rpc<{ contentBase64: string }>(
app,
ada,
"artifacts/get",
{ groupId: group.id, artifactId: artifact.id },
);
expect(Buffer.from(downloadedAfterUploaderRemoval.contentBase64, "base64").toString()).toBe(
attachmentText,
);
const archiveMember = await rpc<Bot>(app, ada, "bots/create", {
name: "Archive Member",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const archivePartner = await rpc<Bot>(app, ada, "bots/create", {
name: "Archive Partner",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const archiveGroup = await rpc<{ id: string }>(app, ada, "groups/create", {
name: "Archive invariant",
botIds: [archiveMember.id, archivePartner.id],
});
const archiveThread = await prisma.thread.findUniqueOrThrow({
where: { groupId: archiveGroup.id },
select: { id: true },
});
await rpc(app, ada, "bots/archive", { botId: archiveMember.id });
expect(
await prisma.chatGroup.findUniqueOrThrow({
where: { id: archiveGroup.id },
include: { members: true, thread: { select: { id: true } } },
}),
).toMatchObject({
members: expect.arrayContaining([
expect.objectContaining({ botId: archiveMember.id }),
expect.objectContaining({ botId: archivePartner.id }),
]),
thread: archiveThread,
});
const groupsWhileUndersized = await rpc<Array<{ id: string }>>(app, ada, "groups/list");
expect(groupsWhileUndersized.some((row) => row.id === archiveGroup.id)).toBe(false);
await expect(rpc(app, ada, "threads/get", { groupId: archiveGroup.id })).rejects.toThrow();
await expect(
rpc(app, ada, "threads/send", {
groupId: archiveGroup.id,
text: "This hidden group must not run with one active member",
}),
).rejects.toThrow();
await rpc(app, ada, "bots/restore", { botId: archiveMember.id });
const restoredArchiveGroup = await rpc<
Array<{ id: string; members: Array<{ botId: string }> }>
>(app, ada, "groups/list");
expect(restoredArchiveGroup.find((row) => row.id === archiveGroup.id)?.members).toEqual(
expect.arrayContaining([
expect.objectContaining({ botId: archiveMember.id }),
expect.objectContaining({ botId: archivePartner.id }),
]),
);
const archiveThird = await rpc<Bot>(app, ada, "bots/create", {
name: "Archive Third",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
await rpc(app, ada, "groups/update", {
groupId: archiveGroup.id,
botIds: [archiveMember.id, archivePartner.id, archiveThird.id],
});
await rpc(app, ada, "bots/archive", { botId: archiveMember.id });
const dissolvingTask = await prisma.task.create({
data: {
spaceId: archivePartner.spaceId,
botId: archivePartner.id,
threadId: archiveThread.id,
userId: adaMe.userId,
prompt: "fake active work while deleting a group member",
status: "running",
},
});
const dissolvingRun = await prisma.run.create({
data: {
spaceId: archivePartner.spaceId,
botId: archivePartner.id,
threadId: archiveThread.id,
taskId: dissolvingTask.id,
userId: adaMe.userId,
status: "running",
trigger: "user",
},
});
await rpc(app, ada, "bots/remove", { botId: archiveThird.id, deleteMemories: true });
expect(await prisma.chatGroup.findUnique({ where: { id: archiveGroup.id } })).toBeNull();
expect(await prisma.run.findUnique({ where: { id: dissolvingRun.id } })).toBeNull();
await rpc(app, ada, "bots/restore", { botId: archiveMember.id });
expect(await prisma.chatGroup.findUnique({ where: { id: archiveGroup.id } })).toBeNull();
const deletionPartner = await rpc<Bot>(app, ada, "bots/create", {
name: "Deletion Partner",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const deletionGroup = await rpc<{ id: string }>(app, ada, "groups/create", {
name: "Deletion invariant",
botIds: [botB.id, deletionPartner.id],
});
await expect(prisma.bot.delete({ where: { id: botB.id } })).rejects.toThrow();
expect(await prisma.chatGroup.findUnique({ where: { id: deletionGroup.id } })).not.toBeNull();
await rpc(app, ada, "bots/remove", { botId: botB.id, deleteMemories: true });
expect(await prisma.chatGroup.findUnique({ where: { id: deletionGroup.id } })).toBeNull();
await rpc(app, ada, "groups/remove", { groupId: group.id });
expect(await prisma.artifact.findUnique({ where: { id: artifact.id } })).toBeNull();
const remainingBotIds = (await rpc<Bot[]>(app, ada, "bots/list")).map((bot) => bot.id);
expect(remainingBotIds).toEqual(
expect.arrayContaining([
...remainingGroupBotIds,
deletionPartner.id,
archiveMember.id,
archivePartner.id,
]),
);
expect(remainingBotIds).not.toContain(artifactOwnerId);
});
it("55: the main bot leads the main conversation and addressed bots join it", async () => {
const cookie = await signup(app, `main-conversation-${stamp}@rakazo.test`, "Main Conversation");
const me = await rpc<Me>(app, cookie, "me");
type MainRef = { botId: string; groupId: string; spaceId: string };
// Reading navigation must not create a main bot before onboarding asks for one.
expect((await rpc<{ main: MainRef | null }>(app, cookie, "spaces/list")).main).toBeNull();
const mainBot = await rpc<Bot & { kind: string }>(app, cookie, "bots/ensureMain");
expect(mainBot.kind).toBe("main");
expect((await rpc<Bot>(app, cookie, "bots/ensureMain")).id).toBe(mainBot.id);
const { main } = await rpc<{ main: MainRef | null }>(app, cookie, "bootstrap", {});
expect(main).toMatchObject({ botId: mainBot.id, spaceId: me.spaceId });
expect((await rpc<{ main: MainRef | null }>(app, cookie, "spaces/list")).main).toEqual(main);
const groupId = main!.groupId;
const { id: threadId } = await prisma.thread.findUniqueOrThrow({
where: { groupId },
select: { id: true },
});
const analyst = await rpc<Bot>(app, cookie, "bots/create", {
name: "Analyst",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const userRunBots = async () =>
(
await prisma.run.findMany({
where: { threadId, trigger: "user" },
orderBy: { createdAt: "asc" },
select: { botId: true },
})
).map((run) => run.botId);
// The lead answers when nobody is addressed, even with a single member.
await sendGroupAndWait(app, cookie, groupId, "hello there");
expect(await userRunBots()).toEqual([mainBot.id]);
// Addressing a same-space bot makes it join the conversation and answer there.
await sendGroupAndWait(app, cookie, groupId, "@Analyst check the numbers", analyst.id);
expect(await userRunBots()).toEqual([mainBot.id, analyst.id]);
const joined = await rpc<{ leadBotId?: string; members: Array<{ botId: string }> }>(
app,
cookie,
"threads/get",
{ groupId },
);
expect(joined.leadBotId).toBe(mainBot.id);
expect(joined.members.map((member) => member.botId).sort()).toEqual(
[mainBot.id, analyst.id].sort(),
);
expect(await prisma.event.count({ where: { threadId, type: "group.updated" } })).toBe(1);
expect(
await prisma.message.count({ where: { threadId, role: "bot", botId: analyst.id } }),
).toBeGreaterThan(0);
// A chip for a member routes only to that member.
await rpc(app, cookie, "threads/send", {
groupId,
text: "and the totals",
mentions: [{ kind: "bot", id: analyst.id }],
});
await waitForDatabase(async () => (await userRunBots()).length === 3);
expect((await userRunBots()).at(-1)).toBe(analyst.id);
for (const [procedure, input] of [
["groups/update", { groupId, botIds: [mainBot.id, analyst.id] }],
["groups/archive", { groupId }],
["groups/remove", { groupId }],
["bots/archive", { botId: mainBot.id }],
["bots/remove", { botId: mainBot.id }],
] as const) {
expect((await raw(app, cookie, procedure, input)).status, procedure).toBe(400);
}
await rpc(app, cookie, "groups/update", { groupId, pinned: true });
await rpc(app, cookie, "bots/update", { botId: mainBot.id, name: "Ada" });
expect((await prisma.chatGroup.findUniqueOrThrow({ where: { id: groupId } })).name).toBe("Ada");
// A member leaving never dissolves the main conversation.
await waitForDatabase(
async () =>
(await prisma.run.count({
where: { threadId, status: { in: [...ACTIVE_RUN_STATUSES] } },
})) === 0,
);
await rpc(app, cookie, "bots/remove", { botId: analyst.id, deleteMemories: true });
const afterLeave = await rpc<{ members: Array<{ botId: string }> }>(
app,
cookie,
"threads/get",
{
groupId,
},
);
expect(afterLeave.members.map((member) => member.botId)).toEqual([mainBot.id]);
await rpc(app, cookie, "threads/clear", { groupId });
const cleared = await prisma.event.findFirstOrThrow({
where: { threadId, type: "thread.cleared" },
orderBy: { seq: "desc" },
});
expect(cleared.botId).toBe(mainBot.id);
});
it("17: teach a task end to end", async () => {
const cookie = await signup(app, `teach-j-${stamp}@rakazo.test`, "Teach Ada");
const bot = await rpc<Bot>(app, cookie, "bots/create", {
name: "Teacher",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
await rpc(app, cookie, "computer/boot", { botId: bot.id });
await rpc(app, cookie, "computer/takeover", { botId: bot.id });
void rpc(app, cookie, "threads/send", {
botId: bot.id,
text: "keep working until I stop you",
});
await new Promise((resolve) => setTimeout(resolve, 100));
const skill = await rpc<{
id: string;
status: string;
playbook: { steps: string[] };
recording: { events: Array<{ kind: string }> };
}>(app, cookie, "skills/start", {
botId: bot.id,
goal: "Export weekly CRM list",
});
expect(skill.status).toBe("recording");
await rpc(app, cookie, "computer/input", {
botId: bot.id,
kind: "pointer",
payload: { x: 120, y: 40, button: "left", type: "click" },
});
await rpc(app, cookie, "computer/input", {
botId: bot.id,
kind: "key",
payload: { key: "x" },
});
const blocked = await raw(app, cookie, "threads/send", {
botId: bot.id,
text: "please act now",
});
expect(blocked.status).toBeGreaterThanOrEqual(400);
const stopped = await rpc<{
status: string;
playbook: { steps: string[] };
recording: { events: Array<{ kind: string }>; snapshots: Array<{ summary: string }> };
}>(app, cookie, "skills/stop", { skillId: skill.id });
expect(stopped.status).toBe("draft");
expect(stopped.playbook.steps.join(" ")).toMatch(/Click|120|40|x/i);
expect(stopped.recording.events.some((event) => event.kind === "pointer")).toBe(true);
expect(stopped.recording.snapshots.length).toBeGreaterThanOrEqual(2);
const computerAfterStop = await rpc<{ controlHolder: string; controlBotId: string | null }>(
app,
cookie,
"computer/status",
{ botId: bot.id },
);
expect(computerAfterStop.controlHolder).toBe("bot");
expect(computerAfterStop.controlBotId).toBeNull();
await rpc(app, cookie, "skills/updateDraft", {
skillId: skill.id,
name: "Export weekly CRM list",
playbook: stopped.playbook,
});
const saved = await rpc<{ status: string; name: string }>(app, cookie, "skills/save", {
skillId: skill.id,
name: "Export weekly CRM list",
});
expect(saved.status).toBe("saved");
const listed = await rpc<Array<{ id: string; name: string }>>(app, cookie, "skills/list", {
botId: bot.id,
});
expect(listed.some((row) => row.id === skill.id)).toBe(true);
const testRun = await rpc<{ runId: string }>(app, cookie, "skills/testRun", {
skillId: skill.id,
});
expect(testRun.runId).toBeTruthy();
await waitFor(
app,
cookie,
bot.id,
(snap) => !snap.run || ["completed", "failed", "cancelled"].includes(snap.run.status),
);
await sendAndWait(app, cookie, bot.id, "run Export weekly CRM list");
const messages = (await rpc<Snap>(app, cookie, "threads/get", { botId: bot.id })).messages;
const botText = JSON.stringify(messages);
expect(botText.toLowerCase()).toContain("taught skill");
});
it("18: teaching expiry auto-stops recording", async () => {
const previousTtl = process.env.TEACH_RECORDING_TTL_MS;
process.env.TEACH_RECORDING_TTL_MS = "1000";
try {
const cookie = await signup(app, `teach-exp-j-${stamp}@rakazo.test`, "Teach Exp Ada");
const bot = await rpc<Bot>(app, cookie, "bots/create", {
name: "Timer",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
await rpc(app, cookie, "computer/boot", { botId: bot.id });
await rpc(app, cookie, "computer/takeover", { botId: bot.id });
const skill = await rpc<{ id: string; status: string }>(app, cookie, "skills/start", {
botId: bot.id,
goal: "Timed demo",
});
await new Promise((resolve) => setTimeout(resolve, 1_500));
const current = await rpc<{ status: string }>(app, cookie, "skills/get", {
skillId: skill.id,
});
expect(["draft", "drafting"].includes(current.status)).toBe(true);
} finally {
if (previousTtl === undefined) delete process.env.TEACH_RECORDING_TTL_MS;
else process.env.TEACH_RECORDING_TTL_MS = previousTtl;
}
});
it("19: destination writes pause for approval before side effects", async () => {
const cookie = await signup(app, `approval-j-${stamp}@rakazo.test`, "Approval");
const bot = await rpc<Bot>(app, cookie, "bots/create", {
name: "Chief",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const recordsBefore = connector.records.length;
await rpc(app, cookie, "approvalRules/set", {
effect: "require_approval",
matchKind: "tool",
matchValue: "destination.write",
});
const sent = await rpc<{ runId: string }>(app, cookie, "threads/send", {
botId: bot.id,
text: "write this to the destination crm as a note",
});
const waiting = await waitFor(
app,
cookie,
bot.id,
(snap) => snap.run?.status === "waiting_input",
);
expect(JSON.stringify(waiting.messages)).toMatch(/allow once|review before/i);
expect(connector.records).toHaveLength(recordsBefore);
await answerPendingApproval(app, cookie, bot.id, sent.runId, "allow", waiting);
await waitFor(
app,
cookie,
bot.id,
(snap) => !snap.run || ["completed", "failed", "cancelled"].includes(snap.run.status),
);
expect(connector.records.length).toBeGreaterThan(recordsBefore);
const task = await prisma.task.findFirstOrThrow({
where: { runs: { some: { id: sent.runId } } },
});
expect(task.prompt).toBe("write this to the destination crm as a note");
const recordsAfterAllow = connector.records.length;
const second = await rpc<{ runId: string }>(app, cookie, "threads/send", {
botId: bot.id,
text: "write this to the destination crm as a note again",
});
const waitingAgain = await waitFor(
app,
cookie,
bot.id,
(snap) => snap.run?.status === "waiting_input",
);
await answerPendingApproval(app, cookie, bot.id, second.runId, "deny", waitingAgain);
const denied = await waitFor(
app,
cookie,
bot.id,
(snap) => !snap.run || ["completed", "failed", "cancelled"].includes(snap.run.status),
);
expect(connector.records).toHaveLength(recordsAfterAllow);
expect(denied.run?.status ?? "completed").toBe("completed");
const deniedEffect = await prisma.externalEffect.findFirst({
where: { runId: second.runId, kind: "destination.write" },
});
expect(deniedEffect?.status).toBe("denied");
});
it("20: actions run by default and specific exceptions override broad review rules", async () => {
const cookie = await signup(app, `always-j-${stamp}@rakazo.test`, "Always");
const bot = await rpc<Bot>(app, cookie, "bots/create", {
name: "Chief",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const prompt = "write this to the destination crm as a note";
const recordsBefore = connector.records.length;
await sendAndWait(app, cookie, bot.id, prompt);
expect(connector.records.length).toBeGreaterThan(recordsBefore);
const recordsAfterDefault = connector.records.length;
await rpc(app, cookie, "approvalRules/set", {
effect: "require_approval",
matchKind: "connector",
matchValue: "destination.write",
});
const second = await rpc<{ runId: string }>(app, cookie, "threads/send", {
botId: bot.id,
text: "write this to the destination crm as a note again",
});
const waiting = await waitFor(
app,
cookie,
bot.id,
(snap) => snap.run?.status === "waiting_input",
);
await answerPendingApproval(app, cookie, bot.id, second.runId, "always", waiting);
await waitFor(
app,
cookie,
bot.id,
(snap) => !snap.run || ["completed", "failed", "cancelled"].includes(snap.run.status),
);
expect(connector.records.length).toBe(recordsAfterDefault + 1);
await sendAndWait(app, cookie, bot.id, "write this to the destination crm once more");
expect(connector.records.length).toBe(recordsAfterDefault + 2);
await rpc(app, cookie, "approvalRules/set", {
effect: "require_approval",
matchKind: "tool",
matchValue: "destination.write",
});
const fourth = await rpc<{ runId: string }>(app, cookie, "threads/send", {
botId: bot.id,
text: "write this to the destination crm one final time",
});
const waitingAgain = await waitFor(
app,
cookie,
bot.id,
(snap) => snap.run?.status === "waiting_input",
);
expect(connector.records.length).toBe(recordsAfterDefault + 2);
await answerPendingApproval(app, cookie, bot.id, fourth.runId, "allow", waitingAgain);
await waitFor(
app,
cookie,
bot.id,
(snap) => !snap.run || ["completed", "failed", "cancelled"].includes(snap.run.status),
);
expect(connector.records.length).toBe(recordsAfterDefault + 3);
});
it("21: routine destination writes pause on the same approval card", async () => {
const cookie = await signup(app, `routine-approval-j-${stamp}@rakazo.test`, "Routine Approval");
const bot = await rpc<Bot>(app, cookie, "bots/create", {
name: "Chief",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
await rpc(app, cookie, "approvalRules/set", {
effect: "require_approval",
matchKind: "tool",
matchValue: "destination.write",
});
const routine = await rpc<{ id: string }>(app, cookie, "routines/create", {
botId: bot.id,
name: "Send note",
prompt: "write this to the destination crm as a note",
crons: ["0 9 * * 1"],
timezone: "UTC",
notify: false,
active: false,
});
const recordsBefore = connector.records.length;
const tested = await rpc<{ runId: string }>(app, cookie, "routines/testRun", {
routineId: routine.id,
});
const waiting = await waitFor(
app,
cookie,
bot.id,
(snap) => snap.run?.status === "waiting_input",
);
expect(connector.records).toHaveLength(recordsBefore);
await answerPendingApproval(app, cookie, bot.id, tested.runId, "allow", waiting);
await waitFor(
app,
cookie,
bot.id,
(snap) => !snap.run || ["completed", "failed", "cancelled"].includes(snap.run.status),
);
expect(connector.records.length).toBeGreaterThan(recordsBefore);
});
it("22: a routine schedule with any malformed or mixed one-shot cron is rejected", async () => {
const cookie = await signup(app, `routine-crons-j-${stamp}@rakazo.test`, "Routine Crons");
const bot = await rpc<Bot>(app, cookie, "bots/create", {
name: "Scheduler",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const partiallyInvalid = await raw(app, cookie, "routines/create", {
botId: bot.id,
name: "Bad schedule",
prompt: "check the fixture",
crons: ["0 9 * * *", "not-a-cron"],
timezone: "UTC",
notify: false,
active: true,
});
expect(partiallyInvalid.status).toBeGreaterThanOrEqual(400);
const mixedOneShot = await raw(app, cookie, "routines/create", {
botId: bot.id,
name: "Mixed schedule",
prompt: "check the fixture",
crons: ["@once", "0 9 * * *"],
timezone: "UTC",
notify: false,
active: false,
});
expect(mixedOneShot.status).toBeGreaterThanOrEqual(400);
});
it("23: never-run one-shot templates can be armed with a future runAt", async () => {
const cookie = await signup(app, `once-arm-j-${stamp}@rakazo.test`, "Once Arm");
const me = await rpc<Me>(app, cookie, "me");
const bot = await rpc<Bot>(app, cookie, "bots/create", {
name: "Once Bot",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const routine = await prisma.routine.create({
data: {
spaceId: me.spaceId,
userId: me.userId,
botId: bot.id,
name: "Remind once",
prompt: "Ping me once",
crons: ["@once"],
timezone: "UTC",
notify: true,
active: false,
nextRunAt: null,
},
});
const withoutTime = await raw(app, cookie, "routines/update", {
routineId: routine.id,
active: true,
});
expect(withoutTime.status).toBeGreaterThanOrEqual(400);
const runAt = new Date(Date.now() + 120_000).toISOString();
const armed = await rpc<{ active: boolean; nextRunAt: string | null }>(
app,
cookie,
"routines/update",
{
routineId: routine.id,
active: true,
runAt,
},
);
expect(armed.active).toBe(true);
expect(armed.nextRunAt).toBe(runAt);
await prisma.routine.update({
where: { id: routine.id },
data: { active: false, nextRunAt: null, lastRunAt: new Date() },
});
const afterFire = await raw(app, cookie, "routines/update", {
routineId: routine.id,
active: true,
runAt: new Date(Date.now() + 180_000).toISOString(),
});
expect(afterFire.status).toBeGreaterThanOrEqual(400);
});
it("24: chat creates a space only after explicit approval", async () => {
const cookie = await signup(app, `space-chat-j-${stamp}@rakazo.test`, "Space Chat");
const me = await rpc<Me>(app, cookie, "me");
const bot = await rpc<Bot>(app, cookie, "bots/create", {
name: "Chief",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const membershipsBefore = await prisma.spaceMember.count({ where: { userId: me.userId } });
const sent = await rpc<{ runId: string }>(app, cookie, "threads/send", {
botId: bot.id,
text: "create a space named Customer support",
});
const waiting = await waitFor(
app,
cookie,
bot.id,
(snap) => snap.run?.id === sent.runId && snap.run.status === "waiting_input",
);
const approval = JSON.stringify(waiting.messages);
expect(approval).toContain("Create space");
expect(approval).toContain("Customer support");
expect(approval).toContain("Cancel");
expect(approval).not.toContain("Always allow this tool");
expect(await prisma.spaceMember.count({ where: { userId: me.userId } })).toBe(
membershipsBefore,
);
await answerPendingApproval(app, cookie, bot.id, sent.runId, "allow", waiting);
await waitFor(
app,
cookie,
bot.id,
(snap) => !snap.run || ["completed", "failed", "cancelled"].includes(snap.run.status),
);
const navigation = await rpc<{ spaces: Array<{ name: string }> }>(app, cookie, "spaces/list");
expect(navigation.spaces.map((space) => space.name)).toContain("Customer support");
expect(await prisma.spaceMember.count({ where: { userId: me.userId } })).toBe(
membershipsBefore + 1,
);
const denied = await rpc<{ runId: string }>(app, cookie, "threads/send", {
botId: bot.id,
text: "create a space named Finance",
});
const deniedWaiting = await waitFor(
app,
cookie,
bot.id,
(snap) => snap.run?.id === denied.runId && snap.run.status === "waiting_input",
);
await answerPendingApproval(app, cookie, bot.id, denied.runId, "deny", deniedWaiting);
await waitFor(
app,
cookie,
bot.id,
(snap) => !snap.run || ["completed", "failed", "cancelled"].includes(snap.run.status),
);
expect(await prisma.spaceMember.count({ where: { userId: me.userId } })).toBe(
membershipsBefore + 1,
);
});
});
type Me = { spaceId: string; userId: string; canChooseHostComputer: boolean };
type Bot = {
id: string;
name: string;
title: string;
description: string;
instructions: string;
notifyOnFinish: boolean;
color: string;
pinned: boolean;
sectionId: string | null;
unread: boolean;
computerMode: "team" | "dedicated";
parentBotId?: string | null;
};
type Snap = {
messages: Array<{
id: string;
seq: number;
runId?: string | null;
blocks: Array<{ kind?: string; status?: string; actions?: unknown[] }>;
}>;
run: { id: string; status: string } | null;
activeRuns?: Array<{ id: string; status: string }>;
};
async function signup(app: App, email: string, name: string) {
const res = await app.request("/api/auth/sign-up/email", {
method: "POST",
headers: {
"content-type": "application/json",
origin: "http://127.0.0.1:5173",
},
body: JSON.stringify({ email, password: "password12", name }),
});
if (res.status >= 400) {
throw new Error(`signup failed ${res.status}: ${await res.text()}`);
}
return sessionCookieHeader(res);
}
async function raw(app: App, cookie: string, proc: string, body: unknown = {}) {
return app.request(`/rpc/${proc}`, {
method: "POST",
headers: {
"content-type": "application/json",
cookie,
origin: "http://127.0.0.1:5173",
},
body: JSON.stringify({ json: body }),
});
}
async function rpc<T>(app: App, cookie: string, proc: string, body: unknown = {}): Promise<T> {
const res = await raw(app, cookie, proc, body);
const text = await res.text();
let parsed: { json?: T; error?: { message?: string } };
try {
parsed = JSON.parse(text) as { json?: T; error?: { message?: string } };
} catch {
throw new Error(`${proc} ${res.status}: ${text}`);
}
if (res.status >= 400 || parsed.error) {
throw new Error(`${proc} ${res.status}: ${parsed.error?.message ?? text}`);
}
return parsed.json as T;
}
async function answerPendingApproval(
app: App,
cookie: string,
botId: string,
runId: string,
answer: "allow" | "always" | "deny",
current?: Snap,
) {
const waiting =
current ??
(await waitFor(
app,
cookie,
botId,
(snap) => snap.run?.id === runId && snap.run.status === "waiting_input",
));
const message = [...waiting.messages]
.reverse()
.find(
(candidate) =>
candidate.runId === runId &&
candidate.blocks.some(
(block) =>
typeof block === "object" &&
block !== null &&
"kind" in block &&
block.kind === "ask" &&
"approvalEffectId" in block &&
typeof block.approvalEffectId === "string" &&
"actions" in block &&
Array.isArray(block.actions),
),
);
if (!message) throw new Error(`run ${runId} did not expose an approval card`);
await rpc(app, cookie, "threads/answer", {
botId,
runId,
messageId: message.id,
answer,
});
}
async function waitFor(app: App, cookie: string, botId: string, pred: (snap: Snap) => boolean) {
const start = Date.now();
let last: Snap | null = null;
while (Date.now() - start < 20_000) {
last = await rpc<Snap>(app, cookie, "threads/get", { botId });
if (pred(last)) return last;
await new Promise((r) => setTimeout(r, 100));
}
throw new Error(`timeout waiting for thread: ${JSON.stringify(last)}`);
}
async function waitForDatabase(pred: () => Promise<boolean>) {
const start = Date.now();
while (Date.now() - start < 10_000) {
if (await pred()) return;
await new Promise((resolve) => setTimeout(resolve, 25));
}
throw new Error("timeout waiting for database state");
}