Skip to content

Commit 622a21b

Browse files
committed
fix(connectors): only flag a Bitbucket listing capped when the cap withheld something
Review round 2. takeIndexableWithinCap reports capReached as soon as the running total equals maxItems, which is also true of a listing that ended at exactly that count. Setting listingCapped there suppressed deletion reconciliation for a complete listing, so upstream-deleted files and pull requests could stay in the knowledge base indefinitely. applyMaxItemsCap now takes whether Bitbucket had more content beyond the page -- a next link, or directories still queued on the frontier -- and flags the listing only when the cap actually withheld something, matching the Databricks, Google Chat, and Workday connectors.
1 parent 5826dd9 commit 622a21b

2 files changed

Lines changed: 54 additions & 4 deletions

File tree

apps/sim/connectors/bitbucket/bitbucket.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,44 @@ describe('bitbucket maxItems cap', () => {
357357
expect(syncContext.listingCapped).toBe(true)
358358
})
359359

360+
it('leaves the listing reconcilable when a complete listing ends exactly on the cap', async () => {
361+
mockApi([[/\/src\//, () => jsonResponse({ values: [fileEntry('a.md'), fileEntry('b.md')] })]])
362+
363+
const syncContext: Record<string, unknown> = {}
364+
const result = await bitbucketConnector.listDocuments(
365+
ACCESS_TOKEN,
366+
{ ...CONFIG, maxItems: '2' },
367+
undefined,
368+
syncContext
369+
)
370+
371+
expect(result.documents).toHaveLength(2)
372+
expect(syncContext.listingCapped).toBeUndefined()
373+
})
374+
375+
it('flags the listing capped when the cap lands on a page boundary with more to come', async () => {
376+
mockApi([
377+
[
378+
/\/src\//,
379+
() =>
380+
jsonResponse({
381+
values: [fileEntry('a.md'), fileEntry('b.md')],
382+
next: 'https://api.bitbucket.org/2.0/repositories/acme/widgets/src/abc/?page=2',
383+
}),
384+
],
385+
])
386+
387+
const syncContext: Record<string, unknown> = {}
388+
await bitbucketConnector.listDocuments(
389+
ACCESS_TOKEN,
390+
{ ...CONFIG, maxItems: '2' },
391+
undefined,
392+
syncContext
393+
)
394+
395+
expect(syncContext.listingCapped).toBe(true)
396+
})
397+
360398
it('leaves the listing reconcilable when no cap is configured', async () => {
361399
mockApi([[/\/src\//, () => jsonResponse({ values: [fileEntry('a.md'), fileEntry('b.md')] })]])
362400

apps/sim/connectors/bitbucket/bitbucket.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -714,11 +714,20 @@ function pendingDirectories(syncContext: Record<string, unknown> | undefined): s
714714
* Applies the optional maxItems cap to a page, tracking the running total in
715715
* syncContext and flagging `listingCapped` when the cap truncates the listing.
716716
* Skipped (oversized) documents ride along without consuming the cap.
717+
*
718+
* `sourceHasMore` is whether Bitbucket still had content beyond this page — a
719+
* `next` link, or directories still queued on the frontier. It is required because
720+
* `takeIndexableWithinCap` reports `capReached` as soon as the running total
721+
* *equals* maxItems, which is also true of a listing that ended at exactly that
722+
* count. Setting `listingCapped` there would suppress deletion reconciliation for
723+
* a listing that was in fact complete, leaving upstream-deleted files indexed
724+
* indefinitely, so the flag is set only when the cap actually withheld something.
717725
*/
718726
function applyMaxItemsCap(
719727
documents: ExternalDocument[],
720728
maxItems: number,
721-
syncContext: Record<string, unknown> | undefined
729+
syncContext: Record<string, unknown> | undefined,
730+
sourceHasMore: boolean
722731
): { documents: ExternalDocument[]; capped: boolean } {
723732
if (maxItems <= 0) return { documents, capped: false }
724733
const alreadyIndexed = (syncContext?.totalDocsFetched as number) ?? 0
@@ -734,7 +743,8 @@ function applyMaxItemsCap(
734743
)
735744
if (syncContext) {
736745
syncContext.totalDocsFetched = alreadyIndexed + indexableCount
737-
if (capReached) syncContext.listingCapped = true
746+
const withheld = taken.length < documents.length || sourceHasMore
747+
if (capReached && withheld) syncContext.listingCapped = true
738748
}
739749
return { documents: taken, capped: capReached }
740750
}
@@ -922,7 +932,8 @@ export const bitbucketConnector: ConnectorConfig = {
922932
const { documents: capped, capped: hitLimit } = applyMaxItemsCap(
923933
documents,
924934
maxItems,
925-
syncContext
935+
syncContext,
936+
Boolean(page.next) || frontier.length > 0
926937
)
927938
if (hitLimit) return { documents: capped, hasMore: false }
928939

@@ -990,7 +1001,8 @@ export const bitbucketConnector: ConnectorConfig = {
9901001
const { documents: capped, capped: hitLimit } = applyMaxItemsCap(
9911002
documents,
9921003
maxItems,
993-
syncContext
1004+
syncContext,
1005+
Boolean(page.next)
9941006
)
9951007
if (hitLimit) return { documents: capped, hasMore: false }
9961008

0 commit comments

Comments
 (0)