@@ -8,7 +8,13 @@ import * as scfTypes from '../../../../src/stack/scfStack/scfTypes';
88import * as stateManager from '../../../../src/common/stateManager' ;
99import * as hashUtils from '../../../../src/common/hashUtils' ;
1010import { ProviderEnum } from '../../../../src/common' ;
11- import { Context , StateFile , CURRENT_STATE_VERSION , ResourceTypeEnum } from '../../../../src/types' ;
11+ import {
12+ Context ,
13+ ResourceState ,
14+ StateFile ,
15+ CURRENT_STATE_VERSION ,
16+ ResourceTypeEnum ,
17+ } from '../../../../src/types' ;
1218
1319const mockScfOperations = {
1420 createFunction : jest . fn ( ) ,
@@ -1043,11 +1049,22 @@ describe('ScfResource', () => {
10431049 ( mockScfOperations . updateFunctionConfiguration as jest . Mock ) . mockResolvedValue ( undefined ) ;
10441050 ( mockScfOperations . updateFunctionCode as jest . Mock ) . mockResolvedValue ( undefined ) ;
10451051 ( stateManager . setResource as jest . Mock ) . mockReturnValue ( newState ) ;
1052+ // Existing state has the SAME config fields (only codeHash differs) → the
1053+ // configuration must NOT be re-pushed, only the code.
1054+ ( stateManager . getResource as jest . Mock ) . mockReturnValue ( {
1055+ mode : 'managed' ,
1056+ region : 'ap-guangzhou' ,
1057+ definition : { ...mockDefinition } ,
1058+ instances : [ ] ,
1059+ lastUpdated : '2025-01-01T00:00:00Z' ,
1060+ } ) ;
10461061
10471062 const result = await updateResource ( mockContext , testFunction , initialState ) ;
10481063
10491064 expect ( scfTypes . functionToScfConfig ) . toHaveBeenCalledWith ( testFunction ) ;
1050- expect ( mockScfOperations . updateFunctionConfiguration ) . toHaveBeenCalledWith ( mockConfig ) ;
1065+ // Config is unchanged (existing definition === desired) → configuration is
1066+ // NOT re-pushed; only the code is updated.
1067+ expect ( mockScfOperations . updateFunctionConfiguration ) . not . toHaveBeenCalled ( ) ;
10511068 expect ( mockScfOperations . updateFunctionCode ) . toHaveBeenCalledWith (
10521069 'test-function' ,
10531070 'base64encodedcontent' ,
@@ -1079,9 +1096,65 @@ describe('ScfResource', () => {
10791096 expect ( result ) . toEqual ( newState ) ;
10801097 } ) ;
10811098
1099+ it ( 'should re-push configuration when a mutable config field changes' , async ( ) => {
1100+ ( mockScfOperations . updateFunctionConfiguration as jest . Mock ) . mockResolvedValue ( undefined ) ;
1101+ ( mockScfOperations . updateFunctionCode as jest . Mock ) . mockResolvedValue ( undefined ) ;
1102+ ( stateManager . setResource as jest . Mock ) . mockReturnValue ( initialState ) ;
1103+ // Existing config differs on a MUTABLE field (memorySize) → config re-pushed.
1104+ ( stateManager . getResource as jest . Mock ) . mockReturnValue ( {
1105+ mode : 'managed' ,
1106+ region : 'ap-guangzhou' ,
1107+ definition : { ...mockDefinition , memorySize : 256 } ,
1108+ instances : [ ] ,
1109+ lastUpdated : '2025-01-01T00:00:00Z' ,
1110+ } ) ;
1111+
1112+ await updateResource ( mockContext , testFunction , initialState ) ;
1113+
1114+ expect ( mockScfOperations . updateFunctionConfiguration ) . toHaveBeenCalledTimes ( 1 ) ;
1115+ expect ( mockScfOperations . updateFunctionConfiguration ) . toHaveBeenCalledWith (
1116+ expect . objectContaining ( { FunctionName : 'test-function' } ) ,
1117+ ) ;
1118+ // Immutable Handler/Runtime stripping happens inside scfOperations
1119+ // (covered by scfOperations.test.ts) — the resource layer passes config
1120+ // through as-is.
1121+ } ) ;
1122+
1123+ it ( 'should throw a clear error when Handler changes on update' , async ( ) => {
1124+ ( mockScfOperations . updateFunctionCode as jest . Mock ) . mockResolvedValue ( undefined ) ;
1125+ // Existing definition has a DIFFERENT handler.
1126+ ( stateManager . getResource as jest . Mock ) . mockReturnValue ( {
1127+ mode : 'managed' ,
1128+ region : 'ap-guangzhou' ,
1129+ definition : { ...mockDefinition , handler : 'old.handler' } ,
1130+ instances : [ ] ,
1131+ lastUpdated : '2025-01-01T00:00:00Z' ,
1132+ } ) ;
1133+ const fnWithNewHandler = {
1134+ ...testFunction ,
1135+ code : { ...testFunction . code , handler : 'new.handler' } ,
1136+ } ;
1137+
1138+ await expect ( updateResource ( mockContext , fnWithNewHandler , initialState ) ) . rejects . toThrow (
1139+ / H a n d l e r i s i m m u t a b l e / ,
1140+ ) ;
1141+ expect ( mockScfOperations . updateFunctionConfiguration ) . not . toHaveBeenCalled ( ) ;
1142+ expect ( mockScfOperations . updateFunctionCode ) . not . toHaveBeenCalled ( ) ;
1143+ } ) ;
1144+
10821145 it ( 'should propagate errors from updateScfFunctionConfiguration' , async ( ) => {
10831146 const error = new Error ( 'Update config failed' ) ;
10841147 ( mockScfOperations . updateFunctionConfiguration as jest . Mock ) . mockRejectedValue ( error ) ;
1148+ ( mockScfOperations . updateFunctionCode as jest . Mock ) . mockResolvedValue ( undefined ) ;
1149+ // Make a mutable config field differ so the configuration is re-pushed.
1150+ const oldConfigResource : ResourceState = {
1151+ mode : 'managed' ,
1152+ region : 'ap-guangzhou' ,
1153+ definition : { ...mockDefinition , memorySize : 256 } ,
1154+ instances : [ ] ,
1155+ lastUpdated : '2025-01-01T00:00:00Z' ,
1156+ } ;
1157+ ( stateManager . getResource as jest . Mock ) . mockReturnValue ( oldConfigResource ) ;
10851158
10861159 await expect ( updateResource ( mockContext , testFunction , initialState ) ) . rejects . toThrow (
10871160 'Update config failed' ,
@@ -1169,12 +1242,11 @@ describe('ScfResource', () => {
11691242
11701243 await updateResource ( mockContext , fnWithIam , stateWithRole ) ;
11711244
1172- // Role config should be injected into SCF config
1173- const expectedConfig = {
1174- ...mockConfig ,
1175- Role : 'existing-role' ,
1176- } ;
1177- expect ( mockScfOperations . updateFunctionConfiguration ) . toHaveBeenCalledWith ( expectedConfig ) ;
1245+ // Role config is injected into the SCF config, but since no mutable config
1246+ // field changed (memory/timeout/env identical), the configuration is not
1247+ // re-pushed — role changes are applied via updateRolePolicy/updateManagedPolicies.
1248+ expect ( mockCamOperations . updateRolePolicy ) . toHaveBeenCalled ( ) ;
1249+ expect ( mockScfOperations . updateFunctionConfiguration ) . not . toHaveBeenCalled ( ) ;
11781250 } ) ;
11791251
11801252 it ( 'should create role during update when no existing role' , async ( ) => {
0 commit comments