11/** @vitest -environment node */
2- import { OPERATION_TARGETS , SUBBLOCK_OPERATIONS } from '@sim/realtime-protocol/constants'
2+ import {
3+ BLOCK_OPERATIONS ,
4+ BLOCKS_OPERATIONS ,
5+ OPERATION_TARGETS ,
6+ SUBBLOCK_OPERATIONS ,
7+ WORKFLOW_OPERATIONS ,
8+ } from '@sim/realtime-protocol/constants'
39import { beforeEach , describe , expect , it , vi } from 'vitest'
410
511const { mockTransaction, mockSelectWhere, mockSet } = vi . hoisted ( ( ) => ( {
@@ -38,13 +44,28 @@ vi.mock('drizzle-orm', () => ({
3844} ) )
3945vi . mock ( 'drizzle-orm/postgres-js' , ( ) => ( { drizzle : ( ) => ( { transaction : mockTransaction } ) } ) )
4046vi . mock ( 'postgres' , ( ) => ( { default : vi . fn ( ) } ) )
41- vi . mock ( '@/env' , ( ) => ( { env : { DATABASE_URL : 'postgres://localhost/test' } } ) )
47+ vi . mock ( '@/env' , ( ) => ( {
48+ env : { DATABASE_URL : 'postgres://localhost/test' } ,
49+ getBaseUrl : ( ) => 'http://localhost:3000' ,
50+ } ) )
4251
52+ import { mergeSubBlockValues } from '@sim/workflow-persistence/subblocks'
53+ import { afterEach } from 'vitest'
4354import { persistWorkflowOperation } from '@/database/operations'
4455
56+ const mockFetch = vi . fn ( )
57+ beforeEach ( ( ) => {
58+ vi . stubGlobal ( 'fetch' , mockFetch )
59+ mockFetch . mockReset ( )
60+ mockFetch . mockImplementation ( async ( ) => Response . json ( { agentToolPermissionModeEnabled : true } ) )
61+ } )
62+ afterEach ( ( ) => vi . unstubAllGlobals ( ) )
63+
4564const transaction = {
4665 select : ( ) => ( { from : ( ) => ( { where : mockSelectWhere } ) } ) ,
4766 update : ( ) => ( { set : mockSet } ) ,
67+ delete : vi . fn ( ) ,
68+ insert : vi . fn ( ) ,
4869}
4970
5071describe ( 'search replacement persistence' , ( ) => {
@@ -70,6 +91,7 @@ describe('search replacement persistence', () => {
7091 mockSelectWhere . mockResolvedValue ( [
7192 {
7293 id : 'agent-1' ,
94+ type : 'agent' ,
7395 locked : false ,
7496 data : { } ,
7597 subBlocks : { tools : { id : 'tools' , type : 'tool-input' , value : stored } } ,
@@ -118,3 +140,72 @@ describe('search replacement persistence', () => {
118140 expect ( mockSet ) . toHaveBeenCalledTimes ( 1 )
119141 } )
120142} )
143+
144+ describe ( 'variable permissions at every realtime transaction write boundary' , ( ) => {
145+ const block = {
146+ id : 'agent-1' ,
147+ type : 'agent' ,
148+ name : 'Agent' ,
149+ position : { x : 0 , y : 0 } ,
150+ locked : false ,
151+ subBlocks : {
152+ tools : { id : 'tools' , type : 'tool-input' , value : [ { usageControlExpression : 'force' } ] } ,
153+ } ,
154+ data : { } ,
155+ }
156+
157+ beforeEach ( ( ) => {
158+ vi . clearAllMocks ( )
159+ mockTransaction . mockImplementation (
160+ async ( callback : ( tx : typeof transaction ) => Promise < void > ) => callback ( transaction )
161+ )
162+ mockSet . mockReturnValue ( { where : vi . fn ( ) . mockResolvedValue ( undefined ) } )
163+ mockSelectWhere . mockImplementation ( ( ) =>
164+ Object . assign ( Promise . resolve ( [ block ] ) , { limit : async ( ) => [ block ] } )
165+ )
166+ mockFetch . mockImplementation ( async ( ) =>
167+ Response . json ( { agentToolPermissionModeEnabled : false } )
168+ )
169+ } )
170+
171+ it . each ( [
172+ {
173+ operation : WORKFLOW_OPERATIONS . REPLACE_STATE ,
174+ target : OPERATION_TARGETS . WORKFLOW ,
175+ payload : { state : { blocks : { 'agent-1' : block } } } ,
176+ } ,
177+ {
178+ operation : BLOCK_OPERATIONS . UPDATE_CANONICAL_MODE ,
179+ target : OPERATION_TARGETS . BLOCK ,
180+ payload : { id : 'agent-1' , canonicalId : '0:agentToolUsageControl' , canonicalMode : 'advanced' } ,
181+ } ,
182+ {
183+ operation : BLOCK_OPERATIONS . REPLACE_CANONICAL_MODES ,
184+ target : OPERATION_TARGETS . BLOCK ,
185+ payload : {
186+ id : 'agent-1' ,
187+ data : { canonicalModes : { '0:agentToolUsageControl' : 'advanced' } } ,
188+ } ,
189+ } ,
190+ {
191+ operation : BLOCKS_OPERATIONS . BATCH_ADD_BLOCKS ,
192+ target : OPERATION_TARGETS . BLOCKS ,
193+ payload : { blocks : [ block ] } ,
194+ } ,
195+ {
196+ operation : SUBBLOCK_OPERATIONS . BATCH_UPDATE ,
197+ target : OPERATION_TARGETS . SUBBLOCK ,
198+ payload : {
199+ updates : [ { blockId : 'agent-1' , subblockId : 'tools' , value : block . subBlocks . tools . value } ] ,
200+ } ,
201+ } ,
202+ ] ) ( 'refuses $operation before changing any block rows' , async ( operation ) => {
203+ vi . mocked ( mergeSubBlockValues ) . mockReturnValue ( block . subBlocks )
204+ await expect (
205+ persistWorkflowOperation ( 'workflow-1' , { ...operation , timestamp : Date . now ( ) } )
206+ ) . rejects . toThrow ( 'disabled' )
207+ expect ( mockSet ) . toHaveBeenCalledTimes ( 1 )
208+ expect ( transaction . delete ) . not . toHaveBeenCalled ( )
209+ expect ( transaction . insert ) . not . toHaveBeenCalled ( )
210+ } )
211+ } )
0 commit comments