devenv/MIGRATION_GUIDE.md
Step-by-step guide for converting a test from integration-tests/smoke/ to the devenv/ pattern. Read AGENTS.md first for module constraints and conventions.
This guide covers migrating a single Chainlink product's smoke tests. If the product already has a configurator in devenv/products/, skip to Step 4.
Read the old test file in integration-tests/smoke/<product>_test.go and identify:
integration-tests/actions or integration-tests/contracts that need local replacementsgithub.com/smartcontractkit/chainlink/v2, integration-tests/, or deployment/ -- all of these must be replacedFor each forbidden import, find the replacement:
| Old import pattern | Replacement |
|---|---|
integration-tests/contracts/<wrapper> | Direct gethwrapper from chainlink-evm/gethwrappers |
integration-tests/actions/<helper> | Reimplement locally in the configurator |
integration-tests/client | chainlink-testing-framework/framework/clclient |
chainlink/v2/core/services/... | Not needed -- use clclient job spec types |
chainlink/v2/core/gethwrappers/... | chainlink-evm/gethwrappers |
Create devenv/products/<name>/basic.toml.
Reference file: products/directrequest/basic.toml (simplest single-node product).
Key decisions:
[[products]] name field must match the switch case you will add in Step 3nodes count depends on the product (1 for single-node products like cron/DR/VRF, 5 for multi-node like OCR2/flux)[[vrf]]) key must match the toml struct tag on the Configurator.Config field you create in Step 2gas_settings if the product deploys contractsCreate devenv/products/<name>/configuration.go.
Reference files:
Define three structs:
Configurator -- top-level, with Config []*<Product> and a toml tag matching the TOML section key<Product> -- product config fields (typically GasSettings + Out)Out -- all deployed state the tests will need (contract addresses, job IDs, key hashes, chain IDs, etc.). Every field must have a toml tag.Load(), Store(), GenerateNodesConfig(), and GenerateNodesSecrets() are nearly identical across products. Copy from any existing product and adjust the type parameter in products.Load[Configurator]().
This is where the real migration work happens. Port the setup logic from the old test file:
clclient.New(ns[0].Out.CLNodes)products.ETHClient()link_token.DeployLinkToken, then bind.WaitDeployed)products.WaitMinedFast() for transaction confirmations after deploymentproducts.FundAddressEIP1559()clclient methods (MustCreateVRFKey, MustCreateBridge, MustCreateJob, etc.)m.Config[0].OutWhen old tests use functions from integration-tests/actions:
configuration.goEncodeOnChainVRFProvingKey and EncodeOnChainExternalJobID as local helpers (see products/vrf/configuration.go)Old tests often use wrappers from integration-tests/contracts that add convenience methods around gethwrappers. In devenv, use the gethwrappers directly:
chainlink-evm/gethwrappers/generated/ or chainlink-evm/gethwrappers/shared/generated/Deploy<Contract>(auth, client, ...) directlyEdit environment.go:
"github.com/smartcontractkit/chainlink/devenv/products/<name>"newProduct() switch matching the product name from your TOML configCreate devenv/tests/<name>/smoke_test.go.
Reference files:
Every test must:
de.LoadOutput[de.Cfg]("../../env-out.toml")products.LoadOutput[<product>.Configurator]("../../env-out.toml")t.Cleanup(func() { framework.SaveContainerLogs(...) })products.ETHClient, clclient.New)chainlink/v2 wrappers)require.EventuallyWithT for async results (typical: 2 min timeout, 2 s poll)The test file should only contain assertion logic. All setup (contract deployment, job creation, funding) belongs in the configurator.
From the devenv/ directory, run targeted builds and lints on the new packages only:
# Overall checks
just build-fakes
# Package specific checks
go build ./products/<name>/...
go build ./tests/<name>/...
golangci-lint run ./products/<name>/... --fix
golangci-lint run ./tests/<name>/... --fix
Both grep commands should return no results.
Open .github/e2e-tests.yml and find the entry matching the old test file path (e.g. path: integration-tests/smoke/vrf_test.go). Delete the entire entry block including its id, path, test_env_type, triggers, test_cmd, and all other fields.
Open .github/workflows/devenv-nightly.yml and add a matrix entry in the matrix.include array of the test-nightly job. Copy an existing entry for a product of similar complexity and update:
display_name -- human-readable nametestcmd -- the go test command with -run filter matching your test function namesenvcmd -- the cl u command pointing to your TOML configsrunner -- ubuntu-latest for simple tests, larger runners for resource-heavy teststests_dir -- the subdirectory name under devenv/tests/logs_archive_name -- name for the uploaded log artifactIf the testcmd includes multiple test names separated by |, escape the pipe as \\| in YAML.
Delete integration-tests/smoke/<product>_test.go. If this was the last test in that file, also check if there are any shared helpers in the same package that are now unused and can be removed.
chainlink/v2, integration-tests, and deployment before committing. The depguard linter in devenv/.golangci.yml enforces this.toml struct tag on Configurator.Config must exactly match the TOML section name (e.g. toml:"vrf" matches [[vrf]]). A mismatch means the config silently loads as empty.toml tags on Out fields -- Every field in the Out struct needs a toml tag or it won't be persisted/loaded from env-out.toml.WaitMinedFast vs bind.WaitDeployed -- Use bind.WaitDeployed for contract deployment transactions (returns the deployed address). Use products.WaitMinedFast for all other transactions (state changes, transfers, registrations).tests/<name>/ imports the product package from products/<name>/ only for the Configurator type. All contract interaction in tests uses gethwrappers directly.devenv/tests/<name>/, so the output file is ../../env-out.toml (two levels up to devenv/).package declaration should match the directory name (e.g. package vrf in tests/vrf/).