mirror of
https://github.com/langgenius/dify.git
synced 2026-09-02 22:36:15 +08:00
402 lines
12 KiB
TypeScript
402 lines
12 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
bulkBackgroundTask,
|
|
decodeBackgroundTaskCursor,
|
|
documentBackgroundTask,
|
|
encodeBackgroundTaskCursor,
|
|
sourceBackgroundTask,
|
|
} from "./background-task";
|
|
import type { BulkOperation } from "./bulk-operation";
|
|
import type { BulkOperationSummary } from "./bulk-operation-summary";
|
|
import type { DocumentProcessingTask } from "./document-processing-task-repository";
|
|
import type { SourceWorkflowRun } from "./source-product-workflow";
|
|
|
|
const SPACE_ID = "018f0d60-7a49-7cc2-9c1b-5b36f18f2c40";
|
|
const CREATED_AT = "2026-07-23T12:00:00.000Z";
|
|
const UPDATED_AT = "2026-07-23T12:01:00.000Z";
|
|
|
|
describe("background task DTOs", () => {
|
|
it("maps document terminal states to retry and cancel controls", () => {
|
|
expect(
|
|
documentBackgroundTask(
|
|
documentTask({
|
|
completedAt: UPDATED_AT,
|
|
errorCode: "PARSER_FAILED",
|
|
errorMessage: "Parser failed",
|
|
progressPercent: 20,
|
|
state: "failed",
|
|
}),
|
|
),
|
|
).toMatchObject({
|
|
canCancel: false,
|
|
canRetry: true,
|
|
errorCode: "DOCUMENT_PARSER_UNAVAILABLE",
|
|
errorMessage: "The document parser is temporarily unavailable.",
|
|
failure: {
|
|
action: "retry",
|
|
category: "dependency",
|
|
code: "DOCUMENT_PARSER_UNAVAILABLE",
|
|
retryPolicy: "automatic",
|
|
},
|
|
operation: "document_processing",
|
|
progressFailed: 1,
|
|
state: "failed",
|
|
taskKind: "document",
|
|
});
|
|
|
|
expect(documentBackgroundTask(documentTask({ state: "retry_wait" }))).toMatchObject({
|
|
canCancel: true,
|
|
canRetry: false,
|
|
state: "queued",
|
|
});
|
|
|
|
expect(
|
|
documentBackgroundTask(
|
|
documentTask({
|
|
errorCode: "MODEL_SELECTION_NOT_FOUND",
|
|
errorMessage: "provider credential and endpoint detail",
|
|
phase: "outline_summary",
|
|
state: "failed",
|
|
}),
|
|
),
|
|
).toMatchObject({
|
|
canRetry: false,
|
|
errorMessage:
|
|
"The selected model is no longer available in this workspace. Select another model before retrying.",
|
|
failure: {
|
|
action: "configure_model",
|
|
code: "MODEL_SELECTION_NOT_FOUND",
|
|
retryPolicy: "after_configuration",
|
|
stage: "outline_summary",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("forwards additive document compilation and graph progress metadata", () => {
|
|
expect(
|
|
documentBackgroundTask(
|
|
documentTask({
|
|
activeOperations: ["chunk", "fts_index", "embedding"],
|
|
phase: "chunking_indexing",
|
|
semanticEnrichment: {
|
|
nodesCompleted: 32,
|
|
nodesTotal: 80,
|
|
state: "running",
|
|
},
|
|
}),
|
|
),
|
|
).toMatchObject({
|
|
activeOperations: ["chunk", "fts_index", "embedding"],
|
|
phase: "chunking_indexing",
|
|
semanticEnrichment: {
|
|
nodesCompleted: 32,
|
|
nodesTotal: 80,
|
|
state: "running",
|
|
},
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
["succeeded", "completed", 1, false, false],
|
|
["canceled", "canceled", 0, false, true],
|
|
["superseded", "canceled", 0, false, true],
|
|
["running", "running", 0, true, false],
|
|
] as const)(
|
|
"maps document state %s to public state %s",
|
|
(state, publicState, progressCompleted, canCancel, canRetry) => {
|
|
expect(documentBackgroundTask(documentTask({ state }))).toMatchObject({
|
|
canCancel,
|
|
canRetry,
|
|
progressCompleted,
|
|
state: publicState,
|
|
});
|
|
},
|
|
);
|
|
|
|
it("maps bulk and source task progress without treating canceled work as failed", () => {
|
|
const operation: BulkOperation = {
|
|
capabilityGrantId: "11111111-1111-4111-8111-111111111111",
|
|
createdAt: CREATED_AT,
|
|
id: "22222222-2222-4222-8222-222222222222",
|
|
items: [],
|
|
knowledgeSpaceId: SPACE_ID,
|
|
tenantId: "tenant-1",
|
|
type: "document_reindex",
|
|
updatedAt: UPDATED_AT,
|
|
};
|
|
const summary: BulkOperationSummary = {
|
|
canceledItems: 1,
|
|
completedItems: 2,
|
|
createdAt: CREATED_AT,
|
|
failedItemIds: [],
|
|
failedItems: 0,
|
|
id: operation.id,
|
|
knowledgeSpaceId: SPACE_ID,
|
|
status: "canceled",
|
|
totalItems: 3,
|
|
type: operation.type,
|
|
updatedAt: UPDATED_AT,
|
|
};
|
|
expect(bulkBackgroundTask(operation, summary)).toMatchObject({
|
|
canCancel: false,
|
|
canRetry: true,
|
|
progressCompleted: 2,
|
|
progressFailed: 0,
|
|
progressPercent: 100,
|
|
state: "canceled",
|
|
taskKind: "document_bulk",
|
|
});
|
|
|
|
expect(
|
|
sourceBackgroundTask(
|
|
sourceRun({
|
|
progressCompleted: 4,
|
|
progressFailed: 1,
|
|
progressSkipped: 1,
|
|
progressTotal: 8,
|
|
state: "syncing",
|
|
}),
|
|
),
|
|
).toMatchObject({
|
|
canCancel: true,
|
|
canRetry: false,
|
|
operation: "source_sync",
|
|
progressCompleted: 4,
|
|
progressFailed: 1,
|
|
progressPercent: 75,
|
|
progressTotal: 8,
|
|
state: "running",
|
|
taskKind: "source",
|
|
});
|
|
});
|
|
|
|
it("maps empty bulk operations and every source terminal state", () => {
|
|
const operation: BulkOperation = {
|
|
createdAt: CREATED_AT,
|
|
id: "22222222-2222-4222-8222-222222222222",
|
|
items: [],
|
|
knowledgeSpaceId: SPACE_ID,
|
|
permissionSnapshot: {
|
|
accessChannel: "interactive",
|
|
id: "11111111-1111-4111-8111-111111111111",
|
|
revision: 2,
|
|
},
|
|
requestedBySubjectId: "member-a",
|
|
tenantId: "tenant-1",
|
|
type: "document_upload",
|
|
updatedAt: UPDATED_AT,
|
|
};
|
|
expect(
|
|
bulkBackgroundTask(operation, {
|
|
canceledItems: 0,
|
|
completedItems: 0,
|
|
createdAt: CREATED_AT,
|
|
failedItemIds: [],
|
|
failedItems: 0,
|
|
id: operation.id,
|
|
knowledgeSpaceId: SPACE_ID,
|
|
status: "completed",
|
|
totalItems: 0,
|
|
type: operation.type,
|
|
updatedAt: UPDATED_AT,
|
|
}),
|
|
).toMatchObject({
|
|
canCancel: false,
|
|
canRetry: false,
|
|
completedAt: UPDATED_AT,
|
|
operation: "document_upload",
|
|
progressPercent: 100,
|
|
state: "completed",
|
|
});
|
|
|
|
expect(
|
|
sourceBackgroundTask(
|
|
sourceRun({
|
|
kind: "crawl-import",
|
|
lastErrorCode: "SOURCE_FAILED",
|
|
lastErrorMessage: "Source failed",
|
|
progressTotal: 0,
|
|
sourceId: undefined,
|
|
state: "failed",
|
|
}),
|
|
),
|
|
).toMatchObject({
|
|
canRetry: true,
|
|
errorCode: "SOURCE_OPERATION_FAILED",
|
|
errorMessage: "The source operation could not be completed.",
|
|
failure: {
|
|
action: "retry",
|
|
code: "SOURCE_OPERATION_FAILED",
|
|
retryPolicy: "manual",
|
|
},
|
|
operation: "source_crawl_import",
|
|
progressPercent: 100,
|
|
state: "failed",
|
|
});
|
|
expect(
|
|
sourceBackgroundTask(
|
|
sourceRun({ kind: "crawl-preview", progressTotal: 2, state: "completed" }),
|
|
),
|
|
).toMatchObject({
|
|
operation: "source_crawl_preview",
|
|
progressCompleted: 2,
|
|
state: "completed",
|
|
});
|
|
expect(
|
|
sourceBackgroundTask(sourceRun({ kind: "online-document-import", state: "zero_results" })),
|
|
).toMatchObject({
|
|
operation: "source_online_document_import",
|
|
state: "completed",
|
|
});
|
|
expect(
|
|
sourceBackgroundTask(sourceRun({ kind: "online-drive-import", state: "preview_ready" })),
|
|
).toMatchObject({
|
|
operation: "source_online_drive_import",
|
|
state: "completed",
|
|
});
|
|
expect(sourceBackgroundTask(sourceRun({ kind: "bulk", state: "canceled" }))).toMatchObject({
|
|
canRetry: true,
|
|
operation: "source_bulk",
|
|
state: "canceled",
|
|
});
|
|
expect(sourceBackgroundTask(sourceRun({ state: "queued" }))).toMatchObject({
|
|
canCancel: true,
|
|
state: "queued",
|
|
});
|
|
});
|
|
|
|
it("replaces raw bulk item diagnostics with the registered public failure", () => {
|
|
const operation: BulkOperation = {
|
|
createdAt: CREATED_AT,
|
|
id: "22222222-2222-4222-8222-222222222222",
|
|
items: [],
|
|
knowledgeSpaceId: SPACE_ID,
|
|
tenantId: "tenant-1",
|
|
type: "document_reindex",
|
|
updatedAt: UPDATED_AT,
|
|
};
|
|
const task = bulkBackgroundTask(operation, {
|
|
canceledItems: 0,
|
|
completedItems: 0,
|
|
createdAt: CREATED_AT,
|
|
failedItemIds: ["document-1"],
|
|
failedItems: 1,
|
|
failures: [
|
|
{
|
|
documentId: "document-1",
|
|
errorCode: "MODEL_CREDENTIAL_INVALID",
|
|
errorMessage: "Authorization: Bearer secret",
|
|
},
|
|
],
|
|
id: operation.id,
|
|
knowledgeSpaceId: SPACE_ID,
|
|
status: "failed",
|
|
totalItems: 1,
|
|
type: operation.type,
|
|
updatedAt: UPDATED_AT,
|
|
});
|
|
|
|
expect(task).toMatchObject({
|
|
canRetry: false,
|
|
errorMessage:
|
|
"The selected model credentials are invalid. Ask an administrator to update the model provider configuration.",
|
|
failures: [
|
|
{
|
|
errorCode: "MODEL_CREDENTIAL_INVALID",
|
|
errorMessage:
|
|
"The selected model credentials are invalid. Ask an administrator to update the model provider configuration.",
|
|
failure: {
|
|
action: "configure_model",
|
|
code: "MODEL_CREDENTIAL_INVALID",
|
|
retryPolicy: "after_configuration",
|
|
},
|
|
},
|
|
],
|
|
});
|
|
expect(JSON.stringify(task)).not.toContain("Bearer secret");
|
|
});
|
|
|
|
it("round-trips opaque composite cursors and rejects malformed values", () => {
|
|
const cursor = {
|
|
bulk: { createdAt: CREATED_AT, id: "bulk-1" },
|
|
document: { createdAt: UPDATED_AT, id: "document-1" },
|
|
version: 1 as const,
|
|
};
|
|
expect(decodeBackgroundTaskCursor(encodeBackgroundTaskCursor(cursor))).toEqual(cursor);
|
|
expect(() => decodeBackgroundTaskCursor("not-a-cursor")).toThrow(
|
|
"Invalid background task cursor",
|
|
);
|
|
expect(() =>
|
|
decodeBackgroundTaskCursor(
|
|
Buffer.from(
|
|
JSON.stringify({ document: { createdAt: "bad", id: "x" }, version: 1 }),
|
|
).toString("base64url"),
|
|
),
|
|
).toThrow("Invalid background task cursor");
|
|
});
|
|
|
|
it("validates every cursor component and preserves a source-only cursor", () => {
|
|
const sourceOnly = {
|
|
source: { createdAt: CREATED_AT, id: "source-1" },
|
|
version: 1 as const,
|
|
};
|
|
expect(decodeBackgroundTaskCursor(encodeBackgroundTaskCursor(sourceOnly))).toEqual(sourceOnly);
|
|
|
|
for (const value of [
|
|
null,
|
|
[],
|
|
{ version: 2 },
|
|
{ document: null, version: 1 },
|
|
{ bulk: [], version: 1 },
|
|
{ source: { createdAt: CREATED_AT, extra: true, id: "source-1" }, version: 1 },
|
|
{ source: { createdAt: 1, id: "source-1" }, version: 1 },
|
|
{ source: { createdAt: "not-a-date", id: "source-1" }, version: 1 },
|
|
{ source: { createdAt: CREATED_AT, id: 1 }, version: 1 },
|
|
{ source: { createdAt: CREATED_AT, id: "" }, version: 1 },
|
|
]) {
|
|
expect(() =>
|
|
decodeBackgroundTaskCursor(Buffer.from(JSON.stringify(value)).toString("base64url")),
|
|
).toThrow("Invalid background task cursor");
|
|
}
|
|
});
|
|
});
|
|
|
|
function documentTask(patch: Partial<DocumentProcessingTask> = {}): DocumentProcessingTask {
|
|
return {
|
|
createdAt: CREATED_AT,
|
|
documentId: "33333333-3333-4333-8333-333333333333",
|
|
documentRevision: 2,
|
|
id: "44444444-4444-4444-8444-444444444444",
|
|
knowledgeSpaceId: SPACE_ID,
|
|
progressPercent: 0,
|
|
stage: "queued",
|
|
state: "queued",
|
|
updatedAt: UPDATED_AT,
|
|
...patch,
|
|
};
|
|
}
|
|
|
|
function sourceRun(patch: Partial<SourceWorkflowRun> = {}): SourceWorkflowRun {
|
|
return {
|
|
checkpoint: "queued",
|
|
createdAt: CREATED_AT,
|
|
executionAttempts: 1,
|
|
id: "55555555-5555-4555-8555-555555555555",
|
|
idempotencyKey: "sync-1",
|
|
knowledgeSpaceId: SPACE_ID,
|
|
kind: "sync",
|
|
maxExecutionAttempts: 5,
|
|
payload: {},
|
|
progressCompleted: 0,
|
|
progressFailed: 0,
|
|
progressSkipped: 0,
|
|
rowVersion: 1,
|
|
sourceId: "66666666-6666-4666-8666-666666666666",
|
|
state: "queued",
|
|
tenantId: "tenant-1",
|
|
updatedAt: UPDATED_AT,
|
|
...patch,
|
|
};
|
|
}
|