419 lines
21 KiB
Go
419 lines
21 KiB
Go
package node_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
nodepkg "iop/apps/node/internal/node"
|
|
"iop/apps/node/internal/workspace"
|
|
iop "iop/proto/gen/iop"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
if handled, exitCode := workspace.RunCommandShim(os.Args); handled {
|
|
os.Exit(exitCode)
|
|
}
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func TestNodeWorkspaceCommandHelperProcess(t *testing.T) {
|
|
mode := os.Getenv("IOP_NODE_WORKSPACE_HELPER")
|
|
if mode == "" {
|
|
return
|
|
}
|
|
switch mode {
|
|
case "success":
|
|
_, _ = fmt.Fprint(os.Stdout, "node-command-stdout")
|
|
_, _ = fmt.Fprint(os.Stderr, "node-command-stderr")
|
|
case "block":
|
|
if err := os.WriteFile(os.Getenv("IOP_NODE_START_FILE"), []byte("started"), 0o600); err != nil {
|
|
os.Exit(21)
|
|
}
|
|
for {
|
|
time.Sleep(time.Hour)
|
|
}
|
|
default:
|
|
os.Exit(22)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
|
|
func workspaceRuntimeForNode(t *testing.T) (*workspace.Runtime, string) {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
runtime, err := workspace.NewRuntime([]*iop.WorkspaceConfig{{
|
|
Ref: "workspace-1", Platform: "darwin", Root: root,
|
|
Operations: []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_READ, iop.WorkspaceOperation_WORKSPACE_OPERATION_LIST, iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE, iop.WorkspaceOperation_WORKSPACE_OPERATION_DELETE},
|
|
MaxReadBytes: 64, MaxWriteBytes: 64, MaxOutputBytes: 64,
|
|
}}, "darwin", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = runtime.Close() })
|
|
return runtime, root
|
|
}
|
|
|
|
func workspaceOpenForNode(requestID string) *iop.WorkspaceOpenRequest {
|
|
return &iop.WorkspaceOpenRequest{
|
|
RequestId: requestID, WorkspaceRef: "workspace-1",
|
|
Operations: []iop.WorkspaceOperation{
|
|
iop.WorkspaceOperation_WORKSPACE_OPERATION_READ,
|
|
iop.WorkspaceOperation_WORKSPACE_OPERATION_LIST,
|
|
iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE,
|
|
iop.WorkspaceOperation_WORKSPACE_OPERATION_DELETE,
|
|
},
|
|
MaxReadBytes: 64, MaxWriteBytes: 64, MaxOutputBytes: 64,
|
|
}
|
|
}
|
|
|
|
func TestNodeWorkspaceOpenAndFileMapping(t *testing.T) {
|
|
n, _ := makeNode(t, nil)
|
|
runtime, root := workspaceRuntimeForNode(t)
|
|
n.SetWorkspaceRuntime(runtime)
|
|
if err := os.WriteFile(filepath.Join(root, "input.txt"), []byte("ok"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
opened, err := n.OnWorkspaceOpen(context.Background(), nil, workspaceOpenForNode("request-1"))
|
|
if err != nil || opened.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || opened.GetRequestId() != "request-1" {
|
|
t.Fatalf("open=%+v err=%v", opened, err)
|
|
}
|
|
read, err := n.OnWorkspaceTool(context.Background(), nil, &iop.WorkspaceToolRequest{RequestId: "request-1", StageId: "work", ToolCallId: "read-1", Operation: iop.WorkspaceOperation_WORKSPACE_OPERATION_READ, Input: &iop.WorkspaceToolRequest_RelativePath{RelativePath: "input.txt"}})
|
|
if err != nil || read.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || string(read.GetContent()) != "ok" {
|
|
t.Fatalf("read=%+v err=%v", read, err)
|
|
}
|
|
write, err := n.OnWorkspaceTool(context.Background(), nil, &iop.WorkspaceToolRequest{
|
|
RequestId: "request-1", StageId: "work", ToolCallId: "write-1",
|
|
Operation: iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE,
|
|
Input: &iop.WorkspaceToolRequest_Write{Write: &iop.WorkspaceWriteInput{RelativePath: "output.txt", Content: []byte("written")}},
|
|
})
|
|
if err != nil || write.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("write=%+v err=%v", write, err)
|
|
}
|
|
written, readErr := os.ReadFile(filepath.Join(root, "output.txt"))
|
|
if readErr != nil || string(written) != "written" {
|
|
t.Fatalf("written=%q err=%v", written, readErr)
|
|
}
|
|
oversized, err := n.OnWorkspaceTool(context.Background(), nil, &iop.WorkspaceToolRequest{
|
|
RequestId: "request-1", StageId: "work", ToolCallId: "write-oversized",
|
|
Operation: iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE,
|
|
Input: &iop.WorkspaceToolRequest_Write{Write: &iop.WorkspaceWriteInput{RelativePath: "oversized.txt", Content: []byte(strings.Repeat("x", 65))}},
|
|
})
|
|
if err != nil || oversized.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST {
|
|
t.Fatalf("oversized=%+v err=%v", oversized, err)
|
|
}
|
|
for name, request := range map[string]func() *iop.WorkspaceToolRequest{
|
|
"legacy": func() *iop.WorkspaceToolRequest {
|
|
return &iop.WorkspaceToolRequest{Input: &iop.WorkspaceToolRequest_WriteContent{WriteContent: []byte("legacy")}}
|
|
},
|
|
"path-only": func() *iop.WorkspaceToolRequest {
|
|
return &iop.WorkspaceToolRequest{Input: &iop.WorkspaceToolRequest_RelativePath{RelativePath: "legacy.txt"}}
|
|
},
|
|
"nil-structured": func() *iop.WorkspaceToolRequest {
|
|
return &iop.WorkspaceToolRequest{Input: &iop.WorkspaceToolRequest_Write{}}
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
toolRequest := request()
|
|
toolRequest.RequestId, toolRequest.StageId, toolRequest.ToolCallId = "request-1", "work", "bad-write"
|
|
toolRequest.Operation = iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE
|
|
response, err := n.OnWorkspaceTool(context.Background(), nil, toolRequest)
|
|
if err != nil || response.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST {
|
|
t.Fatalf("response=%+v err=%v", response, err)
|
|
}
|
|
})
|
|
}
|
|
command, err := n.OnWorkspaceTool(context.Background(), nil, &iop.WorkspaceToolRequest{RequestId: "request-1", StageId: "work", ToolCallId: "command-1", Operation: iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND})
|
|
if err != nil || command.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR || command.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST {
|
|
t.Fatalf("command=%+v err=%v", command, err)
|
|
}
|
|
}
|
|
|
|
func TestNodeWorkspaceArtifactMapping(t *testing.T) {
|
|
n, _ := makeNode(t, nil)
|
|
runtime, root := workspaceRuntimeForNode(t)
|
|
n.SetWorkspaceRuntime(runtime)
|
|
if opened, err := n.OnWorkspaceOpen(context.Background(), nil, workspaceOpenForNode("request-artifact")); err != nil || opened.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("open = %+v, %v", opened, err)
|
|
}
|
|
|
|
readPlan := &iop.WorkspaceArtifactRequest{
|
|
RequestId: "request-artifact", Kind: iop.WorkspaceArtifactKind_WORKSPACE_ARTIFACT_KIND_PLAN,
|
|
Operation: iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_READ,
|
|
}
|
|
missing, err := n.OnWorkspaceArtifact(context.Background(), nil, readPlan)
|
|
if err != nil || missing.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR || missing.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_FOUND || missing.GetContent() != nil {
|
|
t.Fatalf("missing plan = %+v, %v", missing, err)
|
|
}
|
|
writePlan := &iop.WorkspaceArtifactRequest{
|
|
RequestId: "request-artifact", Kind: iop.WorkspaceArtifactKind_WORKSPACE_ARTIFACT_KIND_PLAN,
|
|
Operation: iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_WRITE, Content: []byte("bounded plan"),
|
|
}
|
|
written, err := n.OnWorkspaceArtifact(context.Background(), nil, writePlan)
|
|
if err != nil || written.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || len(written.GetContent()) != 0 {
|
|
t.Fatalf("write plan = %+v, %v", written, err)
|
|
}
|
|
read, err := n.OnWorkspaceArtifact(context.Background(), nil, readPlan)
|
|
if err != nil || read.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || string(read.GetContent()) != "bounded plan" {
|
|
t.Fatalf("read plan = %+v, %v", read, err)
|
|
}
|
|
if data, err := os.ReadFile(filepath.Join(root, ".iop", "job", "request-artifact", "plan.md")); err != nil || string(data) != "bounded plan" {
|
|
t.Fatalf("mapped plan = %q, %v", data, err)
|
|
}
|
|
|
|
for name, request := range map[string]*iop.WorkspaceArtifactRequest{
|
|
"unknown-kind": {
|
|
RequestId: "request-artifact", Kind: iop.WorkspaceArtifactKind_WORKSPACE_ARTIFACT_KIND_UNSPECIFIED,
|
|
Operation: iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_READ,
|
|
},
|
|
"unknown-operation": {
|
|
RequestId: "request-artifact", Kind: iop.WorkspaceArtifactKind_WORKSPACE_ARTIFACT_KIND_REVIEW,
|
|
Operation: iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_UNSPECIFIED,
|
|
},
|
|
"read-content": {
|
|
RequestId: "request-artifact", Kind: iop.WorkspaceArtifactKind_WORKSPACE_ARTIFACT_KIND_REVIEW,
|
|
Operation: iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_READ, Content: []byte("must not be accepted"),
|
|
},
|
|
"oversized-write": {
|
|
RequestId: "request-artifact", Kind: iop.WorkspaceArtifactKind_WORKSPACE_ARTIFACT_KIND_REVIEW,
|
|
Operation: iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_WRITE, Content: []byte(strings.Repeat("x", 1<<20+1)),
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
response, callErr := n.OnWorkspaceArtifact(context.Background(), nil, request)
|
|
if callErr != nil || response.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR || response.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST || len(response.GetContent()) != 0 {
|
|
t.Fatalf("response = %+v, %v", response, callErr)
|
|
}
|
|
})
|
|
}
|
|
if _, err := os.Stat(filepath.Join(root, ".iop", "job", "request-artifact", "review.md")); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("malformed artifact request created review.md: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNodeWorkspaceArtifactStableFailures(t *testing.T) {
|
|
n, _ := makeNode(t, nil)
|
|
request := &iop.WorkspaceArtifactRequest{
|
|
RequestId: "request-artifact", Kind: iop.WorkspaceArtifactKind_WORKSPACE_ARTIFACT_KIND_PLAN,
|
|
Operation: iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_READ,
|
|
}
|
|
missingRuntime, err := n.OnWorkspaceArtifact(context.Background(), nil, request)
|
|
if err != nil || missingRuntime.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_UNSUPPORTED || missingRuntime.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_READY {
|
|
t.Fatalf("missing runtime = %+v, %v", missingRuntime, err)
|
|
}
|
|
invalid, err := n.OnWorkspaceArtifact(context.Background(), nil, nil)
|
|
if err != nil || invalid.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST {
|
|
t.Fatalf("nil request = %+v, %v", invalid, err)
|
|
}
|
|
|
|
runtime, root := workspaceRuntimeForNode(t)
|
|
n.SetWorkspaceRuntime(runtime)
|
|
if opened, err := n.OnWorkspaceOpen(context.Background(), nil, workspaceOpenForNode("request-artifact")); err != nil || opened.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("open = %+v, %v", opened, err)
|
|
}
|
|
write := proto.Clone(request).(*iop.WorkspaceArtifactRequest)
|
|
write.Operation = iop.WorkspaceArtifactOperation_WORKSPACE_ARTIFACT_OPERATION_WRITE
|
|
write.Content = []byte("owned")
|
|
if response, err := n.OnWorkspaceArtifact(context.Background(), nil, write); err != nil || response.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("write = %+v, %v", response, err)
|
|
}
|
|
target := filepath.Join(root, ".iop", "job", "request-artifact", "plan.md")
|
|
if err := os.Remove(target); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(target, []byte("raw replacement sentinel"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
replaced, err := n.OnWorkspaceArtifact(context.Background(), nil, request)
|
|
if err != nil || replaced.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR || replaced.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INTERNAL || strings.Contains(replaced.GetError(), "replacement") || len(replaced.GetContent()) != 0 {
|
|
t.Fatalf("replacement response = %+v, %v", replaced, err)
|
|
}
|
|
}
|
|
|
|
func TestNodeWorkspaceCleanupStableFailuresAndLifecycle(t *testing.T) {
|
|
n, _ := makeNode(t, nil)
|
|
missing, err := n.OnWorkspaceOpen(context.Background(), nil, workspaceOpenForNode("request-1"))
|
|
if err != nil || missing.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_UNSUPPORTED || missing.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_READY {
|
|
t.Fatalf("missing=%+v err=%v", missing, err)
|
|
}
|
|
runtime, _ := workspaceRuntimeForNode(t)
|
|
n.SetWorkspaceRuntime(runtime)
|
|
opened, _ := n.OnWorkspaceOpen(context.Background(), nil, workspaceOpenForNode("request-1"))
|
|
if opened.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("open=%+v", opened)
|
|
}
|
|
badPath := ".iop/job/request-1/secret"
|
|
result, err := n.OnWorkspaceTool(context.Background(), nil, &iop.WorkspaceToolRequest{RequestId: "request-1", StageId: "work", ToolCallId: "tool-1", Operation: iop.WorkspaceOperation_WORKSPACE_OPERATION_READ, Input: &iop.WorkspaceToolRequest_RelativePath{RelativePath: badPath}})
|
|
if err != nil || result.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST || strings.Contains(result.GetError(), badPath) {
|
|
t.Fatalf("result=%+v err=%v", result, err)
|
|
}
|
|
cancel, _ := n.OnWorkspaceCancel(context.Background(), nil, &iop.WorkspaceCancelRequest{RequestId: "request-1", StageId: "work", ToolCallId: "tool-1"})
|
|
cleanup, _ := n.OnWorkspaceCleanup(context.Background(), nil, &iop.WorkspaceCleanupRequest{RequestId: "request-1"})
|
|
if cancel.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR || cancel.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_FOUND || cleanup.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || cleanup.GetCleanedArtifacts() != 1 {
|
|
t.Fatalf("cancel=%+v cleanup=%+v", cancel, cleanup)
|
|
}
|
|
}
|
|
|
|
func TestNodeWorkspaceCleanupMappingAndIdempotence(t *testing.T) {
|
|
n, _ := makeNode(t, nil)
|
|
missing, err := n.OnWorkspaceCleanup(context.Background(), nil, &iop.WorkspaceCleanupRequest{RequestId: "request-cleanup"})
|
|
if err != nil || missing.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_UNSUPPORTED || missing.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_READY {
|
|
t.Fatalf("missing runtime cleanup = %+v, %v", missing, err)
|
|
}
|
|
invalid, err := n.OnWorkspaceCleanup(context.Background(), nil, nil)
|
|
if err != nil || invalid.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR || invalid.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST {
|
|
t.Fatalf("invalid cleanup = %+v, %v", invalid, err)
|
|
}
|
|
|
|
runtime, root := workspaceRuntimeForNode(t)
|
|
n.SetWorkspaceRuntime(runtime)
|
|
opened, err := n.OnWorkspaceOpen(context.Background(), nil, workspaceOpenForNode("request-cleanup"))
|
|
if err != nil || opened.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("open = %+v, %v", opened, err)
|
|
}
|
|
if err := runtime.WriteInternalArtifact("request-cleanup", "plan.md", []byte("plan")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := &iop.WorkspaceCleanupRequest{RequestId: "request-cleanup"}
|
|
first, err := n.OnWorkspaceCleanup(context.Background(), nil, request)
|
|
if err != nil || first.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || first.GetCleanedArtifacts() != 2 {
|
|
t.Fatalf("first cleanup = %+v, %v", first, err)
|
|
}
|
|
second, err := n.OnWorkspaceCleanup(context.Background(), nil, request)
|
|
if err != nil || second.GetStatus() != first.GetStatus() || second.GetErrorCode() != first.GetErrorCode() || second.GetCleanedArtifacts() != first.GetCleanedArtifacts() {
|
|
t.Fatalf("second cleanup = %+v, %v; first=%+v", second, err, first)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(root, ".iop", "job", "request-cleanup")); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("request artifacts remain: %v", err)
|
|
}
|
|
unknown, err := n.OnWorkspaceCleanup(context.Background(), nil, &iop.WorkspaceCleanupRequest{RequestId: "request-unknown"})
|
|
if err != nil || unknown.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_FOUND {
|
|
t.Fatalf("unknown cleanup = %+v, %v", unknown, err)
|
|
}
|
|
}
|
|
|
|
func workspaceCommandRuntimeForNode(t *testing.T) (*workspace.Runtime, string) {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
executable, err := os.Executable()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
runtime, err := workspace.NewRuntime([]*iop.WorkspaceConfig{{
|
|
Ref: "workspace-command", Platform: "darwin", Root: root,
|
|
Operations: []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND},
|
|
Commands: []*iop.WorkspaceCommandConfig{{
|
|
Id: "helper", Executable: executable,
|
|
Args: []string{"-test.run=^TestNodeWorkspaceCommandHelperProcess$"},
|
|
}},
|
|
EnvironmentAllowlist: []string{"IOP_NODE_WORKSPACE_HELPER", "IOP_NODE_START_FILE"},
|
|
MaxOutputBytes: 128, MaxCommandTimeoutMs: 5000,
|
|
}}, "darwin", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = runtime.Close() })
|
|
return runtime, root
|
|
}
|
|
|
|
func openNodeCommandWorkspace(t *testing.T, n *nodepkg.Node) {
|
|
t.Helper()
|
|
response, err := n.OnWorkspaceOpen(context.Background(), nil, &iop.WorkspaceOpenRequest{
|
|
RequestId: "request-command", WorkspaceRef: "workspace-command",
|
|
Operations: []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND},
|
|
CommandIds: []string{"helper"}, MaxOutputBytes: 128, MaxCommandTimeoutMs: 5000,
|
|
})
|
|
if err != nil || response.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("open command workspace = %+v, %v", response, err)
|
|
}
|
|
}
|
|
|
|
func TestNodeWorkspaceCommand(t *testing.T) {
|
|
n, _ := makeNode(t, nil)
|
|
runtime, _ := workspaceCommandRuntimeForNode(t)
|
|
n.SetWorkspaceRuntime(runtime)
|
|
openNodeCommandWorkspace(t, n)
|
|
|
|
response, err := n.OnWorkspaceTool(context.Background(), nil, &iop.WorkspaceToolRequest{
|
|
RequestId: "request-command", StageId: "work", ToolCallId: "tool-success",
|
|
Operation: iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND, TimeoutMs: 4000,
|
|
Input: &iop.WorkspaceToolRequest_CommandId{CommandId: "helper"},
|
|
Environment: map[string]string{"IOP_NODE_WORKSPACE_HELPER": "success"},
|
|
})
|
|
if err != nil || response.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || response.GetExitCode() != 0 || string(response.GetStdout()) != "node-command-stdout" || string(response.GetStderr()) != "node-command-stderr" {
|
|
t.Fatalf("command response = %+v, %v", response, err)
|
|
}
|
|
|
|
const sentinel = "raw-command-or-environment-sentinel"
|
|
for name, request := range map[string]*iop.WorkspaceToolRequest{
|
|
"unknown-command": {
|
|
RequestId: "request-command", StageId: "work", ToolCallId: "tool-unknown",
|
|
Operation: iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND, TimeoutMs: 4000,
|
|
Input: &iop.WorkspaceToolRequest_CommandId{CommandId: sentinel},
|
|
},
|
|
"unapproved-environment": {
|
|
RequestId: "request-command", StageId: "work", ToolCallId: "tool-environment",
|
|
Operation: iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND, TimeoutMs: 4000,
|
|
Input: &iop.WorkspaceToolRequest_CommandId{CommandId: "helper"},
|
|
Environment: map[string]string{"HOME": sentinel},
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
result, callErr := n.OnWorkspaceTool(context.Background(), nil, request)
|
|
if callErr != nil || result.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST || strings.Contains(result.GetError(), sentinel) {
|
|
t.Fatalf("result = %+v, %v", result, callErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNodeWorkspaceCancel(t *testing.T) {
|
|
n, _ := makeNode(t, nil)
|
|
runtime, root := workspaceCommandRuntimeForNode(t)
|
|
n.SetWorkspaceRuntime(runtime)
|
|
openNodeCommandWorkspace(t, n)
|
|
started := filepath.Join(root, "command-started")
|
|
resultChannel := make(chan *iop.WorkspaceToolResponse, 1)
|
|
go func() {
|
|
response, _ := n.OnWorkspaceTool(context.Background(), nil, &iop.WorkspaceToolRequest{
|
|
RequestId: "request-command", StageId: "work", ToolCallId: "tool-cancel",
|
|
Operation: iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND, TimeoutMs: 4000,
|
|
Input: &iop.WorkspaceToolRequest_CommandId{CommandId: "helper"},
|
|
Environment: map[string]string{"IOP_NODE_WORKSPACE_HELPER": "block", "IOP_NODE_START_FILE": started},
|
|
})
|
|
resultChannel <- response
|
|
}()
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if _, err := os.Stat(started); err == nil {
|
|
break
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
if _, err := os.Stat(started); err != nil {
|
|
t.Fatalf("command did not start: %v", err)
|
|
}
|
|
request := &iop.WorkspaceCancelRequest{RequestId: "request-command", StageId: "work", ToolCallId: "tool-cancel"}
|
|
first, err := n.OnWorkspaceCancel(context.Background(), nil, request)
|
|
if err != nil || first.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_CANCELLED || first.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_CANCELLED {
|
|
t.Fatalf("first cancel = %+v, %v", first, err)
|
|
}
|
|
second, err := n.OnWorkspaceCancel(context.Background(), nil, request)
|
|
if err != nil || second.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_CANCELLED || second.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_CANCELLED {
|
|
t.Fatalf("duplicate cancel = %+v, %v", second, err)
|
|
}
|
|
if result := <-resultChannel; result.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_CANCELLED || result.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_CANCELLED {
|
|
t.Fatalf("command result = %+v", result)
|
|
}
|
|
notFound, _ := n.OnWorkspaceCancel(context.Background(), nil, &iop.WorkspaceCancelRequest{RequestId: "request-command", StageId: "work", ToolCallId: "missing"})
|
|
if notFound.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_FOUND {
|
|
t.Fatalf("not found cancel = %+v", notFound)
|
|
}
|
|
}
|