Skip to content

Commit 2432a30

Browse files
committed
endpoint
1 parent b5ab9d3 commit 2432a30

File tree

6 files changed

+229
-1
lines changed

6 files changed

+229
-1
lines changed

coderd/database/dbmem/dbmem.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13233,6 +13233,17 @@ func (q *FakeQuerier) GetAuthorizedTemplates(ctx context.Context, arg database.G
1323313233
continue
1323413234
}
1323513235
}
13236+
13237+
if arg.HasAITask.Valid {
13238+
build, err := q.getTemplateVersionByIDNoLock(ctx, template.ActiveVersionID)
13239+
if err != nil {
13240+
return nil, xerrors.Errorf("get latest build: %w", err)
13241+
}
13242+
if build.HasAITask != arg.HasAITask.Bool {
13243+
continue
13244+
}
13245+
}
13246+
1323613247
templates = append(templates, template)
1323713248
}
1323813249
if len(templates) > 0 {
@@ -13562,6 +13573,16 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
1356213573
}
1356313574
}
1356413575

13576+
if arg.HasAITask.Valid {
13577+
build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspace.ID)
13578+
if err != nil {
13579+
return nil, xerrors.Errorf("get latest build: %w", err)
13580+
}
13581+
if build.HasAITask != arg.HasAITask.Bool {
13582+
continue
13583+
}
13584+
}
13585+
1356513586
// If the filter exists, ensure the object is authorized.
1356613587
if prepared != nil && prepared.Authorize(ctx, workspace.RBACObject()) != nil {
1356713588
continue

coderd/searchquery/search.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ func Workspaces(ctx context.Context, db database.Store, query string, page coder
146146
// which will return all workspaces.
147147
Valid: values.Has("outdated"),
148148
}
149+
filter.HasAITask = parser.NullableBoolean(values, sql.NullBool{}, "has-ai-task")
149150
filter.OrganizationID = parseOrganization(ctx, db, parser, values, "organization")
150151

151152
type paramMatch struct {
@@ -206,6 +207,7 @@ func Templates(ctx context.Context, db database.Store, query string) (database.G
206207
IDs: parser.UUIDs(values, []uuid.UUID{}, "ids"),
207208
Deprecated: parser.NullableBoolean(values, sql.NullBool{}, "deprecated"),
208209
OrganizationID: parseOrganization(ctx, db, parser, values, "organization"),
210+
HasAITask: parser.NullableBoolean(values, sql.NullBool{}, "has-ai-task"),
209211
}
210212

211213
parser.ErrorExcessParams(values)

coderd/searchquery/search_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,36 @@ func TestSearchWorkspace(t *testing.T) {
222222
OrganizationID: uuid.MustParse("08eb6715-02f8-45c5-b86d-03786fcfbb4e"),
223223
},
224224
},
225+
{
226+
Name: "HasAITaskTrue",
227+
Query: "has-ai-task:true",
228+
Expected: database.GetWorkspacesParams{
229+
HasAITask: sql.NullBool{
230+
Bool: true,
231+
Valid: true,
232+
},
233+
},
234+
},
235+
{
236+
Name: "HasAITaskFalse",
237+
Query: "has-ai-task:false",
238+
Expected: database.GetWorkspacesParams{
239+
HasAITask: sql.NullBool{
240+
Bool: false,
241+
Valid: true,
242+
},
243+
},
244+
},
245+
{
246+
Name: "HasAITaskMissing",
247+
Query: "",
248+
Expected: database.GetWorkspacesParams{
249+
HasAITask: sql.NullBool{
250+
Bool: false,
251+
Valid: false,
252+
},
253+
},
254+
},
225255

226256
// Failures
227257
{
@@ -559,6 +589,36 @@ func TestSearchTemplates(t *testing.T) {
559589
FuzzyName: "foobar",
560590
},
561591
},
592+
{
593+
Name: "HasAITaskTrue",
594+
Query: "has-ai-task:true",
595+
Expected: database.GetTemplatesWithFilterParams{
596+
HasAITask: sql.NullBool{
597+
Bool: true,
598+
Valid: true,
599+
},
600+
},
601+
},
602+
{
603+
Name: "HasAITaskFalse",
604+
Query: "has-ai-task:false",
605+
Expected: database.GetTemplatesWithFilterParams{
606+
HasAITask: sql.NullBool{
607+
Bool: false,
608+
Valid: true,
609+
},
610+
},
611+
},
612+
{
613+
Name: "HasAITaskMissing",
614+
Query: "",
615+
Expected: database.GetTemplatesWithFilterParams{
616+
HasAITask: sql.NullBool{
617+
Bool: false,
618+
Valid: false,
619+
},
620+
},
621+
},
562622
}
563623

564624
for _, c := range testCases {

coderd/templates_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/coder/coder/v2/coderd/coderdtest"
1717
"github.com/coder/coder/v2/coderd/database"
1818
"github.com/coder/coder/v2/coderd/database/dbauthz"
19+
"github.com/coder/coder/v2/coderd/database/dbgen"
1920
"github.com/coder/coder/v2/coderd/database/dbtestutil"
2021
"github.com/coder/coder/v2/coderd/database/dbtime"
2122
"github.com/coder/coder/v2/coderd/notifications"
@@ -1809,3 +1810,66 @@ func TestTemplateNotifications(t *testing.T) {
18091810
})
18101811
})
18111812
}
1813+
1814+
func TestTemplateFilterHasAITask(t *testing.T) {
1815+
t.Parallel()
1816+
1817+
db, pubsub := dbtestutil.NewDB(t)
1818+
client := coderdtest.New(t, &coderdtest.Options{
1819+
Database: db,
1820+
Pubsub: pubsub,
1821+
IncludeProvisionerDaemon: true,
1822+
})
1823+
user := coderdtest.CreateFirstUser(t, client)
1824+
1825+
jobWithAI := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
1826+
OrganizationID: user.OrganizationID,
1827+
InitiatorID: user.UserID,
1828+
Tags: database.StringMap{},
1829+
Type: database.ProvisionerJobTypeTemplateVersionImport,
1830+
})
1831+
jobWithoutAI := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
1832+
OrganizationID: user.OrganizationID,
1833+
InitiatorID: user.UserID,
1834+
Tags: database.StringMap{},
1835+
Type: database.ProvisionerJobTypeTemplateVersionImport,
1836+
})
1837+
versionWithAI := dbgen.TemplateVersion(t, db, database.TemplateVersion{
1838+
OrganizationID: user.OrganizationID,
1839+
CreatedBy: user.UserID,
1840+
HasAITask: true,
1841+
JobID: jobWithAI.ID,
1842+
})
1843+
versionWithoutAI := dbgen.TemplateVersion(t, db, database.TemplateVersion{
1844+
OrganizationID: user.OrganizationID,
1845+
CreatedBy: user.UserID,
1846+
HasAITask: false,
1847+
JobID: jobWithoutAI.ID,
1848+
})
1849+
templateWithAI := coderdtest.CreateTemplate(t, client, user.OrganizationID, versionWithAI.ID)
1850+
templateWithoutAI := coderdtest.CreateTemplate(t, client, user.OrganizationID, versionWithoutAI.ID)
1851+
1852+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
1853+
defer cancel()
1854+
1855+
// Test filtering
1856+
templates, err := client.Templates(ctx, codersdk.TemplateFilter{
1857+
SearchQuery: "has-ai-task:true",
1858+
})
1859+
require.NoError(t, err)
1860+
require.Len(t, templates, 1)
1861+
require.Equal(t, templateWithAI.ID, templates[0].ID)
1862+
1863+
templates, err = client.Templates(ctx, codersdk.TemplateFilter{
1864+
SearchQuery: "has-ai-task:false",
1865+
})
1866+
require.NoError(t, err)
1867+
require.Len(t, templates, 1)
1868+
require.Equal(t, templateWithoutAI.ID, templates[0].ID)
1869+
1870+
templates, err = client.Templates(ctx, codersdk.TemplateFilter{})
1871+
require.NoError(t, err)
1872+
require.Len(t, templates, 2)
1873+
require.Contains(t, templates, templateWithAI)
1874+
require.Contains(t, templates, templateWithoutAI)
1875+
}

coderd/workspaces.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) {
136136
// @Security CoderSessionToken
137137
// @Produce json
138138
// @Tags Workspaces
139-
// @Param q query string false "Search query in the format `key:value`. Available keys are: owner, template, name, status, has-agent, dormant, last_used_after, last_used_before."
139+
// @Param q query string false "Search query in the format `key:value`. Available keys are: owner, template, name, status, has-agent, dormant, last_used_after, last_used_before, has-ai-task."
140140
// @Param limit query int false "Page limit"
141141
// @Param offset query int false "Page offset"
142142
// @Success 200 {object} codersdk.WorkspacesResponse

coderd/workspaces_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4494,3 +4494,84 @@ func TestOIDCRemoved(t *testing.T) {
44944494
require.NoError(t, err, "delete the workspace")
44954495
coderdtest.AwaitWorkspaceBuildJobCompleted(t, owner, deleteBuild.ID)
44964496
}
4497+
4498+
func TestWorkspaceFilterHasAITask(t *testing.T) {
4499+
t.Parallel()
4500+
4501+
db, pubsub := dbtestutil.NewDB(t)
4502+
client := coderdtest.New(t, &coderdtest.Options{
4503+
Database: db,
4504+
Pubsub: pubsub,
4505+
IncludeProvisionerDaemon: true,
4506+
})
4507+
user := coderdtest.CreateFirstUser(t, client)
4508+
4509+
// Create template versions with different HasAITask values
4510+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
4511+
4512+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
4513+
4514+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
4515+
4516+
wsWithAI := dbgen.Workspace(t, db, database.WorkspaceTable{
4517+
OwnerID: user.UserID,
4518+
OrganizationID: user.OrganizationID,
4519+
TemplateID: template.ID,
4520+
})
4521+
jobWithAI := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
4522+
OrganizationID: user.OrganizationID,
4523+
InitiatorID: user.UserID,
4524+
Tags: database.StringMap{},
4525+
})
4526+
dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
4527+
WorkspaceID: wsWithAI.ID,
4528+
TemplateVersionID: version.ID,
4529+
InitiatorID: user.UserID,
4530+
JobID: jobWithAI.ID,
4531+
BuildNumber: 1,
4532+
HasAITask: true,
4533+
})
4534+
4535+
wsWithoutAI := dbgen.Workspace(t, db, database.WorkspaceTable{
4536+
OwnerID: user.UserID,
4537+
OrganizationID: user.OrganizationID,
4538+
TemplateID: template.ID,
4539+
})
4540+
jobWithoutAI := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
4541+
OrganizationID: user.OrganizationID,
4542+
InitiatorID: user.UserID,
4543+
Tags: database.StringMap{},
4544+
})
4545+
dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
4546+
WorkspaceID: wsWithoutAI.ID,
4547+
TemplateVersionID: version.ID,
4548+
InitiatorID: user.UserID,
4549+
JobID: jobWithoutAI.ID,
4550+
BuildNumber: 1,
4551+
HasAITask: false,
4552+
})
4553+
4554+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
4555+
defer cancel()
4556+
4557+
// Test filtering for workspaces with AI tasks
4558+
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
4559+
FilterQuery: "has-ai-task:true",
4560+
})
4561+
require.NoError(t, err)
4562+
require.Len(t, res.Workspaces, 1)
4563+
require.Equal(t, wsWithAI.ID, res.Workspaces[0].ID)
4564+
4565+
// Test filtering for workspaces without AI tasks
4566+
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
4567+
FilterQuery: "has-ai-task:false",
4568+
})
4569+
require.NoError(t, err)
4570+
require.Len(t, res.Workspaces, 1)
4571+
require.Equal(t, wsWithoutAI.ID, res.Workspaces[0].ID)
4572+
4573+
// Test no filter returns both
4574+
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{})
4575+
require.NoError(t, err)
4576+
require.Len(t, res.Workspaces, 2)
4577+
}

0 commit comments

Comments
 (0)