Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
@@ -0,0 +1,300 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import {
|
||||
buildAiBrowserTaskJob,
|
||||
buildAiChatCompletionJob,
|
||||
buildAiEmbeddingJob,
|
||||
buildBrowserSessionCleanupJob,
|
||||
buildBrowserSessionRunJob,
|
||||
buildBrowserUsageFlushJob,
|
||||
buildCleanupJob,
|
||||
buildExportJob,
|
||||
buildFilePreviewJob,
|
||||
buildFileScanJob,
|
||||
buildImportJob,
|
||||
buildNotificationJob,
|
||||
buildPluginInvokeJob,
|
||||
buildRetryJobOptions,
|
||||
buildTransactionalMailJob,
|
||||
buildWebhookDeliveryJob,
|
||||
enqueueJob,
|
||||
queueNames,
|
||||
type ProducerJobDescriptor,
|
||||
type QueueName,
|
||||
type TypedQueue,
|
||||
} from "../src/index.js";
|
||||
import * as producerExports from "../src/producers.js";
|
||||
|
||||
type RetryMetrics = Pick<
|
||||
ReturnType<typeof buildRetryJobOptions>,
|
||||
"attempts" | "backoff" | "removeOnComplete" | "removeOnFail"
|
||||
>;
|
||||
|
||||
const descriptorBuilders = {
|
||||
buildAiBrowserTaskJob: () => buildAiBrowserTaskJob({ tenantId: "tenant_1", task: { action: "research" } }),
|
||||
buildAiChatCompletionJob: () =>
|
||||
buildAiChatCompletionJob({ tenantId: "tenant_1", messages: [{ role: "user", content: "Hello" }] }),
|
||||
buildAiEmbeddingJob: () =>
|
||||
buildAiEmbeddingJob({ tenantId: "tenant_1", messages: [{ role: "user", content: "Embed this" }] }),
|
||||
buildBrowserSessionCleanupJob: () => buildBrowserSessionCleanupJob({ tenantId: "tenant_1", sessionId: "session_1" }),
|
||||
buildBrowserSessionRunJob: () =>
|
||||
buildBrowserSessionRunJob({ tenantId: "tenant_1", sessionId: "session_1", task: { action: "capture" } }),
|
||||
buildBrowserUsageFlushJob: () => buildBrowserUsageFlushJob({ tenantId: "tenant_1", sessionId: "session_1" }),
|
||||
buildCleanupJob: () => buildCleanupJob({ task: "expired-sessions" }),
|
||||
buildExportJob: () =>
|
||||
buildExportJob({
|
||||
exportJobId: "export_job_metrics_1",
|
||||
tenantId: "tenant_1",
|
||||
requestedByUserId: "user_1",
|
||||
exportType: "members",
|
||||
format: "csv",
|
||||
}),
|
||||
buildFilePreviewJob: () =>
|
||||
buildFilePreviewJob({ fileId: "file_1", tenantId: "tenant_1", bucket: "uploads", storageKey: "tenants/tenant_1/files/file_1/original", mimeType: "image/heic" }),
|
||||
buildFileScanJob: () => buildFileScanJob({ bucket: "uploads", key: "tenant_1/upload_1/file.pdf" }),
|
||||
buildImportJob: () =>
|
||||
buildImportJob({
|
||||
importJobId: "import_job_metrics_1",
|
||||
tenantId: "tenant_1",
|
||||
requestedByUserId: "user_1",
|
||||
importType: "members",
|
||||
format: "csv",
|
||||
sourceBucket: "uploads",
|
||||
sourceKey: "tenant_1/imports/members.csv",
|
||||
}),
|
||||
buildNotificationJob: () => buildNotificationJob({ tenantId: "tenant_1", userId: "user_1", channel: "in-app", messageKey: "message.ready" }),
|
||||
buildPluginInvokeJob: () => buildPluginInvokeJob({ tenantId: "tenant_1", pluginId: "plugin_1", version: "1.0.0", hookName: "onEvent" }),
|
||||
buildTransactionalMailJob: () => buildTransactionalMailJob({ to: ["user@example.com"], templateId: "welcome" }),
|
||||
buildWebhookDeliveryJob: () => buildWebhookDeliveryJob({ url: "https://example.com/webhook", eventName: "entity.created" }),
|
||||
} satisfies Record<string, () => ProducerJobDescriptor>;
|
||||
|
||||
function producerHelperExportNames(): string[] {
|
||||
return Object.keys(producerExports)
|
||||
.filter((name) => /^build[A-Z].*Job$/.test(name))
|
||||
.sort();
|
||||
}
|
||||
|
||||
function retryMetricsFor(descriptor: ProducerJobDescriptor): RetryMetrics {
|
||||
const { attempts, backoff, removeOnComplete, removeOnFail } = buildRetryJobOptions(descriptor);
|
||||
|
||||
return { attempts, backoff, removeOnComplete, removeOnFail };
|
||||
}
|
||||
|
||||
function sortedRetryMetricsSnapshot(): Record<string, RetryMetrics> {
|
||||
return Object.fromEntries(
|
||||
Object.values(descriptorBuilders)
|
||||
.map((buildDescriptor) => {
|
||||
const descriptor = buildDescriptor();
|
||||
|
||||
return [`${descriptor.queueName}:${descriptor.jobName}`, retryMetricsFor(descriptor)] as const;
|
||||
})
|
||||
.sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
|
||||
);
|
||||
}
|
||||
|
||||
function createMockQueue<Name extends QueueName>(): TypedQueue<Name> {
|
||||
return { add: vi.fn().mockResolvedValue({ id: "queued-job" }) } as unknown as TypedQueue<Name>;
|
||||
}
|
||||
|
||||
async function enqueueOptionsFor(descriptor: ProducerJobDescriptor, options?: Parameters<typeof enqueueJob>[2]) {
|
||||
const queue = createMockQueue<typeof descriptor.queueName>();
|
||||
|
||||
await enqueueJob(queue, descriptor, options);
|
||||
|
||||
return vi.mocked(queue.add).mock.calls[0]?.[2];
|
||||
}
|
||||
|
||||
describe("queue retry metrics snapshot", () => {
|
||||
test("covers every exported producer descriptor helper", () => {
|
||||
expect(Object.keys(descriptorBuilders).sort()).toEqual(producerHelperExportNames());
|
||||
});
|
||||
|
||||
test("locks retry defaults for every queue producer descriptor", () => {
|
||||
expect(sortedRetryMetricsSnapshot()).toMatchInlineSnapshot(`
|
||||
{
|
||||
"${queueNames.aiChatbot}:browser-task": {
|
||||
"attempts": 2,
|
||||
"backoff": {
|
||||
"delay": 5000,
|
||||
"type": "exponential",
|
||||
},
|
||||
"removeOnComplete": true,
|
||||
"removeOnFail": 1000,
|
||||
},
|
||||
"${queueNames.aiChatbot}:chat-completion": {
|
||||
"attempts": 3,
|
||||
"backoff": {
|
||||
"delay": 2000,
|
||||
"type": "exponential",
|
||||
},
|
||||
"removeOnComplete": true,
|
||||
"removeOnFail": 1000,
|
||||
},
|
||||
"${queueNames.aiChatbot}:create-embedding": {
|
||||
"attempts": 3,
|
||||
"backoff": {
|
||||
"delay": 2000,
|
||||
"type": "exponential",
|
||||
},
|
||||
"removeOnComplete": true,
|
||||
"removeOnFail": 1000,
|
||||
},
|
||||
"${queueNames.browserAutomation}:browser-session-cleanup": {
|
||||
"attempts": 3,
|
||||
"backoff": {
|
||||
"delay": 10000,
|
||||
"type": "fixed",
|
||||
},
|
||||
"removeOnComplete": true,
|
||||
"removeOnFail": 1000,
|
||||
},
|
||||
"${queueNames.browserAutomation}:browser-session-run": {
|
||||
"attempts": 2,
|
||||
"backoff": {
|
||||
"delay": 5000,
|
||||
"type": "exponential",
|
||||
},
|
||||
"removeOnComplete": true,
|
||||
"removeOnFail": 1000,
|
||||
},
|
||||
"${queueNames.browserAutomation}:usage-flush": {
|
||||
"attempts": 3,
|
||||
"backoff": {
|
||||
"delay": 10000,
|
||||
"type": "fixed",
|
||||
},
|
||||
"removeOnComplete": true,
|
||||
"removeOnFail": 1000,
|
||||
},
|
||||
"${queueNames.email}:deliver-webhook": {
|
||||
"attempts": 5,
|
||||
"backoff": {
|
||||
"delay": 2000,
|
||||
"type": "exponential",
|
||||
},
|
||||
"removeOnComplete": true,
|
||||
"removeOnFail": 1000,
|
||||
},
|
||||
"${queueNames.email}:send-transactional-email": {
|
||||
"attempts": 3,
|
||||
"backoff": {
|
||||
"delay": 1000,
|
||||
"type": "exponential",
|
||||
},
|
||||
"removeOnComplete": true,
|
||||
"removeOnFail": 1000,
|
||||
},
|
||||
"${queueNames.exports}:run-export": {
|
||||
"attempts": 2,
|
||||
"backoff": {
|
||||
"delay": 10000,
|
||||
"type": "exponential",
|
||||
},
|
||||
"removeOnComplete": true,
|
||||
"removeOnFail": 1000,
|
||||
},
|
||||
"${queueNames.filePreview}:generate-file-preview": {
|
||||
"attempts": 3,
|
||||
"backoff": {
|
||||
"delay": 3000,
|
||||
"type": "exponential",
|
||||
},
|
||||
"removeOnComplete": true,
|
||||
"removeOnFail": 1000,
|
||||
},
|
||||
"${queueNames.fileScan}:scan-file": {
|
||||
"attempts": 3,
|
||||
"backoff": {
|
||||
"delay": 2000,
|
||||
"type": "exponential",
|
||||
},
|
||||
"removeOnComplete": true,
|
||||
"removeOnFail": 1000,
|
||||
},
|
||||
"${queueNames.imports}:run-import": {
|
||||
"attempts": 2,
|
||||
"backoff": {
|
||||
"delay": 10000,
|
||||
"type": "exponential",
|
||||
},
|
||||
"removeOnComplete": true,
|
||||
"removeOnFail": 1000,
|
||||
},
|
||||
"${queueNames.maintenance}:run-cleanup": {
|
||||
"attempts": 2,
|
||||
"backoff": {
|
||||
"delay": 5000,
|
||||
"type": "fixed",
|
||||
},
|
||||
"removeOnComplete": true,
|
||||
"removeOnFail": 1000,
|
||||
},
|
||||
"${queueNames.notifications}:send-notification": {
|
||||
"attempts": 3,
|
||||
"backoff": {
|
||||
"delay": 1000,
|
||||
"type": "exponential",
|
||||
},
|
||||
"removeOnComplete": true,
|
||||
"removeOnFail": 1000,
|
||||
},
|
||||
"${queueNames.pluginInvoke}:invoke-plugin": {
|
||||
"attempts": 3,
|
||||
"backoff": {
|
||||
"delay": 2000,
|
||||
"type": "exponential",
|
||||
},
|
||||
"removeOnComplete": true,
|
||||
"removeOnFail": 1000,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("queue idempotency diagnostics", () => {
|
||||
test("uses an explicit jobId before an idempotencyKey", async () => {
|
||||
const options = await enqueueOptionsFor(buildTransactionalMailJob({ to: ["user@example.com"], templateId: "welcome" }), {
|
||||
idempotencyKey: "idempotency-key",
|
||||
jobId: "explicit-job-id",
|
||||
});
|
||||
|
||||
expect(options?.jobId).toBe("explicit-job-id");
|
||||
expect(options).not.toHaveProperty("idempotencyKey");
|
||||
});
|
||||
|
||||
test("uses an explicit idempotencyKey before payload metadata", async () => {
|
||||
const options = await enqueueOptionsFor(
|
||||
buildNotificationJob({
|
||||
tenantId: "tenant_1",
|
||||
userId: "user_1",
|
||||
channel: "in-app",
|
||||
messageKey: "message.ready",
|
||||
metadata: { idempotencyKey: "metadata-key" },
|
||||
}),
|
||||
{ idempotencyKey: "explicit-idempotency-key" }
|
||||
);
|
||||
|
||||
expect(options?.jobId).toBe("explicit-idempotency-key");
|
||||
expect(options).not.toHaveProperty("idempotencyKey");
|
||||
});
|
||||
|
||||
test("uses metadata idempotencyKey when no explicit key is present", async () => {
|
||||
const options = await enqueueOptionsFor(
|
||||
buildCleanupJob({ task: "temporary-files", metadata: { idempotencyKey: "metadata-key" } })
|
||||
);
|
||||
|
||||
expect(options?.jobId).toBe("metadata-key");
|
||||
});
|
||||
|
||||
test("does not derive jobId values from body payloads", async () => {
|
||||
const options = await enqueueOptionsFor(
|
||||
buildWebhookDeliveryJob({
|
||||
url: "https://example.com/webhook",
|
||||
eventName: "entity.created",
|
||||
body: { idempotencyKey: "unsafe-body-key", jobId: "unsafe-body-job-id" },
|
||||
})
|
||||
);
|
||||
|
||||
expect(options).not.toHaveProperty("jobId");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user