feat: add task workflow and asset downloads
This commit is contained in:
@@ -17,16 +17,16 @@ let previousSupabaseKey: string | undefined;
|
||||
describe("local data store concurrency", () => {
|
||||
beforeEach(async () => {
|
||||
runtimeDir = await mkdtemp(join(tmpdir(), "zhinian-store-"));
|
||||
previousRuntimeDir = process.env.NIANXXPLAY_RUNTIME_DIR;
|
||||
previousRuntimeDir = process.env.ZHINIAN_RUNTIME_DIR;
|
||||
previousSupabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
previousSupabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
process.env.NIANXXPLAY_RUNTIME_DIR = runtimeDir;
|
||||
process.env.ZHINIAN_RUNTIME_DIR = runtimeDir;
|
||||
delete process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
delete process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
restoreEnv("NIANXXPLAY_RUNTIME_DIR", previousRuntimeDir);
|
||||
restoreEnv("ZHINIAN_RUNTIME_DIR", previousRuntimeDir);
|
||||
restoreEnv("NEXT_PUBLIC_SUPABASE_URL", previousSupabaseUrl);
|
||||
restoreEnv("SUPABASE_SERVICE_ROLE_KEY", previousSupabaseKey);
|
||||
await rm(runtimeDir, { force: true, recursive: true });
|
||||
|
||||
155
tests/task-management.test.ts
Normal file
155
tests/task-management.test.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
authenticatePublicApiRequest,
|
||||
PublicApiAuthError,
|
||||
type PublicApiClient
|
||||
} from "@/lib/server/public-api-auth";
|
||||
import { createPublicGenerationJob, PublicApiConflictError } from "@/lib/server/public-api-jobs";
|
||||
import { claimGenerationJobs, createGenerationJob, getGenerationJob, listGenerationJobs } from "@/lib/server/data-store";
|
||||
import { runWorkerTick } from "@/lib/server/task-manager";
|
||||
import { DEFAULT_OWNER_ID } from "@/lib/server/runtime";
|
||||
import { signWebhookBody } from "@/lib/server/webhook";
|
||||
|
||||
let runtimeDir = "";
|
||||
const previousEnv = new Map<string, string | undefined>();
|
||||
const envNames = [
|
||||
"ZHINIAN_RUNTIME_DIR",
|
||||
"NEXT_PUBLIC_SUPABASE_URL",
|
||||
"SUPABASE_SERVICE_ROLE_KEY",
|
||||
"ZHINIAN_API_KEYS",
|
||||
"JIMENG_VISUAL_MOCK",
|
||||
"VOLCENGINE_ACCESS_KEY_ID",
|
||||
"VOLCENGINE_SECRET_ACCESS_KEY",
|
||||
"ZHINIAN_WEBHOOK_SECRET"
|
||||
];
|
||||
|
||||
describe("task management and public API helpers", () => {
|
||||
beforeEach(async () => {
|
||||
runtimeDir = await mkdtemp(join(tmpdir(), "zhinian-tasks-"));
|
||||
for (const name of envNames) previousEnv.set(name, process.env[name]);
|
||||
process.env.ZHINIAN_RUNTIME_DIR = runtimeDir;
|
||||
process.env.ZHINIAN_API_KEYS = "agent-a:secret-a,agent-b:secret-b";
|
||||
process.env.JIMENG_VISUAL_MOCK = "true";
|
||||
delete process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
delete process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
delete process.env.VOLCENGINE_ACCESS_KEY_ID;
|
||||
delete process.env.VOLCENGINE_SECRET_ACCESS_KEY;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
for (const name of envNames) restoreEnv(name, previousEnv.get(name));
|
||||
previousEnv.clear();
|
||||
await rm(runtimeDir, { force: true, recursive: true });
|
||||
});
|
||||
|
||||
it("authenticates public API requests with bearer and header keys", () => {
|
||||
expect(authenticatePublicApiRequest(new Request("http://local.test", {
|
||||
headers: { Authorization: "Bearer secret-a" }
|
||||
})).id).toBe("agent-a");
|
||||
expect(authenticatePublicApiRequest(new Request("http://local.test", {
|
||||
headers: { "X-Zhinian-Api-Key": "secret-b" }
|
||||
})).id).toBe("agent-b");
|
||||
expect(() => authenticatePublicApiRequest(new Request("http://local.test"))).toThrow(PublicApiAuthError);
|
||||
});
|
||||
|
||||
it("deduplicates public job creation by idempotency key and rejects conflicts", async () => {
|
||||
const client: PublicApiClient = { id: "agent-a", key: "secret-a" };
|
||||
const request = new Request("http://local.test/api/v1/jobs", {
|
||||
headers: { "Idempotency-Key": "idem-1" }
|
||||
});
|
||||
const first = await createPublicGenerationJob({
|
||||
client,
|
||||
request,
|
||||
origin: "http://local.test",
|
||||
body: {
|
||||
capability: "image.generate",
|
||||
prompt: "生成一张专业产品主图"
|
||||
}
|
||||
});
|
||||
const second = await createPublicGenerationJob({
|
||||
client,
|
||||
request,
|
||||
origin: "http://local.test",
|
||||
body: {
|
||||
capability: "image.generate",
|
||||
prompt: "生成一张专业产品主图"
|
||||
}
|
||||
});
|
||||
expect(second.reused).toBe(true);
|
||||
expect(second.job.id).toBe(first.job.id);
|
||||
await expect(createPublicGenerationJob({
|
||||
client,
|
||||
request,
|
||||
origin: "http://local.test",
|
||||
body: {
|
||||
capability: "image.generate",
|
||||
prompt: "不同的提示词"
|
||||
}
|
||||
})).rejects.toBeInstanceOf(PublicApiConflictError);
|
||||
});
|
||||
|
||||
it("claims local jobs without duplicate ownership", async () => {
|
||||
await Promise.all(Array.from({ length: 6 }, (_, index) => createGenerationJob({
|
||||
ownerId: DEFAULT_OWNER_ID,
|
||||
capability: "image.generate",
|
||||
provider: "mock",
|
||||
reqKey: "jimeng_seedream46_cvtob",
|
||||
status: "queued",
|
||||
prompt: `job ${index}`,
|
||||
inputAssetIds: [],
|
||||
inputUrls: [],
|
||||
outputAssetIds: [],
|
||||
requestPayload: { index },
|
||||
priority: index
|
||||
})));
|
||||
|
||||
const [left, right] = await Promise.all([
|
||||
claimGenerationJobs({ workerId: "worker-a", limit: 3 }),
|
||||
claimGenerationJobs({ workerId: "worker-b", limit: 3 })
|
||||
]);
|
||||
const claimedIds = [...left, ...right].map((job) => job.id);
|
||||
expect(new Set(claimedIds).size).toBe(6);
|
||||
expect(await claimGenerationJobs({ workerId: "worker-c", limit: 1 })).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("processes a mock queued job to a terminal result through the worker tick", async () => {
|
||||
const job = await createPublicGenerationJob({
|
||||
client: { id: "agent-a", key: "secret-a" },
|
||||
request: new Request("http://local.test/api/v1/jobs"),
|
||||
origin: "http://local.test",
|
||||
body: {
|
||||
capability: "image.generate",
|
||||
prompt: "生成一张适合社媒传播的品牌主视觉"
|
||||
}
|
||||
});
|
||||
expect(job.job.status).toBe("queued");
|
||||
const tick = await runWorkerTick({
|
||||
workerId: "test-worker",
|
||||
origin: "http://local.test",
|
||||
limit: 1
|
||||
});
|
||||
expect(tick.claimed).toBe(1);
|
||||
const stored = await getGenerationJob(job.job.id);
|
||||
expect(stored?.status).toBe("succeeded");
|
||||
expect(stored?.completedAt).toBeTruthy();
|
||||
expect(stored?.outputAssetIds.length).toBe(1);
|
||||
expect(await listGenerationJobs(DEFAULT_OWNER_ID, 10)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("signs webhook bodies with the configured secret", () => {
|
||||
expect(signWebhookBody("{\"ok\":true}", "secret")).toBe(
|
||||
"sha256=f6b4a2841c93f8bf2fb8f2c13d8fb0b6c8e8019f09ee405d248daa8385fad638"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function restoreEnv(name: string, value: string | undefined) {
|
||||
if (value === undefined) {
|
||||
delete process.env[name];
|
||||
return;
|
||||
}
|
||||
process.env[name] = value;
|
||||
}
|
||||
Reference in New Issue
Block a user