승인된 execution preset을 Edge 조정 경계와 Node workspace/tool 실행 경계로 연결해 단일 요청 수명주기와 관측 계약을 일관되게 처리한다.
303 lines
12 KiB
Go
303 lines
12 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
edgenode "iop/apps/edge/internal/node"
|
|
"iop/packages/go/config"
|
|
)
|
|
|
|
type recordingWorkspaceExecutor struct {
|
|
calls atomic.Int32
|
|
seen chan *SingleRequestBinding
|
|
}
|
|
|
|
func (e *recordingWorkspaceExecutor) ExecuteSingleRequest(_ context.Context, req SingleRequestRequest, _ SingleRequestController) error {
|
|
e.calls.Add(1)
|
|
e.seen <- req.Binding.Clone()
|
|
return nil
|
|
}
|
|
|
|
func (e *recordingWorkspaceExecutor) binding(t *testing.T) *SingleRequestBinding {
|
|
t.Helper()
|
|
select {
|
|
case binding := <-e.seen:
|
|
return binding
|
|
case <-time.After(time.Second):
|
|
t.Fatal("executor did not receive an admitted binding")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func newRecordingWorkspaceExecutor() *recordingWorkspaceExecutor {
|
|
return &recordingWorkspaceExecutor{seen: make(chan *SingleRequestBinding, 1)}
|
|
}
|
|
|
|
func workspaceDefinition(ref string, operations ...config.WorkspaceOperation) config.WorkspaceDefinition {
|
|
workspace := config.WorkspaceDefinition{
|
|
Ref: ref,
|
|
Platform: "darwin",
|
|
Root: "/Users/operator/project",
|
|
Operations: operations,
|
|
MaxReadBytes: 64,
|
|
MaxWriteBytes: 64,
|
|
MaxOutputBytes: 128,
|
|
MaxCommandTimeoutMS: 500,
|
|
}
|
|
for _, operation := range operations {
|
|
if operation == config.WorkspaceOpCommand {
|
|
workspace.Commands = []config.WorkspaceCommandDefinition{{ID: "test"}}
|
|
}
|
|
}
|
|
return workspace
|
|
}
|
|
|
|
func workspaceStore(nodeID string, workspace config.WorkspaceDefinition) *edgenode.NodeStore {
|
|
store := edgenode.NewNodeStore()
|
|
store.Add(&edgenode.NodeRecord{ID: nodeID, Alias: nodeID, Token: nodeID + "-token", Workspaces: []config.WorkspaceDefinition{workspace}})
|
|
return store
|
|
}
|
|
|
|
func readyWorkspaceService(t *testing.T, nodeID string, workspace config.WorkspaceDefinition, executor *recordingWorkspaceExecutor) (*Service, *edgenode.Registry) {
|
|
t.Helper()
|
|
registry := edgenode.NewRegistry()
|
|
registry.Register(&edgenode.NodeEntry{NodeID: nodeID, Alias: nodeID})
|
|
service := New(registry, nil)
|
|
service.SetNodeStore(workspaceStore(nodeID, workspace))
|
|
service.SetSingleRequestExecutor(executor)
|
|
return service, registry
|
|
}
|
|
|
|
func workspaceRequest(t *testing.T, ref string) SingleRequestRequest {
|
|
t.Helper()
|
|
binding := createTestBinding(t)
|
|
binding.WorkspaceRef = ref
|
|
return SingleRequestRequest{RequestID: "workspace-request", Binding: binding, Prompt: "complete task"}
|
|
}
|
|
|
|
func TestSingleRequestWorkspaceRejectsMissingRuntimeDependencies(t *testing.T) {
|
|
for name, service := range map[string]*Service{
|
|
"missing store and registry": {},
|
|
"missing store": New(edgenode.NewRegistry(), nil),
|
|
"missing registry": &Service{nodeStore: workspaceStore("node-a", workspaceDefinition("approved", config.WorkspaceOpRead))},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
executor := newRecordingWorkspaceExecutor()
|
|
service.SetSingleRequestExecutor(executor)
|
|
request := workspaceRequest(t, "approved")
|
|
_, err := service.StartSingleRequest(context.Background(), request)
|
|
if !errors.Is(err, ErrSingleRequestWorkspaceUnavailable) {
|
|
t.Fatalf("StartSingleRequest error = %v, want workspace unavailable", err)
|
|
}
|
|
if got := executor.calls.Load(); got != 0 {
|
|
t.Fatalf("executor calls = %d, want 0", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSingleRequestWorkspaceOperationSpecificLimits(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
operations []config.WorkspaceOperation
|
|
limits SingleRequestWorkspaceLimits
|
|
commands []string
|
|
valid bool
|
|
}{
|
|
{"read", []config.WorkspaceOperation{config.WorkspaceOpRead}, SingleRequestWorkspaceLimits{MaxReadBytes: 1}, nil, true},
|
|
{"write", []config.WorkspaceOperation{config.WorkspaceOpWrite}, SingleRequestWorkspaceLimits{MaxWriteBytes: 1}, nil, true},
|
|
{"list", []config.WorkspaceOperation{config.WorkspaceOpList}, SingleRequestWorkspaceLimits{MaxOutputBytes: 1}, nil, true},
|
|
{"delete", []config.WorkspaceOperation{config.WorkspaceOpDelete}, SingleRequestWorkspaceLimits{}, nil, true},
|
|
{"command", []config.WorkspaceOperation{config.WorkspaceOpCommand}, SingleRequestWorkspaceLimits{MaxOutputBytes: 1, MaxCommandTimeoutMS: 1}, []string{"command"}, true},
|
|
{"read requires bound", []config.WorkspaceOperation{config.WorkspaceOpRead}, SingleRequestWorkspaceLimits{}, nil, false},
|
|
{"list requires output", []config.WorkspaceOperation{config.WorkspaceOpList}, SingleRequestWorkspaceLimits{}, nil, false},
|
|
{"command requires id", []config.WorkspaceOperation{config.WorkspaceOpCommand}, SingleRequestWorkspaceLimits{MaxOutputBytes: 1, MaxCommandTimeoutMS: 1}, nil, false},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
workspace := workspaceDefinition("approved", test.operations...)
|
|
workspace.MaxReadBytes = test.limits.MaxReadBytes
|
|
workspace.MaxWriteBytes = test.limits.MaxWriteBytes
|
|
workspace.MaxOutputBytes = test.limits.MaxOutputBytes
|
|
workspace.MaxCommandTimeoutMS = test.limits.MaxCommandTimeoutMS
|
|
workspace.Commands = make([]config.WorkspaceCommandDefinition, len(test.commands))
|
|
for i, id := range test.commands {
|
|
workspace.Commands[i] = config.WorkspaceCommandDefinition{ID: id}
|
|
}
|
|
_, err := compileSingleRequestWorkspaceBinding(workspace, &edgenode.NodeEntry{NodeID: "node-a", ConnectionGeneration: 1})
|
|
if test.valid && err != nil {
|
|
t.Fatalf("compileSingleRequestWorkspaceBinding: %v", err)
|
|
}
|
|
if !test.valid && !errors.Is(err, errSingleRequestWorkspaceMalformed) {
|
|
t.Fatalf("compileSingleRequestWorkspaceBinding error = %v, want malformed", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSingleRequestWorkspaceAdmissionMatrix(t *testing.T) {
|
|
approved := workspaceDefinition("approved", config.WorkspaceOpRead)
|
|
for _, test := range []struct {
|
|
name string
|
|
setup func(*edgenode.Registry) *edgenode.NodeStore
|
|
ref string
|
|
}{
|
|
{
|
|
name: "unapproved ref",
|
|
setup: func(registry *edgenode.Registry) *edgenode.NodeStore {
|
|
registry.Register(&edgenode.NodeEntry{NodeID: "node-a"})
|
|
return workspaceStore("node-a", approved)
|
|
},
|
|
ref: "unapproved",
|
|
},
|
|
{
|
|
name: "foreign ready node is not a fallback",
|
|
setup: func(registry *edgenode.Registry) *edgenode.NodeStore {
|
|
registry.Register(&edgenode.NodeEntry{NodeID: "node-b"})
|
|
return workspaceStore("node-a", approved)
|
|
},
|
|
ref: "approved",
|
|
},
|
|
{
|
|
name: "configured owner is pending",
|
|
setup: func(registry *edgenode.Registry) *edgenode.NodeStore {
|
|
registry.RegisterIfAbsent(&edgenode.NodeEntry{NodeID: "node-a"})
|
|
registry.Register(&edgenode.NodeEntry{NodeID: "node-b"})
|
|
return workspaceStore("node-a", approved)
|
|
},
|
|
ref: "approved",
|
|
},
|
|
{
|
|
name: "malformed empty workspace operations",
|
|
setup: func(registry *edgenode.Registry) *edgenode.NodeStore {
|
|
registry.Register(&edgenode.NodeEntry{NodeID: "node-a"})
|
|
return workspaceStore("node-a", workspaceDefinition("approved"))
|
|
},
|
|
ref: "approved",
|
|
},
|
|
{
|
|
name: "duplicate workspace operation",
|
|
setup: func(registry *edgenode.Registry) *edgenode.NodeStore {
|
|
registry.Register(&edgenode.NodeEntry{NodeID: "node-a"})
|
|
workspace := workspaceDefinition("approved", config.WorkspaceOpRead, config.WorkspaceOpRead)
|
|
return workspaceStore("node-a", workspace)
|
|
},
|
|
ref: "approved",
|
|
},
|
|
{
|
|
name: "unsupported workspace operation",
|
|
setup: func(registry *edgenode.Registry) *edgenode.NodeStore {
|
|
registry.Register(&edgenode.NodeEntry{NodeID: "node-a"})
|
|
workspace := workspaceDefinition("approved", config.WorkspaceOperation("unsupported"))
|
|
return workspaceStore("node-a", workspace)
|
|
},
|
|
ref: "approved",
|
|
},
|
|
{
|
|
name: "command without command operation",
|
|
setup: func(registry *edgenode.Registry) *edgenode.NodeStore {
|
|
registry.Register(&edgenode.NodeEntry{NodeID: "node-a"})
|
|
workspace := workspaceDefinition("approved", config.WorkspaceOpRead)
|
|
workspace.Commands = []config.WorkspaceCommandDefinition{{ID: "test"}}
|
|
return workspaceStore("node-a", workspace)
|
|
},
|
|
ref: "approved",
|
|
},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
registry := edgenode.NewRegistry()
|
|
executor := newRecordingWorkspaceExecutor()
|
|
service := New(registry, nil)
|
|
service.SetNodeStore(test.setup(registry))
|
|
service.SetSingleRequestExecutor(executor)
|
|
_, err := service.StartSingleRequest(context.Background(), workspaceRequest(t, test.ref))
|
|
if !errors.Is(err, ErrSingleRequestWorkspaceUnavailable) {
|
|
t.Fatalf("StartSingleRequest error = %v, want unavailable", err)
|
|
}
|
|
if got := executor.calls.Load(); got != 0 {
|
|
t.Fatalf("executor calls = %d, want 0", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSingleRequestWorkspaceFreezesOwnerAcrossRefreshAndReconnect(t *testing.T) {
|
|
workspace := workspaceDefinition("approved", config.WorkspaceOpCommand)
|
|
executor := newRecordingWorkspaceExecutor()
|
|
service, registry := readyWorkspaceService(t, "node-a", workspace, executor)
|
|
handoff := make(chan struct{})
|
|
refreshDone := make(chan struct{})
|
|
actorDone := make(chan struct{})
|
|
service.beforeSingleRequestHandoff = func() {
|
|
close(handoff)
|
|
<-refreshDone
|
|
}
|
|
go func() {
|
|
defer close(actorDone)
|
|
<-handoff
|
|
service.SetNodeStore(workspaceStore("node-b", workspaceDefinition("approved", config.WorkspaceOpRead)))
|
|
close(refreshDone)
|
|
}()
|
|
if _, err := service.StartSingleRequest(context.Background(), workspaceRequest(t, "approved")); err != nil {
|
|
t.Fatalf("StartSingleRequest: %v", err)
|
|
}
|
|
<-actorDone
|
|
bound := executor.binding(t)
|
|
if bound.Workspace.NodeID != "node-a" || bound.Workspace.ConnectionGeneration == 0 {
|
|
t.Fatalf("executor binding = %#v, want original ready owner", bound.Workspace)
|
|
}
|
|
if len(bound.Workspace.CommandIDs) != 1 || bound.Workspace.CommandIDs[0] != "test" {
|
|
t.Fatalf("executor binding lost frozen command capability: %#v", bound.Workspace)
|
|
}
|
|
if !registry.IsCurrentOwnerGeneration("node-a", bound.Workspace.ConnectionGeneration) {
|
|
t.Fatal("refresh retargeted the admitted owner")
|
|
}
|
|
}
|
|
|
|
func TestSingleRequestWorkspaceRejectsStaleGenerationBeforeExecutor(t *testing.T) {
|
|
workspace := workspaceDefinition("approved", config.WorkspaceOpRead)
|
|
executor := newRecordingWorkspaceExecutor()
|
|
service, registry := readyWorkspaceService(t, "node-a", workspace, executor)
|
|
handoff := make(chan struct{})
|
|
reconnectDone := make(chan struct{})
|
|
actorDone := make(chan struct{})
|
|
service.beforeSingleRequestHandoff = func() {
|
|
close(handoff)
|
|
<-reconnectDone
|
|
}
|
|
go func() {
|
|
defer close(actorDone)
|
|
<-handoff
|
|
registry.Unregister("node-a")
|
|
registry.Register(&edgenode.NodeEntry{NodeID: "node-a", Alias: "node-a"})
|
|
close(reconnectDone)
|
|
}()
|
|
_, err := service.StartSingleRequest(context.Background(), workspaceRequest(t, "approved"))
|
|
<-actorDone
|
|
if !errors.Is(err, ErrSingleRequestWorkspaceStale) {
|
|
t.Fatalf("StartSingleRequest error = %v, want stale workspace", err)
|
|
}
|
|
if got := executor.calls.Load(); got != 0 {
|
|
t.Fatalf("executor calls = %d, want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestSingleRequestWorkspaceAppliesPresetMinima(t *testing.T) {
|
|
workspace := workspaceDefinition("approved", config.WorkspaceOpCommand)
|
|
preset := createTestBinding(t).Limits
|
|
workspace.MaxOutputBytes = preset.MaxOutputBytes + 1
|
|
workspace.MaxCommandTimeoutMS = preset.StageTimeoutMS + 1
|
|
executor := newRecordingWorkspaceExecutor()
|
|
service, _ := readyWorkspaceService(t, "node-a", workspace, executor)
|
|
if _, err := service.StartSingleRequest(context.Background(), workspaceRequest(t, "approved")); err != nil {
|
|
t.Fatalf("StartSingleRequest: %v", err)
|
|
}
|
|
bound := executor.binding(t)
|
|
if bound.Workspace.Limits.MaxOutputBytes != preset.MaxOutputBytes || bound.Workspace.Limits.MaxCommandTimeoutMS != preset.StageTimeoutMS {
|
|
t.Fatalf("effective limits = %#v, want preset minima", bound.Workspace.Limits)
|
|
}
|
|
}
|