mirror of
https://github.com/coder/coder.git
synced 2026-06-04 13:38:21 +00:00
ce627bf23f
closes: https://github.com/coder/coder/issues/10352 closes: https://github.com/coder/internal/issues/1094 closes: https://github.com/coder/internal/issues/1095 In this pull request, we enable a new set of experimental cli commands grouped under `coder exp sync`. These commands allow any process acting within a coder workspace to inform the coder agent of its requirements and execution progress. The coder agent will then relay this information to other processes that have subscribed. These commands are: ``` # Check if this feature is enabled in your environment coder exp sync ping # express that your unit depends on another coder exp sync want <unit> <dependency_unit> # express that your unit intends to start a portion of the script that requires # other units to have completed first. This command blocks until all dependencies have been met coder exp sync start <unit> # express that your unit has completes its work, allowing dependent units to begin their execution coder exp sync complete <unit> ``` Example: In order to automatically run claude code in a new workspace, it must first have a git repository cloned. The scripts responsible for cloning the repository and for running claude code would coordinate in the following way: ```bash # Script A: Claude code # Inform the agent that the claude script wants the git script. # That is, the git script must have completed before the claude script can begin its execution coder exp sync want claude git # Inform the agent that we would now like to begin execution of claude. # This command will block until the git script (and any other defined dependencies) # have completed coder exp sync start claude # Now we run claude code and any other commands we need claude ... # Once our script has completed, we inform the agent, so that any scripts that depend on this one # may begin their execution coder exp sync complete claude ``` ```bash # Script B: Git # Because the git script does not have any dependencies, we can simply inform the agent that we # intend to start coder exp sync start git git clone ssh://git@github.com/coder/coder # Once the repository have been cloned, we inform the agent that this script is complete, so that # scripts that depend on it may begin their execution. coder exp sync complete git ``` Notes: * Unit names (ie. `claude` and `git`) given as input to the sync commands are arbitrary strings. You do not have to conform to specific identifiers. We recommend naming your scripts descriptively, but succinctly. * Scripts unit names should be well documented. Other scripts will need to know the names you've chosen in order to depend on yours. Therefore, you --------- Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
139 lines
3.7 KiB
Go
139 lines
3.7 KiB
Go
package agentsocket_test
|
|
|
|
import (
|
|
"context"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"cdr.dev/slog"
|
|
"github.com/coder/coder/v2/agent"
|
|
"github.com/coder/coder/v2/agent/agentsocket"
|
|
"github.com/coder/coder/v2/agent/agenttest"
|
|
agentproto "github.com/coder/coder/v2/agent/proto"
|
|
"github.com/coder/coder/v2/codersdk/agentsdk"
|
|
"github.com/coder/coder/v2/tailnet"
|
|
"github.com/coder/coder/v2/tailnet/tailnettest"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
func TestServer(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("agentsocket is not supported on Windows")
|
|
}
|
|
|
|
t.Run("StartStop", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
socketPath := filepath.Join(t.TempDir(), "test.sock")
|
|
logger := slog.Make().Leveled(slog.LevelDebug)
|
|
server, err := agentsocket.NewServer(logger, agentsocket.WithPath(socketPath))
|
|
require.NoError(t, err)
|
|
require.NoError(t, server.Close())
|
|
})
|
|
|
|
t.Run("AlreadyStarted", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
socketPath := filepath.Join(t.TempDir(), "test.sock")
|
|
logger := slog.Make().Leveled(slog.LevelDebug)
|
|
server1, err := agentsocket.NewServer(logger, agentsocket.WithPath(socketPath))
|
|
require.NoError(t, err)
|
|
defer server1.Close()
|
|
_, err = agentsocket.NewServer(logger, agentsocket.WithPath(socketPath))
|
|
require.ErrorContains(t, err, "create socket")
|
|
})
|
|
|
|
t.Run("AutoSocketPath", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
socketPath := filepath.Join(t.TempDir(), "test.sock")
|
|
logger := slog.Make().Leveled(slog.LevelDebug)
|
|
server, err := agentsocket.NewServer(logger, agentsocket.WithPath(socketPath))
|
|
require.NoError(t, err)
|
|
require.NoError(t, server.Close())
|
|
})
|
|
}
|
|
|
|
func TestServerWindowsNotSupported(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if runtime.GOOS != "windows" {
|
|
t.Skip("this test only runs on Windows")
|
|
}
|
|
|
|
t.Run("NewServer", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
socketPath := filepath.Join(t.TempDir(), "test.sock")
|
|
logger := slog.Make().Leveled(slog.LevelDebug)
|
|
_, err := agentsocket.NewServer(logger, agentsocket.WithPath(socketPath))
|
|
require.ErrorContains(t, err, "agentsocket is not supported on Windows")
|
|
})
|
|
|
|
t.Run("NewClient", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := agentsocket.NewClient(context.Background(), agentsocket.WithPath("test.sock"))
|
|
require.ErrorContains(t, err, "agentsocket is not supported on Windows")
|
|
})
|
|
}
|
|
|
|
func TestAgentInitializesOnWindowsWithoutSocketServer(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if runtime.GOOS != "windows" {
|
|
t.Skip("this test only runs on Windows")
|
|
}
|
|
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
logger := testutil.Logger(t).Named("agent")
|
|
|
|
derpMap, _ := tailnettest.RunDERPAndSTUN(t)
|
|
|
|
coordinator := tailnet.NewCoordinator(logger)
|
|
t.Cleanup(func() {
|
|
_ = coordinator.Close()
|
|
})
|
|
|
|
statsCh := make(chan *agentproto.Stats, 50)
|
|
agentID := uuid.New()
|
|
manifest := agentsdk.Manifest{
|
|
AgentID: agentID,
|
|
AgentName: "test-agent",
|
|
WorkspaceName: "test-workspace",
|
|
OwnerName: "test-user",
|
|
WorkspaceID: uuid.New(),
|
|
DERPMap: derpMap,
|
|
}
|
|
|
|
client := agenttest.NewClient(t, logger.Named("agenttest"), agentID, manifest, statsCh, coordinator)
|
|
t.Cleanup(client.Close)
|
|
|
|
options := agent.Options{
|
|
Client: client,
|
|
Filesystem: afero.NewMemMapFs(),
|
|
Logger: logger.Named("agent"),
|
|
ReconnectingPTYTimeout: testutil.WaitShort,
|
|
EnvironmentVariables: map[string]string{},
|
|
SocketPath: "",
|
|
}
|
|
|
|
agnt := agent.New(options)
|
|
t.Cleanup(func() {
|
|
_ = agnt.Close()
|
|
})
|
|
|
|
startup := testutil.TryReceive(ctx, t, client.GetStartup())
|
|
require.NotNil(t, startup, "agent should send startup message")
|
|
|
|
err := agnt.Close()
|
|
require.NoError(t, err, "agent should close cleanly")
|
|
}
|