Skip to content

Commit b19fd1b

Browse files
fix(knowledge): restore chunk retry metadata
1 parent d4f4889 commit b19fd1b

6 files changed

Lines changed: 168 additions & 5 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
5+
import { authMockFns, createMockRequest } from '@sim/testing'
6+
import { beforeEach, describe, expect, it, vi } from 'vitest'
7+
8+
const mocks = vi.hoisted(() => ({
9+
list: vi.fn(),
10+
create: vi.fn(),
11+
bulk: vi.fn(),
12+
}))
13+
14+
vi.mock('@/lib/knowledge/application/chunks', () => ({
15+
listKnowledgeChunks: {
16+
operation: { id: 'knowledge.chunks.list' },
17+
execute: mocks.list,
18+
},
19+
createKnowledgeChunk: {
20+
operation: { id: 'knowledge.chunks.create' },
21+
execute: mocks.create,
22+
},
23+
bulkUpdateKnowledgeChunks: {
24+
operation: { id: 'knowledge.chunks.bulk' },
25+
execute: mocks.bulk,
26+
},
27+
}))
28+
29+
vi.mock('@/app/api/knowledge/secret-provenance', () => ({
30+
finalizeKnowledgePersistedResponse: vi.fn(),
31+
finalizeKnowledgeProvenanceResponse: vi.fn(),
32+
resolveKnowledgeWriteSecretProvenance: vi.fn(),
33+
}))
34+
35+
import { KnowledgeDocumentNotReadyError } from '@/lib/knowledge/application/chunk-errors'
36+
import { GET } from '@/app/api/knowledge/[id]/documents/[documentId]/chunks/route'
37+
38+
const params = () => ({
39+
params: Promise.resolve({ id: 'knowledge-1', documentId: 'document-1' }),
40+
})
41+
42+
describe('/api/knowledge/[id]/documents/[documentId]/chunks internal route composition', () => {
43+
beforeEach(() => {
44+
vi.clearAllMocks()
45+
authMockFns.mockGetSession.mockResolvedValue({
46+
user: { id: 'user-1' },
47+
session: { id: 'session-1' },
48+
})
49+
})
50+
51+
it('preserves retry metadata when a document is still processing', async () => {
52+
mocks.list.mockRejectedValueOnce(new KnowledgeDocumentNotReadyError('processing'))
53+
54+
const response = await GET(createMockRequest('GET'), params())
55+
56+
expect(response.status).toBe(400)
57+
await expect(response.json()).resolves.toEqual({
58+
error: 'Document is not ready for access',
59+
details: 'Document status: processing',
60+
retryAfter: 5,
61+
})
62+
})
63+
})

apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export const GET = defineInternalJsonRoute({
5454
auth: internalKnowledgeSessionOrExecutorAuth,
5555
operation: knowledgeOperations.listChunks,
5656
rateLimit: internalRateLimits.none({ reason: 'Preserve existing internal chunk-list behavior' }),
57-
errorPolicy: internalKnowledgeErrorPolicies.chunks,
57+
errorPolicy: internalKnowledgeErrorPolicies.chunkList,
5858
mapInput: ({ params, query }) => ({
5959
knowledgeBaseId: params.id,
6060
documentId: params.documentId,

apps/sim/lib/knowledge/api/route-policies.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
createInternalResourceConcealmentPolicy,
33
createInternalSessionOrExecutorAuth,
44
createV2ResourceConcealmentPolicy,
5+
extendInternalErrorPolicy,
56
type InternalErrorPolicy,
67
internalErrorResponse,
78
internalOrchestrationErrorPolicy,
@@ -11,6 +12,7 @@ import {
1112
import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
1213
import { KNOWLEDGE_DELEGATION_AUDIENCE } from '@/lib/knowledge/application/authorization'
1314
import { KnowledgeUsageLimitExceededError } from '@/lib/knowledge/application/billing'
15+
import { KnowledgeDocumentNotReadyError } from '@/lib/knowledge/application/chunk-errors'
1416
import { KnowledgeSearchProvenanceUnavailableError } from '@/lib/knowledge/application/search'
1517
import { KnowledgeDocumentUnsupportedMediaTypeError } from '@/lib/knowledge/application/upload-sessions'
1618
import { v2Error } from '@/app/api/v2/lib/response'
@@ -82,6 +84,19 @@ export const internalKnowledgeErrorPolicies = {
8284
chunks: concealKnowledgeBase(
8385
internalKnowledgeErrorPolicy('Failed to process knowledge chunk request')
8486
),
87+
chunkList: concealKnowledgeBase(
88+
extendInternalErrorPolicy(
89+
internalKnowledgeErrorPolicy('Failed to process knowledge chunk request'),
90+
(error) =>
91+
error instanceof KnowledgeDocumentNotReadyError
92+
? internalErrorResponse(400, {
93+
error: 'Document is not ready for access',
94+
details: `Document status: ${error.processingStatus}`,
95+
retryAfter: error.processingStatus === 'processing' ? 5 : null,
96+
})
97+
: null
98+
)
99+
),
85100
upsert: concealKnowledgeBase(internalKnowledgeUploadErrorPolicy),
86101
search: concealKnowledgeBase(internalKnowledgeSearchErrorPolicy),
87102
tags: concealKnowledgeBase(
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { OrchestrationError } from '@/lib/core/orchestration/types'
2+
3+
export class KnowledgeDocumentNotReadyError extends OrchestrationError {
4+
constructor(readonly processingStatus: string) {
5+
super('validation', `Document is not ready for access (status: ${processingStatus})`)
6+
this.name = 'KnowledgeDocumentNotReadyError'
7+
}
8+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
5+
import { beforeEach, describe, expect, it, vi } from 'vitest'
6+
7+
const mocks = vi.hoisted(() => ({
8+
resolveDocument: vi.fn(),
9+
resolvePermission: vi.fn(),
10+
queryChunks: vi.fn(),
11+
}))
12+
13+
vi.mock('@sim/platform-authz/workspace', () => ({
14+
permissionSatisfies: (actual: string | null, required: string) => {
15+
const rank = { read: 1, write: 2, admin: 3 } as const
16+
return (
17+
actual !== null && rank[actual as keyof typeof rank] >= rank[required as keyof typeof rank]
18+
)
19+
},
20+
resolveEffectiveWorkspacePermission: mocks.resolvePermission,
21+
}))
22+
23+
vi.mock('@/lib/knowledge/application/contexts', () => ({
24+
resolveCanonicalActiveKnowledgeDocumentContext: mocks.resolveDocument,
25+
resolveActiveKnowledgeChunkContext: vi.fn(),
26+
}))
27+
28+
vi.mock('@/lib/knowledge/chunks/service', () => ({
29+
batchChunkOperation: vi.fn(),
30+
createChunk: vi.fn(),
31+
deleteChunk: vi.fn(),
32+
queryChunks: mocks.queryChunks,
33+
updateChunk: vi.fn(),
34+
}))
35+
36+
vi.mock('@/lib/execution/durable-secret-provenance', () => ({
37+
createDurableSecretProvenanceRegistry: vi.fn(),
38+
}))
39+
40+
vi.mock('@/lib/knowledge/model-input-provenance', () => ({
41+
runWithKnowledgeModelInputProvenance: vi.fn(),
42+
}))
43+
44+
vi.mock('@/providers/utils', () => ({ calculateCost: vi.fn() }))
45+
46+
import { KnowledgeDocumentNotReadyError } from '@/lib/knowledge/application/chunk-errors'
47+
import { listKnowledgeChunks } from '@/lib/knowledge/application/chunks'
48+
49+
describe('knowledge chunk application use cases', () => {
50+
beforeEach(() => {
51+
vi.clearAllMocks()
52+
mocks.resolvePermission.mockResolvedValue('read')
53+
mocks.resolveDocument.mockResolvedValue({
54+
workspaceId: 'workspace-1',
55+
workspaceOrganizationId: null,
56+
allowPersonalApiKeys: true,
57+
billedAccountUserId: 'billing-owner-1',
58+
knowledgeBaseId: 'knowledge-1',
59+
knowledgeBase: { id: 'knowledge-1' },
60+
documentId: 'document-1',
61+
document: { id: 'document-1', processingStatus: 'processing' },
62+
})
63+
})
64+
65+
it('returns a typed transient failure before querying chunks for a processing document', async () => {
66+
const promise = listKnowledgeChunks.execute({
67+
principal: { kind: 'session', userId: 'user-1', sessionId: 'session-1' },
68+
input: { knowledgeBaseId: 'knowledge-1', documentId: 'document-1' },
69+
})
70+
71+
await expect(promise).rejects.toBeInstanceOf(KnowledgeDocumentNotReadyError)
72+
await expect(promise).rejects.toMatchObject({
73+
code: 'validation',
74+
processingStatus: 'processing',
75+
message: 'Document is not ready for access (status: processing)',
76+
})
77+
expect(mocks.queryChunks).not.toHaveBeenCalled()
78+
})
79+
})

apps/sim/lib/knowledge/application/chunks.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from '@/lib/execution/durable-secret-provenance'
88
import { defineAuthorizedKnowledgeUseCase } from '@/lib/knowledge/application/authorized-knowledge-use-case'
99
import { resolveKnowledgeAttributedUserId } from '@/lib/knowledge/application/billing'
10+
import { KnowledgeDocumentNotReadyError } from '@/lib/knowledge/application/chunk-errors'
1011
import {
1112
type ActiveKnowledgeDocumentContext,
1213
resolveActiveKnowledgeChunkContext,
@@ -62,10 +63,7 @@ export interface BulkKnowledgeChunksInput extends KnowledgeDocumentChunkInput {
6263

6364
function requireChunkReadable(context: ActiveKnowledgeDocumentContext): void {
6465
if (context.document.processingStatus !== 'completed') {
65-
throw new OrchestrationError(
66-
'validation',
67-
`Document is not ready for access (status: ${context.document.processingStatus})`
68-
)
66+
throw new KnowledgeDocumentNotReadyError(context.document.processingStatus)
6967
}
7068
}
7169

0 commit comments

Comments
 (0)