Freeze the shared contracts every Media Studio slice builds on: zod DTOs, EDL, platform profile, brand kit, gate and render plan schemas; the media RPC surface with a scaffold router; provider interfaces for sources, inference, rendering, motion graphics and publishing; media background jobs; the Prisma models and migration; and the @rakazo/media domain package signatures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0138YqDj3ttfayRwuMKKrqVj
272 lines
9.3 KiB
TypeScript
272 lines
9.3 KiB
TypeScript
import type { BackgroundJobHandlers } from "@rakazo/adapter-kit";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { InMemoryJobQueue } from "./wakeup.js";
|
|
|
|
function handlers(): BackgroundJobHandlers {
|
|
return {
|
|
"run.continue": vi.fn(async () => undefined),
|
|
"routine.wakeup": vi.fn(async () => undefined),
|
|
"computer.update": vi.fn(async () => undefined),
|
|
"computer.sleep": vi.fn(async () => undefined),
|
|
"computer.control-expire": vi.fn(async () => undefined),
|
|
"skill.teaching-expire": vi.fn(async () => undefined),
|
|
"history.compact": vi.fn(async () => undefined),
|
|
"messaging.deliver": vi.fn(async () => undefined),
|
|
"cloud_agent.poll": vi.fn(async () => undefined),
|
|
"profile.consolidate": vi.fn(async () => undefined),
|
|
"media.sync": vi.fn(async () => undefined),
|
|
"media.proxy": vi.fn(async () => undefined),
|
|
"media.analyze": vi.fn(async () => undefined),
|
|
"media.cluster": vi.fn(async () => undefined),
|
|
"media.render": vi.fn(async () => undefined),
|
|
"media.publish": vi.fn(async () => undefined),
|
|
};
|
|
}
|
|
|
|
describe("InMemoryJobQueue", () => {
|
|
afterEach(() => vi.useRealTimers());
|
|
|
|
it("delivers delayed jobs", async () => {
|
|
vi.useFakeTimers();
|
|
const queue = new InMemoryJobQueue();
|
|
const target = handlers();
|
|
await queue.start(target);
|
|
await queue.enqueue({
|
|
name: "run.continue",
|
|
payload: { runId: "run-1" },
|
|
availableAt: new Date(Date.now() + 1_000),
|
|
});
|
|
|
|
await vi.advanceTimersByTimeAsync(999);
|
|
expect(target["run.continue"]).not.toHaveBeenCalled();
|
|
await vi.advanceTimersByTimeAsync(1);
|
|
expect(target["run.continue"]).toHaveBeenCalledWith({ runId: "run-1" });
|
|
await queue.close();
|
|
});
|
|
|
|
it("replaces and cancels keyed jobs", async () => {
|
|
vi.useFakeTimers();
|
|
const queue = new InMemoryJobQueue();
|
|
const target = handlers();
|
|
await queue.start(target);
|
|
await queue.enqueue({
|
|
name: "computer.sleep",
|
|
payload: { computerId: "old" },
|
|
availableAt: new Date(Date.now() + 1_000),
|
|
replaceKey: "computer.sleep:1",
|
|
});
|
|
await queue.enqueue({
|
|
name: "computer.sleep",
|
|
payload: { computerId: "new" },
|
|
availableAt: new Date(Date.now() + 1_000),
|
|
replaceKey: "computer.sleep:1",
|
|
});
|
|
await vi.advanceTimersByTimeAsync(1_000);
|
|
expect(target["computer.sleep"]).toHaveBeenCalledTimes(1);
|
|
expect(target["computer.sleep"]).toHaveBeenCalledWith({ computerId: "new" });
|
|
|
|
await queue.enqueue({
|
|
name: "computer.sleep",
|
|
payload: { computerId: "cancelled" },
|
|
availableAt: new Date(Date.now() + 1_000),
|
|
replaceKey: "computer.sleep:2",
|
|
});
|
|
await queue.cancel("computer.sleep:2");
|
|
await vi.advanceTimersByTimeAsync(1_000);
|
|
expect(target["computer.sleep"]).toHaveBeenCalledTimes(1);
|
|
await queue.close();
|
|
});
|
|
|
|
it.each([
|
|
["stop", "stop"],
|
|
["close", "close"],
|
|
["stop", "close"],
|
|
["close", "stop"],
|
|
] as const)("waits for an active handler before %s/%s", async (first, second) => {
|
|
const queue = new InMemoryJobQueue();
|
|
let releaseHandler: () => void = () => undefined;
|
|
let markStarted: () => void = () => undefined;
|
|
let nestedEnqueueFailed = false;
|
|
let delayedEnqueueFailed = false;
|
|
let chainedEnqueueFailed = false;
|
|
const started = new Promise<void>((resolve) => {
|
|
markStarted = resolve;
|
|
});
|
|
const target = handlers();
|
|
target["computer.sleep"] = vi.fn(async () => {
|
|
try {
|
|
await queue.enqueue({ name: "run.continue", payload: { runId: "chained-run" } });
|
|
} catch {
|
|
chainedEnqueueFailed = true;
|
|
}
|
|
});
|
|
target["run.continue"] = vi.fn(async () => {
|
|
markStarted();
|
|
await new Promise<void>((resolve) => {
|
|
releaseHandler = resolve;
|
|
});
|
|
try {
|
|
await queue.enqueue({
|
|
name: "computer.sleep",
|
|
payload: { computerId: "computer-1" },
|
|
replaceKey: "keep-during-close",
|
|
});
|
|
await queue.enqueue({
|
|
name: "computer.sleep",
|
|
payload: { computerId: "cancelled-computer" },
|
|
replaceKey: "cancel-during-close",
|
|
});
|
|
await queue.cancel("cancel-during-close");
|
|
} catch {
|
|
nestedEnqueueFailed = true;
|
|
}
|
|
try {
|
|
await queue.enqueue({
|
|
name: "routine.wakeup",
|
|
payload: { routineId: "routine-1", scheduledFor: "2099-01-01T00:00:00.000Z" },
|
|
availableAt: new Date("2099-01-01T00:00:00.000Z"),
|
|
});
|
|
} catch {
|
|
delayedEnqueueFailed = true;
|
|
}
|
|
});
|
|
await queue.start(target);
|
|
await queue.enqueue({ name: "run.continue", payload: { runId: "run-1" } });
|
|
await started;
|
|
|
|
const closing = Promise.all([queue[first](), queue[second]()] as const);
|
|
const closeState = await Promise.race([
|
|
closing.then(() => "closed" as const),
|
|
new Promise<"waiting">((resolve) => setTimeout(() => resolve("waiting"), 5)),
|
|
]);
|
|
releaseHandler();
|
|
await closing;
|
|
expect(closeState).toBe("waiting");
|
|
expect(nestedEnqueueFailed).toBe(false);
|
|
expect(delayedEnqueueFailed).toBe(false);
|
|
expect(chainedEnqueueFailed).toBe(true);
|
|
expect(target["computer.sleep"]).toHaveBeenCalledTimes(1);
|
|
expect(target["run.continue"]).toHaveBeenCalledTimes(1);
|
|
expect(target["routine.wakeup"]).not.toHaveBeenCalled();
|
|
await queue.close();
|
|
});
|
|
|
|
it("rejects a keyed enqueue that resumes after close", async () => {
|
|
const queue = new InMemoryJobQueue();
|
|
let releaseCancel: () => void = () => undefined;
|
|
let markCancelling: () => void = () => undefined;
|
|
const cancelling = new Promise<void>((resolve) => {
|
|
markCancelling = resolve;
|
|
});
|
|
vi.spyOn(queue, "cancel").mockImplementationOnce(async () => {
|
|
markCancelling();
|
|
await new Promise<void>((resolve) => {
|
|
releaseCancel = resolve;
|
|
});
|
|
});
|
|
|
|
const enqueue = queue.enqueue({
|
|
name: "computer.sleep",
|
|
payload: { computerId: "computer-1" },
|
|
replaceKey: "computer:computer-1",
|
|
});
|
|
await cancelling;
|
|
await queue.close();
|
|
releaseCancel();
|
|
|
|
await expect(enqueue).rejects.toThrow("Background job publisher is closed");
|
|
});
|
|
|
|
it("rejects a keyed enqueue that resumes after stop", async () => {
|
|
const queue = new InMemoryJobQueue();
|
|
await queue.start(handlers());
|
|
let releaseCancel: () => void = () => undefined;
|
|
let markCancelling: () => void = () => undefined;
|
|
const cancelling = new Promise<void>((resolve) => {
|
|
markCancelling = resolve;
|
|
});
|
|
vi.spyOn(queue, "cancel").mockImplementationOnce(async () => {
|
|
markCancelling();
|
|
await new Promise<void>((resolve) => {
|
|
releaseCancel = resolve;
|
|
});
|
|
});
|
|
|
|
const enqueue = queue.enqueue({
|
|
name: "computer.sleep",
|
|
payload: { computerId: "computer-1" },
|
|
replaceKey: "computer:computer-1",
|
|
});
|
|
await cancelling;
|
|
await queue.stop();
|
|
releaseCancel();
|
|
|
|
await expect(enqueue).rejects.toThrow("Background job publisher is stopped");
|
|
});
|
|
|
|
it("drains an accepted immediate timer during close", async () => {
|
|
const queue = new InMemoryJobQueue();
|
|
const target = handlers();
|
|
await queue.start(target);
|
|
|
|
await queue.enqueue({ name: "computer.sleep", payload: { computerId: "computer-1" } });
|
|
await queue.close();
|
|
|
|
expect(target["computer.sleep"]).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("rejects jobs after stop until the worker restarts", async () => {
|
|
const queue = new InMemoryJobQueue();
|
|
const target = handlers();
|
|
await queue.start(target);
|
|
await queue.stop();
|
|
|
|
await expect(
|
|
queue.enqueue({ name: "computer.sleep", payload: { computerId: "computer-1" } }),
|
|
).rejects.toThrow("Background job publisher is stopped");
|
|
|
|
await queue.start(target);
|
|
await queue.enqueue({ name: "computer.sleep", payload: { computerId: "computer-1" } });
|
|
await queue.close();
|
|
expect(target["computer.sleep"]).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("unwraps correlation envelopes and restores request traces", async () => {
|
|
const { createLogger, createTestSink, installLogger, runWithLogContext } = await import(
|
|
"@rakazo/logging"
|
|
);
|
|
const sink = createTestSink();
|
|
installLogger(createLogger({ service: "rakazo-worker", sinks: [sink] }));
|
|
const queue = new InMemoryJobQueue();
|
|
const target = handlers();
|
|
await queue.start(target);
|
|
await runWithLogContext(
|
|
{ "trace.id": "a".repeat(32), "span.id": "b".repeat(16), "request.id": "req-1" },
|
|
async () => {
|
|
await queue.enqueue({ name: "run.continue", payload: { runId: "run-traced" } });
|
|
},
|
|
);
|
|
await queue.close();
|
|
expect(target["run.continue"]).toHaveBeenCalledWith({ runId: "run-traced" });
|
|
expect(sink.events.some((event) => event.message === "job.completed")).toBe(true);
|
|
expect(
|
|
sink.events.some(
|
|
(event) => event["trace.id"] === "a".repeat(32) && event["run.id"] === "run-traced",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("keeps wrapped payloads readable by workers that do not unwrap envelopes", async () => {
|
|
const { wrapJobPayload } = await import("@rakazo/logging");
|
|
const { parseBackgroundJob } = await import("@rakazo/adapter-kit");
|
|
const wrapped = wrapJobPayload(
|
|
{ runId: "run-rollback" },
|
|
{ jobId: "job-1", traceId: "a".repeat(32) },
|
|
);
|
|
expect(parseBackgroundJob("run.continue", wrapped)).toEqual({
|
|
name: "run.continue",
|
|
payload: { runId: "run-rollback" },
|
|
});
|
|
});
|
|
});
|