@@ -21,6 +21,7 @@ vi.mock('@/connectors/registry.server', () => ({ CONNECTOR_REGISTRY: {} }))
2121const { drizzle } = await import ( 'drizzle-orm/postgres-js' )
2222const schema = await import ( '@sim/db/schema' )
2323const { persistDocumentAcls } = await import ( '@/lib/knowledge/connectors/sync-persistence' )
24+ const { materializeDocumentAcls } = await import ( '@/lib/knowledge/connectors/member-observations' )
2425const { mergeMirroredAcls, hideUnlistedDocuments } = await import (
2526 '@/lib/knowledge/connectors/mirrored-acls'
2627)
@@ -87,7 +88,8 @@ describe.runIf(Boolean(databaseUrl))('knowledge ACLs in PostgreSQL', () => {
8788 join = false ,
8889 githubInstallationGrants ?: GitHubInstallationReadGrant [ ] ,
8990 userId = 'reader' ,
90- confluenceSiteGrants ?: ConfluenceSiteReadGrant [ ]
91+ confluenceSiteGrants ?: ConfluenceSiteReadGrant [ ] ,
92+ storedAclFreshness = false
9193 ) : Promise < boolean > {
9294 const query = new PgDialect ( ) . sqlToQuery (
9395 knowledgeAccessCondition ( {
@@ -96,6 +98,7 @@ describe.runIf(Boolean(databaseUrl))('knowledge ACLs in PostgreSQL', () => {
9698 tokens,
9799 githubInstallationGrants,
98100 confluenceSiteGrants,
101+ storedAclFreshness,
99102 } )
100103 )
101104 const values = query . params . map ( ( value : unknown ) => {
@@ -495,6 +498,75 @@ describe.runIf(Boolean(databaseUrl))('knowledge ACLs in PostgreSQL', () => {
495498 expect ( await readable ( [ 'ws' ] , 'upload' ) ) . toBe ( true )
496499 } )
497500
501+ /**
502+ * The stored expiry every ACL writer records must decide mirrored freshness exactly as the
503+ * per-candidate proof does, except where it is deliberately stricter: a members-mode ACL expires
504+ * with the earliest observation it names, so one stale observer hides the document from readers
505+ * whose own observation is still current, until the sweep drops it and re-materializes.
506+ */
507+ it ( 'decides mirrored freshness from the row, never admitting what per-candidate evidence refuses' , async ( ) => {
508+ const proof = ( tokens : string [ ] , id : string ) => readable ( tokens , id )
509+ const stored = ( tokens : string [ ] , id : string ) =>
510+ readable ( tokens , id , false , undefined , 'reader' , undefined , true )
511+
512+ await putDocument ( 'admin-fresh' , [ 'u:alice@corp.com' ] )
513+ await connection . unsafe (
514+ "UPDATE document SET acl_valid_until = acl_verified_at + interval '24 hours' WHERE id = 'admin-fresh'"
515+ )
516+ await putDocument ( 'admin-stale' , [ 'u:alice@corp.com' ] )
517+ await connection . unsafe ( `UPDATE document SET acl_verified_at = statement_timestamp() - interval '25 hours',
518+ acl_valid_until = statement_timestamp() - interval '1 hour' WHERE id = 'admin-stale'` )
519+ await putDocument ( 'admin-unbackfilled' , [ 'u:alice@corp.com' ] )
520+ await connection . unsafe (
521+ "UPDATE document SET acl_valid_until = NULL WHERE id = 'admin-unbackfilled'"
522+ )
523+
524+ await connection . unsafe (
525+ `INSERT INTO knowledge_connector_member(id, workspace_id, connector_id, subject_token, status) VALUES
526+ ('alice', 'workspace', 'members', $1, 'active'), ('bob', 'workspace', 'members', $2, 'active')` ,
527+ [ alice , bob ]
528+ )
529+ const putMembers = async ( id : string , seen : string [ ] ) => {
530+ await connection . unsafe (
531+ "INSERT INTO document(id, connector_id, acl) VALUES ($1, 'members', string_to_array($2, ','))" ,
532+ [ id , [ alice , bob ] . join ( ',' ) ]
533+ )
534+ await connection . unsafe (
535+ `INSERT INTO knowledge_document_observation VALUES
536+ ($1, 'alice', statement_timestamp() - ($2)::interval),
537+ ($1, 'bob', statement_timestamp() - ($3)::interval)` ,
538+ [ id , seen [ 0 ] , seen [ 1 ] ]
539+ )
540+ /** The real writer decides both the ACL and its expiry, so this cannot drift from it. */
541+ await materializeDocumentAcls ( 'members' , [ id ] , drizzle ( connection , { schema } ) )
542+ }
543+ await putMembers ( 'members-fresh' , [ '1 hour' , '2 hours' ] )
544+ await putMembers ( 'members-stale' , [ '25 hours' , '26 hours' ] )
545+ await putMembers ( 'members-mixed' , [ '1 hour' , '26 hours' ] )
546+
547+ for ( const [ id , tokens , expected ] of [
548+ [ 'admin-fresh' , [ 'u:alice@corp.com' ] , true ] ,
549+ [ 'admin-stale' , [ 'u:alice@corp.com' ] , false ] ,
550+ [ 'members-fresh' , [ alice ] , true ] ,
551+ [ 'members-stale' , [ alice ] , false ] ,
552+ ] as const ) {
553+ expect ( await proof ( [ ...tokens ] , id ) ) . toBe ( expected )
554+ expect ( await stored ( [ ...tokens ] , id ) ) . toBe ( expected )
555+ }
556+ /** Stricter, never wider: the fresh observer waits for the sweep. */
557+ expect ( await proof ( [ alice ] , 'members-mixed' ) ) . toBe ( true )
558+ expect ( await stored ( [ alice ] , 'members-mixed' ) ) . toBe ( false )
559+ expect ( await proof ( [ bob ] , 'members-mixed' ) ) . toBe ( false )
560+ expect ( await stored ( [ bob ] , 'members-mixed' ) ) . toBe ( false )
561+ /** A row the backfill has not reached yet is unreadable rather than assumed current. */
562+ expect ( await proof ( [ 'u:alice@corp.com' ] , 'admin-unbackfilled' ) ) . toBe ( true )
563+ expect ( await stored ( [ 'u:alice@corp.com' ] , 'admin-unbackfilled' ) ) . toBe ( false )
564+ /** Uploads and workspace-mode documents carry no expiry and are unaffected. */
565+ await connection . unsafe ( "INSERT INTO document(id) VALUES ('upload')" )
566+ expect ( await proof ( [ 'ws' ] , 'upload' ) ) . toBe ( true )
567+ expect ( await stored ( [ 'ws' ] , 'upload' ) ) . toBe ( true )
568+ } )
569+
498570 it ( 'does not let one member refresh another member’s stale observation' , async ( ) => {
499571 await connection . unsafe (
500572 "INSERT INTO document(id, connector_id, acl) VALUES ('shared', 'members', string_to_array($1, ','))" ,
@@ -545,6 +617,17 @@ describe.runIf(Boolean(databaseUrl))('knowledge ACLs in PostgreSQL', () => {
545617 "SELECT jsonb_typeof(acl_requirements) AS shape, acl_requirements FROM document WHERE id = 'persisted'"
546618 )
547619 expect ( stored ) . toEqual ( { shape : 'array' , acl_requirements : [ [ space ] , [ page ] ] } )
620+ /** The writer records when its evidence expires, so a reader never re-derives it. */
621+ const [ expiry ] = await connection . unsafe (
622+ `SELECT acl_valid_until = acl_verified_at + interval '24 hours' AS matches_evidence
623+ FROM document WHERE id = 'persisted'`
624+ )
625+ expect ( expiry ) . toEqual ( { matches_evidence : true } )
626+ await persistDocumentAcls ( 'admin' , new Map ( [ [ 'page' , { acl : [ ] , requirements : [ ] } ] ] ) , executor )
627+ const [ revoked ] = await connection . unsafe (
628+ "SELECT acl_valid_until FROM document WHERE id = 'persisted'"
629+ )
630+ expect ( revoked ) . toEqual ( { acl_valid_until : null } )
548631 } )
549632
550633 it . each ( [
0 commit comments