1+ import { AuditAction } from '@sim/audit'
12import { db } from '@sim/db'
23import {
4+ auditLog ,
35 folder ,
46 outboxEvent ,
57 permissions ,
@@ -15,8 +17,8 @@ import {
1517 workspaceSandbox ,
1618} from '@sim/db/schema'
1719import { generateId } from '@sim/utils/id'
18- import { and , eq } from 'drizzle-orm'
19- import { afterAll , beforeAll , describe , expect , it } from 'vitest'
20+ import { and , eq , inArray , sql } from 'drizzle-orm'
21+ import { afterAll , beforeAll , describe , expect , it , vi } from 'vitest'
2022import { withWorkspaceInvocationScope } from '@/lib/core/application/workspace-invocation-scope'
2123import { processOutboxEventById } from '@/lib/core/outbox/service'
2224import { createScopedCliTransport } from '@/lib/mothership/agent-cli/scoped-transport'
@@ -765,9 +767,10 @@ describe('authorized fork and sync against PostgreSQL', () => {
765767
766768 /**
767769 * The opt-in policy end to end: set from a fork, it reaches the parent and changes only
768- * what differs; a genuinely new workflow (created, duplicated, or a fork's starter) and a
769- * new fork take it; a forked copy stays synced; no existing workflow moves; and an archived
770- * member is walked through for the lineage root but never written.
770+ * what differs, filing one audit entry in each changed workspace's own log; a genuinely
771+ * new workflow (created, duplicated, or a fork's starter) and a new fork take it; a forked
772+ * copy stays synced; no existing workflow moves; and an archived member is walked through
773+ * for the lineage root but never written.
771774 */
772775 it ( 'gives new workflows the lineage fork-sync default while copies stay synced' , async ( ) => {
773776 const childId = await createChild ( )
@@ -787,13 +790,64 @@ describe('authorized fork and sync against PostgreSQL', () => {
787790 ) [ 0 ] ?. excluded
788791 const setDefault = ( workspaceId : string , excludeNewWorkflows : boolean ) =>
789792 setForkSyncDefault . execute ( { principal, input : { workspaceId, excludeNewWorkflows } } )
793+ /** Every audit entry a change issued from `originId` filed, whichever workspace it named. */
794+ const auditedFrom = ( originId : string ) =>
795+ db
796+ . select ( {
797+ workspaceId : auditLog . workspaceId ,
798+ resourceId : auditLog . resourceId ,
799+ resourceName : auditLog . resourceName ,
800+ metadata : auditLog . metadata ,
801+ } )
802+ . from ( auditLog )
803+ . where (
804+ and (
805+ eq ( auditLog . action , AuditAction . WORKSPACE_FORK_SYNC_DEFAULT_CHANGED ) ,
806+ sql `${ auditLog . metadata } ->> 'originWorkspaceId' = ${ originId } `
807+ )
808+ )
790809 try {
791810 const first = await setDefault ( childId , true )
792811 expect ( first . changedWorkspaces . map ( ( member ) => member . id ) ) . toEqual (
793812 expect . arrayContaining ( [ sourceWorkspaceId , childId ] )
794813 )
795814 expect ( ( await setDefault ( childId , true ) ) . changedWorkspaces ) . toEqual ( [ ] )
796815 expect ( await policyOf ( sourceWorkspaceId ) ) . toBe ( true )
816+
817+ // Each changed member's admins see the change in their own log, under that workspace's name.
818+ const changed = new Map (
819+ (
820+ await db
821+ . select ( { id : workspace . id , name : workspace . name } )
822+ . from ( workspace )
823+ . where (
824+ inArray (
825+ workspace . id ,
826+ first . changedWorkspaces . map ( ( member ) => member . id )
827+ )
828+ )
829+ ) . map ( ( member ) => [ member . id , member . name ] )
830+ )
831+ await vi . waitFor (
832+ async ( ) => {
833+ const entries = await auditedFrom ( childId )
834+ // Exactly the changed members, once each: no missing, duplicate, or extra entry,
835+ // including from the no-op repeat issued from the same workspace.
836+ expect ( entries . map ( ( entry ) => entry . resourceId ) . sort ( ) ) . toEqual (
837+ [ ...changed . keys ( ) ] . sort ( )
838+ )
839+ for ( const entry of entries ) {
840+ expect ( entry . workspaceId ) . toBe ( entry . resourceId )
841+ expect ( entry . resourceName ) . toBe ( changed . get ( entry . resourceId ! ) )
842+ expect ( entry . metadata ) . toMatchObject ( {
843+ forkSyncNewWorkflowsExcluded : true ,
844+ originWorkspaceId : childId ,
845+ originWorkspaceName : changed . get ( childId ) ,
846+ } )
847+ }
848+ } ,
849+ { timeout : 5000 }
850+ )
797851 expect ( await excludedFor ( sourceWorkflowId ) ) . toBe ( false )
798852
799853 const [ copy ] = await db
@@ -838,10 +892,26 @@ describe('authorized fork and sync against PostgreSQL', () => {
838892 ) . toEqual ( [ { excluded : true } ] )
839893
840894 await db . update ( workspace ) . set ( { archivedAt : new Date ( ) } ) . where ( eq ( workspace . id , childId ) )
841- await setDefault ( grandchildId , false )
842- expect ( await policyOf ( sourceWorkspaceId ) ) . toBe ( false )
843- expect ( await policyOf ( grandchildId ) ) . toBe ( false )
895+ const fromGrandchild = await setDefault ( grandchildId , false )
896+ // Every live member flips back - the ones the first change covered and the two forks
897+ // created since - while the archived child is neither written nor audited.
898+ const expectedFromGrandchild = [
899+ ...[ ...changed . keys ( ) ] . filter ( ( id ) => id !== childId ) ,
900+ newForkId ,
901+ grandchildId ,
902+ ] . sort ( )
903+ expect ( fromGrandchild . changedWorkspaces . map ( ( member ) => member . id ) . sort ( ) ) . toEqual (
904+ expectedFromGrandchild
905+ )
906+ for ( const id of expectedFromGrandchild ) expect ( await policyOf ( id ) ) . toBe ( false )
844907 expect ( await policyOf ( childId ) ) . toBe ( true )
908+ await vi . waitFor (
909+ async ( ) => {
910+ const entries = await auditedFrom ( grandchildId )
911+ expect ( entries . map ( ( entry ) => entry . resourceId ) . sort ( ) ) . toEqual ( expectedFromGrandchild )
912+ } ,
913+ { timeout : 5000 }
914+ )
845915 } finally {
846916 await db . update ( workspace ) . set ( { archivedAt : null } ) . where ( eq ( workspace . id , childId ) )
847917 await setDefault ( sourceWorkspaceId , false )
0 commit comments