@@ -26,11 +26,13 @@ import (
2626 "os"
2727 "path/filepath"
2828 "regexp"
29+ goRuntime "runtime"
2930 "strings"
3031
3132 cdcTests "github.com/onflow/cadence-tools/test"
3233 "github.com/onflow/cadence/common"
3334 "github.com/onflow/cadence/runtime"
35+ flowGo "github.com/onflow/flow-go/model/flow"
3436 "github.com/rs/zerolog"
3537 "github.com/spf13/cobra"
3638
@@ -40,6 +42,7 @@ import (
4042
4143 "github.com/onflow/flow-cli/common/branding"
4244
45+ "github.com/onflow/flow-cli/build"
4346 "github.com/onflow/flow-cli/internal/command"
4447 "github.com/onflow/flow-cli/internal/util"
4548)
@@ -183,7 +186,37 @@ func testCode(
183186 flags flagsTests ,
184187) (* result , error ) {
185188 logger := zerolog .New (zerolog.ConsoleWriter {Out : os .Stderr }).With ().Timestamp ().Logger ()
186- runner := cdcTests .NewTestRunner ().WithLogger (logger )
189+
190+ // Track network resolutions per file for pragma-based fork detection
191+ // Map: filename -> resolved network name
192+ fileNetworkResolutions := make (map [string ]string )
193+ var currentTestFile string
194+
195+ // Resolve network labels using flow.json state
196+ resolveNetworkFromState := func (label string ) (string , bool ) {
197+ network , err := state .Networks ().ByName (strings .ToLower (strings .TrimSpace (label )))
198+ if err != nil || network == nil {
199+ return "" , false
200+ }
201+ if strings .TrimSpace (network .Host ) == "" {
202+ return "" , false
203+ }
204+
205+ // Track network resolution for current test file (indicates pragma-based fork usage)
206+ // Only track if it's not the default "testing" network
207+ normalizedLabel := strings .ToLower (strings .TrimSpace (label ))
208+ if currentTestFile != "" && normalizedLabel != "testing" {
209+ if _ , exists := fileNetworkResolutions [currentTestFile ]; ! exists {
210+ fileNetworkResolutions [currentTestFile ] = normalizedLabel
211+ }
212+ }
213+
214+ return network .Host , true
215+ }
216+
217+ runner := cdcTests .NewTestRunner ().
218+ WithLogger (logger ).
219+ WithNetworkResolver (resolveNetworkFromState )
187220
188221 // Configure fork mode if requested
189222 var effectiveForkHost string
@@ -204,20 +237,42 @@ func testCode(
204237 }
205238 }
206239
240+ // Determine network label (used by resolver/addresses); default to testing
241+ networkLabel := "testing"
242+ if strings .TrimSpace (flags .Fork ) != "" {
243+ networkLabel = strings .ToLower (flags .Fork )
244+ }
245+
207246 // If fork mode is enabled, query the host to get chain ID
247+ var forkCfg * cdcTests.ForkConfig
208248 if effectiveForkHost != "" {
209249 forkChainID , err := util .GetChainIDFromHost (effectiveForkHost )
210250 if err != nil {
211251 return nil , fmt .Errorf ("failed to get chain ID from fork host %q: %w" , effectiveForkHost , err )
212252 }
213253
214- runner = runner . WithFork ( cdcTests.ForkConfig {
254+ cfg := cdcTests.ForkConfig {
215255 ForkHost : effectiveForkHost ,
216256 ChainID : forkChainID ,
217257 ForkHeight : flags .ForkHeight ,
218- })
258+ }
259+ forkCfg = & cfg
260+ runner = runner .WithFork (cfg )
261+
262+ // Map chain ID to a sensible network label if not provided explicitly
263+ if strings .TrimSpace (flags .Fork ) == "" {
264+ switch forkChainID {
265+ case flowGo .Mainnet :
266+ networkLabel = "mainnet"
267+ case flowGo .Testnet :
268+ networkLabel = "testnet"
269+ }
270+ }
219271 }
220272
273+ // Apply the network label on the base runner now that it is known
274+ runner = runner .WithNetworkLabel (networkLabel )
275+
221276 var coverageReport * runtime.CoverageReport
222277 if flags .Cover {
223278 coverageReport = state .CreateCoverageReport ("testing" )
@@ -244,30 +299,43 @@ func testCode(
244299 runner = runner .WithRandomSeed (seed )
245300 }
246301
247- contractsConfig := * state .Contracts ()
248- contracts := make (map [string ]common.Address , len (contractsConfig ))
249- // Choose alias network: default to "testing", but in fork mode use selected chain (mainnet/testnet)
250- aliasNetwork := "testing"
251- if strings .TrimSpace (flags .Fork ) != "" {
252- aliasNetwork = strings .ToLower (flags .Fork )
253- }
254- for _ , contract := range contractsConfig {
255- alias := contract .Aliases .ByNetwork (aliasNetwork )
256- if alias != nil {
257- contracts [contract .Name ] = common .Address (alias .Address )
258- }
259- }
260-
261302 testResults := make (map [string ]cdcTests.Results , 0 )
262303 exitCode := 0
263304 for scriptPath , code := range testFiles {
264- runner := runner .
305+ // Set current test file for network resolution tracking
306+ currentTestFile = scriptPath
307+
308+ fileRunner := runner .
265309 WithImportResolver (importResolver (scriptPath , state )).
266310 WithFileResolver (fileResolver (scriptPath , state )).
267- WithContracts (contracts )
311+ WithContractAddressResolver (func (network string , contractName string ) (common.Address , error ) {
312+ // Build name -> contract map once per file run
313+ contractsByName := make (map [string ]config.Contract )
314+ for _ , c := range * state .Contracts () {
315+ contractsByName [c .Name ] = c
316+ }
317+
318+ contract , exists := contractsByName [contractName ]
319+ if ! exists {
320+ return common.Address {}, fmt .Errorf ("contract not found: %s" , contractName )
321+ }
322+
323+ alias := contract .Aliases .ByNetwork (network )
324+ if alias != nil {
325+ return common .Address (alias .Address ), nil
326+ }
327+
328+ return common.Address {}, fmt .Errorf ("no address for contract %s on network %s" , contractName , network )
329+ })
330+
331+ // Ensure the file runner has the correct network label and fork config
332+ fileRunner = fileRunner .WithNetworkLabel (networkLabel )
333+ if forkCfg != nil {
334+ fileRunner = fileRunner .WithFork (* forkCfg )
335+ }
268336
269337 if flags .Name != "" {
270- testFunctions , err := runner .GetTests (string (code ))
338+ testFunctions , err := fileRunner .GetTests (string (code ))
271339 if err != nil {
272340 return nil , err
273341 }
@@ -277,14 +345,14 @@ func testCode(
277345 continue
278346 }
279347
280- result , err := runner .RunTest (string (code ), flags .Name )
348+ result , err := fileRunner .RunTest (string (code ), flags .Name )
281349 if err != nil {
282350 return nil , err
283351 }
284352 testResults [scriptPath ] = []cdcTests.Result {* result }
285353 }
286354 } else {
287- results , err := runner .RunTests (string (code ))
355+ results , err := fileRunner .RunTests (string (code ))
288356 if err != nil {
289357 return nil , err
290358 }
@@ -297,6 +365,61 @@ func testCode(
297365 break
298366 }
299367 }
368+
369+ // Clear current test file after processing
370+ currentTestFile = ""
371+ }
372+
373+ // Track fork test usage metrics - aggregate into single event
374+ hasPragmaFiles := len (fileNetworkResolutions ) > 0
375+ hasStaticFork := forkCfg != nil
376+
377+ if hasPragmaFiles || hasStaticFork {
378+ // Determine primary fork source
379+ forkSource := "none"
380+ var primaryNetwork string
381+ var chainID string
382+ hasHeight := false
383+
384+ if hasPragmaFiles {
385+ // Pragma takes priority - collect unique networks
386+ forkSource = "pragma"
387+ networkSet := make (map [string ]bool )
388+ for _ , network := range fileNetworkResolutions {
389+ networkSet [network ] = true
390+ }
391+ // Use first resolved network as primary (for single-value tracking)
392+ for _ , network := range fileNetworkResolutions {
393+ primaryNetwork = network
394+ break
395+ }
396+ // If multiple networks, note that in source
397+ if len (networkSet ) > 1 {
398+ forkSource = "pragma-mixed"
399+ }
400+ } else if hasStaticFork {
401+ // Static flags
402+ if flags .ForkHost != "" {
403+ forkSource = "fork-host-flag"
404+ } else if flags .Fork != "" {
405+ forkSource = "fork-flag"
406+ }
407+ primaryNetwork = networkLabel
408+ chainID = forkCfg .ChainID .String ()
409+ hasHeight = forkCfg .ForkHeight > 0
410+ }
411+
412+ command .TrackEvent ("test-fork" , map [string ]any {
413+ "fork_source" : forkSource ,
414+ "network" : primaryNetwork ,
415+ "chain_id" : chainID ,
416+ "has_height" : hasHeight ,
417+ "pragma_files" : len (fileNetworkResolutions ),
418+ "total_files" : len (testFiles ),
419+ "version" : build .Semver (),
420+ "os" : goRuntime .GOOS ,
421+ "ci" : os .Getenv ("CI" ) != "" ,
422+ })
300423 }
301424
302425 return & result {
@@ -313,7 +436,7 @@ func importResolver(scriptPath string, state *flowkit.State) cdcTests.ImportReso
313436 contracts [contract .Name ] = contract
314437 }
315438
316- return func (location common.Location ) (string , error ) {
439+ return func (network string , location common.Location ) (string , error ) {
317440 contract := config.Contract {}
318441
319442 switch location := location .(type ) {
0 commit comments