dify/web/features/new-rag/__tests__/document-model.spec.ts
Stephen Zhou 799c7eea3a
feat(dataset): add document processing tasks (#39326)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-24 12:58:15 +00:00

119 lines
3.9 KiB
TypeScript

import type {
DocumentProcessingTask,
LogicalDocument,
} from '@dify/contracts/knowledge-fs/types.gen'
import {
documentDisplayStatus,
newestTaskByDocument,
taskCanRetry,
taskIsActive,
taskNeedsAttention,
taskVersionIsAfter,
} from '../document-model'
const document = (overrides: Partial<LogicalDocument> = {}): LogicalDocument => ({
active: {
contentHash: 'hash',
createdAt: '2026-07-20T10:00:00Z',
documentAssetId: 'asset-1',
documentAssetVersion: 1,
documentId: 'document-1',
knowledgeSpaceId: 'space-1',
mimeType: 'text/plain',
revision: 2,
sizeBytes: 10,
state: 'active',
},
activeRevision: 2,
createdAt: '2026-07-20T10:00:00Z',
id: 'document-1',
knowledgeSpaceId: 'space-1',
rowVersion: 1,
status: 'ready',
title: 'Document',
updatedAt: '2026-07-20T10:00:00Z',
userMetadata: {},
...overrides,
})
const task = (overrides: Partial<DocumentProcessingTask> = {}): DocumentProcessingTask => ({
createdAt: '2026-07-20T10:00:00Z',
documentId: 'document-1',
documentRevision: 3,
id: 'task-1',
knowledgeSpaceId: 'space-1',
progressPercent: 10,
stage: 'queued',
state: 'queued',
updatedAt: '2026-07-20T10:00:00Z',
...overrides,
})
describe('new Knowledge document model', () => {
it('chooses the highest revision before a more recently updated stale task', () => {
const result = newestTaskByDocument([
task({ documentRevision: 2, id: 'stale', updatedAt: '2026-07-20T10:10:00Z' }),
task({ documentRevision: 3, id: 'current', updatedAt: '2026-07-20T10:01:00Z' }),
])
expect(result.get('document-1')?.id).toBe('current')
})
it('compares task versions without losing sub-millisecond precision', () => {
expect(taskVersionIsAfter('2026-07-20T10:00:00.123789Z', '2026-07-20T10:00:00.123456Z')).toBe(
true,
)
expect(taskVersionIsAfter('2026-07-20T10:00:00.000001Z', '2026-07-20T10:00:00Z')).toBe(true)
expect(taskVersionIsAfter('2026-07-20T10:00:00Z', '2026-07-20T10:00:00.000001Z')).toBe(false)
})
it('selects the newest task when versions differ only after milliseconds', () => {
const result = newestTaskByDocument([
task({ id: 'older', updatedAt: '2026-07-20T10:00:00.123456Z' }),
task({ id: 'newer', updatedAt: '2026-07-20T10:00:00.123789Z' }),
])
expect(result.get('document-1')?.id).toBe('newer')
})
it.each([
['dispatch_pending', 'queued'],
['queued', 'queued'],
['retry_wait', 'queued'],
['running', 'processing'],
['failed', 'failed'],
] as const)('maps a latest %s task to %s', (state, expected) => {
expect(documentDisplayStatus(document(), task({ state }))).toBe(expected)
})
it('keeps a last ready revision available after a canceled re-index', () => {
expect(documentDisplayStatus(document(), task({ state: 'canceled' }))).toBe('ready')
expect(
documentDisplayStatus(
document({ active: null, activeRevision: undefined, status: 'pending' }),
task({ state: 'canceled' }),
),
).toBe('failed')
})
it('does not let an older failed task override the active revision', () => {
expect(documentDisplayStatus(document(), task({ documentRevision: 1, state: 'failed' }))).toBe(
'ready',
)
})
it('maps a disabled source and deleting lifecycle to the designed disabled state', () => {
expect(documentDisplayStatus(document(), undefined, true)).toBe('disabled')
expect(documentDisplayStatus(document({ status: 'deleting' }))).toBe('disabled')
})
it('keeps task badge and actions aligned with the contract states', () => {
expect(taskIsActive(task({ state: 'running' }))).toBe(true)
expect(taskNeedsAttention(task({ state: 'failed' }))).toBe(true)
expect(taskCanRetry(task({ state: 'canceled' }))).toBe(false)
expect(taskNeedsAttention(task({ state: 'canceled' }))).toBe(false)
expect(taskNeedsAttention(task({ state: 'succeeded' }))).toBe(false)
expect(taskCanRetry(task({ state: 'superseded' }))).toBe(false)
})
})