Claude의 단일 Anthropic 요청 안에서 IOP가 Plan, Work, Review와 workspace 도구 실행을 끝내고 실제 dev smoke로 계약을 검증할 수 있어야 한다.\n\n완료 task evidence와 마일스톤 검토 상태도 같은 변경에 고정한다.
150 lines
5.8 KiB
Go
150 lines
5.8 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zaptest/observer"
|
|
|
|
"iop/apps/node/internal/transport"
|
|
"iop/packages/go/config"
|
|
iop "iop/proto/gen/iop"
|
|
)
|
|
|
|
func TestWorkspaceRuntimeCompositionBeforeReady(t *testing.T) {
|
|
for _, hostOS := range []string{config.WorkspacePlatformDarwin, config.WorkspacePlatformLinux} {
|
|
t.Run(hostOS, func(t *testing.T) {
|
|
t.Chdir(t.TempDir())
|
|
root := t.TempDir()
|
|
configPayload := &iop.NodeConfigPayload{
|
|
Runtime: &iop.NodeRuntimeConfig{Concurrency: 1},
|
|
Workspaces: []*iop.WorkspaceConfig{{
|
|
Ref: "workspace-1", Platform: hostOS, Root: root,
|
|
Operations: []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_READ},
|
|
MaxReadBytes: 8,
|
|
}},
|
|
}
|
|
dialer := func(context.Context, string, string, *zap.Logger) (*transport.RegisterResult, error) {
|
|
return &transport.RegisterResult{NodeID: "node-1", Config: configPayload}, nil
|
|
}
|
|
events := make([]string, 0, 2)
|
|
handlerInstalled := false
|
|
var installedWorkspaceHandler transport.WorkspaceHandler
|
|
owner, err := connectRuntime(context.Background(), &config.NodeConfig{}, zap.NewNop(), nil, connectRuntimeOptions{
|
|
dialer: dialer,
|
|
hostOS: func() string { return hostOS },
|
|
setHandler: func(_ *transport.Session, handler transport.Handler) {
|
|
workspaceHandler, ok := handler.(transport.WorkspaceHandler)
|
|
if !ok {
|
|
t.Fatal("composed handler does not implement WorkspaceHandler")
|
|
}
|
|
installedWorkspaceHandler = workspaceHandler
|
|
response, callErr := workspaceHandler.OnWorkspaceOpen(context.Background(), nil, &iop.WorkspaceOpenRequest{
|
|
RequestId: "request-1", WorkspaceRef: "workspace-1",
|
|
Operations: []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_READ},
|
|
MaxReadBytes: 8,
|
|
})
|
|
if callErr != nil || response.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("workspace handler before ready = %+v, %v", response, callErr)
|
|
}
|
|
handlerInstalled = true
|
|
events = append(events, "handler")
|
|
},
|
|
signalReady: func(_ *transport.Session, _ time.Duration) error {
|
|
if !handlerInstalled {
|
|
t.Fatal("ready signalled before handler installation")
|
|
}
|
|
events = append(events, "ready")
|
|
return nil
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("connectRuntime: %v", err)
|
|
}
|
|
owner.close()
|
|
closedResponse, callErr := installedWorkspaceHandler.OnWorkspaceOpen(context.Background(), nil, &iop.WorkspaceOpenRequest{
|
|
RequestId: "request-2", WorkspaceRef: "workspace-1",
|
|
Operations: []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_READ}, MaxReadBytes: 8,
|
|
})
|
|
if callErr != nil || closedResponse.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_READY {
|
|
t.Fatalf("workspace runtime after owner close = %+v, %v", closedResponse, callErr)
|
|
}
|
|
if strings.Join(events, ",") != "handler,ready" {
|
|
t.Fatalf("composition order = %v", events)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWorkspaceRuntimeCatalogFailureIsRedactedBeforeReady(t *testing.T) {
|
|
t.Chdir(t.TempDir())
|
|
const sentinel = "workspace-startup-root-sentinel-8403"
|
|
core, observed := observer.New(zap.DebugLevel)
|
|
logger := zap.New(core)
|
|
readyReached := false
|
|
_, err := connectRuntime(context.Background(), &config.NodeConfig{}, logger, nil, connectRuntimeOptions{
|
|
dialer: func(context.Context, string, string, *zap.Logger) (*transport.RegisterResult, error) {
|
|
return &transport.RegisterResult{NodeID: "node-1", Config: &iop.NodeConfigPayload{
|
|
Runtime: &iop.NodeRuntimeConfig{Concurrency: 1},
|
|
Workspaces: []*iop.WorkspaceConfig{{
|
|
Ref: "workspace-1", Platform: "darwin", Root: t.TempDir() + "/" + sentinel,
|
|
Operations: []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_READ}, MaxReadBytes: 1,
|
|
}},
|
|
}}, nil
|
|
},
|
|
hostOS: func() string { return "darwin" },
|
|
setHandler: func(*transport.Session, transport.Handler) {
|
|
t.Fatal("handler installed after invalid workspace catalog")
|
|
},
|
|
signalReady: func(*transport.Session, time.Duration) error {
|
|
readyReached = true
|
|
return nil
|
|
},
|
|
})
|
|
if err == nil || readyReached {
|
|
t.Fatalf("catalog error=%v ready=%v", err, readyReached)
|
|
}
|
|
if strings.Contains(err.Error(), sentinel) || strings.Contains(fmt.Sprint(observed.All()), sentinel) {
|
|
t.Fatalf("workspace root leaked: error=%q logs=%v", err, observed.All())
|
|
}
|
|
}
|
|
|
|
func TestWorkspaceRuntimeOwnerCloseOrderAndReconnectReplacement(t *testing.T) {
|
|
var events []string
|
|
owner := &runtimeOwner{
|
|
closeRegistry: func() { events = append(events, "registry") },
|
|
closeWorkspace: func() { events = append(events, "workspace") },
|
|
closeSession: func() { events = append(events, "session") },
|
|
closeStore: func() { events = append(events, "store") },
|
|
}
|
|
owner.close()
|
|
owner.close()
|
|
if strings.Join(events, ",") != "registry,workspace,session,store" {
|
|
t.Fatalf("direct close order = %v", events)
|
|
}
|
|
|
|
events = nil
|
|
previous := &runtimeOwner{
|
|
closeRegistry: func() { events = append(events, "old-registry") },
|
|
closeWorkspace: func() { events = append(events, "old-workspace") },
|
|
closeSession: func() { events = append(events, "old-session") },
|
|
closeStore: func() { events = append(events, "old-store") },
|
|
}
|
|
next := &runtimeOwner{
|
|
closeRegistry: func() { events = append(events, "new-registry") },
|
|
closeWorkspace: func() { events = append(events, "new-workspace") },
|
|
closeSession: func() { events = append(events, "new-session") },
|
|
closeStore: func() { events = append(events, "new-store") },
|
|
}
|
|
supervisor := &runtimeSupervisor{current: previous}
|
|
supervisor.swapOwner(next)
|
|
supervisor.clearCurrent()
|
|
want := "old-registry,old-workspace,old-session,old-store,new-registry,new-workspace,new-session,new-store"
|
|
if strings.Join(events, ",") != want {
|
|
t.Fatalf("replacement close order = %v", events)
|
|
}
|
|
}
|