mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
fix(mcp): report task status correctly (#17187)
This commit is contained in:
@ -4,14 +4,18 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
"cdr.dev/slog"
|
"cdr.dev/slog"
|
||||||
"cdr.dev/slog/sloggers/sloghuman"
|
"cdr.dev/slog/sloggers/sloghuman"
|
||||||
|
"github.com/coder/coder/v2/buildinfo"
|
||||||
"github.com/coder/coder/v2/cli/cliui"
|
"github.com/coder/coder/v2/cli/cliui"
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||||
codermcp "github.com/coder/coder/v2/mcp"
|
codermcp "github.com/coder/coder/v2/mcp"
|
||||||
"github.com/coder/serpent"
|
"github.com/coder/serpent"
|
||||||
)
|
)
|
||||||
@ -191,14 +195,16 @@ func (*RootCmd) mcpConfigureCursor() *serpent.Command {
|
|||||||
|
|
||||||
func (r *RootCmd) mcpServer() *serpent.Command {
|
func (r *RootCmd) mcpServer() *serpent.Command {
|
||||||
var (
|
var (
|
||||||
client = new(codersdk.Client)
|
client = new(codersdk.Client)
|
||||||
instructions string
|
instructions string
|
||||||
allowedTools []string
|
allowedTools []string
|
||||||
|
appStatusSlug string
|
||||||
|
mcpServerAgent bool
|
||||||
)
|
)
|
||||||
return &serpent.Command{
|
return &serpent.Command{
|
||||||
Use: "server",
|
Use: "server",
|
||||||
Handler: func(inv *serpent.Invocation) error {
|
Handler: func(inv *serpent.Invocation) error {
|
||||||
return mcpServerHandler(inv, client, instructions, allowedTools)
|
return mcpServerHandler(inv, client, instructions, allowedTools, appStatusSlug, mcpServerAgent)
|
||||||
},
|
},
|
||||||
Short: "Start the Coder MCP server.",
|
Short: "Start the Coder MCP server.",
|
||||||
Middleware: serpent.Chain(
|
Middleware: serpent.Chain(
|
||||||
@ -209,24 +215,39 @@ func (r *RootCmd) mcpServer() *serpent.Command {
|
|||||||
Name: "instructions",
|
Name: "instructions",
|
||||||
Description: "The instructions to pass to the MCP server.",
|
Description: "The instructions to pass to the MCP server.",
|
||||||
Flag: "instructions",
|
Flag: "instructions",
|
||||||
|
Env: "CODER_MCP_INSTRUCTIONS",
|
||||||
Value: serpent.StringOf(&instructions),
|
Value: serpent.StringOf(&instructions),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "allowed-tools",
|
Name: "allowed-tools",
|
||||||
Description: "Comma-separated list of allowed tools. If not specified, all tools are allowed.",
|
Description: "Comma-separated list of allowed tools. If not specified, all tools are allowed.",
|
||||||
Flag: "allowed-tools",
|
Flag: "allowed-tools",
|
||||||
|
Env: "CODER_MCP_ALLOWED_TOOLS",
|
||||||
Value: serpent.StringArrayOf(&allowedTools),
|
Value: serpent.StringArrayOf(&allowedTools),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "app-status-slug",
|
||||||
|
Description: "When reporting a task, the coder_app slug under which to report the task.",
|
||||||
|
Flag: "app-status-slug",
|
||||||
|
Env: "CODER_MCP_APP_STATUS_SLUG",
|
||||||
|
Value: serpent.StringOf(&appStatusSlug),
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Flag: "agent",
|
||||||
|
Env: "CODER_MCP_SERVER_AGENT",
|
||||||
|
Description: "Start the MCP server in agent mode, with a different set of tools.",
|
||||||
|
Value: serpent.BoolOf(&mcpServerAgent),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mcpServerHandler(inv *serpent.Invocation, client *codersdk.Client, instructions string, allowedTools []string) error {
|
//nolint:revive // control coupling
|
||||||
|
func mcpServerHandler(inv *serpent.Invocation, client *codersdk.Client, instructions string, allowedTools []string, appStatusSlug string, mcpServerAgent bool) error {
|
||||||
ctx, cancel := context.WithCancel(inv.Context())
|
ctx, cancel := context.WithCancel(inv.Context())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
logger := slog.Make(sloghuman.Sink(inv.Stdout))
|
|
||||||
|
|
||||||
me, err := client.User(ctx, codersdk.Me)
|
me, err := client.User(ctx, codersdk.Me)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cliui.Errorf(inv.Stderr, "Failed to log in to the Coder deployment.")
|
cliui.Errorf(inv.Stderr, "Failed to log in to the Coder deployment.")
|
||||||
@ -253,19 +274,40 @@ func mcpServerHandler(inv *serpent.Invocation, client *codersdk.Client, instruct
|
|||||||
inv.Stderr = invStderr
|
inv.Stderr = invStderr
|
||||||
}()
|
}()
|
||||||
|
|
||||||
options := []codermcp.Option{
|
mcpSrv := server.NewMCPServer(
|
||||||
codermcp.WithInstructions(instructions),
|
"Coder Agent",
|
||||||
codermcp.WithLogger(&logger),
|
buildinfo.Version(),
|
||||||
|
server.WithInstructions(instructions),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create a separate logger for the tools.
|
||||||
|
toolLogger := slog.Make(sloghuman.Sink(invStderr))
|
||||||
|
|
||||||
|
toolDeps := codermcp.ToolDeps{
|
||||||
|
Client: client,
|
||||||
|
Logger: &toolLogger,
|
||||||
|
AppStatusSlug: appStatusSlug,
|
||||||
|
AgentClient: agentsdk.New(client.URL),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add allowed tools option if specified
|
if mcpServerAgent {
|
||||||
|
// Get the workspace agent token from the environment.
|
||||||
|
agentToken, ok := os.LookupEnv("CODER_AGENT_TOKEN")
|
||||||
|
if !ok || agentToken == "" {
|
||||||
|
return xerrors.New("CODER_AGENT_TOKEN is not set")
|
||||||
|
}
|
||||||
|
toolDeps.AgentClient.SetSessionToken(agentToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register tools based on the allowlist (if specified)
|
||||||
|
reg := codermcp.AllTools()
|
||||||
if len(allowedTools) > 0 {
|
if len(allowedTools) > 0 {
|
||||||
options = append(options, codermcp.WithAllowedTools(allowedTools))
|
reg = reg.WithOnlyAllowed(allowedTools...)
|
||||||
}
|
}
|
||||||
|
|
||||||
srv := codermcp.NewStdio(client, options...)
|
reg.Register(mcpSrv, toolDeps)
|
||||||
srv.SetErrorLogger(log.New(invStderr, "", log.LstdFlags))
|
|
||||||
|
|
||||||
|
srv := server.NewStdioServer(mcpSrv)
|
||||||
done := make(chan error)
|
done := make(chan error)
|
||||||
go func() {
|
go func() {
|
||||||
defer close(done)
|
defer close(done)
|
||||||
|
135
mcp/mcp.go
135
mcp/mcp.go
@ -6,7 +6,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -17,76 +16,12 @@ import (
|
|||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
"cdr.dev/slog"
|
"cdr.dev/slog"
|
||||||
"cdr.dev/slog/sloggers/sloghuman"
|
|
||||||
"github.com/coder/coder/v2/buildinfo"
|
|
||||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||||
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mcpOptions struct {
|
|
||||||
instructions string
|
|
||||||
logger *slog.Logger
|
|
||||||
allowedTools []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Option is a function that configures the MCP server.
|
|
||||||
type Option func(*mcpOptions)
|
|
||||||
|
|
||||||
// WithInstructions sets the instructions for the MCP server.
|
|
||||||
func WithInstructions(instructions string) Option {
|
|
||||||
return func(o *mcpOptions) {
|
|
||||||
o.instructions = instructions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithLogger sets the logger for the MCP server.
|
|
||||||
func WithLogger(logger *slog.Logger) Option {
|
|
||||||
return func(o *mcpOptions) {
|
|
||||||
o.logger = logger
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithAllowedTools sets the allowed tools for the MCP server.
|
|
||||||
func WithAllowedTools(tools []string) Option {
|
|
||||||
return func(o *mcpOptions) {
|
|
||||||
o.allowedTools = tools
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStdio creates a new MCP stdio server with the given client and options.
|
|
||||||
// It is the responsibility of the caller to start and stop the server.
|
|
||||||
func NewStdio(client *codersdk.Client, opts ...Option) *server.StdioServer {
|
|
||||||
options := &mcpOptions{
|
|
||||||
instructions: ``,
|
|
||||||
logger: ptr.Ref(slog.Make(sloghuman.Sink(os.Stdout))),
|
|
||||||
}
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
mcpSrv := server.NewMCPServer(
|
|
||||||
"Coder Agent",
|
|
||||||
buildinfo.Version(),
|
|
||||||
server.WithInstructions(options.instructions),
|
|
||||||
)
|
|
||||||
|
|
||||||
logger := slog.Make(sloghuman.Sink(os.Stdout))
|
|
||||||
|
|
||||||
// Register tools based on the allowed list (if specified)
|
|
||||||
reg := AllTools()
|
|
||||||
if len(options.allowedTools) > 0 {
|
|
||||||
reg = reg.WithOnlyAllowed(options.allowedTools...)
|
|
||||||
}
|
|
||||||
reg.Register(mcpSrv, ToolDeps{
|
|
||||||
Client: client,
|
|
||||||
Logger: &logger,
|
|
||||||
})
|
|
||||||
|
|
||||||
srv := server.NewStdioServer(mcpSrv)
|
|
||||||
return srv
|
|
||||||
}
|
|
||||||
|
|
||||||
// allTools is the list of all available tools. When adding a new tool,
|
// allTools is the list of all available tools. When adding a new tool,
|
||||||
// make sure to update this list.
|
// make sure to update this list.
|
||||||
var allTools = ToolRegistry{
|
var allTools = ToolRegistry{
|
||||||
@ -120,6 +55,8 @@ Choose an emoji that helps the user understand the current phase at a glance.`),
|
|||||||
mcp.WithBoolean("done", mcp.Description(`Whether the overall task the user requested is complete.
|
mcp.WithBoolean("done", mcp.Description(`Whether the overall task the user requested is complete.
|
||||||
Set to true only when the entire requested operation is finished successfully.
|
Set to true only when the entire requested operation is finished successfully.
|
||||||
For multi-step processes, use false until all steps are complete.`), mcp.Required()),
|
For multi-step processes, use false until all steps are complete.`), mcp.Required()),
|
||||||
|
mcp.WithBoolean("need_user_attention", mcp.Description(`Whether the user needs to take action on the task.
|
||||||
|
Set to true if the task is in a failed state or if the user needs to take action to continue.`), mcp.Required()),
|
||||||
),
|
),
|
||||||
MakeHandler: handleCoderReportTask,
|
MakeHandler: handleCoderReportTask,
|
||||||
},
|
},
|
||||||
@ -265,8 +202,10 @@ Can be either "start" or "stop".`)),
|
|||||||
|
|
||||||
// ToolDeps contains all dependencies needed by tool handlers
|
// ToolDeps contains all dependencies needed by tool handlers
|
||||||
type ToolDeps struct {
|
type ToolDeps struct {
|
||||||
Client *codersdk.Client
|
Client *codersdk.Client
|
||||||
Logger *slog.Logger
|
AgentClient *agentsdk.Client
|
||||||
|
Logger *slog.Logger
|
||||||
|
AppStatusSlug string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToolHandler associates a tool with its handler creation function
|
// ToolHandler associates a tool with its handler creation function
|
||||||
@ -313,18 +252,23 @@ func AllTools() ToolRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type handleCoderReportTaskArgs struct {
|
type handleCoderReportTaskArgs struct {
|
||||||
Summary string `json:"summary"`
|
Summary string `json:"summary"`
|
||||||
Link string `json:"link"`
|
Link string `json:"link"`
|
||||||
Emoji string `json:"emoji"`
|
Emoji string `json:"emoji"`
|
||||||
Done bool `json:"done"`
|
Done bool `json:"done"`
|
||||||
|
NeedUserAttention bool `json:"need_user_attention"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example payload:
|
// Example payload:
|
||||||
// {"jsonrpc":"2.0","id":1,"method":"tools/call", "params": {"name": "coder_report_task", "arguments": {"summary": "I'm working on the login page.", "link": "https://github.com/coder/coder/pull/1234", "emoji": "🔍", "done": false}}}
|
// {"jsonrpc":"2.0","id":1,"method":"tools/call", "params": {"name": "coder_report_task", "arguments": {"summary": "I need help with the login page.", "link": "https://github.com/coder/coder/pull/1234", "emoji": "🔍", "done": false, "need_user_attention": true}}}
|
||||||
func handleCoderReportTask(deps ToolDeps) server.ToolHandlerFunc {
|
func handleCoderReportTask(deps ToolDeps) server.ToolHandlerFunc {
|
||||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
if deps.Client == nil {
|
if deps.AgentClient == nil {
|
||||||
return nil, xerrors.New("developer error: client is required")
|
return nil, xerrors.New("developer error: agent client is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if deps.AppStatusSlug == "" {
|
||||||
|
return nil, xerrors.New("No app status slug provided, set CODER_MCP_APP_STATUS_SLUG when running the MCP server to report tasks.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the request parameters to a json.RawMessage so we can unmarshal
|
// Convert the request parameters to a json.RawMessage so we can unmarshal
|
||||||
@ -334,20 +278,33 @@ func handleCoderReportTask(deps ToolDeps) server.ToolHandlerFunc {
|
|||||||
return nil, xerrors.Errorf("failed to unmarshal arguments: %w", err)
|
return nil, xerrors.Errorf("failed to unmarshal arguments: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Waiting on support for tasks.
|
deps.Logger.Info(ctx, "report task tool called",
|
||||||
deps.Logger.Info(ctx, "report task tool called", slog.F("summary", args.Summary), slog.F("link", args.Link), slog.F("done", args.Done), slog.F("emoji", args.Emoji))
|
slog.F("summary", args.Summary),
|
||||||
/*
|
slog.F("link", args.Link),
|
||||||
err := sdk.PostTask(ctx, agentsdk.PostTaskRequest{
|
slog.F("emoji", args.Emoji),
|
||||||
Reporter: "claude",
|
slog.F("done", args.Done),
|
||||||
Summary: summary,
|
slog.F("need_user_attention", args.NeedUserAttention),
|
||||||
URL: link,
|
)
|
||||||
Completion: done,
|
|
||||||
Icon: emoji,
|
newStatus := agentsdk.PatchAppStatus{
|
||||||
})
|
AppSlug: deps.AppStatusSlug,
|
||||||
if err != nil {
|
Message: args.Summary,
|
||||||
return nil, err
|
URI: args.Link,
|
||||||
}
|
Icon: args.Emoji,
|
||||||
*/
|
NeedsUserAttention: args.NeedUserAttention,
|
||||||
|
State: codersdk.WorkspaceAppStatusStateWorking,
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.Done {
|
||||||
|
newStatus.State = codersdk.WorkspaceAppStatusStateComplete
|
||||||
|
}
|
||||||
|
if args.NeedUserAttention {
|
||||||
|
newStatus.State = codersdk.WorkspaceAppStatusStateFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := deps.AgentClient.PatchAppStatus(ctx, newStatus); err != nil {
|
||||||
|
return nil, xerrors.Errorf("failed to patch app status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return &mcp.CallToolResult{
|
return &mcp.CallToolResult{
|
||||||
Content: []mcp.Content{
|
Content: []mcp.Content{
|
||||||
|
@ -17,7 +17,9 @@ import (
|
|||||||
"github.com/coder/coder/v2/coderd/database"
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
"github.com/coder/coder/v2/coderd/database/dbfake"
|
"github.com/coder/coder/v2/coderd/database/dbfake"
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||||
codermcp "github.com/coder/coder/v2/mcp"
|
codermcp "github.com/coder/coder/v2/mcp"
|
||||||
|
"github.com/coder/coder/v2/provisionersdk/proto"
|
||||||
"github.com/coder/coder/v2/pty/ptytest"
|
"github.com/coder/coder/v2/pty/ptytest"
|
||||||
"github.com/coder/coder/v2/testutil"
|
"github.com/coder/coder/v2/testutil"
|
||||||
)
|
)
|
||||||
@ -39,7 +41,14 @@ func TestCoderTools(t *testing.T) {
|
|||||||
r := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{
|
r := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{
|
||||||
OrganizationID: owner.OrganizationID,
|
OrganizationID: owner.OrganizationID,
|
||||||
OwnerID: member.ID,
|
OwnerID: member.ID,
|
||||||
}).WithAgent().Do()
|
}).WithAgent(func(agents []*proto.Agent) []*proto.Agent {
|
||||||
|
agents[0].Apps = []*proto.App{
|
||||||
|
{
|
||||||
|
Slug: "some-agent-app",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return agents
|
||||||
|
}).Do()
|
||||||
|
|
||||||
// Note: we want to test the list_workspaces tool before starting the
|
// Note: we want to test the list_workspaces tool before starting the
|
||||||
// workspace agent. Starting the workspace agent will modify the workspace
|
// workspace agent. Starting the workspace agent will modify the workspace
|
||||||
@ -65,9 +74,12 @@ func TestCoderTools(t *testing.T) {
|
|||||||
|
|
||||||
// Register tools using our registry
|
// Register tools using our registry
|
||||||
logger := slogtest.Make(t, nil)
|
logger := slogtest.Make(t, nil)
|
||||||
|
agentClient := agentsdk.New(memberClient.URL)
|
||||||
codermcp.AllTools().Register(mcpSrv, codermcp.ToolDeps{
|
codermcp.AllTools().Register(mcpSrv, codermcp.ToolDeps{
|
||||||
Client: memberClient,
|
Client: memberClient,
|
||||||
Logger: &logger,
|
Logger: &logger,
|
||||||
|
AppStatusSlug: "some-agent-app",
|
||||||
|
AgentClient: agentClient,
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("coder_list_templates", func(t *testing.T) {
|
t.Run("coder_list_templates", func(t *testing.T) {
|
||||||
@ -86,24 +98,44 @@ func TestCoderTools(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("coder_report_task", func(t *testing.T) {
|
t.Run("coder_report_task", func(t *testing.T) {
|
||||||
|
// Given: the MCP server has an agent token.
|
||||||
|
oldAgentToken := agentClient.SDK.SessionToken()
|
||||||
|
agentClient.SetSessionToken(r.AgentToken)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
agentClient.SDK.SetSessionToken(oldAgentToken)
|
||||||
|
})
|
||||||
// When: the coder_report_task tool is called
|
// When: the coder_report_task tool is called
|
||||||
ctr := makeJSONRPCRequest(t, "tools/call", "coder_report_task", map[string]any{
|
ctr := makeJSONRPCRequest(t, "tools/call", "coder_report_task", map[string]any{
|
||||||
"summary": "Test summary",
|
"summary": "Test summary",
|
||||||
"link": "https://example.com",
|
"link": "https://example.com",
|
||||||
"emoji": "🔍",
|
"emoji": "🔍",
|
||||||
"done": false,
|
"done": false,
|
||||||
"coder_url": client.URL.String(),
|
"need_user_attention": true,
|
||||||
"coder_session_token": client.SessionToken(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
pty.WriteLine(ctr)
|
pty.WriteLine(ctr)
|
||||||
_ = pty.ReadLine(ctx) // skip the echo
|
_ = pty.ReadLine(ctx) // skip the echo
|
||||||
|
|
||||||
// Then: the response is a success message.
|
// Then: positive feedback is given to the reporting agent.
|
||||||
// TODO: check the task was created. This functionality is not yet implemented.
|
|
||||||
expected := makeJSONRPCTextResponse(t, "Thanks for reporting!")
|
|
||||||
actual := pty.ReadLine(ctx)
|
actual := pty.ReadLine(ctx)
|
||||||
testutil.RequireJSONEq(t, expected, actual)
|
require.Contains(t, actual, "Thanks for reporting!")
|
||||||
|
|
||||||
|
// Then: the response is a success message.
|
||||||
|
ws, err := memberClient.Workspace(ctx, r.Workspace.ID)
|
||||||
|
require.NoError(t, err, "failed to get workspace")
|
||||||
|
agt, err := memberClient.WorkspaceAgent(ctx, ws.LatestBuild.Resources[0].Agents[0].ID)
|
||||||
|
require.NoError(t, err, "failed to get workspace agent")
|
||||||
|
require.NotEmpty(t, agt.Apps, "workspace agent should have an app")
|
||||||
|
require.NotEmpty(t, agt.Apps[0].Statuses, "workspace agent app should have a status")
|
||||||
|
st := agt.Apps[0].Statuses[0]
|
||||||
|
// require.Equal(t, ws.ID, st.WorkspaceID, "workspace app status should have the correct workspace id")
|
||||||
|
require.Equal(t, agt.ID, st.AgentID, "workspace app status should have the correct agent id")
|
||||||
|
require.Equal(t, agt.Apps[0].ID, st.AppID, "workspace app status should have the correct app id")
|
||||||
|
require.Equal(t, codersdk.WorkspaceAppStatusStateFailure, st.State, "workspace app status should be in the failure state")
|
||||||
|
require.Equal(t, "Test summary", st.Message, "workspace app status should have the correct message")
|
||||||
|
require.Equal(t, "https://example.com", st.URI, "workspace app status should have the correct uri")
|
||||||
|
require.Equal(t, "🔍", st.Icon, "workspace app status should have the correct icon")
|
||||||
|
require.True(t, st.NeedsUserAttention, "workspace app status should need user attention")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("coder_whoami", func(t *testing.T) {
|
t.Run("coder_whoami", func(t *testing.T) {
|
||||||
|
Reference in New Issue
Block a user