Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions coderd/exp_chats.go
Original file line number Diff line number Diff line change
Expand Up @@ -3202,6 +3202,11 @@ func (api *API) chatCreateWorkspace(
// chatStartWorkspace starts a stopped workspace by creating a new
// build with the "start" transition. It mirrors chatCreateWorkspace
// but for the start path.
//
// Aliased as ChatStartWorkspace in coderd/export_test.go so external
// tests in the coderd_test package can drive the auto-update path
// end-to-end. The proper fix is to extract the request building into
// a pure function; tracked in CODAGT-292.
func (api *API) chatStartWorkspace(
ctx context.Context,
ownerID uuid.UUID,
Expand Down
53 changes: 53 additions & 0 deletions coderd/exp_chats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12187,6 +12187,59 @@ func TestPostChats_DynamicToolValidation(t *testing.T) {
})
}

// requireActiveVersionStore always returns RequireActiveVersion: true so
// tests can exercise relevant code paths without an enterprise license.
type requireActiveVersionStore struct{}

func (requireActiveVersionStore) GetTemplateAccessControl(_ database.Template) dbauthz.TemplateAccessControl {
return dbauthz.TemplateAccessControl{RequireActiveVersion: true}
}

func (requireActiveVersionStore) SetTemplateAccessControl(_ context.Context, _ database.Store, _ uuid.UUID, _ dbauthz.TemplateAccessControl) error {
return nil
}

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

ctx := testutil.Context(t, testutil.WaitLong)
rawClient, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{})
var store dbauthz.AccessControlStore = requireActiveVersionStore{}
api.AccessControlStore.Store(&store)
db := api.Database
user := coderdtest.CreateFirstUser(t, rawClient)

// Given: active template version v1 plus workspace stopped on v1.
wsResp := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
OwnerID: user.UserID,
OrganizationID: user.OrganizationID,
}).Seed(database.WorkspaceBuild{
Transition: database.WorkspaceTransitionStop,
}).Do()
tmplID := wsResp.Workspace.TemplateID
v1ID := wsResp.Build.TemplateVersionID

// Given: a new active version v2 is published.
v2Resp := dbfake.TemplateVersion(t, db).Seed(database.TemplateVersion{
TemplateID: uuid.NullUUID{UUID: tmplID, Valid: true},
OrganizationID: user.OrganizationID,
CreatedBy: user.UserID,
}).Do()
v2 := v2Resp.TemplateVersion
require.NotEqual(t, v1ID, v2.ID, "v2 must differ from v1")

// When: we start the workspace through chatStartWorkspace.
build, err := coderd.ChatStartWorkspace(api, ctx, user.UserID, wsResp.Workspace.ID,
codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStart,
})

// Then: the build is auto-updated to the active version.
require.NoError(t, err)
require.Equal(t, v2.ID, build.TemplateVersionID, "build must be on the active version")
require.Nil(t, build.TemplateVersionPresetID, "no preset must be applied")
}

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

Expand Down
9 changes: 9 additions & 0 deletions coderd/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,12 @@ package coderd

// InsertAgentChatTestModelConfig exposes insertAgentChatTestModelConfig for external tests.
var InsertAgentChatTestModelConfig = insertAgentChatTestModelConfig

// ChatStartWorkspace exposes chatStartWorkspace for external tests.
//
// chatStartWorkspace is intentionally unexported to keep symmetry with
// its sister chatCreateWorkspace. The alias lets external tests drive
// the RequireActiveVersion auto-update path end-to-end without
// stubbing the entire DB layer. The proper fix is to extract a pure
// request builder; tracked in CODAGT-292.
var ChatStartWorkspace = (*API).chatStartWorkspace
18 changes: 17 additions & 1 deletion coderd/x/chatd/chattool/createworkspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ type createWorkspaceArgs struct {
TemplateID string `json:"template_id" description:"The UUIDv4 of the template to create the workspace from. Obtain this from list_templates."`
Name string `json:"name,omitempty" description:"The name of the workspace to create. If not provided, a random name will be generated."`
Parameters map[string]string `json:"parameters,omitempty" description:"Key-value pairs of template parameters to use when creating the workspace. Obtain available parameters from read_template."`
PresetID string `json:"preset_id,omitempty" description:"The UUIDv4 of a template version preset to use. Obtain available presets from read_template. When provided, the preset's parameters are applied automatically and the workspace may claim a prebuilt instance for faster startup."`
}

// CreateWorkspace returns a tool that creates a new workspace from a
Expand All @@ -91,7 +92,10 @@ func CreateWorkspace(organizationID uuid.UUID, db database.Store, options Create
"template_id (from list_templates). Optionally provide "+
"a name and parameter values (from read_template). "+
"If no name is given, one will be generated. "+
"This tool is idempotent — if the chat already has a "+
"Provide a preset_id (from read_template) to apply "+
"preset parameters and potentially claim a prebuilt "+
"workspace for faster startup. "+
"This tool is idempotent. If the chat already has a "+
"workspace that is building or running, the existing "+
"workspace is returned.",
func(ctx context.Context, args createWorkspaceArgs, _ fantasy.ToolCall) (fantasy.ToolResponse, error) {
Expand Down Expand Up @@ -184,6 +188,18 @@ func CreateWorkspace(organizationID uuid.UUID, db database.Store, options Create
TTLMillis: ttlMs,
}

// Apply preset if provided.
presetIDStr := strings.TrimSpace(args.PresetID)
if presetIDStr != "" {
presetID, err := uuid.Parse(presetIDStr)
if err != nil {
return fantasy.NewTextErrorResponse(
xerrors.Errorf("invalid preset_id: %w", err).Error(),
), nil
}
createReq.TemplateVersionPresetID = presetID
}

name := strings.TrimSpace(args.Name)
if name == "" {
name = generatedWorkspaceName(tmpl.Name)
Expand Down
239 changes: 239 additions & 0 deletions coderd/x/chatd/chattool/createworkspace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1332,3 +1332,242 @@ func TestCreateWorkspace_OnChatUpdatedFiresAfterBuild(t *testing.T) {
func validNullTime(t time.Time) sql.NullTime {
return sql.NullTime{Time: t, Valid: true}
}

// createWorkspacePresetTestSetup holds common test dependencies
// for create_workspace preset tests.
type createWorkspacePresetTestSetup struct {
DB *dbmock.MockStore
OwnerID uuid.UUID
OrgID uuid.UUID
TemplateID uuid.UUID
ChatID uuid.UUID
WorkspaceID uuid.UUID
BuildID uuid.UUID
AgentID uuid.UUID
}

// setupCreateWorkspacePresetTest creates common mock expectations
// for preset-related create_workspace tests. It sets up RBAC,
// template lookup, TTL, and chat lookup.
func setupCreateWorkspacePresetTest(t *testing.T) createWorkspacePresetTestSetup {
t.Helper()

ctrl := gomock.NewController(t)
db := dbmock.NewMockStore(ctrl)

s := createWorkspacePresetTestSetup{
DB: db,
OwnerID: uuid.New(),
OrgID: uuid.New(),
TemplateID: uuid.New(),
ChatID: uuid.New(),
WorkspaceID: uuid.New(),
BuildID: uuid.New(),
AgentID: uuid.New(),
}

// RBAC.
db.EXPECT().
GetAuthorizationUserRoles(gomock.Any(), s.OwnerID).
Return(database.GetAuthorizationUserRolesRow{
ID: s.OwnerID,
Username: "testuser",
Status: "active",
}, nil)

// Template lookup.
db.EXPECT().
GetTemplateByID(gomock.Any(), s.TemplateID).
Return(database.Template{
ID: s.TemplateID,
OrganizationID: s.OrgID,
Name: "test-template",
ActiveVersionID: uuid.New(),
}, nil)

// Chat workspace TTL.
db.EXPECT().
GetChatWorkspaceTTL(gomock.Any()).
Return("", sql.ErrNoRows)

// Check for existing workspace (no existing).
db.EXPECT().
GetChatByID(gomock.Any(), s.ChatID).
Return(database.Chat{ID: s.ChatID}, nil)

return s
}

// expectSuccessfulBuild adds mock expectations for a successful
// build, agent lookup, and agent lifecycle check.
func (s createWorkspacePresetTestSetup) expectSuccessfulBuild() {
s.DB.EXPECT().
UpdateChatWorkspaceBinding(gomock.Any(), gomock.Any()).
Return(database.Chat{ID: s.ChatID}, nil)

s.DB.EXPECT().
GetWorkspaceBuildByID(gomock.Any(), s.BuildID).
Return(database.WorkspaceBuild{
ID: s.BuildID,
JobID: uuid.New(),
}, nil)
s.DB.EXPECT().
GetProvisionerJobByID(gomock.Any(), gomock.Any()).
Return(database.ProvisionerJob{
JobStatus: database.ProvisionerJobStatusSucceeded,
}, nil)

s.DB.EXPECT().
GetWorkspaceAgentsInLatestBuildByWorkspaceID(gomock.Any(), s.WorkspaceID).
Return([]database.WorkspaceAgent{{
ID: s.AgentID,
Name: "main",
}}, nil)

s.DB.EXPECT().
GetWorkspaceAgentLifecycleStateByID(gomock.Any(), s.AgentID).
Return(database.GetWorkspaceAgentLifecycleStateByIDRow{
LifecycleState: database.WorkspaceAgentLifecycleStateReady,
}, nil)
}

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

s := setupCreateWorkspacePresetTest(t)
s.expectSuccessfulBuild()

presetID := uuid.New()

var capturedReq codersdk.CreateWorkspaceRequest
createFn := func(_ context.Context, _ uuid.UUID, req codersdk.CreateWorkspaceRequest) (codersdk.Workspace, error) {
capturedReq = req
return codersdk.Workspace{
ID: s.WorkspaceID,
Name: req.Name,
LatestBuild: codersdk.WorkspaceBuild{
ID: s.BuildID,
},
}, nil
}

agentConnFn := func(_ context.Context, _ uuid.UUID) (workspacesdk.AgentConn, func(), error) {
return nil, func() {}, nil
}

tool := CreateWorkspace(s.OrgID, s.DB, CreateWorkspaceOptions{
OwnerID: s.OwnerID,
ChatID: s.ChatID,
CreateFn: createFn,
AgentConnFn: agentConnFn,
WorkspaceMu: &sync.Mutex{},
Logger: slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}),
})

input := fmt.Sprintf(
`{"template_id":%q,"preset_id":%q,"name":"test-ws"}`,
s.TemplateID.String(), presetID.String(),
)

ctx := context.Background()
resp, err := tool.Run(ctx, fantasy.ToolCall{
ID: "call-preset",
Name: "create_workspace",
Input: input,
})
require.NoError(t, err)
require.False(t, resp.IsError, "unexpected error: %s", resp.Content)

require.Equal(t, presetID, capturedReq.TemplateVersionPresetID,
"expected preset ID to be set on CreateWorkspaceRequest")
}

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

s := setupCreateWorkspacePresetTest(t)

tool := CreateWorkspace(s.OrgID, s.DB, CreateWorkspaceOptions{
OwnerID: s.OwnerID,
ChatID: s.ChatID,
CreateFn: func(_ context.Context, _ uuid.UUID, _ codersdk.CreateWorkspaceRequest) (codersdk.Workspace, error) {
t.Fatal("CreateFn should not be called with invalid preset_id")
return codersdk.Workspace{}, nil
},
WorkspaceMu: &sync.Mutex{},
Logger: slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}),
})

input := fmt.Sprintf(
`{"template_id":%q,"preset_id":"not-a-uuid","name":"test-ws"}`,
s.TemplateID.String(),
)

ctx := context.Background()
resp, err := tool.Run(ctx, fantasy.ToolCall{
ID: "call-bad-preset",
Name: "create_workspace",
Input: input,
})
require.NoError(t, err)
require.True(t, resp.IsError)
require.Contains(t, resp.Content, "invalid preset_id")
}

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

s := setupCreateWorkspacePresetTest(t)
s.expectSuccessfulBuild()

presetID := uuid.New()

var capturedReq codersdk.CreateWorkspaceRequest
createFn := func(_ context.Context, _ uuid.UUID, req codersdk.CreateWorkspaceRequest) (codersdk.Workspace, error) {
capturedReq = req
return codersdk.Workspace{
ID: s.WorkspaceID,
Name: req.Name,
LatestBuild: codersdk.WorkspaceBuild{
ID: s.BuildID,
},
}, nil
}

agentConnFn := func(_ context.Context, _ uuid.UUID) (workspacesdk.AgentConn, func(), error) {
return nil, func() {}, nil
}

tool := CreateWorkspace(s.OrgID, s.DB, CreateWorkspaceOptions{
OwnerID: s.OwnerID,
ChatID: s.ChatID,
CreateFn: createFn,
AgentConnFn: agentConnFn,
WorkspaceMu: &sync.Mutex{},
Logger: slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}),
})

input := fmt.Sprintf(
`{"template_id":%q,"preset_id":%q,"name":"test-ws","parameters":{"region":"us-east"}}`,
s.TemplateID.String(), presetID.String(),
)

ctx := context.Background()
resp, err := tool.Run(ctx, fantasy.ToolCall{
ID: "call-preset-params",
Name: "create_workspace",
Input: input,
})
require.NoError(t, err)
require.False(t, resp.IsError, "unexpected error: %s", resp.Content)

// Verify preset ID is set.
require.Equal(t, presetID, capturedReq.TemplateVersionPresetID,
"expected preset ID to be set")

// Verify parameters are also populated.
require.Len(t, capturedReq.RichParameterValues, 1,
"expected rich parameter values to be set")
require.Equal(t, "region", capturedReq.RichParameterValues[0].Name)
require.Equal(t, "us-east", capturedReq.RichParameterValues[0].Value)
}
Loading
Loading