Skip to content

Commit 847a499

Browse files
committed
improvement(knowledge): extract shouldReconcileDeletions gate as tested pure function, tighten engine comments
1 parent 1f1f1af commit 847a499

2 files changed

Lines changed: 67 additions & 20 deletions

File tree

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,46 @@ vi.mock('@/connectors/registry', () => ({
3737
},
3838
}))
3939

40+
describe('shouldReconcileDeletions', () => {
41+
it('runs on a clean full listing', async () => {
42+
const { shouldReconcileDeletions } = await import('@/lib/knowledge/connectors/sync-engine')
43+
44+
expect(shouldReconcileDeletions(false, {}, undefined)).toBe(true)
45+
expect(shouldReconcileDeletions(false, undefined, undefined)).toBe(true)
46+
})
47+
48+
it('never runs on incremental syncs', async () => {
49+
const { shouldReconcileDeletions } = await import('@/lib/knowledge/connectors/sync-engine')
50+
51+
expect(shouldReconcileDeletions(true, {}, undefined)).toBe(false)
52+
expect(shouldReconcileDeletions(true, {}, true)).toBe(false)
53+
expect(shouldReconcileDeletions(true, { listingCapped: true }, true)).toBe(false)
54+
})
55+
56+
it('skips when a connector capped the listing', async () => {
57+
const { shouldReconcileDeletions } = await import('@/lib/knowledge/connectors/sync-engine')
58+
59+
expect(shouldReconcileDeletions(false, { listingCapped: true }, undefined)).toBe(false)
60+
expect(shouldReconcileDeletions(false, { listingCapped: true }, false)).toBe(false)
61+
})
62+
63+
it('lets a forced fullSync override a connector cap', async () => {
64+
const { shouldReconcileDeletions } = await import('@/lib/knowledge/connectors/sync-engine')
65+
66+
expect(shouldReconcileDeletions(false, { listingCapped: true }, true)).toBe(true)
67+
})
68+
69+
it('never runs when the engine truncated pagination, even on a forced fullSync', async () => {
70+
const { shouldReconcileDeletions } = await import('@/lib/knowledge/connectors/sync-engine')
71+
72+
expect(shouldReconcileDeletions(false, { listingTruncated: true }, undefined)).toBe(false)
73+
expect(shouldReconcileDeletions(false, { listingTruncated: true }, true)).toBe(false)
74+
expect(
75+
shouldReconcileDeletions(false, { listingCapped: true, listingTruncated: true }, true)
76+
).toBe(false)
77+
})
78+
})
79+
4080
describe('resolveTagMapping', () => {
4181
beforeEach(() => {
4282
vi.clearAllMocks()

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

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,27 @@ async function completeSyncLog(
128128
.where(eq(knowledgeConnectorSyncLog.id, syncLogId))
129129
}
130130

131+
/**
132+
* Decides whether deletion reconciliation may run for a sync.
133+
*
134+
* Reconciliation hard-deletes every stored document absent from the listing,
135+
* so it must only run against a complete source set:
136+
* - never on incremental syncs (they list only changed documents)
137+
* - never when the engine truncated pagination (`listingTruncated`) — a forced
138+
* fullSync cannot fix truncation, so it cannot override it
139+
* - not when a connector capped its listing (`listingCapped`), unless a forced
140+
* fullSync deliberately overrides the cap to reconcile the capped scope
141+
*/
142+
export function shouldReconcileDeletions(
143+
isIncremental: boolean | undefined,
144+
syncContext: Record<string, unknown> | undefined,
145+
fullSync: boolean | undefined
146+
): boolean {
147+
if (isIncremental) return false
148+
if (syncContext?.listingTruncated) return false
149+
return !syncContext?.listingCapped || Boolean(fullSync)
150+
}
151+
131152
/**
132153
* Resolves tag values from connector metadata using the connector's mapTags function.
133154
* Translates semantic keys returned by mapTags to actual DB slots using the
@@ -417,17 +438,11 @@ export async function executeSync(
417438

418439
if (hasMore) {
419440
/**
420-
* Pagination stopped before the source was exhausted — either the
421-
* MAX_PAGES guard tripped or the connector reported hasMore without a
422-
* cursor. The listing is incomplete, so flag it to suppress deletion
423-
* reconciliation; otherwise documents beyond the truncation point would
424-
* be removed even though they still exist in the source.
425-
*
426-
* `listingTruncated` is distinct from connector-set `listingCapped`:
427-
* a forced fullSync may legitimately override a connector's soft cap
428-
* (the user opted to reconcile the capped scope), but engine-level
429-
* truncation can never be resolved by forcing a fullSync — the next
430-
* fullSync truncates identically — so it is an absolute deletion block.
441+
* Pagination stopped before source exhaustion (MAX_PAGES or a missing
442+
* cursor), so the listing is incomplete. `listingTruncated` blocks
443+
* deletion reconciliation absolutely — unlike connector-set
444+
* `listingCapped`, it cannot be overridden by a forced fullSync, since
445+
* re-running one truncates identically.
431446
*/
432447
syncContext.listingCapped = true
433448
syncContext.listingTruncated = true
@@ -657,15 +672,7 @@ export async function executeSync(
657672
}
658673
}
659674

660-
// Reconcile deletions for non-incremental syncs that returned ALL docs.
661-
// Skip when listing was capped (maxFiles/maxThreads) — unseen docs may still exist in the source.
662-
// A forced fullSync overrides connector-set caps, but never engine-level truncation
663-
// (listingTruncated): a truncated listing is incomplete no matter how the sync was triggered.
664-
if (
665-
!isIncremental &&
666-
!syncContext?.listingTruncated &&
667-
(!syncContext?.listingCapped || options?.fullSync)
668-
) {
675+
if (shouldReconcileDeletions(isIncremental, syncContext, options?.fullSync)) {
669676
const removedIds = existingDocs
670677
.filter((d) => d.externalId && !seenExternalIds.has(d.externalId))
671678
.map((d) => d.id)

0 commit comments

Comments
 (0)