Hi Markus!
Evolution is technically for proposing language direction and changes, but since this is borderline that I think this category will be fine for discussion 
Hah yeah testing version upgrades can be rough, we've suffered through the same in my past life with Akka and didn't find great solutions (we'd test manually some cases, and document "how to" and call it a day). In persistent format compatibility cases we'd store "old" binary data raw in tests and make sure we can keep reading it and similar tricks etc, but we've never had great tests for big version rollouts either. And of course there are also behavioral changes that are another beast one would like to be testing like this.
~~
To the point though: I think we actually may have a shot at making this possible in Swift, with slightly extending two features that could be used to enable this, these features are:
- module aliasing for disambiguation - for "import module under different name"
- distributed actors - if we'd want to intercept calls between "nodes" in the same process, and handle them using one or the other "version" of an actor
Both are missing some functionality to achieve what you're truly after here, but not that much.
~~
1) The module aliasing allows us to depend on a module with an aliased name -- which renames the entire module, and all of its symbols (since mangling includes module name) under a different name -- so e.g. if two projects include "Utils" normally you'd have a naming conflict if depending on both you'd get a naming clash, but with this feature we can
.product(name: "Game", package: "swift-game", moduleAliases: ["Utils": "GameUtils"]),
which allows an existing module "Utils" to be used as GameUtils and all the symbols in the entire binary are changed to "GameUtils" for it, even inside the game-package.
So that's almost enough to handle the multiple versions of the same package: the same mechanism would be used to resolve the conflicts and we could end up getting Lib_2_20_4 and Lib_2_30_0 in the same process without clashing.
What is not solved today though is resolving the dependencies such that this would just automatically work:
// TODO: we'd need some way to express this "same lib in diff version here"
dependencies: [
.product(name: "Alpha", package: "AlphaLib", moduleAliases: ["Alpha": "AlphaOne"]),
.product(name: "Alpha", package: "AlphaLib", moduleAliases: ["Alpha": "AlphaTwo"]),
]
'lemma': ignoring duplicate product 'Alpha' from package 'alpha'
error: multiple aliases: ['AlphaOne', 'AlphaTwo'] found for target 'Alpha' in product 'Alpha' from package 'Alpha'
TODO: We'd need to improve dependency resolution to allow resolving this, and then apply the aliasing to it. This may be tricky, but seems possible to do -- given that all the aliasing work exists and works. There were other groups which were interested in the "multi version" solution as well, so perhaps there'd be enough of a case to drive it.
This would be enough to have simple tests which "test calling Lib2 with some outputs of Lib1" is compatible, but doesn't give us the "entire cluster simulation" since we still would need to somehow "weave through which lib to call where".
~~
This is where 2) is has an interesting potential we could utilize.
A distributed actor effectively is a proxy to call "some method, somewhere" and we're very free to do whatever we want with the intercepted calls.
Obtaining a distributed actor reference is always done via MyActor.resolve(id: ..., using: someSystem) where the ID basically is an "endpoint" or "address". In your case, as many others, this address would have information on which node we're looking for the actor to be resolved or created...
You could imagine an implementation that inspects the ID (which are arbitrary types, and can have extra metadata (!)), for a hint what module it should use an actor from; E.g. Echo.resolve(id: ID("127.0.0.1:7337", uid: .wellKnownEcho).replaceModule("Lib_20")) could allow us to pull some tricks and use the Lib_20.Echo rather than the Lib.Echo actor that the code was invoked on... We could return a "remote reference" that actually just proxies to an Lib_20.Echo implementation rather than actually goes over network.
To be a bit more clear about the substituting the resolves idea -- you totally could have a dictionary in the actor system that says "127.0.0.1:7337": .replaceResolveModule("Lib", "Lib_20") and you'd just set that up on the actor system (ActorSystem is basically like your sim2 instance -- a shared instance in this scenario) before kicking off the tests. So it doesn't even really have to be in the ID, as long as the actor system knows what to look up we could make it work I think.
That'll again need some extra capabilities so that we're able to executeDistributedTarget with a different mangled name -- but that's a) something I wanted to get to anyway, to get stable identifiers for RPC endpoints rather than rely on mangled names, and b) perhaps not even necessary, we could do this with some tricks outside the language perhaps already...
~~
So that's the idea I'd have for this. It feels like we have the actually hard building blocks in the language, but the dependency resolution is tricky and would have to be solved (or done manually). The distributed actor trickery I'm actually optimistic we could pull off, I'd be willing to give it a PoC shot once we're a bit less busy after WWDC 
I should probably mention thought that 1) would require that everything is implemented in Swift, as only Swift modules get this symbol renaming treatment -- but since we're talking in terms of "what if", it's still worthwhile exploring.
Let me know what you think and if any of that made sense or we should dive deeper into these!
PS: It is lovely hearing about all those excellent use-cases pushing the language where no language has really gone and seeing how we can solve those use-cases in an elegant manner. I really think we can do something great for distributed systems here, and use-cases like yours will help us get there!