$key)!==serialize($before->$key))throw new RuntimeException('Изменён соседний JSON тип'); if(isset($actual->utm->registration,$actual->utm->bchoice))throw new RuntimeException('Остались старые имена'); if(json_encode($actual->utm->reg)!=='{"weights":{"1":3,"2":7},"default":"2"}'||json_encode($actual->utm->banch)!=='{"weights":{"2":8,"1":2},"default":"1"}')throw new RuntimeException('Изменён смысл reg/banch'); if($actual->utm->theme->default!=='2'||$actual->utm->focus->default!=='1'||serialize($actual->utm->custom)!==serialize($before->utm->custom))throw new RuntimeException('Неверная миграция enum'); $first=file_get_contents($file);exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file),$out,$code); if($code!==0||file_get_contents($file)!==$first)throw new RuntimeException('Patch не идемпотентен'); file_put_contents($file,'{"utm":{"theme":{"weights":{"1":0,"2":1},"default":2}}}'); $numeric=file_get_contents($file);exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file),$out,$code); if($code!==0||file_get_contents($file)!==$numeric)throw new RuntimeException('Уже числовой integer default должен сохраняться'); file_put_contents($file,'{"utm":{"registration":{"weights":{"before":1},"default":"before"},"reg":{"weights":{"1":1},"default":"1"}}}'); $conflict=file_get_contents($file);exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file).' 2>/dev/null',$out,$code); if($code!==2||file_get_contents($file)!==$conflict)throw new RuntimeException('Неоднозначный config должен блокировать patch без записи'); $untouched='"foreign" : {"customFloat":1.0,"big":9223372036854775809,"nested":[1.00,{},[],{"exponent":2e+03,"negative":-0.0}]}'; $tokenFixture='{'.$untouched.',"utm":{"theme":{"weights":{"dark":1.0,"light":0},"default":"dark"},"custom":{"big":9223372036854775809}}}'; file_put_contents($file,$tokenFixture); exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file),$out,$code); $patched=file_get_contents($file); if($code!==10||!str_contains($patched,$untouched)||!str_contains($patched,'"custom":{"big":9223372036854775809}')||!str_contains($patched,'"weights":{"1":1.0,"2":0}'))throw new RuntimeException('Patch изменил исходные foreign/weight numeric tokens'); $expectedTokens=str_replace(['"dark":','"light":','"default":"dark"'],['"1":','"2":','"default":"1"'],$tokenFixture); if($patched!==$expectedTokens)throw new RuntimeException('Изменены байты вне согласованных source ranges'); exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file),$out,$code); if($code!==0||file_get_contents($file)!==$patched)throw new RuntimeException('Token patch не идемпотентен'); $current='{ "other":1.0,"huge":9223372036854775809,"utm": {"chob" : { "weights":{"2":9e+01,"1":-0.0},"default":2 },"nested":{"chob":"keep"}}}'; file_put_contents($file,$current);exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file),$out,$code); $renamed=str_replace('"chob" :','"banch" :',$current); if($code!==10||file_get_contents($file)!==$renamed)throw new RuntimeException('api#327: миграция chob→banch должна менять только токен имени'); exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file),$out,$code); if($code!==0||file_get_contents($file)!==$renamed)throw new RuntimeException('api#327: повторная миграция не должна писать'); foreach(['"chob":{"weights":{"1":1},"default":"1"},"banch":{"weights":{"2":1},"default":"2"}', '"bchoice":{"weights":{"a":1},"default":"a"},"chob":{"weights":{"1":1},"default":"1"}'] as $conflicting){ $conflict='{"utm":{'.$conflicting.'}}';file_put_contents($file,$conflict); exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file).' 2>/dev/null',$out,$code); if($code!==2||file_get_contents($file)!==$conflict)throw new RuntimeException('api#327: конфликт aliases должен блокировать patch без записи'); } echo "OK: numeric config migration preserves weights/default/foreign JSON types\n"; }finally{unlink($file);}