승인된 execution preset을 Edge 조정 경계와 Node workspace/tool 실행 경계로 연결해 단일 요청 수명주기와 관측 계약을 일관되게 처리한다.
247 lines
8.5 KiB
Go
247 lines
8.5 KiB
Go
package workspace
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
iop "iop/proto/gen/iop"
|
|
)
|
|
|
|
func testWorkspaceConfig(root string) *iop.WorkspaceConfig {
|
|
return &iop.WorkspaceConfig{
|
|
Ref: "mac-workspace", 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,
|
|
}
|
|
}
|
|
|
|
func testRequestAuthority(requestID string) RequestAuthority {
|
|
return RequestAuthority{
|
|
RequestID: requestID, WorkspaceRef: "mac-workspace",
|
|
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 TestRuntimeCatalog(t *testing.T) {
|
|
root := t.TempDir()
|
|
if _, err := NewRuntime([]*iop.WorkspaceConfig{testWorkspaceConfig(root)}, "darwin", nil); err != nil {
|
|
t.Fatalf("NewRuntime(valid): %v", err)
|
|
}
|
|
for name, configs := range map[string][]*iop.WorkspaceConfig{
|
|
"wrong host": []*iop.WorkspaceConfig{testWorkspaceConfig(root)},
|
|
"missing": []*iop.WorkspaceConfig{testWorkspaceConfig(root + "/missing")},
|
|
"root": []*iop.WorkspaceConfig{testWorkspaceConfig("/")},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
host := "darwin"
|
|
if name == "wrong host" {
|
|
host = "linux"
|
|
}
|
|
if _, err := NewRuntime(configs, host, nil); err == nil {
|
|
t.Fatal("NewRuntime succeeded")
|
|
}
|
|
})
|
|
}
|
|
link := root + "-link"
|
|
if err := os.Symlink(root, link); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := NewRuntime([]*iop.WorkspaceConfig{testWorkspaceConfig(link)}, "darwin", nil); err == nil {
|
|
t.Fatal("symlink root was admitted")
|
|
}
|
|
const sentinel = "workspace-root-sentinel-do-not-disclose"
|
|
_, err := NewRuntime([]*iop.WorkspaceConfig{testWorkspaceConfig(root + "/" + sentinel)}, "darwin", nil)
|
|
if err == nil || strings.Contains(err.Error(), sentinel) {
|
|
t.Fatalf("startup error = %v", err)
|
|
}
|
|
|
|
readOnly := testWorkspaceConfig(root)
|
|
readOnly.Operations = []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_READ}
|
|
readOnly.MaxWriteBytes = 0
|
|
readOnly.MaxOutputBytes = 0
|
|
readRuntime, err := NewRuntime([]*iop.WorkspaceConfig{readOnly}, "darwin", nil)
|
|
if err != nil {
|
|
t.Fatalf("read-only catalog: %v", err)
|
|
}
|
|
_ = readRuntime.Close()
|
|
|
|
commandOnly := testWorkspaceConfig(root)
|
|
commandOnly.Operations = []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND}
|
|
commandOnly.Commands = []*iop.WorkspaceCommandConfig{{Id: "test", Executable: "/usr/bin/true"}}
|
|
commandOnly.MaxReadBytes = 0
|
|
commandOnly.MaxWriteBytes = 0
|
|
commandOnly.MaxOutputBytes = 1
|
|
commandOnly.MaxCommandTimeoutMs = 1
|
|
commandRuntime, err := NewRuntime([]*iop.WorkspaceConfig{commandOnly}, "darwin", nil)
|
|
if err != nil {
|
|
t.Fatalf("command-only catalog: %v", err)
|
|
}
|
|
_ = commandRuntime.Close()
|
|
}
|
|
|
|
func TestRuntimeOpen(t *testing.T) {
|
|
rt, err := NewRuntime([]*iop.WorkspaceConfig{testWorkspaceConfig(t.TempDir())}, "darwin", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = rt.Close() })
|
|
authority := testRequestAuthority("request-1")
|
|
first, err := rt.Open(authority)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first.internalPrefix != ".iop/job/request-1" {
|
|
t.Fatalf("prefix=%q", first.internalPrefix)
|
|
}
|
|
if _, err := rt.Open(authority); err != nil {
|
|
t.Fatalf("idempotent open: %v", err)
|
|
}
|
|
conflict := authority
|
|
conflict.MaxReadBytes = 32
|
|
if _, err := rt.Open(conflict); err != ErrRequestConflict {
|
|
t.Fatalf("conflict=%v", err)
|
|
}
|
|
invalid := authority
|
|
invalid.RequestID = "bad/id"
|
|
if _, err := rt.Open(invalid); err != ErrInvalidRequest {
|
|
t.Fatalf("invalid=%v", err)
|
|
}
|
|
for name, mutate := range map[string]func(*RequestAuthority){
|
|
"operation widening": func(value *RequestAuthority) {
|
|
value.Operations = append(value.Operations, iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND)
|
|
},
|
|
"read limit widening": func(value *RequestAuthority) { value.MaxReadBytes = 65 },
|
|
"disabled limit": func(value *RequestAuthority) {
|
|
value.Operations = []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_READ}
|
|
value.MaxWriteBytes = 1
|
|
value.MaxOutputBytes = 0
|
|
},
|
|
"unknown command": func(value *RequestAuthority) {
|
|
value.Operations = []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND}
|
|
value.CommandIDs = []string{"missing"}
|
|
value.MaxReadBytes, value.MaxWriteBytes, value.MaxOutputBytes, value.MaxCommandTimeoutMS = 0, 0, 1, 1
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
candidate := testRequestAuthority("request-" + strings.ReplaceAll(name, " ", "-"))
|
|
mutate(&candidate)
|
|
if _, err := rt.Open(candidate); err != ErrInvalidRequest {
|
|
t.Fatalf("Open = %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
lowered := RequestAuthority{
|
|
RequestID: "request-lowered", WorkspaceRef: "mac-workspace",
|
|
Operations: []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_READ},
|
|
MaxReadBytes: 32,
|
|
}
|
|
request, err := rt.Open(lowered)
|
|
if err != nil {
|
|
t.Fatalf("lowered read-only authority: %v", err)
|
|
}
|
|
lowered.Operations[0] = iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE
|
|
lowered.MaxReadBytes = 1
|
|
if _, ok := request.operations[iop.WorkspaceOperation_WORKSPACE_OPERATION_READ]; !ok || request.maxRead != 32 {
|
|
t.Fatalf("request authority changed through caller mutation: %+v", request)
|
|
}
|
|
if _, err := first.internalPath(".iop/job/request-2/plan.md"); err == nil {
|
|
t.Fatal("sibling internal path admitted")
|
|
}
|
|
if _, err := first.internalPath(".iop/job/request-1/plan.md"); err != nil {
|
|
t.Fatalf("owned internal path: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRuntimeCloseAndConcurrentIsolation(t *testing.T) {
|
|
root := t.TempDir()
|
|
rt, err := NewRuntime([]*iop.WorkspaceConfig{testWorkspaceConfig(root)}, "darwin", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var group sync.WaitGroup
|
|
for i := 0; i < 32; i++ {
|
|
group.Add(1)
|
|
go func(index int) {
|
|
defer group.Done()
|
|
id := fmt.Sprintf("request-%d", index)
|
|
authority := testRequestAuthority(id)
|
|
if _, err := rt.Open(authority); err != nil {
|
|
t.Errorf("Open(%s): %v", id, err)
|
|
}
|
|
}(i)
|
|
}
|
|
group.Wait()
|
|
if err := rt.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := rt.Request("request-a"); err != ErrClosed {
|
|
t.Fatalf("Request after close=%v", err)
|
|
}
|
|
if err := rt.Close(); err != nil {
|
|
t.Fatalf("second close: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRuntimeOpenCommandAuthority(t *testing.T) {
|
|
config := testWorkspaceConfig(t.TempDir())
|
|
config.Operations = []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND}
|
|
config.Commands = []*iop.WorkspaceCommandConfig{
|
|
{Id: "format", Executable: "/usr/bin/true"},
|
|
{Id: "test", Executable: "/usr/bin/true"},
|
|
}
|
|
config.MaxReadBytes = 0
|
|
config.MaxWriteBytes = 0
|
|
config.MaxOutputBytes = 64
|
|
config.MaxCommandTimeoutMs = 1000
|
|
rt, err := NewRuntime([]*iop.WorkspaceConfig{config}, "darwin", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = rt.Close() })
|
|
authority := RequestAuthority{
|
|
RequestID: "request-command", WorkspaceRef: "mac-workspace",
|
|
Operations: []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND},
|
|
CommandIDs: []string{"test"}, MaxOutputBytes: 32, MaxCommandTimeoutMS: 500,
|
|
}
|
|
request, err := rt.Open(authority)
|
|
if err != nil {
|
|
t.Fatalf("command-only open: %v", err)
|
|
}
|
|
authority.CommandIDs[0] = "format"
|
|
if len(request.commandIDs) != 1 || request.commandIDs[0] != "test" || request.maxCommandTimeout != 500 {
|
|
t.Fatalf("command authority was not copied: %+v", request)
|
|
}
|
|
for name, mutate := range map[string]func(*RequestAuthority){
|
|
"unknown command": func(value *RequestAuthority) { value.CommandIDs = []string{"unknown"} },
|
|
"missing command": func(value *RequestAuthority) { value.CommandIDs = nil },
|
|
"output widening": func(value *RequestAuthority) { value.MaxOutputBytes = 65 },
|
|
"timeout widening": func(value *RequestAuthority) { value.MaxCommandTimeoutMS = 1001 },
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
candidate := RequestAuthority{
|
|
RequestID: "request-" + strings.ReplaceAll(name, " ", "-"), WorkspaceRef: "mac-workspace",
|
|
Operations: []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND},
|
|
CommandIDs: []string{"test"}, MaxOutputBytes: 32, MaxCommandTimeoutMS: 500,
|
|
}
|
|
mutate(&candidate)
|
|
if _, err := rt.Open(candidate); err != ErrInvalidRequest {
|
|
t.Fatalf("Open = %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|