feat: add jobs worker and webhook core
This commit is contained in:
55
tests/jobs-go-contract.test.ts
Normal file
55
tests/jobs-go-contract.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
28
tests/webhook-go-contract.test.ts
Normal file
28
tests/webhook-go-contract.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user