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) { t.Chdir(t.TempDir()) root := t.TempDir() configPayload := &iop.NodeConfigPayload{ Runtime: &iop.NodeRuntimeConfig{Concurrency: 1}, Workspaces: []*iop.WorkspaceConfig{{ Ref: "workspace-1", Platform: "darwin", 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 "darwin" }, 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) } }