Skip to content

Commit d3dfab0

Browse files
committed
fix(knowledge): carry the MIME type through hydration
A listing stub is built before the file is fetched and declares `text/plain` for everything, so a hydrated PDF kept claiming plain text at the top level. Nothing broke today only because storage reads `sourceFile.mimeType` — which is exactly what makes it a trap: anything later reaching for `extDoc.mimeType`, the obvious field, silently loses the OCR routing this change exists to restore. The merge is now `mergeHydratedDocument` rather than an inline spread, so what hydration must carry is a stated contract with a test behind it instead of a literal that is easy to under-specify — which is how the field was missed.
1 parent bdb9979 commit d3dfab0

2 files changed

Lines changed: 107 additions & 13 deletions

File tree

apps/sim/lib/knowledge/connectors/sync-engine.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
77
import {
88
classifySuspectListing,
99
evaluateListingSafety,
10+
mergeHydratedDocument,
1011
type PreviousListingObservation,
1112
} from '@/lib/knowledge/connectors/sync-engine'
13+
import type { ExternalDocument } from '@/connectors/types'
1214

1315
vi.mock('drizzle-orm', () => ({
1416
and: vi.fn(),
@@ -627,3 +629,76 @@ describe('evaluateListingSafety', () => {
627629
})
628630
})
629631
})
632+
633+
describe('mergeHydratedDocument', () => {
634+
const stub = (): ExternalDocument => ({
635+
externalId: 'file-1',
636+
title: 'Report.pdf',
637+
content: '',
638+
mimeType: 'text/plain',
639+
contentHash: 'sharepoint:file-1:v1',
640+
contentDeferred: true,
641+
metadata: { fileSize: 2_400_000 },
642+
})
643+
644+
/**
645+
* A stub is built during listing, before the file is fetched, so it declares
646+
* `text/plain` for everything. Leaving that behind makes a hydrated PDF keep
647+
* claiming plain text — invisible while storage reads `sourceFile.mimeType`,
648+
* and a trap for anything that reaches for the obvious field instead.
649+
*/
650+
it('carries the hydrated MIME type over the stub placeholder', () => {
651+
const merged = mergeHydratedDocument(
652+
stub(),
653+
{
654+
...stub(),
655+
content: '',
656+
mimeType: 'application/pdf',
657+
sourceFile: {
658+
bytes: Buffer.from('%PDF'),
659+
fileName: 'Report.pdf',
660+
mimeType: 'application/pdf',
661+
},
662+
},
663+
'sharepoint:file-1:v2'
664+
)
665+
666+
expect(merged.mimeType).toBe('application/pdf')
667+
expect(merged.sourceFile?.mimeType).toBe('application/pdf')
668+
})
669+
670+
it('carries the source file and clears the deferred flag', () => {
671+
const merged = mergeHydratedDocument(
672+
stub(),
673+
{ ...stub(), sourceFile: { bytes: Buffer.from('x'), fileName: 'a.pdf', mimeType: 'a/b' } },
674+
'h'
675+
)
676+
677+
expect(merged.sourceFile?.bytes.toString()).toBe('x')
678+
expect(merged.contentDeferred).toBe(false)
679+
expect(merged.contentHash).toBe('h')
680+
})
681+
682+
it('keeps text-path content and merges metadata over the stub', () => {
683+
const merged = mergeHydratedDocument(
684+
stub(),
685+
{ ...stub(), content: 'plain notes', metadata: { createdBy: 'A' } },
686+
'h'
687+
)
688+
689+
expect(merged.content).toBe('plain notes')
690+
expect(merged.sourceFile).toBeUndefined()
691+
expect(merged.metadata).toEqual({ fileSize: 2_400_000, createdBy: 'A' })
692+
})
693+
694+
it('falls back to the stub title and sourceUrl when hydration omits them', () => {
695+
const merged = mergeHydratedDocument(
696+
{ ...stub(), sourceUrl: 'https://example.com/a' },
697+
{ ...stub(), title: '', content: 'x' },
698+
'h'
699+
)
700+
701+
expect(merged.title).toBe('Report.pdf')
702+
expect(merged.sourceUrl).toBe('https://example.com/a')
703+
})
704+
})

apps/sim/lib/knowledge/connectors/sync-engine.ts

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,37 @@ export function classifyExternalDoc(
172172
return { type: 'unchanged' }
173173
}
174174

175+
/**
176+
* Merges a hydrated document over the listing stub it was fetched for.
177+
*
178+
* Every field the connector restates on hydration has to be carried, not just the
179+
* content. A stub is built before the file is fetched and declares `text/plain`,
180+
* so any field left behind keeps a value that is wrong for the bytes now attached
181+
* — which is how a hydrated PDF ends up still claiming plain text. Storage reads
182+
* `sourceFile.mimeType`, so that particular staleness is invisible until
183+
* something reaches for the obvious field instead.
184+
*
185+
* Extracted from the hydration loop so the merge is a stated contract with a test
186+
* rather than an inline spread that is easy to under-specify.
187+
*/
188+
export function mergeHydratedDocument(
189+
stub: ExternalDocument,
190+
hydrated: ExternalDocument,
191+
contentHash: string
192+
): ExternalDocument {
193+
return {
194+
...stub,
195+
title: hydrated.title || stub.title,
196+
content: hydrated.content,
197+
sourceFile: hydrated.sourceFile,
198+
mimeType: hydrated.mimeType,
199+
contentHash,
200+
contentDeferred: false,
201+
sourceUrl: hydrated.sourceUrl ?? stub.sourceUrl,
202+
metadata: { ...stub.metadata, ...hydrated.metadata },
203+
}
204+
}
205+
175206
/** Whether a document carries anything to index — extracted text or the source file. */
176207
function hasPayload(extDoc: Pick<ExternalDocument, 'content' | 'sourceFile'>): boolean {
177208
return extDoc.sourceFile !== undefined || extDoc.content.trim().length > 0
@@ -1087,19 +1118,7 @@ export async function executeSync(
10871118
result.docsUnchanged++
10881119
return null
10891120
}
1090-
return {
1091-
...op,
1092-
extDoc: {
1093-
...op.extDoc,
1094-
title: fullDoc.title || op.extDoc.title,
1095-
content: fullDoc.content,
1096-
sourceFile: fullDoc.sourceFile,
1097-
contentHash: hydratedHash,
1098-
contentDeferred: false,
1099-
sourceUrl: fullDoc.sourceUrl ?? op.extDoc.sourceUrl,
1100-
metadata: { ...op.extDoc.metadata, ...fullDoc.metadata },
1101-
},
1102-
}
1121+
return { ...op, extDoc: mergeHydratedDocument(op.extDoc, fullDoc, hydratedHash) }
11031122
})
11041123
)
11051124

0 commit comments

Comments
 (0)