iop/apps/node/internal/node/workspace_handler_test.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)
}
}