evOo-local/packages/adapters/src/wakeup.test.ts
Claude a663764ee8
feat(media): contracts and foundation for Media Studio
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
2026-10-03 17:32:52 +00:00

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" },
});
});
});