Ctrl K
ComunidadGuías…

cli/start_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package cli_test

import (
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/coder/coder/v2/cli/clitest"
	"github.com/coder/coder/v2/coderd/coderdtest"
	"github.com/coder/coder/v2/coderd/database"
	"github.com/coder/coder/v2/coderd/database/dbfake"
	"github.com/coder/coder/v2/coderd/database/dbtestutil"
	"github.com/coder/coder/v2/codersdk"
	"github.com/coder/coder/v2/provisioner/echo"
	"github.com/coder/coder/v2/provisionersdk/proto"
	"github.com/coder/coder/v2/testutil"
	"github.com/coder/coder/v2/testutil/expecter"
)

const (
	ephemeralParameterName        = "ephemeral_parameter"
	ephemeralParameterDescription = "This is ephemeral parameter"
	ephemeralParameterValue       = "3"

	immutableParameterName        = "immutable_parameter"
	immutableParameterDescription = "This is immutable parameter"
	immutableParameterValue       = "abc"

	mutableParameterName  = "mutable_parameter"
	mutableParameterValue = "hello"
)

func mutableParamsResponse() *echo.Responses {
	return &echo.Responses{
		Parse: echo.ParseComplete,
		ProvisionGraph: []*proto.Response{
			{
				Type: &proto.Response_Graph{
					Graph: &proto.GraphComplete{
						Parameters: []*proto.RichParameter{
							{
								Name:        mutableParameterName,
								Description: "This is a mutable parameter",
								Required:    true,
								Mutable:     true,
							},
						},
					},
				},
			},
		},
		ProvisionApply: echo.ApplyComplete,
	}
}

func immutableParamsResponse() *echo.Responses {
	return &echo.Responses{
		Parse: echo.ParseComplete,
		ProvisionGraph: []*proto.Response{
			{
				Type: &proto.Response_Graph{
					Graph: &proto.GraphComplete{
						Parameters: []*proto.RichParameter{
							{
								Name:        immutableParameterName,
								Description: immutableParameterDescription,
								Required:    true,
							},
						},
					},
				},
			},
		},
		ProvisionApply: echo.ApplyComplete,
	}
}

func TestStart(t *testing.T) {
	t.Parallel()

	echoResponses := func() *echo.Responses {
		return &echo.Responses{
			Parse:         echo.ParseComplete,
			ProvisionInit: echo.InitComplete,
			ProvisionPlan: echo.PlanComplete,
			ProvisionGraph: []*proto.Response{
				{
					Type: &proto.Response_Graph{
						Graph: &proto.GraphComplete{
							Parameters: []*proto.RichParameter{
								{
									Name:        ephemeralParameterName,
									Description: ephemeralParameterDescription,
									Mutable:     true,
									Ephemeral:   true,
								},
							},
						},
					},
				},
			},
			ProvisionApply: echo.ApplyComplete,
		}
	}

	t.Run("BuildOptions", func(t *testing.T) {
		t.Parallel()

		logger := testutil.Logger(t)
		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses())
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
		workspace := coderdtest.CreateWorkspace(t, member, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
			request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
				{Name: ephemeralParameterName, Value: "foo"}, // Value is required, set it to something
			}
		})
		coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
		// Stop the workspace
		workspaceBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop, func(request *codersdk.CreateWorkspaceBuildRequest) {
			request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
				{Name: ephemeralParameterName, Value: "foo"}, // Value is required, set it to something
			}
		})
		coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceBuild.ID)

		inv, root := clitest.New(t, "start", workspace.Name, "--prompt-ephemeral-parameters")
		clitest.SetupConfig(t, member, root)
		doneChan := make(chan struct{})
		stdout := expecter.NewAttachedToInvocation(t, inv)
		stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
		ctx := testutil.Context(t, testutil.WaitMedium)
		go func() {
			defer close(doneChan)
			err := inv.Run()
			assert.NoError(t, err)
		}()

		matches := []string{
			ephemeralParameterDescription, ephemeralParameterValue,
			"workspace has been started", "",
		}
		for i := 0; i < len(matches); i += 2 {
			match := matches[i]
			value := matches[i+1]
			stdout.ExpectMatch(ctx, match)

			if value != "" {
				stdin.WriteLine(value)
			}
		}
		<-doneChan

		// Verify if ephemeral parameter is set
		workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
		require.NoError(t, err)
		actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
		require.NoError(t, err)
		require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{
			Name:  ephemeralParameterName,
			Value: ephemeralParameterValue,
		})
	})

	t.Run("EphemeralParameterFlags", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses())
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
		workspace := coderdtest.CreateWorkspace(t, member, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
			request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
				{Name: ephemeralParameterName, Value: "foo"}, // Value is required, set it to something
			}
		})
		coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
		// Stop the workspace
		workspaceBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop, func(request *codersdk.CreateWorkspaceBuildRequest) {
			request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
				{Name: ephemeralParameterName, Value: "foo"}, // Value is required, set it to something
			}
		})
		coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceBuild.ID)

		inv, root := clitest.New(t, "start", workspace.Name,
			"--ephemeral-parameter", fmt.Sprintf("%s=%s", ephemeralParameterName, ephemeralParameterValue))
		clitest.SetupConfig(t, member, root)
		doneChan := make(chan struct{})
		stdout := expecter.NewAttachedToInvocation(t, inv)
		ctx := testutil.Context(t, testutil.WaitMedium)
		go func() {
			defer close(doneChan)
			err := inv.Run()
			assert.NoError(t, err)
		}()

		stdout.ExpectMatch(ctx, "workspace has been started")
		<-doneChan

		// Verify if ephemeral parameter is set
		workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
		require.NoError(t, err)
		actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
		require.NoError(t, err)
		require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{
			Name:  ephemeralParameterName,
			Value: ephemeralParameterValue,
		})
	})
}

func TestStartWithParameters(t *testing.T) {
	t.Parallel()

	t.Run("DoNotAskForImmutables", func(t *testing.T) {
		t.Parallel()

		// Create the workspace
		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, immutableParamsResponse())
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
		workspace := coderdtest.CreateWorkspace(t, member, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
			cwr.RichParameterValues = []codersdk.WorkspaceBuildParameter{
				{
					Name:  immutableParameterName,
					Value: immutableParameterValue,
				},
			}
		})
		coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)

		// Stop the workspace
		workspaceBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop)
		coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceBuild.ID)

		// Start the workspace again
		inv, root := clitest.New(t, "start", workspace.Name)
		clitest.SetupConfig(t, member, root)
		doneChan := make(chan struct{})
		stdout := expecter.NewAttachedToInvocation(t, inv)
		ctx := testutil.Context(t, testutil.WaitMedium)
		go func() {
			defer close(doneChan)
			err := inv.Run()
			assert.NoError(t, err)
		}()

		stdout.ExpectMatch(ctx, "workspace has been started")
		<-doneChan

		// Verify if immutable parameter is set
		workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
		require.NoError(t, err)
		actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
		require.NoError(t, err)
		require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{
			Name:  immutableParameterName,
			Value: immutableParameterValue,
		})
	})

	t.Run("AlwaysPrompt", func(t *testing.T) {
		t.Parallel()

		logger := testutil.Logger(t)
		// Create the workspace
		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, mutableParamsResponse())
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
		workspace := coderdtest.CreateWorkspace(t, member, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
			cwr.RichParameterValues = []codersdk.WorkspaceBuildParameter{
				{
					Name:  mutableParameterName,
					Value: mutableParameterValue,
				},
			}
		})
		coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)

		// Stop the workspace
		workspaceBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop)
		coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceBuild.ID)

		// Start the workspace again
		inv, root := clitest.New(t, "start", workspace.Name, "--always-prompt")
		clitest.SetupConfig(t, member, root)
		doneChan := make(chan struct{})
		stdout := expecter.NewAttachedToInvocation(t, inv)
		stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
		ctx := testutil.Context(t, testutil.WaitMedium)
		go func() {
			defer close(doneChan)
			err := inv.Run()
			assert.NoError(t, err)
		}()

		newValue := "xyz"
		stdout.ExpectMatch(ctx, mutableParameterName)
		stdin.WriteLine(newValue)
		stdout.ExpectMatch(ctx, "workspace has been started")
		<-doneChan

		// Verify that the updated values are persisted.
		workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
		require.NoError(t, err)
		actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
		require.NoError(t, err)
		require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{
			Name:  mutableParameterName,
			Value: newValue,
		})
	})
}

func TestStartUseParameterDefaults(t *testing.T) {
	t.Parallel()

	client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
	owner := coderdtest.CreateFirstUser(t, client)
	member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)

	// Create a template with no parameters and a workspace that
	// auto-updates so `start` picks up the new active version.
	version1 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
	coderdtest.AwaitTemplateVersionJobCompleted(t, client, version1.ID)
	template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version1.ID)
	workspace := coderdtest.CreateWorkspace(t, member, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
		cwr.AutomaticUpdates = codersdk.AutomaticUpdatesAlways
	})
	coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)

	// Stop the workspace.
	coderdtest.MustTransitionWorkspace(t, member, workspace.ID,
		codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)

	// Push a new template version that adds a parameter with a default.
	version2 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID,
		prepareEchoResponses([]*proto.RichParameter{
			{Name: "new_param", Type: "string", Mutable: true, DefaultValue: "foobar"},
		}), func(ctvr *codersdk.CreateTemplateVersionRequest) {
			ctvr.TemplateID = template.ID
		})
	coderdtest.AwaitTemplateVersionJobCompleted(t, client, version2.ID)
	ctx := testutil.Context(t, testutil.WaitLong)
	err := client.UpdateActiveTemplateVersion(ctx, template.ID, codersdk.UpdateActiveTemplateVersion{ID: version2.ID})
	require.NoError(t, err)

	// Start the workspace with --use-parameter-defaults.
	// The new parameter should be auto-accepted.
	inv, root := clitest.New(t, "start", workspace.Name, "--use-parameter-defaults")
	clitest.SetupConfig(t, member, root)
	stdout := expecter.NewAttachedToInvocation(t, inv)
	doneChan := make(chan struct{})
	go func() {
		defer close(doneChan)
		err := inv.Run()
		assert.NoError(t, err)
	}()

	stdout.ExpectMatch(ctx, "workspace has been started")
	_ = testutil.TryReceive(ctx, t, doneChan)

	// Verify the new parameter was resolved to its default.
	ws, err := member.WorkspaceByOwnerAndName(ctx, codersdk.Me, workspace.Name, codersdk.WorkspaceOptions{})
	require.NoError(t, err)
	buildParams, err := member.WorkspaceBuildParameters(ctx, ws.LatestBuild.ID)
	require.NoError(t, err)
	assert.Contains(t, buildParams, codersdk.WorkspaceBuildParameter{Name: "new_param", Value: "foobar"})
}

// TestStartAutoUpdate also tests restart since the flows are virtually identical.
func TestStartAutoUpdate(t *testing.T) {
	t.Parallel()

	const (
		stringParameterName  = "myparam"
		stringParameterValue = "abc"
	)

	stringRichParameters := []*proto.RichParameter{
		{Name: stringParameterName, Type: "string", Mutable: true, Required: true},
	}

	type testcase struct {
		Name string
		Cmd  string
	}

	cases := []testcase{
		{
			Name: "StartOK",
			Cmd:  "start",
		},
		{
			Name: "RestartOK",
			Cmd:  "restart",
		},
	}

	for _, c := range cases {
		t.Run(c.Name, func(t *testing.T) {
			t.Parallel()

			logger := testutil.Logger(t)
			client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
			owner := coderdtest.CreateFirstUser(t, client)
			member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
			version1 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) {
				ctvr.Name = "v1"
			})
			coderdtest.AwaitTemplateVersionJobCompleted(t, client, version1.ID)
			template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version1.ID)
			workspace := coderdtest.CreateWorkspace(t, member, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
				cwr.AutomaticUpdates = codersdk.AutomaticUpdatesAlways
			})
			coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)

			if c.Cmd == "start" {
				coderdtest.MustTransitionWorkspace(t, member, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
			}
			version2 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses(stringRichParameters), func(ctvr *codersdk.CreateTemplateVersionRequest) {
				ctvr.Name = "v2"
				ctvr.TemplateID = template.ID
			})
			coderdtest.AwaitTemplateVersionJobCompleted(t, client, version2.ID)
			coderdtest.UpdateActiveTemplateVersion(t, client, template.ID, version2.ID)

			inv, root := clitest.New(t, c.Cmd, "-y", workspace.Name)
			clitest.SetupConfig(t, member, root)
			doneChan := make(chan struct{})
			stdout := expecter.NewAttachedToInvocation(t, inv)
			stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
			ctx := testutil.Context(t, testutil.WaitMedium)
			go func() {
				defer close(doneChan)
				err := inv.Run()
				assert.NoError(t, err)
			}()

			stdout.ExpectMatch(ctx, stringParameterName)
			stdin.WriteLine(stringParameterValue)
			<-doneChan

			workspace = coderdtest.MustWorkspace(t, member, workspace.ID)
			require.Equal(t, version2.ID, workspace.LatestBuild.TemplateVersionID)
		})
	}
}

func TestStart_AlreadyRunning(t *testing.T) {
	t.Parallel()
	ctx := testutil.Context(t, testutil.WaitShort)

	client, db := coderdtest.NewWithDatabase(t, nil)
	owner := coderdtest.CreateFirstUser(t, client)
	memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
	r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
		OwnerID:        member.ID,
		OrganizationID: owner.OrganizationID,
	}).Do()

	inv, root := clitest.New(t, "start", r.Workspace.Name)
	clitest.SetupConfig(t, memberClient, root)
	doneChan := make(chan struct{})
	stdout := expecter.NewAttachedToInvocation(t, inv)
	go func() {
		defer close(doneChan)
		err := inv.Run()
		assert.NoError(t, err)
	}()

	stdout.ExpectMatch(ctx, "workspace is already running")
	_ = testutil.TryReceive(ctx, t, doneChan)
}

func TestStart_Starting(t *testing.T) {
	t.Parallel()
	ctx := testutil.Context(t, testutil.WaitShort)

	store, ps := dbtestutil.NewDB(t)
	client := coderdtest.New(t, &coderdtest.Options{Pubsub: ps, Database: store})
	owner := coderdtest.CreateFirstUser(t, client)
	memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
	r := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{
		OwnerID:        member.ID,
		OrganizationID: owner.OrganizationID,
	}).
		Starting().
		Do()

	inv, root := clitest.New(t, "start", r.Workspace.Name)
	clitest.SetupConfig(t, memberClient, root)
	doneChan := make(chan struct{})
	stdout := expecter.NewAttachedToInvocation(t, inv)
	go func() {
		defer close(doneChan)
		err := inv.Run()
		assert.NoError(t, err)
	}()

	stdout.ExpectMatch(ctx, "workspace is already starting")

	_ = dbfake.JobComplete(t, store, r.Build.JobID).Pubsub(ps).Do()
	stdout.ExpectMatch(ctx, "workspace has been started")

	_ = testutil.TryReceive(ctx, t, doneChan)
}

func TestStart_NoWait(t *testing.T) {
	t.Parallel()
	ctx := testutil.Context(t, testutil.WaitShort)

	// Prepare user, template, workspace
	client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
	owner := coderdtest.CreateFirstUser(t, client)
	member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
	version1 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
	coderdtest.AwaitTemplateVersionJobCompleted(t, client, version1.ID)
	template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version1.ID)
	workspace := coderdtest.CreateWorkspace(t, member, template.ID)
	coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)

	// Stop the workspace
	build := coderdtest.CreateWorkspaceBuild(t, member, workspace, database.WorkspaceTransitionStop)
	coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)

	// Start in no-wait mode
	inv, root := clitest.New(t, "start", workspace.Name, "--no-wait")
	clitest.SetupConfig(t, member, root)
	doneChan := make(chan struct{})
	stdout := expecter.NewAttachedToInvocation(t, inv)
	go func() {
		defer close(doneChan)
		err := inv.Run()
		assert.NoError(t, err)
	}()

	stdout.ExpectMatch(ctx, "workspace has been started in no-wait mode")
	_ = testutil.TryReceive(ctx, t, doneChan)
}

func TestStart_WithReason(t *testing.T) {
	t.Parallel()
	ctx := testutil.Context(t, testutil.WaitShort)

	// Prepare user, template, workspace
	client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
	owner := coderdtest.CreateFirstUser(t, client)
	member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
	version1 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
	coderdtest.AwaitTemplateVersionJobCompleted(t, client, version1.ID)
	template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version1.ID)
	workspace := coderdtest.CreateWorkspace(t, member, template.ID)
	coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)

	// Stop the workspace
	build := coderdtest.CreateWorkspaceBuild(t, member, workspace, database.WorkspaceTransitionStop)
	coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)

	// Start the workspace with reason
	inv, root := clitest.New(t, "start", workspace.Name, "--reason", "cli")
	clitest.SetupConfig(t, member, root)
	doneChan := make(chan struct{})
	stdout := expecter.NewAttachedToInvocation(t, inv)
	go func() {
		defer close(doneChan)
		err := inv.Run()
		assert.NoError(t, err)
	}()

	stdout.ExpectMatch(ctx, "workspace has been started")
	_ = testutil.TryReceive(ctx, t, doneChan)

	workspace = coderdtest.MustWorkspace(t, member, workspace.ID)
	require.Equal(t, codersdk.BuildReasonCLI, workspace.LatestBuild.Reason)
}

func TestStart_FailedStartCleansUp(t *testing.T) {
	t.Parallel()
	for _, name := range []string{"Default", "TemplateVersion"} {
		t.Run(name, func(t *testing.T) {
			t.Parallel()
			ctx := testutil.Context(t, testutil.WaitLong)

			store, ps := dbtestutil.NewDB(t)
			client := coderdtest.New(t, &coderdtest.Options{
				Database:                 store,
				Pubsub:                   ps,
				IncludeProvisionerDaemon: true,
			})
			owner := coderdtest.CreateFirstUser(t, client)
			memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)

			version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
			coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
			template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
			workspace := coderdtest.CreateWorkspace(t, memberClient, template.ID)
			coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)

			target := version
			if name == "TemplateVersion" {
				target = coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil,
					func(req *codersdk.CreateTemplateVersionRequest) {
						req.TemplateID = template.ID
					})
				coderdtest.AwaitTemplateVersionJobCompleted(t, client, target.ID)
			}

			// Insert a failed start build directly into the database so that
			// the workspace's latest build is a failed "start" transition.
			failed := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{
				ID:             workspace.ID,
				OwnerID:        member.ID,
				OrganizationID: owner.OrganizationID,
				TemplateID:     template.ID,
			}).
				Seed(database.WorkspaceBuild{
					TemplateVersionID: version.ID,
					Transition:        database.WorkspaceTransitionStart,
					BuildNumber:       workspace.LatestBuild.BuildNumber + 1,
				}).
				Failed().
				Do()

			args := []string{"start", workspace.Name}
			if name == "TemplateVersion" {
				args = append(args, "--template-version", target.Name)
			}
			inv, root := clitest.New(t, args...)
			clitest.SetupConfig(t, memberClient, root)
			stdout := expecter.NewAttachedToInvocation(t, inv)
			doneChan := make(chan struct{})
			go func() {
				defer close(doneChan)
				err := inv.Run()
				assert.NoError(t, err)
			}()

			// The CLI should detect the failed start and clean up first.
			stdout.ExpectMatch(ctx, "Cleaning up before retrying")
			stdout.ExpectMatch(ctx, "workspace has been started")

			_ = testutil.TryReceive(ctx, t, doneChan)
			cleanup, err := memberClient.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber(
				ctx, workspace.OwnerName, workspace.Name, fmt.Sprint(failed.Build.BuildNumber+1))
			require.NoError(t, err)
			require.Equal(t, codersdk.WorkspaceTransitionStop, cleanup.Transition)
			require.Equal(t, version.ID, cleanup.TemplateVersionID)
			after := coderdtest.MustWorkspace(t, memberClient, workspace.ID)
			require.Equal(t, failed.Build.BuildNumber+2, after.LatestBuild.BuildNumber)
			require.Equal(t, codersdk.WorkspaceTransitionStart, after.LatestBuild.Transition)
			require.Equal(t, target.ID, after.LatestBuild.TemplateVersionID)
		})
	}
}