evOo-local/packages/db/src/events.test.ts
Cursor Agent c8c6d9e042 fix(db): require run owner for all secret and approval answers
answerRunInput already blocked cross-member answers for credential
secret asks and always-allow approvals, but one-time allow/deny and
generic OTP secret asks had no run.userId check. Align the DB gate so
shared-space prompt answers cannot be hijacked at the persistence layer.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-09-22 07:04:47 +00:00

2060 lines
64 KiB
TypeScript

import type { RealtimeFanout } from "@rakazo/adapter-kit";
import { describe, expect, it, vi } from "vitest";
import type { PrismaClient } from "./client.js";
import {
answerRunInput,
appendEvent,
claimSteering,
clearThread,
completedRunBlocks,
finalizeComputerControlRelease,
finalizeRun,
followThreadEvents,
pauseRunForInput,
pauseRunForTakeover,
sendUserMessage,
} from "./events.js";
import { RunHistoryWriteError } from "./messages.js";
class TestFanout implements RealtimeFanout {
subscriber: ((payload: string) => void) | undefined;
unsubscribed = false;
describe() {
return {
id: "test",
contractVersion: "1",
adapterVersion: "1",
capabilities: { distributed: false, push: true },
};
}
async publish(_topic: string, payload: string) {
this.subscriber?.(payload);
}
async subscribe(_topic: string, subscriber: (payload: string) => void) {
this.subscriber = subscriber;
return async () => {
this.unsubscribed = true;
this.subscriber = undefined;
};
}
async close() {}
}
function event(seq: number) {
return {
id: `event-${seq}`,
spaceId: "workspace-1",
threadId: "thread-1",
botId: "bot-1",
seq,
type: "run.started",
payload: {},
runId: null,
createdAt: new Date("2026-08-15T12:00:00.000Z"),
};
}
describe("finalizeRun", () => {
it("stamps the final steps block with wall-clock run duration", () => {
const blocks = [
{ kind: "steps" as const, steps: [{ label: "Read file", count: 1 }] },
{ kind: "text" as const, text: "Then I checked it." },
{ kind: "steps" as const, steps: [{ label: "Run tests", count: 1 }] },
];
expect(
completedRunBlocks(
blocks,
new Date("2026-09-01T12:00:00.000Z"),
new Date("2026-09-01T12:01:43.000Z"),
),
).toEqual([blocks[0], blocks[1], { ...blocks[2], durationMs: 103_000 }]);
expect(completedRunBlocks(blocks, null, new Date())).toBe(blocks);
});
it("retries a transaction conflict without duplicating the terminal event or notification", async () => {
const conflict = Object.assign(new Error("serialization conflict"), { code: "P2034" });
const createEvent = vi.fn(async () => ({ threadId: "thread-1", seq: 0 }));
const tx = {
$queryRaw: vi.fn(async () => []),
run: {
findUnique: vi.fn(async () => ({ status: "running" })),
findUniqueOrThrow: vi.fn(async () => ({ sourceMessage: null })),
findFirst: vi.fn(async () => null),
updateMany: vi.fn(async () => ({ count: 1 })),
},
attempt: { updateMany: vi.fn(async () => ({ count: 1 })) },
task: { updateMany: vi.fn(async () => ({ count: 1 })) },
thread: { update: vi.fn(async () => ({ nextEventSeq: 1 })) },
event: { create: createEvent, deleteMany: vi.fn(async () => ({ count: 0 })) },
steeringMessage: {
findMany: vi.fn(async () => []),
updateMany: vi.fn(async () => ({ count: 0 })),
},
bot: { update: vi.fn(async () => ({})) },
};
const transaction = vi
.fn()
.mockRejectedValueOnce(conflict)
.mockImplementation(async (operation: (client: typeof tx) => unknown) => operation(tx));
const publish = vi.fn(async () => undefined);
await expect(
finalizeRun(
{ $transaction: transaction } as unknown as PrismaClient,
{
spaceId: "space-1",
threadId: "thread-1",
botId: "bot-1",
runId: "run-1",
taskId: "task-1",
attemptId: "attempt-1",
leaseOwner: "worker-1",
leaseFence: 1,
outcome: "failed",
error: "failed",
},
{ publish } as never,
),
).resolves.toEqual({ continuationRunId: null });
expect(transaction).toHaveBeenCalledTimes(2);
expect(createEvent).toHaveBeenCalledOnce();
expect(publish).toHaveBeenCalledOnce();
});
});
describe("followThreadEvents", () => {
it("does not lose a notification that arrives while querying", async () => {
const fanout = new TestFanout();
const findMany = vi
.fn()
.mockImplementationOnce(async () => {
await fanout.publish("thread:thread-1", "wake");
return [];
})
.mockResolvedValueOnce([event(0)])
.mockResolvedValue([]);
const prisma = { event: { findMany } } as unknown as PrismaClient;
const abort = new AbortController();
const stream = followThreadEvents(prisma, "thread-1", -1, fanout, abort.signal, 10_000);
await expect(stream.next()).resolves.toMatchObject({ value: { seq: 0 }, done: false });
expect(findMany).toHaveBeenCalledTimes(2);
abort.abort();
await stream.return(undefined);
expect(fanout.unsubscribed).toBe(true);
});
it("periodically catches up when a signal is missed", async () => {
const findMany = vi
.fn()
.mockResolvedValueOnce([])
.mockResolvedValueOnce([event(0)]);
const prisma = { event: { findMany } } as unknown as PrismaClient;
const abort = new AbortController();
const stream = followThreadEvents(prisma, "thread-1", -1, undefined, abort.signal, 1);
await expect(stream.next()).resolves.toMatchObject({ value: { seq: 0 }, done: false });
abort.abort();
await stream.return(undefined);
});
});
describe("finalizeComputerControlRelease", () => {
it("clears the matching lease and appends its release event in one transaction", async () => {
const fanout = new TestFanout();
const publish = vi.spyOn(fanout, "publish");
const tx = {
computer: { updateMany: vi.fn().mockResolvedValue({ count: 1 }) },
bot: {
findFirst: vi.fn().mockResolvedValue({
computerId: "computer-1",
thread: { id: "thread-1" },
}),
},
run: {
findUnique: vi.fn().mockResolvedValue({ status: "waiting_takeover" }),
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
},
thread: { update: vi.fn().mockResolvedValue({ nextEventSeq: 8 }) },
event: {
create: vi.fn().mockResolvedValue({
...event(7),
type: "computer.takeover.released",
payload: { holder: "none", leaseId: "lease-1", reason: "expired" },
}),
},
};
const prisma = {
message: { findUnique: vi.fn().mockResolvedValue(null) },
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
finalizeComputerControlRelease(
prisma,
{
spaceId: "workspace-1",
computerId: "computer-1",
botId: "bot-1",
runId: "run-1",
leaseId: "lease-1",
holder: "none",
reason: "expired",
},
fanout,
),
).resolves.toEqual({ runId: "run-1" });
expect(tx.computer.updateMany).toHaveBeenCalledWith({
where: {
id: "computer-1",
spaceId: "workspace-1",
controlBotId: "bot-1",
controlLeaseId: "lease-1",
controlRunId: "run-1",
},
data: {
controlHolder: "none",
controlLeaseId: null,
controlLeaseExpiresAt: null,
controlBotId: null,
controlRunId: null,
},
});
expect(tx.run.updateMany).toHaveBeenCalledWith({
where: {
id: "run-1",
spaceId: "workspace-1",
botId: "bot-1",
status: "waiting_takeover",
},
data: { status: "queued", checkpoint: "takeover-skipped" },
});
expect(tx.event.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
type: "computer.takeover.released",
runId: "run-1",
payload: { holder: "none", leaseId: "lease-1", reason: "expired" },
}),
}),
);
expect(publish).toHaveBeenCalledWith("thread:thread-1", JSON.stringify({ cursor: 7 }));
});
it("stamps a takeover checkpoint on a steered running run without clearing its lease", async () => {
const tx = {
computer: { updateMany: vi.fn().mockResolvedValue({ count: 1 }) },
bot: {
findFirst: vi.fn().mockResolvedValue({
computerId: "computer-1",
thread: { id: "thread-1" },
}),
},
run: {
findUnique: vi.fn().mockResolvedValue({ status: "running" }),
updateMany: vi.fn().mockResolvedValueOnce({ count: 0 }).mockResolvedValueOnce({ count: 1 }),
},
thread: { update: vi.fn().mockResolvedValue({ nextEventSeq: 8 }) },
event: {
create: vi.fn().mockResolvedValue({
...event(7),
type: "computer.takeover.released",
payload: { holder: "bot", leaseId: "lease-1", reason: "released" },
}),
},
};
const prisma = {
message: { findUnique: vi.fn().mockResolvedValue(null) },
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
finalizeComputerControlRelease(prisma, {
spaceId: "workspace-1",
computerId: "computer-1",
botId: "bot-1",
runId: "run-1",
leaseId: "lease-1",
holder: "bot",
reason: "released",
}),
).resolves.toEqual({ runId: "run-1" });
expect(tx.run.updateMany).toHaveBeenNthCalledWith(1, {
where: {
id: "run-1",
spaceId: "workspace-1",
botId: "bot-1",
status: "waiting_takeover",
},
data: { status: "queued", checkpoint: "takeover" },
});
expect(tx.run.updateMany).toHaveBeenNthCalledWith(2, {
where: {
id: "run-1",
spaceId: "workspace-1",
botId: "bot-1",
status: { in: ["leased", "running"] },
},
data: { checkpoint: "takeover" },
});
expect(tx.event.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
type: "computer.takeover.released",
runId: "run-1",
}),
}),
);
});
it("clears the lease even if its controlling bot was deleted", async () => {
const tx = {
computer: { updateMany: vi.fn().mockResolvedValue({ count: 1 }) },
bot: { findFirst: vi.fn().mockResolvedValue(null) },
};
const prisma = {
message: { findUnique: vi.fn().mockResolvedValue(null) },
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
finalizeComputerControlRelease(prisma, {
spaceId: "workspace-1",
computerId: "computer-1",
botId: "deleted-bot",
runId: null,
leaseId: "lease-1",
holder: "none",
reason: "expired",
}),
).resolves.toEqual({ runId: null });
expect(tx.computer.updateMany).toHaveBeenCalledOnce();
});
});
describe("pauseRunForInput", () => {
it("stores the paused run, prompt, and status event in one transaction", async () => {
const fanout = new TestFanout();
const publish = vi.spyOn(fanout, "publish");
const tx = {
$queryRaw: vi.fn().mockResolvedValue([{ id: "thread-1" }]),
run: {
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
findUnique: vi.fn().mockResolvedValue({
status: "running",
createdAt: new Date("2026-08-16T12:00:00.000Z"),
threadId: "thread-1",
}),
},
attempt: { updateMany: vi.fn().mockResolvedValue({ count: 1 }) },
thread: {
update: vi
.fn()
.mockResolvedValueOnce({ nextMessageSeq: 4 })
.mockResolvedValueOnce({ nextEventSeq: 8 })
.mockResolvedValueOnce({ nextEventSeq: 9 }),
},
message: { create: vi.fn().mockResolvedValue({ id: "message-1" }) },
event: {
create: vi.fn(async ({ data }: { data: { seq: number; type: string } }) => ({
...event(data.seq),
type: data.type,
})),
deleteMany: vi.fn().mockResolvedValue({ count: 1 }),
findFirst: vi.fn().mockResolvedValue(null),
},
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
pauseRunForInput(
prisma,
{
spaceId: "workspace-1",
threadId: "thread-1",
botId: "bot-1",
runId: "run-1",
attemptId: "attempt-1",
leaseOwner: "worker-1",
leaseFence: 3,
blocks: [{ kind: "ask", text: "Which city?" }],
},
fanout,
),
).resolves.toBe(true);
expect(tx.message.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
botId: "bot-1",
runId: "run-1",
role: "bot",
}),
}),
);
expect(tx.run.updateMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({ status: "running", leaseFence: 3 }),
data: { status: "waiting_input", leaseOwner: null, leaseExpiresAt: null },
}),
);
expect(tx.event.create.mock.calls.map(([input]) => input.data.type)).toEqual([
"thread.message.created",
"run.waiting_input",
]);
expect(publish).toHaveBeenCalledWith("thread:thread-1", JSON.stringify({ cursor: 8 }));
});
it("stores offered choice actions on the run checkpoint for resume", async () => {
const fanout = new TestFanout();
const tx = {
$queryRaw: vi.fn().mockResolvedValue([{ id: "thread-1" }]),
run: {
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
findUnique: vi.fn().mockResolvedValue({
status: "running",
createdAt: new Date("2026-08-16T12:00:00.000Z"),
threadId: "thread-1",
}),
},
attempt: { updateMany: vi.fn().mockResolvedValue({ count: 1 }) },
thread: {
update: vi
.fn()
.mockResolvedValueOnce({ nextMessageSeq: 4 })
.mockResolvedValueOnce({ nextEventSeq: 8 })
.mockResolvedValueOnce({ nextEventSeq: 9 }),
},
message: { create: vi.fn().mockResolvedValue({ id: "message-1" }) },
event: {
create: vi.fn(async ({ data }: { data: { seq: number; type: string } }) => ({
...event(data.seq),
type: data.type,
})),
deleteMany: vi.fn().mockResolvedValue({ count: 1 }),
findFirst: vi.fn().mockResolvedValue(null),
},
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
pauseRunForInput(
prisma,
{
spaceId: "workspace-1",
threadId: "thread-1",
botId: "bot-1",
runId: "run-1",
attemptId: "attempt-1",
leaseOwner: "worker-1",
leaseFence: 3,
blocks: [
{
kind: "ask",
text: "Which token?",
status: "pending",
actions: [
{ id: "choice-1", label: "use [redacted]" },
{ id: "choice-2", label: "use plain" },
],
},
],
offeredActions: [
{ id: "choice-1", label: "use sk-live-choice-secret" },
{ id: "choice-2", label: "use plain" },
],
},
fanout,
),
).resolves.toBe(true);
expect(tx.run.updateMany).toHaveBeenCalledWith(
expect.objectContaining({
data: {
status: "waiting_input",
leaseOwner: null,
leaseExpiresAt: null,
checkpoint: JSON.stringify({
kind: "choice_ask_v1",
actions: [
{ id: "choice-1", label: "use sk-live-choice-secret" },
{ id: "choice-2", label: "use plain" },
],
}),
},
}),
);
});
});
describe("pauseRunForTakeover", () => {
it("stores the paused run, attempt, computer takeover mark, and event in one transaction", async () => {
const fanout = new TestFanout();
const publish = vi.spyOn(fanout, "publish");
const tx = {
$queryRaw: vi.fn().mockResolvedValue([{ id: "thread-1" }]),
run: {
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
findUnique: vi.fn().mockResolvedValue({ status: "waiting_takeover" }),
},
attempt: { updateMany: vi.fn().mockResolvedValue({ count: 1 }) },
computer: {
findFirst: vi.fn().mockResolvedValue({
controlHolder: "none",
controlBotId: null,
controlLeaseId: null,
controlLeaseExpiresAt: null,
}),
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
},
thread: { update: vi.fn().mockResolvedValue({ nextEventSeq: 8 }) },
event: {
create: vi.fn(async ({ data }: { data: { seq: number; type: string } }) => ({
...event(data.seq),
type: data.type,
})),
deleteMany: vi.fn().mockResolvedValue({ count: 1 }),
},
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
pauseRunForTakeover(
prisma,
{
spaceId: "workspace-1",
threadId: "thread-1",
botId: "bot-1",
runId: "run-1",
attemptId: "attempt-1",
leaseOwner: "worker-1",
leaseFence: 3,
reason: "Sign in",
computerId: "computer-1",
},
fanout,
),
).resolves.toBe(true);
expect(tx.run.updateMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({ status: "running", leaseFence: 3 }),
data: {
status: "waiting_takeover",
leaseOwner: null,
leaseExpiresAt: null,
checkpoint: null,
},
}),
);
expect(tx.attempt.updateMany).toHaveBeenCalledWith(
expect.objectContaining({ data: expect.objectContaining({ status: "waiting_takeover" }) }),
);
expect(tx.computer.updateMany).toHaveBeenCalledWith({
where: { id: "computer-1", spaceId: "workspace-1" },
data: {
state: "running",
controlHolder: "none",
controlLeaseId: null,
controlLeaseExpiresAt: null,
controlBotId: null,
controlRunId: "run-1",
},
});
expect(tx.event.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
type: "computer.takeover.requested",
payload: {
reason: "Sign in",
takeoverRequested: true,
retainedControl: false,
},
}),
}),
);
expect(publish).toHaveBeenCalledWith("thread:thread-1", JSON.stringify({ cursor: 7 }));
});
it("keeps an active user control lease and only binds controlRunId", async () => {
const expiresAt = new Date(Date.now() + 60_000);
const tx = {
$queryRaw: vi.fn().mockResolvedValue([{ id: "thread-1" }]),
run: {
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
findUnique: vi.fn().mockResolvedValue({ status: "waiting_takeover" }),
},
attempt: { updateMany: vi.fn().mockResolvedValue({ count: 1 }) },
computer: {
findFirst: vi.fn().mockResolvedValue({
controlHolder: "user",
controlBotId: "bot-1",
controlLeaseId: "lease-1",
controlLeaseExpiresAt: expiresAt,
}),
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
},
thread: { update: vi.fn().mockResolvedValue({ nextEventSeq: 8 }) },
event: {
create: vi.fn(async ({ data }: { data: { seq: number; type: string } }) => ({
...event(data.seq),
type: data.type,
})),
deleteMany: vi.fn().mockResolvedValue({ count: 1 }),
},
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
pauseRunForTakeover(prisma, {
spaceId: "workspace-1",
threadId: "thread-1",
botId: "bot-1",
runId: "run-1",
attemptId: "attempt-1",
leaseOwner: "worker-1",
leaseFence: 3,
reason: "Sign in",
computerId: "computer-1",
}),
).resolves.toBe(true);
expect(tx.computer.updateMany).toHaveBeenCalledWith({
where: { id: "computer-1", spaceId: "workspace-1" },
data: { state: "running", controlRunId: "run-1" },
});
expect(tx.event.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
payload: expect.objectContaining({ retainedControl: true, takeoverRequested: true }),
}),
}),
);
});
it("preserves another bot's active user lease without binding this run to it", async () => {
const expiresAt = new Date(Date.now() + 60_000);
const tx = {
$queryRaw: vi.fn().mockResolvedValue([{ id: "computer-1" }]),
run: {
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
findUnique: vi.fn().mockResolvedValue({ status: "waiting_takeover" }),
},
attempt: { updateMany: vi.fn().mockResolvedValue({ count: 1 }) },
computer: {
findFirst: vi.fn().mockResolvedValue({
controlHolder: "user",
controlBotId: "other-bot",
controlLeaseId: "lease-other",
controlLeaseExpiresAt: expiresAt,
}),
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
},
thread: { update: vi.fn().mockResolvedValue({ nextEventSeq: 8 }) },
event: {
create: vi.fn(async ({ data }: { data: { seq: number; type: string } }) => ({
...event(data.seq),
type: data.type,
})),
deleteMany: vi.fn().mockResolvedValue({ count: 1 }),
},
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
pauseRunForTakeover(prisma, {
spaceId: "workspace-1",
threadId: "thread-1",
botId: "bot-1",
runId: "run-1",
attemptId: "attempt-1",
leaseOwner: "worker-1",
leaseFence: 3,
reason: "Sign in",
computerId: "computer-1",
}),
).resolves.toBe(true);
expect(tx.computer.updateMany).toHaveBeenCalledWith({
where: { id: "computer-1", spaceId: "workspace-1" },
data: { state: "running" },
});
expect(tx.event.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
payload: expect.objectContaining({ retainedControl: false, takeoverRequested: true }),
}),
}),
);
});
});
describe("answerRunInput", () => {
it("answers only the selected pending prompt and publishes its update", async () => {
const fanout = new TestFanout();
const publish = vi.spyOn(fanout, "publish");
publish.mockRejectedValueOnce(new Error("realtime unavailable"));
const tx = {
$queryRaw: vi.fn().mockResolvedValue([{ id: "thread-1" }]),
message: {
findFirst: vi.fn().mockResolvedValue({
id: "message-1",
blocks: [
{
kind: "ask",
text: "Which city?",
status: "pending",
actions: [
{ id: "choice-1", label: "Berlin" },
{ id: "choice-2", label: "Paris" },
],
},
],
}),
update: vi.fn().mockResolvedValue({ id: "message-1" }),
},
run: {
findFirst: vi.fn().mockResolvedValue({ botId: "bot-2" }),
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
findUnique: vi.fn().mockResolvedValue({
status: "queued",
createdAt: new Date("2026-08-16T12:00:00.000Z"),
threadId: "thread-1",
}),
},
task: { updateMany: vi.fn().mockResolvedValue({ count: 1 }) },
thread: { update: vi.fn().mockResolvedValue({ nextEventSeq: 10 }) },
event: {
create: vi.fn(async ({ data }: { data: { seq: number; type: string } }) => ({
...event(data.seq),
type: data.type,
})),
findFirst: vi.fn().mockResolvedValue(null),
},
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
answerRunInput(
prisma,
{
spaceId: "workspace-1",
threadId: "thread-1",
runId: "run-1",
messageId: "message-1",
answeredByUserId: "user-1",
answer: "choice-2",
},
fanout,
),
).resolves.toBe(true);
expect(tx.run.updateMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({ status: "waiting_input" }),
data: { status: "queued", checkpoint: null },
}),
);
expect(tx.message.update).toHaveBeenCalledWith({
where: { id: "message-1" },
data: {
blocks: [
{
kind: "ask",
text: "Which city?",
status: "answered",
answer: "choice-2",
actions: [
{ id: "choice-1", label: "Berlin" },
{ id: "choice-2", label: "Paris" },
],
},
],
},
});
expect(tx.task.updateMany).toHaveBeenCalledWith({
where: { runs: { some: { id: "run-1" } } },
data: { prompt: "Selected choice choice-2: Paris" },
});
expect(tx.event.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ type: "thread.message.updated", botId: "bot-2" }),
}),
);
expect(publish).toHaveBeenCalledWith("thread:thread-1", JSON.stringify({ cursor: 9 }));
});
it("resumes with the offered choice label when the persisted label was redacted", async () => {
const fanout = new TestFanout();
const secret = "sk-live-choice-secret";
const tx = {
$queryRaw: vi.fn().mockResolvedValue([{ id: "thread-1" }]),
message: {
findFirst: vi.fn().mockResolvedValue({
id: "message-1",
blocks: [
{
kind: "ask",
text: "Which token?",
status: "pending",
actions: [
{ id: "choice-1", label: `use [redacted]` },
{ id: "choice-2", label: "use plain" },
],
},
],
}),
update: vi.fn().mockResolvedValue({ id: "message-1" }),
},
run: {
findFirst: vi.fn().mockResolvedValue({
botId: "bot-2",
userId: "user-1",
checkpoint: JSON.stringify({
kind: "choice_ask_v1",
actions: [
{ id: "choice-1", label: `use ${secret}` },
{ id: "choice-2", label: "use plain" },
],
}),
}),
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
findUnique: vi.fn().mockResolvedValue({
status: "queued",
createdAt: new Date("2026-08-16T12:00:00.000Z"),
threadId: "thread-1",
}),
},
task: { updateMany: vi.fn().mockResolvedValue({ count: 1 }) },
thread: { update: vi.fn().mockResolvedValue({ nextEventSeq: 11 }) },
event: {
create: vi.fn(async ({ data }: { data: { seq: number; type: string } }) => ({
...event(data.seq),
type: data.type,
})),
findFirst: vi.fn().mockResolvedValue(null),
},
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
answerRunInput(
prisma,
{
spaceId: "workspace-1",
threadId: "thread-1",
runId: "run-1",
messageId: "message-1",
answeredByUserId: "user-1",
answer: "choice-1",
},
fanout,
),
).resolves.toBe(true);
expect(tx.task.updateMany).toHaveBeenCalledWith({
where: { runs: { some: { id: "run-1" } } },
data: { prompt: `Selected choice choice-1: use ${secret}` },
});
expect(tx.message.update).toHaveBeenCalledWith({
where: { id: "message-1" },
data: {
blocks: [
{
kind: "ask",
text: "Which token?",
status: "answered",
answer: "choice-1",
actions: [
{ id: "choice-1", label: "use [redacted]" },
{ id: "choice-2", label: "use plain" },
],
},
],
},
});
expect(tx.run.updateMany).toHaveBeenCalledWith(
expect.objectContaining({
data: { status: "queued", checkpoint: null },
}),
);
});
it("does not queue a run for a choice the card did not offer", async () => {
const tx = {
$queryRaw: vi.fn().mockResolvedValue([{ id: "thread-1" }]),
message: {
findFirst: vi.fn().mockResolvedValue({
id: "message-1",
blocks: [
{
kind: "ask",
text: "Which city?",
status: "pending",
actions: [
{ id: "Berlin", label: "Berlin" },
{ id: "Seoul", label: "Seoul" },
],
},
],
}),
},
run: {
findFirst: vi.fn().mockResolvedValue({ botId: "bot-2", userId: "user-1" }),
updateMany: vi.fn(),
},
task: { updateMany: vi.fn() },
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
answerRunInput(prisma, {
spaceId: "workspace-1",
threadId: "thread-1",
runId: "run-1",
messageId: "message-1",
answeredByUserId: "user-1",
answer: "Toronto",
}),
).resolves.toBe(false);
expect(tx.run.updateMany).not.toHaveBeenCalled();
expect(tx.task.updateMany).not.toHaveBeenCalled();
});
it("approves consequential actions without overwriting the task prompt", async () => {
const fanout = new TestFanout();
const tx = {
$queryRaw: vi.fn().mockResolvedValue([{ id: "thread-1" }]),
message: {
findFirst: vi.fn().mockResolvedValue({
id: "message-1",
blocks: [
{
kind: "ask",
approvalEffectId: "effect-1",
text: "Review before writing",
status: "pending",
actions: [
{ id: "allow", label: "Allow once" },
{ id: "deny", label: "Deny" },
],
},
],
}),
update: vi.fn().mockResolvedValue({ id: "message-1" }),
},
run: {
findFirst: vi.fn().mockResolvedValue({ botId: "bot-1", userId: "user-1" }),
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
findUnique: vi.fn().mockResolvedValue({ status: "queued" }),
},
task: { updateMany: vi.fn() },
externalEffect: {
findFirst: vi.fn().mockResolvedValue({ id: "effect-1", status: "intended" }),
update: vi.fn().mockResolvedValue({ id: "effect-1" }),
},
thread: { update: vi.fn().mockResolvedValue({ nextEventSeq: 10 }) },
event: {
create: vi.fn(async ({ data }: { data: { seq: number; type: string } }) => ({
...event(data.seq),
type: data.type,
})),
},
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
answerRunInput(
prisma,
{
spaceId: "workspace-1",
threadId: "thread-1",
runId: "run-1",
messageId: "message-1",
answeredByUserId: "user-1",
answer: "allow",
},
fanout,
),
).resolves.toBe(true);
expect(tx.task.updateMany).not.toHaveBeenCalled();
expect(tx.externalEffect.findFirst).toHaveBeenCalledWith({
where: {
id: "effect-1",
spaceId: "workspace-1",
runId: "run-1",
status: "intended",
},
});
expect(tx.externalEffect.update).toHaveBeenCalledWith({
where: { id: "effect-1" },
data: { status: "approved" },
});
});
it("approves and upserts always-allow without overwriting the task prompt", async () => {
const fanout = new TestFanout();
const tx = {
$queryRaw: vi.fn().mockResolvedValue([{ id: "thread-1" }]),
message: {
findFirst: vi.fn().mockResolvedValue({
id: "message-1",
blocks: [
{
kind: "ask",
approvalEffectId: "effect-1",
text: "Review before writing",
status: "pending",
actions: [
{ id: "allow", label: "Allow once" },
{ id: "always", label: "Always allow" },
{ id: "deny", label: "Deny" },
],
},
],
}),
update: vi.fn().mockResolvedValue({ id: "message-1" }),
},
run: {
findFirst: vi.fn().mockResolvedValue({ botId: "bot-1", userId: "user-1" }),
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
findUnique: vi.fn().mockResolvedValue({ userId: "user-1", status: "queued" }),
},
task: { updateMany: vi.fn() },
externalEffect: {
findFirst: vi
.fn()
.mockResolvedValue({ id: "effect-1", status: "intended", kind: "destination.write" }),
update: vi.fn().mockResolvedValue({ id: "effect-1" }),
},
actionApprovalRule: {
upsert: vi.fn().mockResolvedValue({ id: "rule-1" }),
},
thread: { update: vi.fn().mockResolvedValue({ nextEventSeq: 10 }) },
event: {
create: vi.fn(async ({ data }: { data: { seq: number; type: string } }) => ({
...event(data.seq),
type: data.type,
})),
},
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
answerRunInput(
prisma,
{
spaceId: "workspace-1",
threadId: "thread-1",
runId: "run-1",
messageId: "message-1",
answeredByUserId: "user-1",
answer: "always",
},
fanout,
),
).resolves.toBe(true);
expect(tx.task.updateMany).not.toHaveBeenCalled();
expect(tx.externalEffect.update).toHaveBeenCalledWith({
where: { id: "effect-1" },
data: { status: "approved" },
});
expect(tx.actionApprovalRule.upsert).toHaveBeenCalledWith({
where: {
spaceId_createdByUserId_effect_matchKind_matchValue: {
spaceId: "workspace-1",
createdByUserId: "user-1",
effect: "always_allow",
matchKind: "tool",
matchValue: "destination.write",
},
},
create: {
spaceId: "workspace-1",
createdByUserId: "user-1",
effect: "always_allow",
matchKind: "tool",
matchValue: "destination.write",
},
update: {},
});
});
it("does not let another workspace member approve a consequential action with allow", async () => {
const tx = {
$queryRaw: vi.fn().mockResolvedValue([{ id: "thread-1" }]),
message: {
findFirst: vi.fn().mockResolvedValue({
id: "message-1",
blocks: [
{
kind: "ask",
approvalEffectId: "effect-1",
text: "Review before writing",
status: "pending",
actions: [
{ id: "allow", label: "Allow once" },
{ id: "deny", label: "Deny" },
],
},
],
}),
},
run: {
findFirst: vi.fn().mockResolvedValue({ botId: "bot-1", userId: "run-owner" }),
updateMany: vi.fn(),
},
externalEffect: {
findFirst: vi.fn().mockResolvedValue({ id: "effect-1", status: "intended" }),
update: vi.fn(),
},
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
answerRunInput(prisma, {
spaceId: "workspace-1",
threadId: "thread-1",
runId: "run-1",
messageId: "message-1",
answeredByUserId: "other-member",
answer: "allow",
}),
).resolves.toBe(false);
expect(tx.externalEffect.update).not.toHaveBeenCalled();
expect(tx.run.updateMany).not.toHaveBeenCalled();
});
it("does not let another workspace member create an always-allow rule for the run owner", async () => {
const tx = {
$queryRaw: vi.fn().mockResolvedValue([{ id: "thread-1" }]),
message: {
findFirst: vi.fn().mockResolvedValue({
id: "message-1",
blocks: [
{
kind: "ask",
approvalEffectId: "effect-1",
text: "Review before writing",
status: "pending",
actions: [
{ id: "allow", label: "Allow once" },
{ id: "always", label: "Always allow" },
{ id: "deny", label: "Deny" },
],
},
],
}),
},
run: {
findFirst: vi.fn().mockResolvedValue({ botId: "bot-1", userId: "run-owner" }),
findUnique: vi.fn().mockResolvedValue({ userId: "run-owner" }),
updateMany: vi.fn(),
},
externalEffect: {
findFirst: vi
.fn()
.mockResolvedValue({ id: "effect-1", status: "intended", kind: "destination.write" }),
},
actionApprovalRule: { upsert: vi.fn() },
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
answerRunInput(prisma, {
spaceId: "workspace-1",
threadId: "thread-1",
runId: "run-1",
messageId: "message-1",
answeredByUserId: "other-member",
answer: "always",
}),
).resolves.toBe(false);
expect(tx.run.updateMany).not.toHaveBeenCalled();
expect(tx.actionApprovalRule.upsert).not.toHaveBeenCalled();
});
it("does not queue a run when an approval card has no matching effect", async () => {
const tx = {
$queryRaw: vi.fn().mockResolvedValue([{ id: "thread-1" }]),
message: {
findFirst: vi.fn().mockResolvedValue({
id: "message-1",
blocks: [
{
kind: "ask",
approvalEffectId: "missing-effect",
text: "Review before writing",
status: "pending",
actions: [
{ id: "allow", label: "Allow once" },
{ id: "deny", label: "Deny" },
],
},
],
}),
},
run: {
findFirst: vi.fn().mockResolvedValue({ botId: "bot-1" }),
updateMany: vi.fn(),
},
externalEffect: { findFirst: vi.fn().mockResolvedValue(null) },
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
answerRunInput(prisma, {
spaceId: "workspace-1",
threadId: "thread-1",
runId: "run-1",
messageId: "message-1",
answeredByUserId: "user-1",
answer: "allow",
}),
).resolves.toBe(false);
expect(tx.run.updateMany).not.toHaveBeenCalled();
});
it("does not queue a run for an action that the approval card did not offer", async () => {
const tx = {
$queryRaw: vi.fn().mockResolvedValue([{ id: "thread-1" }]),
message: {
findFirst: vi.fn().mockResolvedValue({
id: "message-1",
blocks: [
{
kind: "ask",
approvalEffectId: "effect-1",
text: "Review before writing",
status: "pending",
actions: [
{ id: "allow", label: "Allow once" },
{ id: "deny", label: "Deny" },
],
},
],
}),
},
run: {
findFirst: vi.fn().mockResolvedValue({ botId: "bot-1" }),
updateMany: vi.fn(),
},
externalEffect: { findFirst: vi.fn() },
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
answerRunInput(prisma, {
spaceId: "workspace-1",
threadId: "thread-1",
runId: "run-1",
messageId: "message-1",
answeredByUserId: "user-1",
answer: "always",
}),
).resolves.toBe(false);
expect(tx.externalEffect.findFirst).not.toHaveBeenCalled();
expect(tx.run.updateMany).not.toHaveBeenCalled();
});
it("rejects an already answered prompt without queuing the run", async () => {
const tx = {
$queryRaw: vi.fn().mockResolvedValue([{ id: "thread-1" }]),
message: {
findFirst: vi.fn().mockResolvedValue({
id: "message-1",
blocks: [{ kind: "ask", text: "Which city?", status: "answered", answer: "Paris" }],
}),
},
run: {
findFirst: vi.fn().mockResolvedValue({ botId: "bot-1" }),
updateMany: vi.fn(),
},
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
answerRunInput(prisma, {
spaceId: "workspace-1",
threadId: "thread-1",
runId: "run-1",
messageId: "message-1",
answeredByUserId: "user-1",
answer: "Rome",
}),
).resolves.toBe(false);
expect(tx.run.updateMany).not.toHaveBeenCalled();
});
it.each([
undefined,
{ name: "example_api", origin: "https://api.example.test", auth: { type: "bearer" } },
])("stores secret asks without writing plaintext to the task prompt (%j)", async (credential) => {
const fanout = new TestFanout();
const store = vi.fn().mockResolvedValue(undefined);
const tx = {
$queryRaw: vi.fn().mockResolvedValue([{ id: "thread-1" }]),
message: {
findFirst: vi.fn().mockResolvedValue({
id: "message-1",
blocks: [
{
kind: "ask",
text: "Enter your code",
input: "secret",
purpose: "api_key",
...(credential ? { credential } : {}),
status: "pending",
},
],
}),
update: vi.fn().mockResolvedValue({ id: "message-1" }),
},
run: {
findFirst: vi.fn().mockResolvedValue({ botId: "bot-1", userId: "user-1" }),
findUnique: vi.fn().mockResolvedValue({ status: "queued" }),
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
},
task: { updateMany: vi.fn() },
externalEffect: { updateMany: vi.fn().mockResolvedValue({ count: 1 }) },
thread: { update: vi.fn().mockResolvedValue({ nextEventSeq: 10 }) },
event: {
create: vi.fn(async ({ data }: { data: { seq: number; type: string } }) => ({
...event(data.seq),
type: data.type,
})),
},
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
answerRunInput(
prisma,
{
spaceId: "workspace-1",
threadId: "thread-1",
runId: "run-1",
messageId: "message-1",
answeredByUserId: "user-1",
answer: "123456",
},
fanout,
{ store },
),
).resolves.toBe(true);
expect(store).toHaveBeenCalledWith(
expect.objectContaining({
runId: "run-1",
plaintext: "123456",
botId: "bot-1",
userId: "user-1",
spaceId: "workspace-1",
credential,
}),
);
expect(tx.task.updateMany).not.toHaveBeenCalled();
expect(JSON.stringify(tx.event.create.mock.calls)).not.toContain("123456");
expect(tx.externalEffect.updateMany).toHaveBeenCalledWith({
where: {
runId: "run-1",
spaceId: "workspace-1",
kind: "request_secret",
status: "intended",
},
data: {
status: "approved",
...(credential ? { result: { credentialSaved: credential } } : {}),
},
});
expect(tx.message.update).toHaveBeenCalledWith({
where: { id: "message-1" },
data: {
blocks: [
{
kind: "ask",
text: "Enter your code",
input: "secret",
purpose: "api_key",
...(credential ? { credential } : {}),
status: "answered",
answer: "",
},
],
},
});
});
it("does not let another workspace member answer a generic secret ask for the run owner", async () => {
const store = vi.fn();
const tx = {
$queryRaw: vi.fn().mockResolvedValue([{ id: "thread-1" }]),
run: {
findFirst: vi.fn().mockResolvedValue({ botId: "bot-1", userId: "owner" }),
updateMany: vi.fn(),
},
message: {
findFirst: vi.fn().mockResolvedValue({
id: "message-1",
blocks: [
{
kind: "ask",
text: "Enter your code",
input: "secret",
status: "pending",
},
],
}),
},
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
expect(
await answerRunInput(
prisma,
{
spaceId: "workspace-1",
threadId: "thread-1",
runId: "run-1",
messageId: "message-1",
answeredByUserId: "other-member",
answer: "123456",
},
new TestFanout(),
{ store },
),
).toBe(false);
expect(store).not.toHaveBeenCalled();
expect(tx.run.updateMany).not.toHaveBeenCalled();
});
it("does not let another workspace member save a credential as the run owner", async () => {
const store = vi.fn();
const tx = {
$queryRaw: vi.fn().mockResolvedValue([{ id: "thread-1" }]),
run: {
findFirst: vi.fn().mockResolvedValue({ botId: "bot-1", userId: "owner" }),
updateMany: vi.fn(),
},
message: {
findFirst: vi.fn().mockResolvedValue({
id: "message-1",
blocks: [
{
kind: "ask",
text: "API key",
input: "secret",
status: "pending",
credential: {
name: "example_api",
origin: "https://api.example.test",
auth: { type: "bearer" },
},
},
],
}),
},
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
expect(
await answerRunInput(
prisma,
{
spaceId: "workspace-1",
threadId: "thread-1",
runId: "run-1",
messageId: "message-1",
answeredByUserId: "other-member",
answer: "fake-key",
},
new TestFanout(),
{ store },
),
).toBe(false);
expect(store).not.toHaveBeenCalled();
expect(tx.run.updateMany).not.toHaveBeenCalled();
});
it("rejects secret asks when no run secret writer is configured", async () => {
const tx = {
$queryRaw: vi.fn().mockResolvedValue([{ id: "thread-1" }]),
message: {
findFirst: vi.fn().mockResolvedValue({
id: "message-1",
blocks: [
{
kind: "ask",
text: "Enter your code",
input: "secret",
status: "pending",
},
],
}),
},
run: {
findFirst: vi.fn().mockResolvedValue({ botId: "bot-1", userId: "user-1" }),
updateMany: vi.fn(),
},
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
answerRunInput(prisma, {
spaceId: "workspace-1",
threadId: "thread-1",
runId: "run-1",
messageId: "message-1",
answeredByUserId: "user-1",
answer: "123456",
}),
).resolves.toBe(false);
expect(tx.run.updateMany).not.toHaveBeenCalled();
});
});
describe("sendUserMessage", () => {
it("creates the message, run, and event in one transaction and publishes once", async () => {
const fanout = new TestFanout();
const publish = vi.spyOn(fanout, "publish");
const tx = {
thread: {
update: vi
.fn()
.mockResolvedValueOnce({ nextMessageSeq: 5 })
.mockResolvedValueOnce({ nextEventSeq: 9 }),
},
message: {
create: vi.fn().mockResolvedValue({ id: "message-1", seq: 4 }),
update: vi.fn(),
},
task: { create: vi.fn().mockResolvedValue({ id: "task-1" }) },
run: {
create: vi.fn().mockResolvedValue({ id: "run-1" }),
findFirst: vi.fn().mockResolvedValue(null),
findUnique: vi.fn().mockResolvedValue({ status: "queued" }),
},
event: {
create: vi.fn(async ({ data }: { data: { seq: number; type: string } }) => ({
...event(data.seq),
type: data.type,
runId: "run-1",
})),
},
};
const prisma = {
message: { findUnique: vi.fn().mockResolvedValue(null) },
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
sendUserMessage(
prisma,
{
spaceId: "workspace-1",
threadId: "thread-1",
botId: "bot-1",
userId: "user-1",
blocks: [{ kind: "text", text: "hello" }],
prompt: "hello",
trigger: "user",
clientNonce: "nonce-1",
linkMessageToRun: true,
},
fanout,
),
).resolves.toEqual({ messageId: "message-1", seq: 4, taskId: "task-1", runId: "run-1" });
expect(tx.run.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
trigger: "user",
clientNonce: "send:message-1",
sourceMessageId: "message-1",
}),
}),
);
expect(tx.message.update).toHaveBeenCalledWith({
where: { id: "message-1" },
data: { runId: "run-1" },
});
expect(tx.event.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ type: "thread.message.created", runId: "run-1" }),
}),
);
expect(publish).toHaveBeenCalledWith("thread:thread-1", JSON.stringify({ cursor: 8 }));
});
it("persists steering instead of starting a parallel run when the bot is busy", async () => {
const tx = {
thread: {
update: vi
.fn()
.mockResolvedValueOnce({ nextMessageSeq: 5 })
.mockResolvedValueOnce({ nextEventSeq: 9 }),
},
message: {
create: vi.fn().mockResolvedValue({ id: "message-1", seq: 4 }),
update: vi.fn().mockResolvedValue({ id: "message-1" }),
},
steeringMessage: { create: vi.fn() },
task: { create: vi.fn() },
run: {
findFirst: vi.fn().mockResolvedValue({ id: "run-0", taskId: "task-0" }),
findUnique: vi.fn().mockResolvedValue({ status: "running" }),
create: vi.fn(),
},
event: {
create: vi.fn(async ({ data }: { data: { seq: number; type: string } }) => ({
...event(data.seq),
type: data.type,
})),
},
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
sendUserMessage(prisma, {
spaceId: "workspace-1",
threadId: "thread-1",
botId: "bot-1",
userId: "user-1",
blocks: [{ kind: "text", text: "hello" }],
prompt: "hello",
trigger: "follow_up",
}),
).resolves.toEqual({ messageId: "message-1", seq: 4, taskId: null, runId: "run-0" });
expect(tx.task.create).not.toHaveBeenCalled();
expect(tx.run.create).not.toHaveBeenCalled();
expect(tx.message.update).toHaveBeenCalledWith({
where: { id: "message-1" },
data: { runId: "run-0" },
});
expect(tx.steeringMessage.create).toHaveBeenCalledWith({
data: { messageId: "message-1", botId: "bot-1", userId: "user-1", runId: "run-0" },
});
});
});
describe("claimSteering", () => {
it("claims pending messages for the fenced run in message order", async () => {
const tx = {
$queryRaw: vi.fn(),
run: { findFirst: vi.fn().mockResolvedValue({ id: "run-1" }) },
steeringMessage: {
findMany: vi.fn().mockResolvedValue([
{
id: "steer-1",
messageId: "message-1",
message: {
seq: 5,
blocks: [
{ kind: "text", text: "First" },
{
kind: "image",
artifactId: "artifact-1",
name: "chart.png",
mimeType: "image/png",
},
],
},
},
{
id: "steer-2",
messageId: "message-2",
message: { seq: 6, blocks: [{ kind: "text", text: "Second" }] },
},
]),
updateMany: vi.fn().mockResolvedValue({ count: 2 }),
},
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
claimSteering(prisma, {
threadId: "thread-1",
botId: "bot-1",
runId: "run-1",
leaseOwner: "worker-1",
leaseFence: 2,
seenIds: [],
}),
).resolves.toEqual([
{
id: "steer-1",
messageId: "message-1",
text: "First\n[image: chart.png]",
blocks: [
{ kind: "text", text: "First" },
{
kind: "image",
artifactId: "artifact-1",
name: "chart.png",
mimeType: "image/png",
},
],
},
{
id: "steer-2",
messageId: "message-2",
text: "Second",
blocks: [{ kind: "text", text: "Second" }],
},
]);
expect(tx.run.findFirst).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({ leaseOwner: "worker-1", leaseFence: 2 }),
}),
);
expect(tx.steeringMessage.updateMany).toHaveBeenCalledWith({
where: { id: { in: ["steer-1", "steer-2"] }, claimedAt: null },
data: { runId: "run-1", claimedAt: expect.any(Date) },
});
});
});
describe("clearThread", () => {
it("refuses to clear while a delegated bot outcome is still pending", async () => {
const fanout = new TestFanout();
const tx = {
thread: {
update: vi
.fn()
.mockResolvedValueOnce({ nextMessageSeq: 4, historyCompactionGeneration: 0 }),
},
run: {
findMany: vi.fn().mockResolvedValue([]),
count: vi.fn().mockResolvedValue(1),
},
attempt: { updateMany: vi.fn() },
task: { updateMany: vi.fn() },
computerExecutionLease: { updateMany: vi.fn() },
computer: { updateMany: vi.fn() },
message: { deleteMany: vi.fn() },
event: { deleteMany: vi.fn(), create: vi.fn() },
bot: { update: vi.fn() },
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
const { ClearThreadPendingDelegatedReturnsError } = await import("./scope.js");
await expect(
clearThread(prisma, { spaceId: "workspace-1", threadId: "thread-1", botId: "bot-1" }, fanout),
).rejects.toBeInstanceOf(ClearThreadPendingDelegatedReturnsError);
expect(tx.run.count).toHaveBeenCalledWith({
where: expect.objectContaining({
trigger: "bot_message",
status: { in: ["completed", "failed"] },
botOutcomeReturnedAt: null,
}),
});
expect(tx.message.deleteMany).not.toHaveBeenCalled();
});
it("releases the computer execution lease without dropping the computer", async () => {
const fanout = new TestFanout();
const publish = vi.spyOn(fanout, "publish");
const tx = {
thread: {
update: vi
.fn()
.mockResolvedValueOnce({ nextMessageSeq: 42, historyCompactionGeneration: 0 })
.mockResolvedValue({ nextEventSeq: 1 }),
},
run: {
findMany: vi.fn().mockResolvedValue([{ id: "run-1", taskId: "task-1" }]),
count: vi.fn().mockResolvedValue(0),
updateMany: vi.fn(),
},
attempt: { updateMany: vi.fn() },
task: { updateMany: vi.fn() },
computerExecutionLease: { updateMany: vi.fn() },
computer: { updateMany: vi.fn() },
message: { deleteMany: vi.fn() },
event: {
deleteMany: vi.fn(),
create: vi.fn().mockResolvedValue({
...event(0),
type: "thread.cleared",
}),
},
bot: { update: vi.fn() },
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
clearThread(prisma, { spaceId: "workspace-1", threadId: "thread-1", botId: "bot-1" }, fanout),
).resolves.toMatchObject({
event: { type: "thread.cleared" },
cancelledRunIds: ["run-1"],
});
expect(tx.computerExecutionLease.updateMany).toHaveBeenCalledWith({
where: { runId: { in: ["run-1"] } },
data: { expiresAt: new Date(0) },
});
expect(tx.computer.updateMany).toHaveBeenCalledWith({
where: { executionRunId: { in: ["run-1"] } },
data: {
executionRunId: null,
executionBotId: null,
executionLeaseExpiresAt: null,
},
});
// Every deleted message counts as compacted, so compaction cannot summarize cleared history.
expect(tx.thread.update).toHaveBeenCalledWith({
where: { id: "thread-1" },
data: {
historyCompactedUpToSeq: 41,
historyCompactionSummary: null,
historyCompactionGeneration: { increment: 1 },
},
});
expect(publish).toHaveBeenCalledWith("thread:thread-1", JSON.stringify({ cursor: 0 }));
});
it("scopes group clear lease cleanup to cancelled run ids", async () => {
const fanout = new TestFanout();
const tx = {
thread: {
update: vi
.fn()
.mockResolvedValueOnce({ nextMessageSeq: 3, historyCompactionGeneration: 0 })
.mockResolvedValue({ nextEventSeq: 1 }),
},
run: {
findMany: vi.fn().mockResolvedValue([
{ id: "group-run-1", taskId: "task-1" },
{ id: "group-run-2", taskId: "task-2" },
]),
count: vi.fn().mockResolvedValue(0),
updateMany: vi.fn(),
},
attempt: { updateMany: vi.fn() },
task: { updateMany: vi.fn() },
computerExecutionLease: { updateMany: vi.fn() },
computer: { updateMany: vi.fn() },
message: { deleteMany: vi.fn() },
event: {
deleteMany: vi.fn(),
create: vi.fn().mockResolvedValue({
...event(0),
type: "thread.cleared",
}),
},
chatGroup: { update: vi.fn() },
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
clearThread(
prisma,
{
spaceId: "workspace-1",
threadId: "thread-group",
botId: "bot-1",
groupId: "group-1",
},
fanout,
),
).resolves.toMatchObject({ cancelledRunIds: ["group-run-1", "group-run-2"] });
expect(tx.run.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
threadId: "thread-group",
spaceId: "workspace-1",
}),
}),
);
expect(tx.computerExecutionLease.updateMany).toHaveBeenCalledWith({
where: { runId: { in: ["group-run-1", "group-run-2"] } },
data: { expiresAt: new Date(0) },
});
expect(tx.computer.updateMany).toHaveBeenCalledWith({
where: { executionRunId: { in: ["group-run-1", "group-run-2"] } },
data: {
executionRunId: null,
executionBotId: null,
executionLeaseExpiresAt: null,
},
});
expect(tx.computerExecutionLease.updateMany).not.toHaveBeenCalledWith(
expect.objectContaining({ where: expect.objectContaining({ botId: expect.anything() }) }),
);
});
});
describe("appendEvent", () => {
it("does not persist output from a cancelled run after history was cleared", async () => {
const fanout = new TestFanout();
const publish = vi.spyOn(fanout, "publish");
const tx = {
thread: { update: vi.fn().mockResolvedValue({ nextEventSeq: 8 }) },
run: { findUnique: vi.fn().mockResolvedValue({ status: "cancelled" }) },
event: { create: vi.fn() },
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
appendEvent(
prisma,
{
spaceId: "workspace-1",
threadId: "thread-1",
botId: "bot-1",
type: "thread.progress",
runId: "run-1",
payload: { text: "stale" },
},
fanout,
),
).rejects.toThrow(RunHistoryWriteError);
expect(tx.event.create).not.toHaveBeenCalled();
expect(publish).not.toHaveBeenCalled();
});
it("lets a new active run write after the thread was cleared", async () => {
const fanout = new TestFanout();
const publish = vi.spyOn(fanout, "publish");
const created = {
...event(8),
type: "thread.progress",
runId: "run-2",
};
const tx = {
thread: { update: vi.fn().mockResolvedValue({ nextEventSeq: 9 }) },
run: { findUnique: vi.fn().mockResolvedValue({ status: "running" }) },
event: { create: vi.fn().mockResolvedValue(created) },
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
await expect(
appendEvent(
prisma,
{
spaceId: "workspace-1",
threadId: "thread-1",
botId: "bot-1",
type: "thread.progress",
runId: "run-2",
payload: { text: "fresh" },
},
fanout,
),
).resolves.toMatchObject({ type: "thread.progress", runId: "run-2" });
expect(tx.event.create).toHaveBeenCalled();
expect(publish).toHaveBeenCalled();
});
it("does not persist a split-emoji high surrogate that would crash JSON insert", async () => {
const fanout = new TestFanout();
const created = {
...event(3),
type: "thread.progress",
runId: "run-3",
payload: { delta: "hello \uFFFD", streaming: true },
};
const tx = {
thread: { update: vi.fn().mockResolvedValue({ nextEventSeq: 4 }) },
run: { findUnique: vi.fn().mockResolvedValue({ status: "running" }) },
event: { create: vi.fn().mockResolvedValue(created) },
};
const prisma = {
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
} as unknown as PrismaClient;
// Orphaned UTF-16 high surrogate (e.g. 😀 split across stream chunks as \uD83D alone).
const orphanedHigh = "hello \uD83D";
await expect(
appendEvent(
prisma,
{
spaceId: "workspace-1",
threadId: "thread-1",
botId: "bot-1",
type: "thread.progress",
runId: "run-3",
payload: { delta: orphanedHigh, streaming: true },
},
fanout,
),
).resolves.toMatchObject({ type: "thread.progress", runId: "run-3" });
expect(tx.event.create).toHaveBeenCalledWith({
data: expect.objectContaining({
payload: { delta: "hello \uFFFD", streaming: true },
}),
});
const persisted = tx.event.create.mock.calls[0]![0].data.payload as { delta: string };
// Postgres rejects unpaired surrogates in json; the sanitized form must not contain any.
expect(persisted.delta).not.toMatch(/[\uD800-\uDFFF]/);
expect(() => JSON.stringify(persisted)).not.toThrow();
});
});