Skip to content

[pull] main from coder:main #222

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 4, 2025
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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,12 @@ jobs:
gotestsum --format standard-quiet --packages "$PACKAGES" \
-- -timeout=20m -v -p $NUM_PARALLEL_PACKAGES -parallel=$NUM_PARALLEL_TESTS $TESTCOUNT
- name: Upload failed test db dumps
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: failed-test-db-dump-${{matrix.os}}
path: "**/*.test.sql"

- name: Upload Go Build Cache
uses: ./.github/actions/test-cache/upload
with:
Expand Down
22 changes: 19 additions & 3 deletions cli/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"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/codersdk/workspacesdk"
"github.com/coder/coder/v2/provisionersdk/proto"
Expand Down Expand Up @@ -67,7 +68,12 @@ func TestWorkspaceAgent(t *testing.T) {
t.Parallel()
instanceID := "instanceidentifier"
certificates, metadataClient := coderdtest.NewAzureInstanceIdentity(t, instanceID)
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
db, ps := dbtestutil.NewDB(t,
dbtestutil.WithDumpOnFailure(),
)
client := coderdtest.New(t, &coderdtest.Options{
Database: db,
Pubsub: ps,
AzureCertificates: certificates,
})
user := coderdtest.CreateFirstUser(t, client)
Expand Down Expand Up @@ -106,7 +112,12 @@ func TestWorkspaceAgent(t *testing.T) {
t.Parallel()
instanceID := "instanceidentifier"
certificates, metadataClient := coderdtest.NewAWSInstanceIdentity(t, instanceID)
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
db, ps := dbtestutil.NewDB(t,
dbtestutil.WithDumpOnFailure(),
)
client := coderdtest.New(t, &coderdtest.Options{
Database: db,
Pubsub: ps,
AWSCertificates: certificates,
})
user := coderdtest.CreateFirstUser(t, client)
Expand Down Expand Up @@ -146,7 +157,12 @@ func TestWorkspaceAgent(t *testing.T) {
t.Parallel()
instanceID := "instanceidentifier"
validator, metadataClient := coderdtest.NewGoogleInstanceIdentity(t, instanceID, false)
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
db, ps := dbtestutil.NewDB(t,
dbtestutil.WithDumpOnFailure(),
)
client := coderdtest.New(t, &coderdtest.Options{
Database: db,
Pubsub: ps,
GoogleTokenValidator: validator,
})
owner := coderdtest.CreateFirstUser(t, client)
Expand Down
66 changes: 60 additions & 6 deletions coderd/database/dbfake/dbfake.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import (
"github.com/sqlc-dev/pqtype"
"github.com/stretchr/testify/require"

"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"

"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbgen"
Expand Down Expand Up @@ -43,6 +46,7 @@ type WorkspaceResponse struct {
// resources.
type WorkspaceBuildBuilder struct {
t testing.TB
logger slog.Logger
db database.Store
ps pubsub.Pubsub
ws database.WorkspaceTable
Expand All @@ -62,7 +66,10 @@ type workspaceBuildDisposition struct {
// Omitting the template ID on a workspace will also generate a new template
// with a template version.
func WorkspaceBuild(t testing.TB, db database.Store, ws database.WorkspaceTable) WorkspaceBuildBuilder {
return WorkspaceBuildBuilder{t: t, db: db, ws: ws}
return WorkspaceBuildBuilder{
t: t, db: db, ws: ws,
logger: slogtest.Make(t, &slogtest.Options{}).Named("dbfake").Leveled(slog.LevelDebug),
}
}

func (b WorkspaceBuildBuilder) Pubsub(ps pubsub.Pubsub) WorkspaceBuildBuilder {
Expand Down Expand Up @@ -131,6 +138,7 @@ func (b WorkspaceBuildBuilder) Do() WorkspaceResponse {
AgentToken: b.agentToken,
}
if b.ws.TemplateID == uuid.Nil {
b.logger.Debug(context.Background(), "creating template and version")
resp.TemplateVersionResponse = TemplateVersion(b.t, b.db).
Resources(b.resources...).
Pubsub(b.ps).
Expand All @@ -145,6 +153,7 @@ func (b WorkspaceBuildBuilder) Do() WorkspaceResponse {

// If no template version is set assume the active version.
if b.seed.TemplateVersionID == uuid.Nil {
b.logger.Debug(context.Background(), "assuming active template version")
template, err := b.db.GetTemplateByID(ownerCtx, b.ws.TemplateID)
require.NoError(b.t, err)
require.NotNil(b.t, template.ActiveVersionID, "active version ID unexpectedly nil")
Expand All @@ -156,6 +165,9 @@ func (b WorkspaceBuildBuilder) Do() WorkspaceResponse {
// nolint: revive
b.ws = dbgen.Workspace(b.t, b.db, b.ws)
resp.Workspace = b.ws
b.logger.Debug(context.Background(), "created workspace",
slog.F("name", resp.Workspace.Name),
slog.F("workspace_id", resp.Workspace.ID))
}
b.seed.WorkspaceID = b.ws.ID
b.seed.InitiatorID = takeFirst(b.seed.InitiatorID, b.ws.OwnerID)
Expand All @@ -182,10 +194,12 @@ func (b WorkspaceBuildBuilder) Do() WorkspaceResponse {
LogsOverflowed: false,
})
require.NoError(b.t, err, "insert job")
b.logger.Debug(context.Background(), "inserted provisioner job", slog.F("job_id", job.ID))

if b.dispo.starting {
// might need to do this multiple times if we got a template version
// import job as well
b.logger.Debug(context.Background(), "looping to acquire provisioner job")
for {
j, err := b.db.AcquireProvisionerJob(ownerCtx, database.AcquireProvisionerJobParams{
OrganizationID: job.OrganizationID,
Expand All @@ -202,10 +216,12 @@ func (b WorkspaceBuildBuilder) Do() WorkspaceResponse {
})
require.NoError(b.t, err, "acquire starting job")
if j.ID == job.ID {
b.logger.Debug(context.Background(), "acquired provisioner job", slog.F("job_id", job.ID))
break
}
}
} else {
b.logger.Debug(context.Background(), "completing the provisioner job")
err = b.db.UpdateProvisionerJobWithCompleteByID(ownerCtx, database.UpdateProvisionerJobWithCompleteByIDParams{
ID: job.ID,
UpdatedAt: dbtime.Now(),
Expand All @@ -221,18 +237,24 @@ func (b WorkspaceBuildBuilder) Do() WorkspaceResponse {
}

resp.Build = dbgen.WorkspaceBuild(b.t, b.db, b.seed)
b.logger.Debug(context.Background(), "created workspace build",
slog.F("build_id", resp.Build.ID),
slog.F("workspace_id", resp.Workspace.ID),
slog.F("build_number", resp.Build.BuildNumber))

for i := range b.params {
b.params[i].WorkspaceBuildID = resp.Build.ID
}
_ = dbgen.WorkspaceBuildParameters(b.t, b.db, b.params)
params := dbgen.WorkspaceBuildParameters(b.t, b.db, b.params)
b.logger.Debug(context.Background(), "created workspace build parameters", slog.F("count", len(params)))

if b.ws.Deleted {
err = b.db.UpdateWorkspaceDeletedByID(ownerCtx, database.UpdateWorkspaceDeletedByIDParams{
ID: b.ws.ID,
Deleted: true,
})
require.NoError(b.t, err)
b.logger.Debug(context.Background(), "deleted workspace", slog.F("workspace_id", resp.Workspace.ID))
}

if b.ps != nil {
Expand All @@ -243,6 +265,9 @@ func (b WorkspaceBuildBuilder) Do() WorkspaceResponse {
require.NoError(b.t, err)
err = b.ps.Publish(wspubsub.WorkspaceEventChannel(resp.Workspace.OwnerID), msg)
require.NoError(b.t, err)
b.logger.Debug(context.Background(), "published workspace event",
slog.F("owner_id", resp.Workspace.ID),
slog.F("owner_id", resp.Workspace.OwnerID))
}

agents, err := b.db.GetWorkspaceAgentsByWorkspaceAndBuildNumber(ownerCtx, database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams{
Expand All @@ -260,7 +285,12 @@ func (b WorkspaceBuildBuilder) Do() WorkspaceResponse {
err = b.db.DeleteWorkspaceSubAgentByID(ownerCtx, subAgent.ID)
require.NoError(b.t, err, "delete workspace agent subagent antagonist")

b.t.Logf("inserted deleted subagent antagonist %s (%v) for workspace agent %s (%v)", subAgent.Name, subAgent.ID, agent.Name, agent.ID)
b.logger.Debug(context.Background(), "inserted deleted subagent antagonist",
slog.F("subagent_name", subAgent.Name),
slog.F("subagent_id", subAgent.ID),
slog.F("agent_name", agent.Name),
slog.F("agent_id", agent.ID),
)
}
}

Expand All @@ -269,6 +299,7 @@ func (b WorkspaceBuildBuilder) Do() WorkspaceResponse {

type ProvisionerJobResourcesBuilder struct {
t testing.TB
logger slog.Logger
db database.Store
jobID uuid.UUID
transition database.WorkspaceTransition
Expand All @@ -281,6 +312,7 @@ func ProvisionerJobResources(
) ProvisionerJobResourcesBuilder {
return ProvisionerJobResourcesBuilder{
t: t,
logger: slogtest.Make(t, &slogtest.Options{}).Named("dbfake").Leveled(slog.LevelDebug).With(slog.F("job_id", jobID)),
db: db,
jobID: jobID,
transition: transition,
Expand All @@ -292,13 +324,17 @@ func (b ProvisionerJobResourcesBuilder) Do() {
b.t.Helper()
transition := b.transition
if transition == "" {
// Default to start!
b.logger.Debug(context.Background(), "setting default transition to start")
transition = database.WorkspaceTransitionStart
}
for _, resource := range b.resources {
//nolint:gocritic // This is only used by tests.
err := provisionerdserver.InsertWorkspaceResource(ownerCtx, b.db, b.jobID, transition, resource, &telemetry.Snapshot{})
require.NoError(b.t, err)
b.logger.Debug(context.Background(), "created workspace resource",
slog.F("resource_name", resource.Name),
slog.F("agent_count", len(resource.Agents)),
)
}
}

Expand All @@ -309,6 +345,7 @@ type TemplateVersionResponse struct {

type TemplateVersionBuilder struct {
t testing.TB
logger slog.Logger
db database.Store
seed database.TemplateVersion
fileID uuid.UUID
Expand All @@ -326,6 +363,7 @@ type TemplateVersionBuilder struct {
func TemplateVersion(t testing.TB, db database.Store) TemplateVersionBuilder {
return TemplateVersionBuilder{
t: t,
logger: slogtest.Make(t, &slogtest.Options{}).Named("dbfake").Leveled(slog.LevelDebug),
db: db,
promote: true,
autoCreateTemplate: true,
Expand Down Expand Up @@ -396,20 +434,30 @@ func (t TemplateVersionBuilder) Do() TemplateVersionResponse {
Valid: true,
UUID: resp.Template.ID,
}
t.logger.Debug(context.Background(), "created template",
slog.F("organization_id", resp.Template.OrganizationID),
slog.F("template_id", resp.Template.CreatedBy),
)
}

version := dbgen.TemplateVersion(t.t, t.db, t.seed)
t.logger.Debug(context.Background(), "created template version",
slog.F("template_version_id", version.ID),
)
if t.promote {
err := t.db.UpdateTemplateActiveVersionByID(ownerCtx, database.UpdateTemplateActiveVersionByIDParams{
ID: t.seed.TemplateID.UUID,
ActiveVersionID: t.seed.ID,
UpdatedAt: dbtime.Now(),
})
require.NoError(t.t, err)
t.logger.Debug(context.Background(), "promoted template version",
slog.F("template_version_id", t.seed.ID),
)
}

for _, preset := range t.presets {
dbgen.Preset(t.t, t.db, database.InsertPresetParams{
prst := dbgen.Preset(t.t, t.db, database.InsertPresetParams{
ID: preset.ID,
TemplateVersionID: version.ID,
Name: preset.Name,
Expand All @@ -421,14 +469,19 @@ func (t TemplateVersionBuilder) Do() TemplateVersionResponse {
Description: preset.Description,
Icon: preset.Icon,
})
t.logger.Debug(context.Background(), "added preset",
slog.F("preset_id", prst.ID),
slog.F("preset_name", prst.Name),
)
}

for _, presetParam := range t.presetParams {
dbgen.PresetParameter(t.t, t.db, database.InsertPresetParametersParams{
prm := dbgen.PresetParameter(t.t, t.db, database.InsertPresetParametersParams{
TemplateVersionPresetID: presetParam.TemplateVersionPresetID,
Names: []string{presetParam.Name},
Values: []string{presetParam.Value},
})
t.logger.Debug(context.Background(), "added preset parameter", slog.F("param_name", prm[0].Name))
}

payload, err := json.Marshal(provisionerdserver.TemplateVersionImportJob{
Expand All @@ -448,6 +501,7 @@ func (t TemplateVersionBuilder) Do() TemplateVersionResponse {
},
FileID: t.fileID,
})
t.logger.Debug(context.Background(), "added template version import job", slog.F("job_id", job.ID))

t.seed.JobID = job.ID

Expand Down
2 changes: 1 addition & 1 deletion coderd/database/dbtestutil/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ func DumpOnFailure(t testing.TB, connectionURL string) {
outPath := filepath.Join(cwd, snakeCaseName+"."+timeSuffix+".test.sql")
dump, err := PGDump(connectionURL)
if err != nil {
t.Errorf("dump on failure: failed to run pg_dump")
t.Errorf("dump on failure: failed to run pg_dump: %s", err.Error())
return
}
if err := os.WriteFile(outPath, normalizeDump(dump), 0o600); err != nil {
Expand Down
36 changes: 32 additions & 4 deletions coderd/mcp/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.streamableServer.ServeHTTP(w, r)
}

// RegisterTools registers all available MCP tools with the server
// Register all available MCP tools with the server excluding:
// - ReportTask - which requires dependencies not available in the remote MCP context
// - ChatGPT search and fetch tools, which are redundant with the standard tools.
func (s *Server) RegisterTools(client *codersdk.Client) error {
if client == nil {
return xerrors.New("client cannot be nil: MCP HTTP server requires authenticated client")
Expand All @@ -79,10 +81,36 @@ func (s *Server) RegisterTools(client *codersdk.Client) error {
return xerrors.Errorf("failed to initialize tool dependencies: %w", err)
}

// Register all available tools, but exclude tools that require dependencies not available in the
// remote MCP context
for _, tool := range toolsdk.All {
if tool.Name == toolsdk.ToolNameReportTask {
// the ReportTask tool requires dependencies not available in the remote MCP context
// the ChatGPT search and fetch tools are redundant with the standard tools.
if tool.Name == toolsdk.ToolNameReportTask ||
tool.Name == toolsdk.ToolNameChatGPTSearch || tool.Name == toolsdk.ToolNameChatGPTFetch {
continue
}

s.mcpServer.AddTools(mcpFromSDK(tool, toolDeps))
}
return nil
}

// ChatGPT tools are the search and fetch tools as defined in https://platform.openai.com/docs/mcp.
// We do not expose any extra ones because ChatGPT has an undocumented "Safety Scan" feature.
// In my experiments, if I included extra tools in the MCP server, ChatGPT would often - but not always -
// refuse to add Coder as a connector.
func (s *Server) RegisterChatGPTTools(client *codersdk.Client) error {
if client == nil {
return xerrors.New("client cannot be nil: MCP HTTP server requires authenticated client")
}

// Create tool dependencies
toolDeps, err := toolsdk.NewDeps(client)
if err != nil {
return xerrors.Errorf("failed to initialize tool dependencies: %w", err)
}

for _, tool := range toolsdk.All {
if tool.Name != toolsdk.ToolNameChatGPTSearch && tool.Name != toolsdk.ToolNameChatGPTFetch {
continue
}

Expand Down
Loading
Loading