Skip to content

Commit 8a5cea9

Browse files
fix(knowledge): preserve upload retry and VFS errors
1 parent caaf056 commit 8a5cea9

4 files changed

Lines changed: 77 additions & 31 deletions

File tree

apps/sim/lib/copilot/tools/handlers/vfs-mutate.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ vi.mock('@/lib/core/telemetry', () => ({
172172
}))
173173

174174
import type { ExecutionContext } from '@/lib/copilot/request/types'
175+
import { OrchestrationError } from '@/lib/core/orchestration/types'
175176
import { executeVfsCp, executeVfsMkdir, executeVfsMv, executeVfsRm } from './vfs-mutate'
176177

177178
const context = {
@@ -649,6 +650,25 @@ describe('vfs mv/cp', () => {
649650
).rejects.toThrow('knowledge database unavailable')
650651
})
651652

653+
it('preserves an actionable knowledge rename conflict', async () => {
654+
mocks.listKnowledgeBases.mockResolvedValue({
655+
knowledgeBases: [{ knowledgeBase: { id: 'kb-1', name: 'Docs' }, folderPath: '/' }],
656+
})
657+
mocks.updateKnowledgeBase.mockRejectedValue(
658+
new OrchestrationError('conflict', 'A knowledge base named Product Docs already exists')
659+
)
660+
661+
const result = await executeVfsMv(
662+
{ sources: ['knowledgebases/Docs'], destination: 'knowledgebases/Product Docs' },
663+
context
664+
)
665+
666+
expect(result).toMatchObject({
667+
success: false,
668+
error: 'A knowledge base named Product Docs already exists',
669+
})
670+
})
671+
652672
it('rejects the reserved knowledgebases/connectors name', async () => {
653673
const result = await executeVfsMv(
654674
{ sources: ['knowledgebases/Docs'], destination: 'knowledgebases/connectors' },
@@ -682,5 +702,29 @@ describe('vfs mv/cp', () => {
682702
)
683703
expect(mocks.knowledgeBaseDeleted).toHaveBeenCalledWith({ knowledgeBaseId: 'kb-1' })
684704
})
705+
706+
it('preserves an actionable knowledge delete failure', async () => {
707+
mocks.listKnowledgeBases.mockResolvedValue({
708+
knowledgeBases: [{ knowledgeBase: { id: 'kb-1', name: 'Docs' }, folderPath: '/' }],
709+
})
710+
mocks.deleteKnowledgeBase.mockRejectedValue(
711+
new OrchestrationError('not_found', 'Knowledge base no longer exists')
712+
)
713+
714+
const result = await executeVfsRm({ paths: ['knowledgebases/Docs'] }, context)
715+
716+
expect(result).toMatchObject({
717+
success: false,
718+
error: 'Knowledge base no longer exists',
719+
output: {
720+
results: [
721+
expect.objectContaining({
722+
from: 'knowledgebases/Docs',
723+
error: 'Knowledge base no longer exists',
724+
}),
725+
],
726+
},
727+
})
728+
})
685729
})
686730
})

apps/sim/lib/copilot/tools/handlers/vfs-mutate.ts

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from '@/lib/copilot/application/execute-file-use-case'
1010
import {
1111
executeCopilotKnowledgeUseCase,
12+
messageForCopilotKnowledgeError,
1213
resolveCopilotKnowledgePrincipal,
1314
} from '@/lib/copilot/application/execute-knowledge-use-case'
1415
import { messageForCopilotFileError } from '@/lib/copilot/auth/file-delegation'
@@ -89,6 +90,14 @@ class KnowledgeVfsInfrastructureError extends Error {
8990
}
9091
}
9192

93+
function messageForKnowledgeVfsError(error: unknown, forbiddenMessage: string): string {
94+
const classified = asOrchestrationError(error)
95+
if (!classified || classified.code === 'internal') {
96+
throw new KnowledgeVfsInfrastructureError(error)
97+
}
98+
return classified.code === 'forbidden' ? forbiddenMessage : messageForCopilotKnowledgeError(error)
99+
}
100+
92101
/** Top-level VFS segment of a raw (possibly encoded) path. */
93102
function topLevelSegment(path: string): string {
94103
return path.trim().replace(/^\/+/, '').split('/')[0] ?? ''
@@ -861,11 +870,10 @@ async function renameFlatResource(
861870
})
862871
knowledgeBases = result.knowledgeBases
863872
} catch (error) {
864-
const classified = asOrchestrationError(error)
865-
if (classified && classified.code !== 'internal') {
866-
return { success: false, error: 'Write access required to rename knowledge bases' }
873+
return {
874+
success: false,
875+
error: messageForKnowledgeVfsError(error, 'Write access required to rename knowledge bases'),
867876
}
868-
throw new KnowledgeVfsInfrastructureError(error)
869877
}
870878
const match = knowledgeBases
871879
.map(({ knowledgeBase }) => knowledgeBase)
@@ -882,14 +890,13 @@ async function renameFlatResource(
882890
source: 'agent',
883891
})
884892
} catch (error) {
885-
const classified = asOrchestrationError(error)
886-
if (classified && classified.code !== 'internal') {
887-
return {
888-
success: false,
889-
error: `Write access required to rename knowledge base "${match.name}"`,
890-
}
893+
return {
894+
success: false,
895+
error: messageForKnowledgeVfsError(
896+
error,
897+
`Write access required to rename knowledge base "${match.name}"`
898+
),
891899
}
892-
throw new KnowledgeVfsInfrastructureError(error)
893900
}
894901
logger.info('Renamed knowledge base via mv', { knowledgeBaseId: match.id, workspaceId })
895902
return buildResult(verb, [
@@ -1193,15 +1200,11 @@ async function removeKnowledgeBasePath(
11931200
})
11941201
knowledgeBases = result.knowledgeBases
11951202
} catch (error) {
1196-
const classified = asOrchestrationError(error)
1197-
if (classified && classified.code !== 'internal') {
1198-
return {
1199-
from: path,
1200-
kind: 'knowledge_base',
1201-
error: 'Write access required to delete knowledge bases',
1202-
}
1203+
return {
1204+
from: path,
1205+
kind: 'knowledge_base',
1206+
error: messageForKnowledgeVfsError(error, 'Write access required to delete knowledge bases'),
12031207
}
1204-
throw new KnowledgeVfsInfrastructureError(error)
12051208
}
12061209
const match = knowledgeBases
12071210
.map(({ knowledgeBase }) => knowledgeBase)
@@ -1216,16 +1219,15 @@ async function removeKnowledgeBasePath(
12161219
source: 'agent',
12171220
})
12181221
} catch (error) {
1219-
const classified = asOrchestrationError(error)
1220-
if (classified && classified.code !== 'internal') {
1221-
return {
1222-
from: path,
1223-
kind: 'knowledge_base',
1224-
id: match.id,
1225-
error: `Write access required to delete knowledge base "${match.name}"`,
1226-
}
1222+
return {
1223+
from: path,
1224+
kind: 'knowledge_base',
1225+
id: match.id,
1226+
error: messageForKnowledgeVfsError(
1227+
error,
1228+
`Write access required to delete knowledge base "${match.name}"`
1229+
),
12271230
}
1228-
throw new KnowledgeVfsInfrastructureError(error)
12291231
}
12301232
PlatformEvents.knowledgeBaseDeleted({ knowledgeBaseId: match.id })
12311233
logger.info('Deleted knowledge base via rm', { knowledgeBaseId: match.id, workspaceId })

apps/sim/lib/knowledge/application/upload-sessions.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ describe('knowledge-document upload application lifecycle', () => {
370370
}>
371371
}) => ({
372372
session: { ...params.session, status: 'completed' as const },
373-
value: (await params.finalize(params.session)).value,
373+
value: (await params.finalize({ ...params.session, error: null })).value,
374374
alreadyCompleted: true,
375375
})
376376
)
@@ -445,7 +445,7 @@ describe('knowledge-document upload application lifecycle', () => {
445445
}>
446446
}) => ({
447447
session: { ...params.session, status: 'completed' as const },
448-
value: (await params.finalize(params.session)).value,
448+
value: (await params.finalize({ ...params.session, error: null })).value,
449449
alreadyCompleted: true,
450450
})
451451
)

apps/sim/lib/knowledge/application/upload-sessions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ export const completeKnowledgeDocumentUpload = defineAuthorizedKnowledgeUseCase(
260260
}
261261
if (bound.status === 'bound') {
262262
if (
263-
claimed.error === PROCESSING_DISPATCH_FAILURE_MESSAGE &&
263+
session.error === PROCESSING_DISPATCH_FAILURE_MESSAGE &&
264264
bound.document.processingStatus === 'pending'
265265
) {
266266
const billingAttribution = await resolveKnowledgeBillingAttribution(

0 commit comments

Comments
 (0)