iop/apps/node/internal/workspace/runtime_test.go
toki dc9a9a8c59 feat(agent): 단일 요청 Agent 실행 경계를 구현한다
승인된 execution preset을 Edge 조정 경계와 Node workspace/tool 실행 경계로 연결해 단일 요청 수명주기와 관측 계약을 일관되게 처리한다.
2026-08-07 07:03:55 +09:00

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)
}
})
}
}