evOo-local/packages/adapters/src/wakeup.postgres.test.ts
Claude 044669edfc
Add a media library: owner folders that bots can search and import from
The deployment owner adds folders of photos and videos in the media settings. A per-folder
background job indexes them (relative path, kind, size, capture date, dimensions, duration),
re-indexes every six hours, and drops files that are gone. The owner's own runs get two tools:
search_media (words in folder and file names, kind, capture date range, newest first) and
import_media (copies one file into the conversation as an artifact). Files are never changed;
hidden entries and symlinks are skipped, and an import must resolve inside its folder.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wy1md9QE1ayJbDCwJ4vAQs
2026-10-05 08:45:30 +00:00

208 lines
6.8 KiB
TypeScript

import type { BackgroundJobHandlers } from "@rakazo/adapter-kit";
import { Pool } from "pg";
import { afterEach, describe, expect, it, vi } from "vitest";
import { GraphileJobPublisher, GraphileJobWorkerHost } from "./wakeup.js";
const databaseUrl = process.env.DATABASE_URL;
const describePostgres =
process.env.VERIFY_DATABASE && databaseUrl ? describe.sequential : describe.skip;
let pool: Pool | undefined;
function testPool(): Pool {
pool ??= new Pool({ connectionString: databaseUrl, max: 4 });
return pool;
}
afterEach(async () => {
await pool?.end();
pool = undefined;
});
function handlers(overrides: Partial<BackgroundJobHandlers> = {}): 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.run": vi.fn(async () => undefined),
"media.library.sync": vi.fn(async () => undefined),
...overrides,
};
}
function deferred() {
let resolve!: () => void;
const promise = new Promise<void>((done) => {
resolve = done;
});
return { promise, resolve };
}
async function waitFor(assertion: () => void, timeoutMs = 10_000): Promise<void> {
await vi.waitFor(assertion, { timeout: timeoutMs, interval: 25 });
}
describePostgres("Graphile background jobs (PostgreSQL contract)", () => {
it("waits for an active handler during graceful shutdown", async () => {
const publisher = new GraphileJobPublisher(testPool());
const host = new GraphileJobWorkerHost(testPool(), { concurrency: 1, pollInterval: 25 });
const entered = deferred();
const release = deferred();
let completed = false;
const target = handlers({
"run.continue": async () => {
entered.resolve();
await release.promise;
completed = true;
},
});
try {
await host.start(target);
await publisher.enqueue({ name: "run.continue", payload: { runId: "graceful-stop" } });
await entered.promise;
let stopped = false;
const stopping = host.stop().then(() => {
stopped = true;
});
await new Promise((resolve) => setTimeout(resolve, 100));
expect(stopped).toBe(false);
release.resolve();
await stopping;
expect(completed).toBe(true);
} finally {
release.resolve();
await host.stop();
await publisher.close();
}
});
it("replaces a keyed delayed job without running the old payload or schedule", async () => {
const publisher = new GraphileJobPublisher(testPool());
const host = new GraphileJobWorkerHost(testPool(), { concurrency: 1, pollInterval: 25 });
const received: string[] = [];
const target = handlers({
"computer.sleep": async ({ computerId }) => {
received.push(computerId);
},
});
const key = `contract:replacement:${Date.now()}`;
try {
await host.start(target);
await publisher.enqueue({
name: "computer.sleep",
payload: { computerId: "old" },
availableAt: new Date(Date.now() + 300),
replaceKey: key,
});
await publisher.enqueue({
name: "computer.sleep",
payload: { computerId: "replacement" },
availableAt: new Date(Date.now() + 800),
replaceKey: key,
});
await new Promise((resolve) => setTimeout(resolve, 500));
expect(received).toEqual([]);
await waitFor(() => expect(received).toEqual(["replacement"]));
} finally {
await host.stop();
await publisher.cancel(key).catch(() => undefined);
await publisher.close();
}
});
it("removes a keyed delayed job before a worker can run it", async () => {
const publisher = new GraphileJobPublisher(testPool());
const host = new GraphileJobWorkerHost(testPool(), { concurrency: 1, pollInterval: 25 });
const target = handlers();
const key = `contract:cancellation:${Date.now()}`;
try {
await host.start(target);
await publisher.enqueue({
name: "computer.sleep",
payload: { computerId: "cancelled" },
availableAt: new Date(Date.now() + 300),
replaceKey: key,
});
await publisher.cancel(key);
await new Promise((resolve) => setTimeout(resolve, 600));
expect(target["computer.sleep"]).not.toHaveBeenCalled();
} finally {
await host.stop();
await publisher.close();
}
});
it("runs jobs that share a queue one at a time across workers", async () => {
const publisher = new GraphileJobPublisher(testPool());
const first = new GraphileJobWorkerHost(testPool(), { concurrency: 2, pollInterval: 25 });
const second = new GraphileJobWorkerHost(testPool(), { concurrency: 2, pollInterval: 25 });
const queue = `contract-queue-${Date.now()}`;
let active = 0;
let maxActive = 0;
const done: string[] = [];
const target = handlers({
"media.run": async ({ jobId }) => {
active += 1;
maxActive = Math.max(maxActive, active);
await new Promise((resolve) => setTimeout(resolve, 150));
active -= 1;
done.push(jobId);
},
});
try {
await first.start(target);
await second.start(target);
for (const jobId of ["q1", "q2", "q3"]) {
await publisher.enqueue({ name: "media.run", payload: { jobId }, queue });
}
await waitFor(() => expect(done).toHaveLength(3));
expect(maxActive).toBe(1);
} finally {
await first.stop();
await second.stop();
await publisher.close();
}
});
it("persists a handler failure and retries the job successfully", async () => {
const publisher = new GraphileJobPublisher(testPool());
const host = new GraphileJobWorkerHost(testPool(), { concurrency: 1, pollInterval: 25 });
let attempts = 0;
const target = handlers({
"run.continue": async () => {
attempts += 1;
if (attempts === 1) throw new Error("intentional contract-test failure");
},
});
const key = `contract:retry:${Date.now()}`;
try {
await host.start(target);
await publisher.enqueue({
name: "run.continue",
payload: { runId: "retry" },
replaceKey: key,
});
await waitFor(() => expect(attempts).toBe(2), 15_000);
await new Promise((resolve) => setTimeout(resolve, 100));
expect(attempts).toBe(2);
} finally {
await host.stop();
await publisher.cancel(key).catch(() => undefined);
await publisher.close();
}
});
});