evOo-local/packages/adapters/src/job-reconciler.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

630 lines
21 KiB
TypeScript

import type { BackgroundJob, JobPublisher } from "@rakazo/adapter-kit";
import type { Pool, PrismaClient, ThreadEvents } from "@rakazo/db";
import { describe, expect, it, vi } from "vitest";
import { returnBotMessageOutcome } from "./bot-messages.js";
import {
createJobReconciler,
createPostgresReconciliationLeadership,
type ReconciliationLeadership,
} from "./job-reconciler.js";
vi.mock("./bot-messages.js", () => ({ returnBotMessageOutcome: vi.fn() }));
function publisher() {
const enqueue = vi.fn(async (_job: BackgroundJob) => undefined);
const jobs: JobPublisher = {
enqueue,
cancel: async () => undefined,
close: async () => undefined,
};
return { jobs, enqueue };
}
function fakePrisma(
runs: Array<{ id: string; updatedAt: Date }> = [],
routines: Array<{ id: string; nextRunAt: Date | null }> = [],
controls: Array<{
id: string;
controlBotId: string | null;
controlLeaseId: string | null;
controlLeaseExpiresAt: Date | null;
updatedAt: Date;
}> = [],
) {
return {
run: { findMany: vi.fn(async () => runs) },
routine: { findMany: vi.fn(async () => routines) },
computer: { findMany: vi.fn(async () => controls) },
messagingOutbound: { findFirst: vi.fn(async () => null) },
} as unknown as PrismaClient;
}
describe("createJobReconciler", () => {
it("continues the main scans when an auxiliary reconciler fails", async () => {
const prisma = fakePrisma();
const { jobs } = publisher();
const reconcileCloudAgents = vi.fn(async () => {
throw new Error("cloud unavailable");
});
const reconcileComputerUpdates = vi.fn(() => {
throw new Error("queue unavailable");
});
await createJobReconciler({
prisma,
jobs,
reconcileCloudAgents,
reconcileComputerUpdates,
}).reconcileOnce();
expect(reconcileCloudAgents).toHaveBeenCalledOnce();
expect(reconcileComputerUpdates).toHaveBeenCalledOnce();
expect(prisma.run.findMany).toHaveBeenCalled();
expect(prisma.routine.findMany).toHaveBeenCalled();
expect(prisma.computer.findMany).toHaveBeenCalled();
expect(prisma.messagingOutbound.findFirst).toHaveBeenCalled();
});
it("restores a due pending messaging outbox drain", async () => {
const prisma = fakePrisma();
vi.mocked(prisma.messagingOutbound.findFirst).mockResolvedValue({ id: "outbound-1" } as never);
const { jobs, enqueue } = publisher();
await createJobReconciler({ prisma, jobs }).reconcileOnce();
expect(prisma.messagingOutbound.findFirst).toHaveBeenCalledWith({
where: {
status: "pending",
OR: [{ nextAttemptAt: null }, { nextAttemptAt: { lte: expect.any(Date) } }],
},
select: { id: true },
});
expect(enqueue).toHaveBeenCalledWith({
name: "messaging.deliver",
payload: {},
replaceKey: "messaging.deliver:drain",
});
});
it("restores a completed messaging run that was not durably mirrored", async () => {
const prisma = fakePrisma();
vi.mocked(prisma.run.findMany)
.mockResolvedValueOnce([])
.mockResolvedValueOnce([{ id: "run-unmirrored" }] as never);
const { jobs, enqueue } = publisher();
await createJobReconciler({ prisma, jobs }).reconcileOnce();
expect(prisma.run.findMany).toHaveBeenCalledWith({
where: { trigger: "messaging", status: "completed", messagingMirroredAt: null },
orderBy: [{ updatedAt: "asc" }, { id: "asc" }],
take: 100,
select: { id: true },
});
expect(enqueue).toHaveBeenCalledWith({
name: "messaging.deliver",
payload: { runId: "run-unmirrored" },
replaceKey: "messaging.deliver:run-unmirrored",
});
});
it("restores queued runs and near-due routines with stable replacement keys", async () => {
const scheduledFor = new Date(Date.now() + 30_000);
const controlExpiresAt = new Date(Date.now() + 15_000);
const prisma = fakePrisma(
[{ id: "run-1", updatedAt: new Date() }],
[{ id: "routine-1", nextRunAt: scheduledFor }],
[
{
id: "computer-1",
controlBotId: "bot-1",
controlLeaseId: "lease-1",
controlLeaseExpiresAt: controlExpiresAt,
updatedAt: new Date(),
},
],
);
const { jobs, enqueue } = publisher();
const reconciler = createJobReconciler({ prisma, jobs });
await reconciler.reconcileOnce();
expect(enqueue).toHaveBeenCalledWith({
name: "run.continue",
payload: { runId: "run-1" },
replaceKey: "run:run-1",
});
expect(enqueue).toHaveBeenCalledWith({
name: "routine.wakeup",
payload: { routineId: "routine-1", scheduledFor: scheduledFor.toISOString() },
availableAt: scheduledFor,
replaceKey: "routine:routine-1",
});
expect(enqueue).toHaveBeenCalledWith({
name: "computer.control-expire",
payload: { computerId: "computer-1", leaseId: "lease-1" },
availableAt: controlExpiresAt,
replaceKey: "computer.control-expire:computer-1:lease-1",
});
expect(vi.mocked(prisma.computer.findMany)).toHaveBeenCalledWith(
expect.objectContaining({
where: {
AND: [
{ controlLeaseId: { not: null } },
{
OR: [
{ controlLeaseExpiresAt: null },
{ controlLeaseExpiresAt: { lte: expect.any(Date) } },
],
},
],
},
orderBy: [{ controlLeaseExpiresAt: "asc" }, { id: "asc" }],
}),
);
});
it("restores expiry for orphaned user-control leases without a control bot", async () => {
const controlExpiresAt = new Date(Date.now() - 5_000);
const prisma = fakePrisma(
[],
[],
[
{
id: "computer-orphan",
controlBotId: null,
controlLeaseId: "lease-orphan",
controlLeaseExpiresAt: controlExpiresAt,
updatedAt: new Date(),
},
],
);
const { jobs, enqueue } = publisher();
await createJobReconciler({ prisma, jobs }).reconcileOnce();
expect(enqueue).toHaveBeenCalledWith({
name: "computer.control-expire",
payload: { computerId: "computer-orphan", leaseId: "lease-orphan" },
availableAt: controlExpiresAt,
replaceKey: "computer.control-expire:computer-orphan:lease-orphan",
});
});
it("finishes a frozen control scan before admitting leases ahead of its cursor", async () => {
const firstExpiry = new Date(Date.now() + 10_000);
const secondExpiry = new Date(Date.now() + 20_000);
const controls = [
{
id: "computer-1",
controlBotId: "bot-1",
controlLeaseId: "lease-1",
controlLeaseExpiresAt: firstExpiry,
},
{
id: "computer-2",
controlBotId: "bot-2",
controlLeaseId: "lease-2",
controlLeaseExpiresAt: secondExpiry,
},
{
id: "computer-3",
controlBotId: "bot-3",
controlLeaseId: "lease-3",
controlLeaseExpiresAt: null,
},
];
const computerFindMany = vi
.fn()
.mockResolvedValueOnce(controls.slice(0, 2))
.mockResolvedValueOnce(controls.slice(2))
.mockResolvedValueOnce([
{
id: "computer-0",
controlBotId: "bot-0",
controlLeaseId: "lease-0",
controlLeaseExpiresAt: firstExpiry,
},
]);
const prisma = {
run: { findMany: vi.fn(async () => []) },
routine: { findMany: vi.fn(async () => []) },
computer: { findMany: computerFindMany },
messagingOutbound: { findFirst: vi.fn(async () => null) },
} as unknown as PrismaClient;
const { jobs, enqueue } = publisher();
const reconciler = createJobReconciler({ prisma, jobs }, { batchSize: 2 });
await reconciler.reconcileOnce();
await reconciler.reconcileOnce();
await reconciler.reconcileOnce();
expect(enqueue).toHaveBeenCalledTimes(4);
expect(computerFindMany.mock.calls[1]?.[0]).toMatchObject({
orderBy: [{ controlLeaseExpiresAt: "asc" }, { id: "asc" }],
where: {
AND: [
expect.anything(),
expect.anything(),
{
OR: [
{ controlLeaseExpiresAt: { gt: secondExpiry } },
{ controlLeaseExpiresAt: secondExpiry, id: { gt: "computer-2" } },
{ controlLeaseExpiresAt: null },
],
},
],
},
});
const firstDeadline =
computerFindMany.mock.calls[0]?.[0].where.AND[1].OR[1].controlLeaseExpiresAt.lte;
const secondDeadline =
computerFindMany.mock.calls[1]?.[0].where.AND[1].OR[1].controlLeaseExpiresAt.lte;
expect(secondDeadline).toEqual(firstDeadline);
expect(computerFindMany.mock.calls[2]?.[0].where.AND).toHaveLength(2);
expect(enqueue).toHaveBeenLastCalledWith(
expect.objectContaining({
payload: { computerId: "computer-0", leaseId: "lease-0" },
}),
);
});
it("advances stable cursors so recoverable work beyond one batch is dispatched", async () => {
const at = new Date("2026-01-01T00:00:00.000Z");
const runs = Array.from({ length: 5 }, (_, index) => ({
id: `run-${index + 1}`,
updatedAt: at,
}));
const routines = Array.from({ length: 5 }, (_, index) => ({
id: `routine-${index + 1}`,
nextRunAt: new Date(at.getTime() + index),
}));
const runPages = [runs.slice(0, 2), runs.slice(2, 4), runs.slice(4)];
let runPage = 0;
const runFindMany = vi.fn(async (args: { where?: Record<string, unknown> } = {}) =>
args.where?.messagingMirroredAt === null ? [] : (runPages[runPage++] ?? []),
);
const routineFindMany = vi
.fn()
.mockResolvedValueOnce(routines.slice(0, 2))
.mockResolvedValueOnce(routines.slice(2, 4))
.mockResolvedValueOnce(routines.slice(4));
const prisma = {
run: { findMany: runFindMany },
routine: { findMany: routineFindMany },
computer: { findMany: vi.fn(async () => []) },
messagingOutbound: { findFirst: vi.fn(async () => null) },
} as unknown as PrismaClient;
const { jobs, enqueue } = publisher();
const reconciler = createJobReconciler({ prisma, jobs }, { batchSize: 2 });
await reconciler.reconcileOnce();
await reconciler.reconcileOnce();
await reconciler.reconcileOnce();
expect(enqueue).toHaveBeenCalledTimes(10);
expect(enqueue.mock.calls.map(([job]) => job.replaceKey)).toEqual([
"run:run-1",
"run:run-2",
"routine:routine-1",
"routine:routine-2",
"run:run-3",
"run:run-4",
"routine:routine-3",
"routine:routine-4",
"run:run-5",
"routine:routine-5",
]);
expect(runFindMany.mock.calls[2]?.[0]).toMatchObject({
orderBy: [{ updatedAt: "asc" }, { id: "asc" }],
where: {
AND: [
expect.anything(),
{
OR: [{ updatedAt: { gt: at } }, { updatedAt: at, id: { gt: "run-2" } }],
},
],
},
});
expect(routineFindMany.mock.calls[1]?.[0]).toMatchObject({
orderBy: [{ nextRunAt: "asc" }, { id: "asc" }],
where: {
AND: [
expect.anything(),
{
OR: [
{ nextRunAt: { gt: routines[1]?.nextRunAt } },
{
nextRunAt: routines[1]?.nextRunAt,
id: { gt: "routine-2" },
},
],
},
],
},
});
});
it("does not scan when another replica is the reconciliation leader", async () => {
const prisma = fakePrisma();
const leadership: ReconciliationLeadership = {
tryAcquire: vi.fn(async () => false),
release: vi.fn(async () => undefined),
};
const { jobs, enqueue } = publisher();
const reconciler = createJobReconciler({ prisma, jobs, leadership });
await reconciler.reconcileOnce();
await reconciler.stop();
expect(prisma.run.findMany).not.toHaveBeenCalled();
expect(prisma.routine.findMany).not.toHaveBeenCalled();
expect(prisma.computer.findMany).not.toHaveBeenCalled();
expect(prisma.messagingOutbound.findFirst).not.toHaveBeenCalled();
expect(enqueue).not.toHaveBeenCalled();
expect(leadership.release).toHaveBeenCalledOnce();
});
it("retries terminal bot outcomes that were not returned", async () => {
const terminalRun = {
id: "run-terminal",
spaceId: "workspace-1",
threadId: "thread-1",
botId: "bot-1",
userId: "user-1",
sourceMessageId: "message-1",
status: "completed",
error: null,
bot: { name: "Researcher" },
};
let terminalScan = 0;
const runFindMany = vi.fn(async (args: { where?: Record<string, unknown> } = {}) => {
if (args.where?.messagingMirroredAt === null) return [];
if (args.where?.trigger === "bot_message") {
terminalScan += 1;
return terminalScan === 2 ? [] : [terminalRun];
}
return [];
});
const prisma = {
run: { findMany: runFindMany, updateMany: vi.fn(async () => ({ count: 1 })) },
routine: { findMany: vi.fn(async () => []) },
computer: { findMany: vi.fn(async () => []) },
messagingOutbound: { findFirst: vi.fn(async () => null) },
message: {
findMany: vi.fn(async () => [
{ blocks: [{ kind: "text", text: "Finished." }], clientNonce: null },
]),
},
} as unknown as PrismaClient;
const { jobs } = publisher();
const events = { notify: vi.fn() } as unknown as ThreadEvents;
vi.mocked(returnBotMessageOutcome).mockResolvedValue(true).mockResolvedValueOnce(false);
const reconciler = createJobReconciler({ prisma, jobs, events }, { batchSize: 1 });
await reconciler.reconcileOnce();
await reconciler.reconcileOnce();
await reconciler.reconcileOnce();
expect(runFindMany).toHaveBeenNthCalledWith(
3,
expect.objectContaining({
orderBy: [{ updatedAt: "asc" }, { id: "asc" }],
where: {
trigger: "bot_message",
status: { in: ["completed", "failed"] },
botOutcomeReturnedAt: null,
},
}),
);
expect(returnBotMessageOutcome).toHaveBeenCalledWith(
{ prisma, jobs, events },
terminalRun,
{ id: "bot-1", name: "Researcher" },
"Finished.",
"result",
);
expect(prisma.run.updateMany).toHaveBeenCalledWith(
expect.objectContaining({
where: { id: terminalRun.id, botOutcomeReturnedAt: null },
data: { updatedAt: expect.any(Date) },
}),
);
expect(returnBotMessageOutcome).toHaveBeenCalledTimes(2);
});
it("prefers the latest final reply over earlier untagged narration", async () => {
const terminalRun = {
id: "run-takeover",
spaceId: "workspace-1",
threadId: "thread-1",
botId: "bot-1",
userId: "user-1",
sourceMessageId: "message-1",
status: "completed",
error: null,
bot: { name: "Researcher" },
};
const runFindMany = vi.fn(async (args: { where?: Record<string, unknown> } = {}) => {
if (args.where?.messagingMirroredAt === null) return [];
if (args.where?.trigger === "bot_message") return [terminalRun];
return [];
});
const prisma = {
run: { findMany: runFindMany, updateMany: vi.fn(async () => ({ count: 1 })) },
routine: { findMany: vi.fn(async () => []) },
computer: { findMany: vi.fn(async () => []) },
messagingOutbound: { findFirst: vi.fn(async () => null) },
message: {
findMany: vi.fn(async () => [
{
blocks: [{ kind: "text", text: "Opening the calendar app…" }],
clientNonce: null,
},
{
blocks: [{ kind: "text", text: "Tuesday afternoon works." }],
clientNonce: null,
},
]),
},
} as unknown as PrismaClient;
const { jobs } = publisher();
const events = { notify: vi.fn() } as unknown as ThreadEvents;
vi.mocked(returnBotMessageOutcome).mockResolvedValue(true);
const reconciler = createJobReconciler({ prisma, jobs, events }, { batchSize: 1 });
await reconciler.reconcileOnce();
expect(returnBotMessageOutcome).toHaveBeenCalledWith(
{ prisma, jobs, events },
terminalRun,
{ id: "bot-1", name: "Researcher" },
"Tuesday afternoon works.",
"result",
);
});
it("prefers the final reply over progress when reconciling", async () => {
const terminalRun = {
id: "run-mixed",
spaceId: "workspace-1",
threadId: "thread-1",
botId: "bot-1",
userId: "user-1",
sourceMessageId: "message-1",
status: "completed",
error: null,
bot: { name: "Researcher" },
};
const runFindMany = vi.fn(async (args: { where?: Record<string, unknown> } = {}) => {
if (args.where?.messagingMirroredAt === null) return [];
if (args.where?.trigger === "bot_message") return [terminalRun];
return [];
});
const prisma = {
run: { findMany: runFindMany, updateMany: vi.fn(async () => ({ count: 1 })) },
routine: { findMany: vi.fn(async () => []) },
computer: { findMany: vi.fn(async () => []) },
messagingOutbound: { findFirst: vi.fn(async () => null) },
message: {
findMany: vi.fn(async () => [
{
blocks: [{ kind: "text", text: "Checking calendars…" }],
clientNonce: "user-progress:run-mixed:0",
},
{
blocks: [{ kind: "text", text: "Tuesday afternoon works." }],
clientNonce: null,
},
]),
},
} as unknown as PrismaClient;
const { jobs } = publisher();
const events = { notify: vi.fn() } as unknown as ThreadEvents;
vi.mocked(returnBotMessageOutcome).mockResolvedValue(true);
const reconciler = createJobReconciler({ prisma, jobs, events }, { batchSize: 1 });
await reconciler.reconcileOnce();
expect(returnBotMessageOutcome).toHaveBeenCalledWith(
{ prisma, jobs, events },
terminalRun,
{ id: "bot-1", name: "Researcher" },
"Tuesday afternoon works.",
"result",
);
});
it("returns progress-only transcripts as status", async () => {
const terminalRun = {
id: "run-progress",
spaceId: "workspace-1",
threadId: "thread-1",
botId: "bot-1",
userId: "user-1",
sourceMessageId: "message-1",
status: "completed",
error: null,
bot: { name: "Researcher" },
};
const runFindMany = vi.fn(async (args: { where?: Record<string, unknown> } = {}) => {
if (args.where?.messagingMirroredAt === null) return [];
if (args.where?.trigger === "bot_message") return [terminalRun];
return [];
});
const prisma = {
run: { findMany: runFindMany, updateMany: vi.fn(async () => ({ count: 1 })) },
routine: { findMany: vi.fn(async () => []) },
computer: { findMany: vi.fn(async () => []) },
messagingOutbound: { findFirst: vi.fn(async () => null) },
message: {
findMany: vi.fn(async () => [
{
blocks: [{ kind: "text", text: "Checking calendars…" }],
clientNonce: "user-progress:run-progress:0",
},
]),
},
} as unknown as PrismaClient;
const { jobs } = publisher();
const events = { notify: vi.fn() } as unknown as ThreadEvents;
vi.mocked(returnBotMessageOutcome).mockResolvedValue(true);
const reconciler = createJobReconciler({ prisma, jobs, events }, { batchSize: 1 });
await reconciler.reconcileOnce();
expect(returnBotMessageOutcome).toHaveBeenCalledWith(
{ prisma, jobs, events },
terminalRun,
{ id: "bot-1", name: "Researcher" },
"Checking calendars…",
"status",
);
});
});
describe("createPostgresReconciliationLeadership", () => {
it("retains leadership for one replica and transfers it after release", async () => {
let locked = false;
const clients: Array<{
query: ReturnType<typeof vi.fn>;
release: ReturnType<typeof vi.fn>;
once: ReturnType<typeof vi.fn>;
removeListener: ReturnType<typeof vi.fn>;
}> = [];
const pool = {
connect: vi.fn(async () => {
const client = {
query: vi.fn(async (sql: string) => {
if (sql.includes("pg_try_advisory_lock")) {
const acquired = !locked;
if (acquired) locked = true;
return { rows: [{ acquired }] };
}
const released = locked;
locked = false;
return { rows: [{ released }] };
}),
release: vi.fn(),
once: vi.fn(),
removeListener: vi.fn(),
};
clients.push(client);
return client;
}),
} as unknown as Pick<Pool, "connect">;
const first = createPostgresReconciliationLeadership(pool);
const second = createPostgresReconciliationLeadership(pool);
await expect(first.tryAcquire()).resolves.toBe(true);
await expect(first.tryAcquire()).resolves.toBe(true);
await expect(second.tryAcquire()).resolves.toBe(false);
expect(pool.connect).toHaveBeenCalledTimes(2);
expect(clients[1]?.release).toHaveBeenCalledOnce();
await first.release();
await expect(second.tryAcquire()).resolves.toBe(true);
expect(clients[0]?.removeListener).toHaveBeenCalledWith("error", expect.any(Function));
expect(clients[0]?.release).toHaveBeenCalledWith(false);
});
});