OK. The engine is working as expected, and what you need to change is your coding style.
Initially, CS supported transient global variables ($$x) and permanent global variables ($x) and call by name ^function(^var1). The nature of call-by-name is that what is passed in is not a value, but the original expression. So if one does ^f2($$test.value) then inside ^f2, whenever the compiler sees ^var1, it replaces that with what was actually passed in. So $$x = ^var1 becomes the same as $$x = $$test.value. This applies to assignment as well. Last year save-restore variables were created (the paren list after the argument list) and CS automatically saves the values of them on entry, sets them to null, and restores them on exit. Then pure local variables ($_x) were created. These are implicitly save-restore variables with the more stringent definition that no one called by ^f2 can see them either. They are completely safe.
UNLESS you need to write back onto an incoming value, you should not use ^var1 notation, you should always use $_var1 in order to keep your code safe from side effects. What actually happened here was, the ^f1 caller used a global transient variable $$test1. That was passed in by name onto ^var1. Then you did save restore on $$test1 and $$test2, so they became null locally. Then you tried to retrieve the value of ^var1, but that is the same as retrievning $$test1.value and $$test1 is now NULL. Or you could use $_ variables when NOT expecting someone lower to use them.. So your ^f1 caller could have been using $_test1 as a variable, passed THAT to ^f2. CS would have converted that to a call by value call because you are NEVER allowed to access local variables outside your scope, etc.