From 42b9181d0d2b84a35c5316a101f6f0e08f7e4f7b Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Thu, 24 Jul 2025 13:00:33 +0000 Subject: [PATCH 1/3] feat: workspace bash background parameter --- codersdk/toolsdk/bash.go | 40 +++++++++++++++++++++--- codersdk/toolsdk/resources/background.sh | 23 ++++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 codersdk/toolsdk/resources/background.sh diff --git a/codersdk/toolsdk/bash.go b/codersdk/toolsdk/bash.go index 5fb15843f1bf1..6fd4bcbaae0a5 100644 --- a/codersdk/toolsdk/bash.go +++ b/codersdk/toolsdk/bash.go @@ -3,6 +3,8 @@ package toolsdk import ( "bytes" "context" + _ "embed" + "encoding/base64" "errors" "fmt" "io" @@ -18,12 +20,14 @@ import ( "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/workspacesdk" + "github.com/coder/coder/v2/cryptorand" ) type WorkspaceBashArgs struct { - Workspace string `json:"workspace"` - Command string `json:"command"` - TimeoutMs int `json:"timeout_ms,omitempty"` + Workspace string `json:"workspace"` + Command string `json:"command"` + TimeoutMs int `json:"timeout_ms,omitempty"` + Background bool `json:"background,omitempty"` } type WorkspaceBashResult struct { @@ -31,6 +35,9 @@ type WorkspaceBashResult struct { ExitCode int `json:"exit_code"` } +//go:embed resources/background.sh +var backgroundScript string + var WorkspaceBash = Tool[WorkspaceBashArgs, WorkspaceBashResult]{ Tool: aisdk.Tool{ Name: ToolNameWorkspaceBash, @@ -53,6 +60,7 @@ If the command times out, all output captured up to that point is returned with Examples: - workspace: "my-workspace", command: "ls -la" - workspace: "john/dev-env", command: "git status", timeout_ms: 30000 +- workspace: "my-workspace", command: "npm run dev", background: true - workspace: "my-workspace.main", command: "docker ps"`, Schema: aisdk.Schema{ Properties: map[string]any{ @@ -70,6 +78,10 @@ Examples: "default": 60000, "minimum": 1, }, + "background": map[string]any{ + "type": "boolean", + "description": "Whether to run the command in the background. The command will not be affected by the timeout.", + }, }, Required: []string{"workspace", "command"}, }, @@ -137,8 +149,26 @@ Examples: // Set default timeout if not specified (60 seconds) timeoutMs := args.TimeoutMs + defaultTimeoutMs := 60000 if timeoutMs <= 0 { - timeoutMs = 60000 + timeoutMs = defaultTimeoutMs + } + command := args.Command + if args.Background { + // Background commands are not affected by the timeout + timeoutMs = defaultTimeoutMs + encodedCommand := base64.StdEncoding.EncodeToString([]byte(args.Command)) + encodedScript := base64.StdEncoding.EncodeToString([]byte(backgroundScript)) + commandID, err := cryptorand.StringCharset(cryptorand.Human, 8) + if err != nil { + return WorkspaceBashResult{}, xerrors.Errorf("failed to generate command ID: %w", err) + } + command = fmt.Sprintf( + "ARG_COMMAND=\"$(echo -n %s | base64 -d)\" ARG_COMMAND_ID=%s bash -c \"$(echo -n %s | base64 -d)\"", + encodedCommand, + commandID, + encodedScript, + ) } // Create context with timeout @@ -146,7 +176,7 @@ Examples: defer cancel() // Execute command with timeout handling - output, err := executeCommandWithTimeout(ctx, session, args.Command) + output, err := executeCommandWithTimeout(ctx, session, command) outputStr := strings.TrimSpace(string(output)) // Handle command execution results diff --git a/codersdk/toolsdk/resources/background.sh b/codersdk/toolsdk/resources/background.sh new file mode 100644 index 0000000000000..fdb3900403377 --- /dev/null +++ b/codersdk/toolsdk/resources/background.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# This script is used to run a command in the background. + +set -o errexit +set -o pipefail + +set -o nounset + +COMMAND="$ARG_COMMAND" +COMMAND_ID="$ARG_COMMAND_ID" + +set +o nounset + +LOG_DIR="/tmp/mcp-bg" +LOG_PATH="$LOG_DIR/$COMMAND_ID.log" +mkdir -p "$LOG_DIR" + +nohup bash -c "$COMMAND" >"$LOG_PATH" 2>&1 & +COMMAND_PID="$!" + +echo "Command started with PID: $COMMAND_PID" +echo "Log path: $LOG_PATH" From 3750aee7bd7dec6babcf1aaab63ef41c2f720595 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Thu, 24 Jul 2025 16:56:21 +0000 Subject: [PATCH 2/3] chore: add workspace bash background tests --- codersdk/toolsdk/bash_test.go | 143 ++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/codersdk/toolsdk/bash_test.go b/codersdk/toolsdk/bash_test.go index 53ac480039278..a13f2ab8a8290 100644 --- a/codersdk/toolsdk/bash_test.go +++ b/codersdk/toolsdk/bash_test.go @@ -9,6 +9,7 @@ import ( "github.com/coder/coder/v2/agent/agenttest" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/codersdk/toolsdk" + "github.com/coder/coder/v2/testutil" ) func TestWorkspaceBash(t *testing.T) { @@ -338,3 +339,145 @@ func TestWorkspaceBashTimeoutIntegration(t *testing.T) { require.NotContains(t, result.Output, "Command canceled due to timeout") }) } + +func TestWorkspaceBashBackgroundIntegration(t *testing.T) { + t.Parallel() + + t.Run("BackgroundCommandReturnsImmediately", func(t *testing.T) { + t.Parallel() + + client, workspace, agentToken := setupWorkspaceForAgent(t) + + // Start the agent and wait for it to be fully ready + _ = agenttest.New(t, client.URL, agentToken) + + // Wait for workspace agents to be ready + coderdtest.NewWorkspaceAgentWaiter(t, client, workspace.ID).Wait() + + deps, err := toolsdk.NewDeps(client) + require.NoError(t, err) + + args := toolsdk.WorkspaceBashArgs{ + Workspace: workspace.Name, + Command: `echo "started" && sleep 5 && echo "completed"`, // Command that would take 5+ seconds + Background: true, // Run in background + } + + result, err := toolsdk.WorkspaceBash.Handler(t.Context(), deps, args) + + // Should not error + require.NoError(t, err) + + t.Logf("Background result: exitCode=%d, output=%q", result.ExitCode, result.Output) + + // Should have exit code 0 (background start successful) + require.Equal(t, 0, result.ExitCode) + + // Should contain PID and log path info, not the actual command output + require.Contains(t, result.Output, "Command started with PID:") + require.Contains(t, result.Output, "Log path: /tmp/mcp-bg/") + + // Should NOT contain the actual command output since it runs in background + // The command was `echo "started" && sleep 5 && echo "completed"` + // So we check that the quoted strings don't appear in the output + require.NotContains(t, result.Output, `"started"`, "Should not contain command output in background mode") + require.NotContains(t, result.Output, `"completed"`, "Should not contain command output in background mode") + }) + + t.Run("BackgroundVsNormalExecution", func(t *testing.T) { + t.Parallel() + + client, workspace, agentToken := setupWorkspaceForAgent(t) + + // Start the agent and wait for it to be fully ready + _ = agenttest.New(t, client.URL, agentToken) + + // Wait for workspace agents to be ready + coderdtest.NewWorkspaceAgentWaiter(t, client, workspace.ID).Wait() + + deps, err := toolsdk.NewDeps(client) + require.NoError(t, err) + + // First run the same command in normal mode + normalArgs := toolsdk.WorkspaceBashArgs{ + Workspace: workspace.Name, + Command: `echo "hello world"`, + Background: false, + } + + normalResult, err := toolsdk.WorkspaceBash.Handler(t.Context(), deps, normalArgs) + require.NoError(t, err) + + // Normal mode should return the actual output + require.Equal(t, 0, normalResult.ExitCode) + require.Equal(t, "hello world", normalResult.Output) + + // Now run the same command in background mode + backgroundArgs := toolsdk.WorkspaceBashArgs{ + Workspace: workspace.Name, + Command: `echo "hello world"`, + Background: true, + } + + backgroundResult, err := toolsdk.WorkspaceBash.Handler(t.Context(), deps, backgroundArgs) + require.NoError(t, err) + + t.Logf("Normal result: %q", normalResult.Output) + t.Logf("Background result: %q", backgroundResult.Output) + + // Background mode should return PID/log info, not the actual output + require.Equal(t, 0, backgroundResult.ExitCode) + require.Contains(t, backgroundResult.Output, "Command started with PID:") + require.Contains(t, backgroundResult.Output, "Log path: /tmp/mcp-bg/") + require.NotContains(t, backgroundResult.Output, "hello world") + }) + + t.Run("BackgroundIgnoresTimeout", func(t *testing.T) { + t.Parallel() + + client, workspace, agentToken := setupWorkspaceForAgent(t) + + // Start the agent and wait for it to be fully ready + _ = agenttest.New(t, client.URL, agentToken) + + // Wait for workspace agents to be ready + coderdtest.NewWorkspaceAgentWaiter(t, client, workspace.ID).Wait() + + deps, err := toolsdk.NewDeps(client) + require.NoError(t, err) + + args := toolsdk.WorkspaceBashArgs{ + Workspace: workspace.Name, + Command: `sleep 1 && echo "done" > /tmp/done`, // Command that would normally timeout + TimeoutMs: 1, // 1 ms timeout (shorter than command duration) + Background: true, // But running in background should ignore timeout + } + + result, err := toolsdk.WorkspaceBash.Handler(t.Context(), deps, args) + + // Should not error and should not timeout + require.NoError(t, err) + + t.Logf("Background with timeout result: exitCode=%d, output=%q", result.ExitCode, result.Output) + + // Should have exit code 0 (background start successful) + require.Equal(t, 0, result.ExitCode) + + // Should return PID/log info, indicating the background command started successfully + require.Contains(t, result.Output, "Command started with PID:") + require.Contains(t, result.Output, "Log path: /tmp/mcp-bg/") + + // Should NOT contain timeout message since background mode ignores timeout + require.NotContains(t, result.Output, "Command canceled due to timeout") + + // Wait for the background command to complete + require.Eventually(t, func() bool { + args := toolsdk.WorkspaceBashArgs{ + Workspace: workspace.Name, + Command: `cat /tmp/done`, + } + result, err := toolsdk.WorkspaceBash.Handler(t.Context(), deps, args) + return err == nil && result.Output == "done" + }, testutil.WaitMedium, testutil.IntervalMedium) + }) +} From d920c64805d22a4ff0e138c25c564f90164a772b Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Mon, 28 Jul 2025 15:14:33 +0200 Subject: [PATCH 3/3] refactor(toolsdk): simplify workspace bash background execution with nohup Change-Id: If67c30717158bdd84e9f733b56365af7c8d0b51a Signed-off-by: Thomas Kosiewski --- codersdk/toolsdk/bash.go | 69 ++++++++++---------- codersdk/toolsdk/bash_test.go | 83 +++++++++++------------- codersdk/toolsdk/resources/background.sh | 23 ------- codersdk/toolsdk/toolsdk_test.go | 24 +++---- 4 files changed, 85 insertions(+), 114 deletions(-) delete mode 100644 codersdk/toolsdk/resources/background.sh diff --git a/codersdk/toolsdk/bash.go b/codersdk/toolsdk/bash.go index 6fd4bcbaae0a5..037227337bfc9 100644 --- a/codersdk/toolsdk/bash.go +++ b/codersdk/toolsdk/bash.go @@ -3,8 +3,6 @@ package toolsdk import ( "bytes" "context" - _ "embed" - "encoding/base64" "errors" "fmt" "io" @@ -20,7 +18,6 @@ import ( "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/workspacesdk" - "github.com/coder/coder/v2/cryptorand" ) type WorkspaceBashArgs struct { @@ -35,9 +32,6 @@ type WorkspaceBashResult struct { ExitCode int `json:"exit_code"` } -//go:embed resources/background.sh -var backgroundScript string - var WorkspaceBash = Tool[WorkspaceBashArgs, WorkspaceBashResult]{ Tool: aisdk.Tool{ Name: ToolNameWorkspaceBash, @@ -57,10 +51,13 @@ The workspace parameter supports various formats: The timeout_ms parameter specifies the command timeout in milliseconds (defaults to 60000ms, maximum of 300000ms). If the command times out, all output captured up to that point is returned with a cancellation message. +For background commands (background: true), output is captured until the timeout is reached, then the command +continues running in the background. The captured output is returned as the result. + Examples: - workspace: "my-workspace", command: "ls -la" - workspace: "john/dev-env", command: "git status", timeout_ms: 30000 -- workspace: "my-workspace", command: "npm run dev", background: true +- workspace: "my-workspace", command: "npm run dev", background: true, timeout_ms: 10000 - workspace: "my-workspace.main", command: "docker ps"`, Schema: aisdk.Schema{ Properties: map[string]any{ @@ -80,7 +77,7 @@ Examples: }, "background": map[string]any{ "type": "boolean", - "description": "Whether to run the command in the background. The command will not be affected by the timeout.", + "description": "Whether to run the command in the background. Output is captured until timeout, then the command continues running in the background.", }, }, Required: []string{"workspace", "command"}, @@ -155,35 +152,29 @@ Examples: } command := args.Command if args.Background { - // Background commands are not affected by the timeout - timeoutMs = defaultTimeoutMs - encodedCommand := base64.StdEncoding.EncodeToString([]byte(args.Command)) - encodedScript := base64.StdEncoding.EncodeToString([]byte(backgroundScript)) - commandID, err := cryptorand.StringCharset(cryptorand.Human, 8) - if err != nil { - return WorkspaceBashResult{}, xerrors.Errorf("failed to generate command ID: %w", err) - } - command = fmt.Sprintf( - "ARG_COMMAND=\"$(echo -n %s | base64 -d)\" ARG_COMMAND_ID=%s bash -c \"$(echo -n %s | base64 -d)\"", - encodedCommand, - commandID, - encodedScript, - ) + // For background commands, use nohup directly to ensure they survive SSH session + // termination. This captures output normally but allows the process to continue + // running even after the SSH connection closes. + command = fmt.Sprintf("nohup %s &1", args.Command) } - // Create context with timeout - ctx, cancel = context.WithTimeout(ctx, time.Duration(timeoutMs)*time.Millisecond) - defer cancel() + // Create context with command timeout (replace the broader MCP timeout) + commandCtx, commandCancel := context.WithTimeout(ctx, time.Duration(timeoutMs)*time.Millisecond) + defer commandCancel() // Execute command with timeout handling - output, err := executeCommandWithTimeout(ctx, session, command) + output, err := executeCommandWithTimeout(commandCtx, session, command) outputStr := strings.TrimSpace(string(output)) // Handle command execution results if err != nil { // Check if the command timed out - if errors.Is(context.Cause(ctx), context.DeadlineExceeded) { - outputStr += "\nCommand canceled due to timeout" + if errors.Is(context.Cause(commandCtx), context.DeadlineExceeded) { + if args.Background { + outputStr += "\nCommand continues running in background" + } else { + outputStr += "\nCommand canceled due to timeout" + } return WorkspaceBashResult{ Output: outputStr, ExitCode: 124, @@ -417,21 +408,27 @@ func executeCommandWithTimeout(ctx context.Context, session *gossh.Session, comm return safeWriter.Bytes(), err case <-ctx.Done(): // Context was canceled (timeout or other cancellation) - // Close the session to stop the command - _ = session.Close() + // Close the session to stop the command, but handle errors gracefully + closeErr := session.Close() - // Give a brief moment to collect any remaining output - timer := time.NewTimer(50 * time.Millisecond) + // Give a brief moment to collect any remaining output and for goroutines to finish + timer := time.NewTimer(100 * time.Millisecond) defer timer.Stop() select { case <-timer.C: // Timer expired, return what we have + break case err := <-done: // Command finished during grace period - return safeWriter.Bytes(), err + if closeErr == nil { + return safeWriter.Bytes(), err + } + // If session close failed, prioritize the context error + break } + // Return the collected output with the context error return safeWriter.Bytes(), context.Cause(ctx) } } @@ -451,5 +448,9 @@ func (sw *syncWriter) Write(p []byte) (n int, err error) { func (sw *syncWriter) Bytes() []byte { sw.mu.Lock() defer sw.mu.Unlock() - return sw.w.Bytes() + // Return a copy to prevent race conditions with the underlying buffer + b := sw.w.Bytes() + result := make([]byte, len(b)) + copy(result, b) + return result } diff --git a/codersdk/toolsdk/bash_test.go b/codersdk/toolsdk/bash_test.go index a13f2ab8a8290..0656b2d8786e6 100644 --- a/codersdk/toolsdk/bash_test.go +++ b/codersdk/toolsdk/bash_test.go @@ -175,8 +175,6 @@ func TestWorkspaceBashTimeout(t *testing.T) { // Test that the TimeoutMs field can be set and read correctly args := toolsdk.WorkspaceBashArgs{ - Workspace: "test-workspace", - Command: "echo test", TimeoutMs: 0, // Should default to 60000 in handler } @@ -193,8 +191,6 @@ func TestWorkspaceBashTimeout(t *testing.T) { // Test that negative values can be set and will be handled by the default logic args := toolsdk.WorkspaceBashArgs{ - Workspace: "test-workspace", - Command: "echo test", TimeoutMs: -100, } @@ -280,7 +276,7 @@ func TestWorkspaceBashTimeoutIntegration(t *testing.T) { TimeoutMs: 2000, // 2 seconds timeout - should timeout after first echo } - result, err := toolsdk.WorkspaceBash.Handler(t.Context(), deps, args) + result, err := testTool(t, toolsdk.WorkspaceBash, deps, args) // Should not error (timeout is handled gracefully) require.NoError(t, err) @@ -314,7 +310,6 @@ func TestWorkspaceBashTimeoutIntegration(t *testing.T) { deps, err := toolsdk.NewDeps(client) require.NoError(t, err) - ctx := context.Background() args := toolsdk.WorkspaceBashArgs{ Workspace: workspace.Name, @@ -322,7 +317,8 @@ func TestWorkspaceBashTimeoutIntegration(t *testing.T) { TimeoutMs: 5000, // 5 second timeout - plenty of time } - result, err := toolsdk.WorkspaceBash.Handler(ctx, deps, args) + // Use testTool to register the tool as tested and satisfy coverage validation + result, err := testTool(t, toolsdk.WorkspaceBash, deps, args) // Should not error require.NoError(t, err) @@ -343,7 +339,7 @@ func TestWorkspaceBashTimeoutIntegration(t *testing.T) { func TestWorkspaceBashBackgroundIntegration(t *testing.T) { t.Parallel() - t.Run("BackgroundCommandReturnsImmediately", func(t *testing.T) { + t.Run("BackgroundCommandCapturesOutput", func(t *testing.T) { t.Parallel() client, workspace, agentToken := setupWorkspaceForAgent(t) @@ -359,29 +355,29 @@ func TestWorkspaceBashBackgroundIntegration(t *testing.T) { args := toolsdk.WorkspaceBashArgs{ Workspace: workspace.Name, - Command: `echo "started" && sleep 5 && echo "completed"`, // Command that would take 5+ seconds - Background: true, // Run in background + Command: `echo "started" && sleep 60 && echo "completed"`, // Command that would take 60+ seconds + Background: true, // Run in background + TimeoutMs: 2000, // 2 second timeout } - result, err := toolsdk.WorkspaceBash.Handler(t.Context(), deps, args) + result, err := testTool(t, toolsdk.WorkspaceBash, deps, args) // Should not error require.NoError(t, err) t.Logf("Background result: exitCode=%d, output=%q", result.ExitCode, result.Output) - // Should have exit code 0 (background start successful) - require.Equal(t, 0, result.ExitCode) + // Should have exit code 124 (timeout) since command times out + require.Equal(t, 124, result.ExitCode) - // Should contain PID and log path info, not the actual command output - require.Contains(t, result.Output, "Command started with PID:") - require.Contains(t, result.Output, "Log path: /tmp/mcp-bg/") + // Should capture output up to timeout point + require.Contains(t, result.Output, "started", "Should contain output captured before timeout") - // Should NOT contain the actual command output since it runs in background - // The command was `echo "started" && sleep 5 && echo "completed"` - // So we check that the quoted strings don't appear in the output - require.NotContains(t, result.Output, `"started"`, "Should not contain command output in background mode") - require.NotContains(t, result.Output, `"completed"`, "Should not contain command output in background mode") + // Should NOT contain the second echo (it never executed due to timeout) + require.NotContains(t, result.Output, "completed", "Should not contain output after timeout") + + // Should contain background continuation message + require.Contains(t, result.Output, "Command continues running in background") }) t.Run("BackgroundVsNormalExecution", func(t *testing.T) { @@ -419,20 +415,18 @@ func TestWorkspaceBashBackgroundIntegration(t *testing.T) { Background: true, } - backgroundResult, err := toolsdk.WorkspaceBash.Handler(t.Context(), deps, backgroundArgs) + backgroundResult, err := testTool(t, toolsdk.WorkspaceBash, deps, backgroundArgs) require.NoError(t, err) t.Logf("Normal result: %q", normalResult.Output) t.Logf("Background result: %q", backgroundResult.Output) - // Background mode should return PID/log info, not the actual output + // Background mode should also return the actual output since command completes quickly require.Equal(t, 0, backgroundResult.ExitCode) - require.Contains(t, backgroundResult.Output, "Command started with PID:") - require.Contains(t, backgroundResult.Output, "Log path: /tmp/mcp-bg/") - require.NotContains(t, backgroundResult.Output, "hello world") + require.Equal(t, "hello world", backgroundResult.Output) }) - t.Run("BackgroundIgnoresTimeout", func(t *testing.T) { + t.Run("BackgroundCommandContinuesAfterTimeout", func(t *testing.T) { t.Parallel() client, workspace, agentToken := setupWorkspaceForAgent(t) @@ -448,36 +442,35 @@ func TestWorkspaceBashBackgroundIntegration(t *testing.T) { args := toolsdk.WorkspaceBashArgs{ Workspace: workspace.Name, - Command: `sleep 1 && echo "done" > /tmp/done`, // Command that would normally timeout - TimeoutMs: 1, // 1 ms timeout (shorter than command duration) - Background: true, // But running in background should ignore timeout + Command: `echo "started" && sleep 4 && echo "done" > /tmp/bg-test-done`, // Command that will timeout but continue + TimeoutMs: 2000, // 2000ms timeout (shorter than command duration) + Background: true, // Run in background } - result, err := toolsdk.WorkspaceBash.Handler(t.Context(), deps, args) + result, err := testTool(t, toolsdk.WorkspaceBash, deps, args) - // Should not error and should not timeout + // Should not error but should timeout require.NoError(t, err) t.Logf("Background with timeout result: exitCode=%d, output=%q", result.ExitCode, result.Output) - // Should have exit code 0 (background start successful) - require.Equal(t, 0, result.ExitCode) + // Should have timeout exit code + require.Equal(t, 124, result.ExitCode) - // Should return PID/log info, indicating the background command started successfully - require.Contains(t, result.Output, "Command started with PID:") - require.Contains(t, result.Output, "Log path: /tmp/mcp-bg/") + // Should capture output before timeout + require.Contains(t, result.Output, "started", "Should contain output captured before timeout") - // Should NOT contain timeout message since background mode ignores timeout - require.NotContains(t, result.Output, "Command canceled due to timeout") + // Should contain background continuation message + require.Contains(t, result.Output, "Command continues running in background") - // Wait for the background command to complete + // Wait for the background command to complete (even though SSH session timed out) require.Eventually(t, func() bool { - args := toolsdk.WorkspaceBashArgs{ + checkArgs := toolsdk.WorkspaceBashArgs{ Workspace: workspace.Name, - Command: `cat /tmp/done`, + Command: `cat /tmp/bg-test-done 2>/dev/null || echo "not found"`, } - result, err := toolsdk.WorkspaceBash.Handler(t.Context(), deps, args) - return err == nil && result.Output == "done" - }, testutil.WaitMedium, testutil.IntervalMedium) + checkResult, err := toolsdk.WorkspaceBash.Handler(t.Context(), deps, checkArgs) + return err == nil && checkResult.Output == "done" + }, testutil.WaitMedium, testutil.IntervalMedium, "Background command should continue running and complete after timeout") }) } diff --git a/codersdk/toolsdk/resources/background.sh b/codersdk/toolsdk/resources/background.sh deleted file mode 100644 index fdb3900403377..0000000000000 --- a/codersdk/toolsdk/resources/background.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -# This script is used to run a command in the background. - -set -o errexit -set -o pipefail - -set -o nounset - -COMMAND="$ARG_COMMAND" -COMMAND_ID="$ARG_COMMAND_ID" - -set +o nounset - -LOG_DIR="/tmp/mcp-bg" -LOG_PATH="$LOG_DIR/$COMMAND_ID.log" -mkdir -p "$LOG_DIR" - -nohup bash -c "$COMMAND" >"$LOG_PATH" 2>&1 & -COMMAND_PID="$!" - -echo "Command started with PID: $COMMAND_PID" -echo "Log path: $LOG_PATH" diff --git a/codersdk/toolsdk/toolsdk_test.go b/codersdk/toolsdk/toolsdk_test.go index c201190bd3456..13e475c80609a 100644 --- a/codersdk/toolsdk/toolsdk_test.go +++ b/codersdk/toolsdk/toolsdk_test.go @@ -456,7 +456,7 @@ var testedTools sync.Map // This is to mimic how we expect external callers to use the tool. func testTool[Arg, Ret any](t *testing.T, tool toolsdk.Tool[Arg, Ret], tb toolsdk.Deps, args Arg) (Ret, error) { t.Helper() - defer func() { testedTools.Store(tool.Tool.Name, true) }() + defer func() { testedTools.Store(tool.Name, true) }() toolArgs, err := json.Marshal(args) require.NoError(t, err, "failed to marshal args") result, err := tool.Generic().Handler(t.Context(), tb, toolArgs) @@ -625,23 +625,23 @@ func TestToolSchemaFields(t *testing.T) { // Test that all tools have the required Schema fields (Properties and Required) for _, tool := range toolsdk.All { - t.Run(tool.Tool.Name, func(t *testing.T) { + t.Run(tool.Name, func(t *testing.T) { t.Parallel() // Check that Properties is not nil - require.NotNil(t, tool.Tool.Schema.Properties, - "Tool %q missing Schema.Properties", tool.Tool.Name) + require.NotNil(t, tool.Schema.Properties, + "Tool %q missing Schema.Properties", tool.Name) // Check that Required is not nil - require.NotNil(t, tool.Tool.Schema.Required, - "Tool %q missing Schema.Required", tool.Tool.Name) + require.NotNil(t, tool.Schema.Required, + "Tool %q missing Schema.Required", tool.Name) // Ensure Properties has entries for all required fields - for _, requiredField := range tool.Tool.Schema.Required { - _, exists := tool.Tool.Schema.Properties[requiredField] + for _, requiredField := range tool.Schema.Required { + _, exists := tool.Schema.Properties[requiredField] require.True(t, exists, "Tool %q requires field %q but it is not defined in Properties", - tool.Tool.Name, requiredField) + tool.Name, requiredField) } }) } @@ -652,7 +652,7 @@ func TestToolSchemaFields(t *testing.T) { func TestMain(m *testing.M) { // Initialize testedTools for _, tool := range toolsdk.All { - testedTools.Store(tool.Tool.Name, false) + testedTools.Store(tool.Name, false) } code := m.Run() @@ -660,8 +660,8 @@ func TestMain(m *testing.M) { // Ensure all tools have been tested var untested []string for _, tool := range toolsdk.All { - if tested, ok := testedTools.Load(tool.Tool.Name); !ok || !tested.(bool) { - untested = append(untested, tool.Tool.Name) + if tested, ok := testedTools.Load(tool.Name); !ok || !tested.(bool) { + untested = append(untested, tool.Name) } } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy