@@ -94,7 +94,10 @@ import {
9494} from '@/lib/knowledge/connectors/member-sync-engine'
9595import { runConnectorContentPass } from '@/lib/knowledge/connectors/sync-content-pass'
9696import { executeSync } from '@/lib/knowledge/connectors/sync-engine'
97- import { SOURCE_CONTENT_ERROR } from '@/lib/knowledge/connectors/sync-limits'
97+ import {
98+ SOURCE_CONTENT_ERROR ,
99+ SOURCE_PERMISSION_ERROR ,
100+ } from '@/lib/knowledge/connectors/sync-limits'
98101import { createContentSyncLease , createMemberSyncLease } from '@/lib/knowledge/connectors/sync-lock'
99102import { addDocument , persistDocumentAcls } from '@/lib/knowledge/connectors/sync-persistence'
100103import * as documentService from '@/lib/knowledge/documents/service'
@@ -240,11 +243,11 @@ describe('durable source and member cycles in PostgreSQL', () => {
240243 forceRehydrate : false ,
241244 deadlineAt : Date . now ( ) + 60_000 ,
242245 onPage : async ( verified ) => {
243- await persistDocumentAcls (
246+ const write = await persistDocumentAcls (
244247 connectorId ,
245248 new Map ( verified . map ( ( item ) => [ item . externalId , item . acl ! ] ) )
246249 )
247- return { permissionsIncomplete : false }
250+ return { permissionsIncomplete : write . rejected > 0 }
248251 } ,
249252 } )
250253 expect ( getDocument . mock . calls . map ( ( [ id ] ) => id ) . sort ( ) ) . toEqual ( [
@@ -279,6 +282,101 @@ describe('durable source and member cycles in PostgreSQL', () => {
279282 } )
280283 } )
281284
285+ it . each ( [
286+ { description : 'a matching-hash body placeholder' , storageKey : null , rejectAcl : false } ,
287+ { description : 'an unusable source ACL' , storageKey : 'kb/verified-body.txt' , rejectAcl : true } ,
288+ ] ) ( 'clears stale permission evidence for $description' , async ( { storageKey, rejectAcl } ) => {
289+ const connectorId = generateId ( )
290+ const runId = generateId ( )
291+ const externalId = 'permission-evidence'
292+ const oldVerifiedAt = new Date ( Date . now ( ) - 25 * 60 * 60 * 1000 )
293+ const ownerAcl = [ `u:${ ids . aliceId } @fixture.test` ]
294+ await db . insert ( knowledgeConnector ) . values ( {
295+ id : connectorId ,
296+ knowledgeBaseId : ids . knowledgeBaseId ,
297+ connectorType : 'google_drive' ,
298+ status : 'syncing' ,
299+ syncLockToken : runId ,
300+ accessMode : 'admin' ,
301+ sourceConfig : { } ,
302+ } )
303+ await db . insert ( document ) . values ( {
304+ id : generateId ( ) ,
305+ knowledgeBaseId : ids . knowledgeBaseId ,
306+ connectorId,
307+ externalId,
308+ filename : externalId ,
309+ fileUrl : '' ,
310+ storageKey,
311+ fileSize : storageKey === null ? 0 : 10 ,
312+ mimeType : 'text/plain' ,
313+ contentHash : `hash-${ externalId } ` ,
314+ processingStatus : storageKey === null ? 'failed' : 'completed' ,
315+ processingError : storageKey === null ? 'Unsupported source file type' : null ,
316+ acl : ownerAcl ,
317+ aclRequirements : [ ownerAcl , [ 'd:fixture.test' ] ] ,
318+ aclVerifiedAt : oldVerifiedAt ,
319+ sourceSeenAt : oldVerifiedAt ,
320+ } )
321+ const listDocuments = vi . fn < ConnectorConfig [ 'listDocuments' ] > ( async ( ) => ( {
322+ documents : [
323+ {
324+ ...sourceDoc ( externalId ) ,
325+ content : '' ,
326+ contentDeferred : true ,
327+ acl : rejectAcl ? [ 'invalid-principal' ] : ownerAcl ,
328+ } ,
329+ ] ,
330+ hasMore : false ,
331+ permissionsOnly : true ,
332+ reconciliationSafe : false ,
333+ } ) )
334+ const getDocument = vi . fn ( async ( ) => sourceDoc ( externalId ) )
335+ const [ connector ] = await db
336+ . select ( )
337+ . from ( knowledgeConnector )
338+ . where ( eq ( knowledgeConnector . id , connectorId ) )
339+ const pass = await runConnectorContentPass ( {
340+ connectorId,
341+ connector,
342+ connectorConfig : { ...CONNECTOR_REGISTRY . google_drive , listDocuments } ,
343+ sourceConfig : { } ,
344+ syncContext : { } ,
345+ kbOwner : { userId : ids . aliceId , workspaceId : ids . workspaceId } ,
346+ billingAttribution : billing ,
347+ result : result ( ) ,
348+ lease : createContentSyncLease ( connectorId , runId ) ,
349+ leaseKind : 'content' ,
350+ runId,
351+ fingerprint : listingFingerprint ( { source : 'permission-evidence' } ) ,
352+ documentAccess : 'admin' ,
353+ getAccessToken : async ( ) => 'fixture' ,
354+ hydration : { getDocument } ,
355+ forceRehydrate : false ,
356+ deadlineAt : Date . now ( ) + 60_000 ,
357+ onPage : async ( verified ) => {
358+ const write = await persistDocumentAcls (
359+ connectorId ,
360+ new Map ( verified . map ( ( item ) => [ item . externalId , item . acl ! ] ) )
361+ )
362+ return { permissionsIncomplete : write . rejected > 0 }
363+ } ,
364+ } )
365+ const [ stored ] = await db . select ( ) . from ( document ) . where ( eq ( document . connectorId , connectorId ) )
366+ expect ( stored ) . toMatchObject ( {
367+ acl : [ ] ,
368+ aclRequirements : [ ] ,
369+ aclVerifiedAt : null ,
370+ sourceSeenAt : oldVerifiedAt ,
371+ contentHash : `hash-${ externalId } ` ,
372+ storageKey,
373+ deletedAt : null ,
374+ } )
375+ expect ( getDocument ) . not . toHaveBeenCalled ( )
376+ expect ( pass . checkpoint . permissionFailures ) . toBe ( rejectAcl )
377+ if ( rejectAcl ) expect ( pass . holdNotice ) . toBe ( SOURCE_PERMISSION_ERROR )
378+ } )
379+
282380 it ( 'preserves a permission-repaired document through the remaining content crawl and EOF reconciliation' , async ( ) => {
283381 const connectorId = generateId ( )
284382 const runId = generateId ( )
@@ -378,11 +476,11 @@ describe('durable source and member cycles in PostgreSQL', () => {
378476 forceRehydrate : false ,
379477 deadlineAt : Date . now ( ) + 60_000 ,
380478 onPage : async ( verified ) => {
381- await persistDocumentAcls (
479+ const write = await persistDocumentAcls (
382480 connectorId ,
383481 new Map ( verified . map ( ( item ) => [ item . externalId , item . acl ! ] ) )
384482 )
385- return { permissionsIncomplete : false }
483+ return { permissionsIncomplete : write . rejected > 0 }
386484 } ,
387485 } )
388486 return { pass, stats }
@@ -520,11 +618,11 @@ describe('durable source and member cycles in PostgreSQL', () => {
520618 forceRehydrate : false ,
521619 deadlineAt : Date . now ( ) + 60_000 ,
522620 onPage : async ( verified ) => {
523- await persistDocumentAcls (
621+ const write = await persistDocumentAcls (
524622 connectorId ,
525623 new Map ( verified . map ( ( item ) => [ item . externalId , item . acl ! ] ) )
526624 )
527- return { permissionsIncomplete : false }
625+ return { permissionsIncomplete : write . rejected > 0 }
528626 } ,
529627 } )
530628 expect ( pass . complete ) . toBe ( true )
0 commit comments