feat: add jobs worker and webhook core

This commit is contained in:
2026-08-13 16:30:15 +08:00
parent 7847c95538
commit 065dcd955a
12 changed files with 1664 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createPublicGenerationJob, PublicApiConflictError } from "@/lib/server/public-api-jobs";
const fixtureUrl = new URL("../contracts/jobs/jobs-v1.json", import.meta.url);
let runtimeDir: string | undefined;
afterEach(async () => {
if (runtimeDir) await rm(runtimeDir, { recursive: true, force: true });
runtimeDir = undefined;
});
describe("jobs v1 cross-language contract", () => {
it("freezes statuses, bounds and retry behavior", async () => {
const fixture = JSON.parse(await readFile(fixtureUrl, "utf8"));
expect(fixture.version).toBe(1);
expect(fixture.statuses).toEqual(["queued", "running", "succeeded", "failed", "expired", "cancelled"]);
expect(fixture.terminalStatuses).toEqual(["succeeded", "failed", "expired", "cancelled"]);
expect(fixture.priority).toEqual({ minimum: -100, maximum: 100, default: 0 });
expect(fixture.claim).toEqual({ minimumBatch: 1, maximumBatch: 20, defaultLockTimeoutSeconds: 300 });
});
it("uses stable request fingerprints and rejects payload drift", async () => {
const fixture = JSON.parse(await readFile(fixtureUrl, "utf8"));
runtimeDir = await mkdtemp(join(tmpdir(), "zhinian-jobs-contract-"));
process.env.ZHINIAN_RUNTIME_DIR = runtimeDir;
process.env.ZHINIAN_DATA_BACKEND = "local";
process.env.JIMENG_VISUAL_MOCK = "true";
const request = new Request("http://local.test/api/v1/jobs", {
headers: { "Idempotency-Key": "header-key" },
});
const input = {
client: { id: "fixture-client", key: "secret" },
request,
origin: "http://local.test",
body: fixture.idempotency.body,
};
const created = await createPublicGenerationJob(input);
expect(created.reused).toBe(false);
expect(created.job.idempotencyKey).toBe("header-key");
expect(created.job.idempotencyFingerprint).toBe(fixture.idempotency.fingerprint);
const replay = await createPublicGenerationJob(input);
expect(replay.reused).toBe(true);
expect(replay.job.id).toBe(created.job.id);
await expect(createPublicGenerationJob({
...input,
body: { ...fixture.idempotency.body, prompt: "hello!" },
})).rejects.toBeInstanceOf(PublicApiConflictError);
});
});

View File

@@ -0,0 +1,28 @@
import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
import type { GenerationJob } from "@/lib/types";
import { buildJobWebhookPayload, signWebhookBody } from "@/lib/server/webhook";
const fixtureUrl = new URL("../contracts/webhook/job-webhook-v1.json", import.meta.url);
describe("job webhook v1 cross-language contract", () => {
it("freezes exact body bytes and HMAC header", async () => {
const fixture = JSON.parse(await readFile(fixtureUrl, "utf8"));
const job = {
...fixture.job,
ownerId: "owner-1",
provider: "mock",
reqKey: "fixture",
inputAssetIds: [],
inputUrls: [],
requestPayload: {},
createdAt: fixture.job.updatedAt,
} as GenerationJob;
const body = JSON.stringify(buildJobWebhookPayload(job));
expect(body).toBe(fixture.body);
expect(signWebhookBody(body, fixture.secret)).toBe(fixture.signature);
expect(fixture.maximumAttempts).toBe(3);
});
});