From dcd31eb251e3cdd84286a74f568b88c243eb8849 Mon Sep 17 00:00:00 2001 From: Grigory Panov Date: Tue, 4 Aug 2026 12:32:41 +0200 Subject: [PATCH 1/2] integration: add e2e tests for environments setup-local MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The setup-local pipeline was covered hermetically (unit + acceptance via --dry-run/stubs) and by one env-gated real-provision test in libs/localenv, but nothing exercised the command end to end through the CLI entrypoint in the cli-isolated integration suite. Add integration/cmd/environments so the feature runs in the isolated AWS/Azure/GCP e2e workflow. Three CLOUD_ENV-gated tests, all against the real public databricks/environments repo (serverless needs no running compute — the version is used verbatim): - serverless full provision: resolve -> fetch -> uv sync -> validate, asserting a real .venv/uv.lock and the --output json contract (ok, compute, resolved). - --dry-run writes nothing. - unpublished version -> E_ENV_UNSUPPORTED at the fetch phase with a non-zero exit. They skip cleanly when CLOUD_ENV is unset, so unit-test CI is unaffected. Co-authored-by: Isaac --- .../cmd/environments/setup_local_test.go | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 integration/cmd/environments/setup_local_test.go diff --git a/integration/cmd/environments/setup_local_test.go b/integration/cmd/environments/setup_local_test.go new file mode 100644 index 0000000000..127f4c9a64 --- /dev/null +++ b/integration/cmd/environments/setup_local_test.go @@ -0,0 +1,141 @@ +package environments_test + +import ( + "encoding/json" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/databricks/cli/integration/internal/acc" + "github.com/databricks/cli/internal/testcli" + "github.com/databricks/cli/libs/localenv" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// The serverless target needs no running compute: --serverless-version is used +// verbatim, so these tests resolve against the real Jobs/Clusters-free path and +// fetch from the public databricks/environments repo. v5 is a published LTS +// target (see the repo's python/serverless/ tree). +const testServerlessVersion = "5" + +// TestSetupLocalServerlessProvision drives the full non-dry-run pipeline against +// the real published constraints: resolve -> fetch -> uv sync -> validate. It +// asserts a real .venv is created and the --output json contract carries the +// resolved versions. This is the one integration test that exercises a real uv +// provision end to end; the acceptance suite covers everything else via --dry-run. +func TestSetupLocalServerlessProvision(t *testing.T) { + ctx, _ := acc.WorkspaceTest(t) + + // setup-local operates on the current working directory; run in a fresh + // greenfield project (no pre-existing pyproject.toml). + dir := t.TempDir() + t.Chdir(dir) + + // Let uv bootstrap itself if the runner's PATH lacks it; CI installs uv, but + // this keeps the test robust on a developer machine that opted in. + t.Setenv(localenv.EnvAutoInstallUv, "1") + + stdout, _ := testcli.RequireSuccessfulRun(t, ctx, + "environments", "setup-local", + "--serverless-version", testServerlessVersion, + "--output", "json", + ) + + var res localenv.Result + require.NoError(t, json.Unmarshal(stdout.Bytes(), &res)) + + assert.True(t, res.OK, "expected ok=true, got result: %s", stdout.String()) + assert.False(t, res.DryRun) + assert.Equal(t, "environments setup-local", res.Command) + assert.Equal(t, "default", res.Mode) + + require.NotNil(t, res.Compute) + assert.Equal(t, "serverless", res.Compute.Source) + assert.Equal(t, "serverless/serverless-v"+testServerlessVersion, res.Compute.EnvKey) + + require.NotNil(t, res.Resolved) + assert.NotEmpty(t, res.Resolved.PythonVersion, "resolved python version should be reported") + // The default mode installs databricks-connect, so the resolved pin is present. + assert.NotEmpty(t, res.Resolved.DBConnectVersion) + // The artifact came from a successful fetch. The cache is shared (UserCacheDir), + // so a prior run may have seeded it; accept either source rather than assuming + // a cold cache and flaking on re-runs. + assert.Contains(t, []string{"network", "cache"}, res.Resolved.ArtifactSource) + + // Every phase must have reached ok, including the real provision and validate. + for _, ph := range res.Phases { + assert.Equalf(t, localenv.StatusOK, ph.Status, "phase %s not ok", ph.Phase) + } + + // A real interpreter and lockfile were written into the project. testcli runs + // in-process, so runtime.GOOS is the runner's OS and picks the right layout. + assert.FileExists(t, venvPython(dir)) + assert.FileExists(t, filepath.Join(dir, "pyproject.toml")) + assert.FileExists(t, filepath.Join(dir, "uv.lock")) +} + +// TestSetupLocalDryRunWritesNothing verifies the --dry-run contract against the +// real repo: the plan resolves and fetches, reports ok, but writes no files. +func TestSetupLocalDryRunWritesNothing(t *testing.T) { + ctx, _ := acc.WorkspaceTest(t) + + dir := t.TempDir() + t.Chdir(dir) + + stdout, _ := testcli.RequireSuccessfulRun(t, ctx, + "environments", "setup-local", + "--serverless-version", testServerlessVersion, + "--dry-run", + "--output", "json", + ) + + var res localenv.Result + require.NoError(t, json.Unmarshal(stdout.Bytes(), &res)) + assert.True(t, res.OK) + assert.True(t, res.DryRun) + + // --dry-run must not touch disk. + entries, err := os.ReadDir(dir) + require.NoError(t, err) + assert.Empty(t, entries, "dry-run wrote files: %v", entries) +} + +// TestSetupLocalUnpublishedVersion verifies the fetch-phase error contract: an +// unpublished serverless version resolves fine but has no artifact, so the run +// fails with E_ENV_UNSUPPORTED and a non-zero exit (surfaced as a run error). +func TestSetupLocalUnpublishedVersion(t *testing.T) { + ctx, _ := acc.WorkspaceTest(t) + + dir := t.TempDir() + t.Chdir(dir) + + // A version far above anything published; resolution succeeds, fetch 404s. + stdout, _, runErr := testcli.RequireErrorRun(t, ctx, + "environments", "setup-local", + "--serverless-version", "9999", + "--dry-run", + "--output", "json", + ) + require.Error(t, runErr) + + var res localenv.Result + require.NoError(t, json.Unmarshal(stdout.Bytes(), &res)) + assert.False(t, res.OK) + require.NotNil(t, res.Error) + assert.Equal(t, localenv.ErrEnvUnsupported, res.Error.Code) + assert.Equal(t, localenv.PhaseFetch, res.Error.FailurePhase) + + // Even a failed fetch must not have provisioned anything on a dry run. + assert.NoFileExists(t, filepath.Join(dir, ".venv", "bin", "python")) +} + +// venvPython returns the path to the created virtualenv's interpreter, accounting +// for the Windows (Scripts/python.exe) vs Unix (bin/python) layout. +func venvPython(dir string) string { + if runtime.GOOS == "windows" { + return filepath.Join(dir, ".venv", "Scripts", "python.exe") + } + return filepath.Join(dir, ".venv", "bin", "python") +} From 9d9c0acc805d9eb1f44fad547e4aed953745eac2 Mon Sep 17 00:00:00 2001 From: Grigory Panov Date: Fri, 7 Aug 2026 14:48:11 +0200 Subject: [PATCH 2/2] Address review: drop uv auto-install, tautological assert, hardcoded venv path Per review on #6155: - Skip when uv is absent instead of setting EnvAutoInstallUv. The opt-in made the pipeline run the remote installer and mutate ~/.local/bin; CI installs uv, so the only case it covered was one where that side effect is unwanted. The old comment also claimed the developer had opted in, when the test set the flag. - Drop the artifactSource assertion. artifactSource() only ever returns "cache" or "network", so asserting membership in that set could never fail. - Assert the dry-run project dir is empty rather than checking a hardcoded .venv/bin/python. That path bypassed the venvPython helper and was vacuous on Windows; --dry-run skips ensureWritable and suppresses cache writes, so the directory must be empty outright. - Route /integration/cmd/environments/ to team:ide. It sits below /integration/ because findOwners is last-match-wins. --- .github/OWNERS | 2 ++ .../cmd/environments/setup_local_test.go | 22 +++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/OWNERS b/.github/OWNERS index 9c98a05cc9..cab1d29803 100644 --- a/.github/OWNERS +++ b/.github/OWNERS @@ -64,6 +64,8 @@ # Integration tests /integration/ team:platform +/integration/cmd/environments/ team:ide + # Internal /internal/ team:platform diff --git a/integration/cmd/environments/setup_local_test.go b/integration/cmd/environments/setup_local_test.go index 127f4c9a64..3d5f83bbd4 100644 --- a/integration/cmd/environments/setup_local_test.go +++ b/integration/cmd/environments/setup_local_test.go @@ -3,6 +3,7 @@ package environments_test import ( "encoding/json" "os" + "os/exec" "path/filepath" "runtime" "testing" @@ -33,9 +34,12 @@ func TestSetupLocalServerlessProvision(t *testing.T) { dir := t.TempDir() t.Chdir(dir) - // Let uv bootstrap itself if the runner's PATH lacks it; CI installs uv, but - // this keeps the test robust on a developer machine that opted in. - t.Setenv(localenv.EnvAutoInstallUv, "1") + // Skip rather than let the pipeline install uv: EnvAutoInstallUv would run the + // remote installer and mutate ~/.local/bin on a developer machine. CI installs + // uv, so this only skips where the side effect would be unwanted. + if _, err := exec.LookPath("uv"); err != nil { + t.Skipf("uv not found on PATH (%v)", err) + } stdout, _ := testcli.RequireSuccessfulRun(t, ctx, "environments", "setup-local", @@ -59,10 +63,6 @@ func TestSetupLocalServerlessProvision(t *testing.T) { assert.NotEmpty(t, res.Resolved.PythonVersion, "resolved python version should be reported") // The default mode installs databricks-connect, so the resolved pin is present. assert.NotEmpty(t, res.Resolved.DBConnectVersion) - // The artifact came from a successful fetch. The cache is shared (UserCacheDir), - // so a prior run may have seeded it; accept either source rather than assuming - // a cold cache and flaking on re-runs. - assert.Contains(t, []string{"network", "cache"}, res.Resolved.ArtifactSource) // Every phase must have reached ok, including the real provision and validate. for _, ph := range res.Phases { @@ -127,8 +127,12 @@ func TestSetupLocalUnpublishedVersion(t *testing.T) { assert.Equal(t, localenv.ErrEnvUnsupported, res.Error.Code) assert.Equal(t, localenv.PhaseFetch, res.Error.FailurePhase) - // Even a failed fetch must not have provisioned anything on a dry run. - assert.NoFileExists(t, filepath.Join(dir, ".venv", "bin", "python")) + // Even a failed fetch must not have written anything on a dry run: preflight + // skips ensureWritable and cache writes are suppressed, so the project dir + // must still be empty. + entries, err := os.ReadDir(dir) + require.NoError(t, err) + assert.Empty(t, entries, "dry-run wrote files: %v", entries) } // venvPython returns the path to the created virtualenv's interpreter, accounting