evOo-local/packages/adapters/src/group-handoff.test.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

181 lines
5.5 KiB
TypeScript

import type { PrismaClient } from "@rakazo/db";
import { describe, expect, it, vi } from "vitest";
import { handoffToGroupBot } from "./group-handoff.js";
const run = {
id: "run-a",
spaceId: "workspace-1",
threadId: "thread-1",
botId: "bot-a",
userId: "user-1",
};
function harness(
sourceBlocks: unknown,
existing?: { sourceRuns: { id: string; botId: string }[] },
) {
const runCreate = vi.fn(async () => ({ id: "run-b" }));
const messageCreate = vi.fn(async () => ({ id: "message-1" }));
const tx = {
$queryRaw: vi.fn(async () => [{ id: "group-1" }]),
chatGroup: {
findFirst: vi.fn(async () => ({
id: "group-1",
members: ["bot-a", "bot-b", "bot-c"].map((id) => ({
bot: { id, name: id.toUpperCase() },
})),
})),
update: vi.fn(async () => ({ id: "group-1" })),
},
run: {
findFirst: vi.fn(async () => ({
id: run.id,
sourceMessage: { blocks: sourceBlocks },
})),
findUnique: vi.fn(async () => ({ status: "running" })),
create: runCreate,
},
message: {
findUnique: vi.fn(async () => existing ?? null),
create: messageCreate,
},
thread: {
update: vi.fn(async (args: { select: { nextMessageSeq?: boolean } }) =>
args.select.nextMessageSeq ? { nextMessageSeq: 2 } : { nextEventSeq: 2 },
),
},
task: { create: vi.fn(async () => ({ id: "task-b" })) },
event: {
findFirst: vi.fn(async () => ({ seq: 1 })),
create: vi.fn(async () => ({ seq: 1 })),
},
};
const prisma = {
$transaction: vi.fn(async (callback: (value: typeof tx) => Promise<unknown>) => callback(tx)),
} as unknown as PrismaClient;
return {
deps: {
prisma,
events: { notify: vi.fn(async () => undefined) },
jobs: { enqueue: vi.fn(async () => undefined) },
},
messageCreate,
runCreate,
};
}
describe("group handoff ownership", () => {
it("marks a new ownership transfer as a follow-up with a chain hop", async () => {
const { deps, messageCreate, runCreate } = harness([{ kind: "text", text: "user request" }]);
await expect(
handoffToGroupBot(deps as never, run, "group-1", {
bot_id: "bot-b",
message: "Do the distinct next stage",
}),
).resolves.toMatchObject({ ok: true, botId: "bot-b" });
expect(messageCreate).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
blocks: [
expect.objectContaining({
kind: "handoff",
fromBotId: "bot-a",
toBotId: "bot-b",
hop: 1,
}),
],
}),
}),
);
expect(runCreate).toHaveBeenCalledWith(
expect.objectContaining({ data: expect.objectContaining({ trigger: "follow_up" }) }),
);
});
it("refuses to re-delegate the same inbound task instead of workshoping", async () => {
const { deps, runCreate } = harness([
{
kind: "handoff",
fromBotId: "bot-b",
toBotId: "bot-a",
text: "Erstelle ein Konzept für mobile Voice-First UI",
hop: 1,
},
]);
await expect(
handoffToGroupBot(deps as never, run, "group-1", {
bot_id: "bot-c",
message: "Erstelle ein Konzept für mobile Voice-First UI",
}),
).resolves.toEqual({
error:
"you own this handed-off stage; workshop with the user and post your concept here instead of re-delegating the same task",
});
expect(runCreate).not.toHaveBeenCalled();
});
it("refuses to bounce a handed-off stage straight back to its sender", async () => {
const { deps, runCreate } = harness([
{ kind: "handoff", fromBotId: "bot-b", toBotId: "bot-a", text: "Investigate", hop: 1 },
]);
await expect(
handoffToGroupBot(deps as never, run, "group-1", {
bot_id: "bot-b",
message: "You investigate it",
}),
).resolves.toEqual({
error:
"do not hand this stage back to its sender; post the result in the shared thread instead",
});
expect(runCreate).not.toHaveBeenCalled();
});
it("caps longer multi-agent handoff chains", async () => {
const { deps, runCreate } = harness([
{ kind: "handoff", fromBotId: "bot-b", toBotId: "bot-a", text: "Stage six", hop: 6 },
]);
await expect(
handoffToGroupBot(deps as never, run, "group-1", {
bot_id: "bot-c",
message: "Stage seven",
}),
).resolves.toEqual({
error:
"group handoff limit reached for this chain; finish the current stage in the shared thread instead",
});
expect(runCreate).not.toHaveBeenCalled();
});
it("reuses the recorded transfer when a source run is retried", async () => {
const { deps, messageCreate, runCreate } = harness([], {
sourceRuns: [{ id: "run-b", botId: "bot-b" }],
});
await expect(
handoffToGroupBot(deps as never, run, "group-1", {
bot_id: "bot-c",
message: "A duplicate stage",
}),
).resolves.toMatchObject({ ok: true, botId: "bot-b", runId: "run-b" });
expect(messageCreate).not.toHaveBeenCalled();
expect(runCreate).not.toHaveBeenCalled();
});
it("rejects malformed source ancestry instead of restarting its hop count", async () => {
const { deps, runCreate } = harness({ kind: "not-an-array" });
await expect(
handoffToGroupBot(deps as never, run, "group-1", {
bot_id: "bot-b",
message: "Continue",
}),
).resolves.toEqual({ error: "cannot verify the group handoff chain" });
expect(runCreate).not.toHaveBeenCalled();
});
});