Skip to content

Commit 043a152

Browse files
committed
fix copy resources
1 parent 5bedaa1 commit 043a152

2 files changed

Lines changed: 177 additions & 77 deletions

File tree

apps/sim/ee/workspace-forking/lib/copy/copy-resources.test.ts

Lines changed: 153 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import {
88
storageServiceMockFns,
99
} from '@sim/testing'
1010
import { beforeEach, describe, expect, it, vi } from 'vitest'
11+
import { hashDurableSecretProvenanceValue } from '@/lib/execution/durable-secret-provenance'
12+
import {
13+
bindKnowledgeDocumentFieldSecretProvenance,
14+
createKnowledgeDocumentSourceValue,
15+
} from '@/lib/knowledge/secret-provenance'
1116

1217
const {
1318
mockIncrementStorageUsageInTx,
@@ -59,6 +64,34 @@ const sourceDoc = {
5964
mimeType: 'application/pdf',
6065
}
6166

67+
function queueMappedDocumentCopy(
68+
source: Record<string, unknown> = sourceDoc,
69+
provenanceRow: Record<string, unknown> = source
70+
): void {
71+
dbChainMockFns.limit
72+
.mockResolvedValueOnce([])
73+
.mockResolvedValueOnce([source])
74+
.mockResolvedValueOnce([provenanceRow])
75+
.mockResolvedValueOnce([])
76+
}
77+
78+
function mappedDocumentPlan(): ForkContentPlan {
79+
return basePlan({
80+
documents: [
81+
{
82+
sourceDocId: 'doc-1',
83+
childDocId: 'child-doc-1',
84+
childKnowledgeBaseId: 'existing-target-kb',
85+
storageKey: 'kb/source-key',
86+
fileUrl: '/api/files/serve/kb%2Fsource-key',
87+
fileSize: 321,
88+
filename: 'report.pdf',
89+
mimeType: 'application/pdf',
90+
},
91+
],
92+
})
93+
}
94+
6295
describe('copyForkResourceContent', () => {
6396
beforeEach(() => {
6497
vi.clearAllMocks()
@@ -421,21 +454,10 @@ describe('copyForkResourceContent', () => {
421454
})
422455

423456
it('U-docs: fills a document copied into an existing target KB (blob re-key + placeholder update)', async () => {
457+
queueMappedDocumentCopy()
458+
424459
const result = await copyForkResourceContent({
425-
contentPlan: basePlan({
426-
documents: [
427-
{
428-
sourceDocId: 'doc-1',
429-
childDocId: 'child-doc-1',
430-
childKnowledgeBaseId: 'existing-target-kb',
431-
storageKey: 'kb/source-key',
432-
fileUrl: '/api/files/serve/kb%2Fsource-key',
433-
fileSize: 321,
434-
filename: 'report.pdf',
435-
mimeType: 'application/pdf',
436-
},
437-
],
438-
}),
460+
contentPlan: mappedDocumentPlan(),
439461
requestId: 'test',
440462
})
441463

@@ -444,29 +466,130 @@ describe('copyForkResourceContent', () => {
444466
// The blob is re-keyed and the pre-created placeholder row's blob fields are updated.
445467
expect(storageServiceMockFns.mockUploadFile).toHaveBeenCalledTimes(1)
446468
expect(dbChainMockFns.update).toHaveBeenCalledTimes(1)
469+
expect(dbChainMockFns.set).toHaveBeenCalledWith(
470+
expect.objectContaining({ secretProvenanceVersion: null })
471+
)
472+
expect(dbChainMockFns.values).not.toHaveBeenCalledWith(
473+
expect.objectContaining({ documentId: 'child-doc-1' })
474+
)
475+
})
476+
477+
it('U-docs: rebinds tracked document provenance through the shared document copier', async () => {
478+
const source = {
479+
...sourceDoc,
480+
...createKnowledgeDocumentSourceValue(sourceDoc),
481+
secretProvenanceVersion: 1,
482+
}
483+
const sourceValue = createKnowledgeDocumentSourceValue(source)
484+
const provenance = bindKnowledgeDocumentFieldSecretProvenance(
485+
{
486+
status: 'exact',
487+
entries: [{ name: 'DOCUMENT_NAME', encryptedValue: 'encrypted-name' }],
488+
},
489+
'filename',
490+
source.filename
491+
)
492+
queueMappedDocumentCopy(source, {
493+
...source,
494+
provenanceSourceHash: hashDurableSecretProvenanceValue(sourceValue),
495+
status: 'exact',
496+
entries: provenance.status === 'exact' ? provenance.entries : [],
497+
})
498+
499+
const result = await copyForkResourceContent({
500+
contentPlan: mappedDocumentPlan(),
501+
requestId: 'test',
502+
})
503+
504+
expect(result).toEqual({ copied: 1, failed: 0, failures: [] })
505+
expect(dbChainMockFns.set).toHaveBeenCalledWith(
506+
expect.objectContaining({ secretProvenanceVersion: 1 })
507+
)
508+
expect(dbChainMockFns.values).toHaveBeenCalledWith(
509+
expect.objectContaining({
510+
documentId: 'child-doc-1',
511+
status: 'exact',
512+
entries: [
513+
expect.objectContaining({
514+
name: 'DOCUMENT_NAME',
515+
encryptedValue: 'encrypted-name',
516+
sourceValueHash: expect.any(String),
517+
}),
518+
],
519+
})
520+
)
521+
})
522+
523+
it('U-docs: keeps exact-empty provenance tracked instead of turning it into legacy state', async () => {
524+
const source = {
525+
...sourceDoc,
526+
...createKnowledgeDocumentSourceValue(sourceDoc),
527+
secretProvenanceVersion: 1,
528+
}
529+
const sourceValue = createKnowledgeDocumentSourceValue(source)
530+
queueMappedDocumentCopy(source, {
531+
...source,
532+
provenanceSourceHash: hashDurableSecretProvenanceValue(sourceValue),
533+
status: 'exact',
534+
entries: [],
535+
})
536+
537+
const result = await copyForkResourceContent({
538+
contentPlan: mappedDocumentPlan(),
539+
requestId: 'test',
540+
})
541+
542+
expect(result).toEqual({ copied: 1, failed: 0, failures: [] })
543+
expect(dbChainMockFns.values).toHaveBeenCalledWith(
544+
expect.objectContaining({
545+
documentId: 'child-doc-1',
546+
status: 'exact',
547+
entries: [],
548+
})
549+
)
550+
})
551+
552+
it('U-docs: preserves tracked unknown provenance instead of laundering it as legacy', async () => {
553+
const source = {
554+
...sourceDoc,
555+
...createKnowledgeDocumentSourceValue(sourceDoc),
556+
secretProvenanceVersion: 1,
557+
}
558+
queueMappedDocumentCopy(source, {
559+
...source,
560+
provenanceSourceHash: null,
561+
status: null,
562+
entries: null,
563+
})
564+
565+
const result = await copyForkResourceContent({
566+
contentPlan: mappedDocumentPlan(),
567+
requestId: 'test',
568+
})
569+
570+
expect(result).toEqual({ copied: 1, failed: 0, failures: [] })
571+
expect(dbChainMockFns.set).toHaveBeenCalledWith(
572+
expect.objectContaining({ secretProvenanceVersion: 1 })
573+
)
574+
expect(dbChainMockFns.values).toHaveBeenCalledWith(
575+
expect.objectContaining({
576+
documentId: 'child-doc-1',
577+
status: 'unknown',
578+
entries: [],
579+
})
580+
)
447581
})
448582

449583
it('U-docs: a failed document fill is reported as a knowledge-document failure (for cleanup)', async () => {
584+
queueMappedDocumentCopy()
585+
450586
// The placeholder blob update throws; the doc fails on its own without touching its KB.
451587
dbChainMockFns.set.mockImplementationOnce(() => {
452588
throw new Error('update failed')
453589
})
454590

455591
const result = await copyForkResourceContent({
456-
contentPlan: basePlan({
457-
documents: [
458-
{
459-
sourceDocId: 'doc-1',
460-
childDocId: 'child-doc-1',
461-
childKnowledgeBaseId: 'existing-target-kb',
462-
storageKey: 'kb/source-key',
463-
fileUrl: '/api/files/serve/kb%2Fsource-key',
464-
fileSize: 321,
465-
filename: 'report.pdf',
466-
mimeType: 'application/pdf',
467-
},
468-
],
469-
}),
592+
contentPlan: mappedDocumentPlan(),
470593
requestId: 'test',
471594
})
472595

@@ -476,23 +599,11 @@ describe('copyForkResourceContent', () => {
476599
})
477600

478601
it('U-docs: refuses to charge when the target knowledge base moved workspaces', async () => {
602+
queueMappedDocumentCopy()
479603
dbChainMockFns.for.mockResolvedValueOnce([{ workspaceId: 'other-workspace' }])
480604

481605
const result = await copyForkResourceContent({
482-
contentPlan: basePlan({
483-
documents: [
484-
{
485-
sourceDocId: 'doc-1',
486-
childDocId: 'child-doc-1',
487-
childKnowledgeBaseId: 'existing-target-kb',
488-
storageKey: 'kb/source-key',
489-
fileUrl: '/api/files/serve/kb%2Fsource-key',
490-
fileSize: 321,
491-
filename: 'report.pdf',
492-
mimeType: 'application/pdf',
493-
},
494-
],
495-
}),
606+
contentPlan: mappedDocumentPlan(),
496607
requestId: 'test',
497608
})
498609

apps/sim/ee/workspace-forking/lib/copy/copy-resources.ts

Lines changed: 24 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,10 @@ export interface ForkContentDocumentEntry {
198198
sourceDocId: string
199199
childDocId: string
200200
childKnowledgeBaseId: string
201-
/** Source blob fields captured at placeholder time, for the post-commit blob re-key. */
201+
/**
202+
* Source blob fields retained in the serialized payload for rolling-deploy and queued-job
203+
* compatibility. Current workers re-read the live source row before copying it.
204+
*/
202205
storageKey: string | null
203206
fileUrl: string
204207
fileSize: number
@@ -1077,43 +1080,29 @@ export async function copyForkResourceContent(params: {
10771080
copiedResources += 1
10781081
continue
10791082
}
1083+
const [source] = await db
1084+
.select()
1085+
.from(document)
1086+
.where(
1087+
and(
1088+
eq(document.id, docEntry.sourceDocId),
1089+
isNull(document.deletedAt),
1090+
isNull(document.archivedAt)
1091+
)
1092+
)
1093+
.limit(1)
1094+
if (!source) {
1095+
throw new Error(`Source document ${docEntry.sourceDocId} is missing`)
1096+
}
10801097
const resolvedBillingContext = await getBillingContext()
1081-
const blob = await copyKbDocumentBlob(
1082-
{
1083-
storageKey: docEntry.storageKey,
1084-
filename: docEntry.filename,
1085-
mimeType: docEntry.mimeType,
1086-
},
1098+
await copyKbDocument({
1099+
source,
1100+
childDocumentId: docEntry.childDocId,
1101+
childKnowledgeBaseId: docEntry.childKnowledgeBaseId,
10871102
childWorkspaceId,
10881103
userId,
1089-
docEntry.childDocId
1090-
)
1091-
try {
1092-
await copyDocumentEmbeddings(
1093-
docEntry.sourceDocId,
1094-
docEntry.childDocId,
1095-
docEntry.childKnowledgeBaseId
1096-
)
1097-
await finalizeKbDocument({
1098-
childDocumentId: docEntry.childDocId,
1099-
childKnowledgeBaseId: docEntry.childKnowledgeBaseId,
1100-
billingContext: resolvedBillingContext,
1101-
bytes: blob ? docEntry.fileSize : 0,
1102-
values: {
1103-
knowledgeBaseId: docEntry.childKnowledgeBaseId,
1104-
connectorId: null,
1105-
storageKey: blob?.storageKey ?? null,
1106-
fileUrl: blob?.fileUrl ?? docEntry.fileUrl,
1107-
fileSize: docEntry.fileSize,
1108-
archivedAt: null,
1109-
deletedAt: null,
1110-
uploadedBy: userId,
1111-
},
1112-
})
1113-
} catch (error) {
1114-
if (blob) await cleanupCopiedKbBlob(blob.storageKey)
1115-
throw error
1116-
}
1104+
billingContext: resolvedBillingContext,
1105+
})
11171106
copiedResources += 1
11181107
} catch (error) {
11191108
failedResources += 1

0 commit comments

Comments
 (0)