feat: replace chb with consent groups and preserve runtime config (api#317, #17)
deploy-preland / deploy zoomstage.online (push) Successful in 42s

This commit is contained in:
Panfilov Konstantin committed 2026-10-24 04:35:44 +03:00
1 parent 188b78a115
commit 89cc16eb4a
12 files changed
+208 -9

No files matched your search

+33
View File
@@ -0,0 +1,33 @@
<?php
/** preland#17: точечный, идемпотентный patch только chb/cons и его defaults/weights. */
declare(strict_types=1);
$script=dirname(__DIR__).'/bin/ensure-cons-config.php';
if(!is_file($script)){fwrite(STDERR,"FAIL: patch cons ещё не реализован.\n");exit(1);}
$fixture=['host'=>'keep.example','requisites'=>['Не менять'],'utm'=>['theme'=>['default'=>'dark','weights'=>['light'=>7]],'chb'=>['default'=>'b','weights'=>['a'=>1,'b'=>0]],'focus'=>['default'=>'face']]];
$path=tempnam(sys_get_temp_dir(),'cons-config-');
try{
file_put_contents($path,json_encode($fixture,JSON_THROW_ON_ERROR));
exec(escapeshellarg(PHP_BINARY).' '.escapeshellarg($script).' '.escapeshellarg($path),$output,$code);
$actual=json_decode(file_get_contents($path),true,512,JSON_THROW_ON_ERROR);
if($code!==10||isset($actual['utm']['chb'])||($actual['utm']['cons']??null)!==['weights'=>['1'=>100,'2'=>0],'default'=>'2'])throw new RuntimeException('Patch должен заменить только согласованный ключ chb на cons.');
$comparison=$actual;unset($comparison['utm']['cons']);$old=$fixture;unset($old['utm']['chb']);
if($comparison!==$old)throw new RuntimeException('Patch изменил соседние поля runtime config.');
$before=file_get_contents($path);exec(escapeshellarg(PHP_BINARY).' '.escapeshellarg($script).' '.escapeshellarg($path),$output,$code);
if($code!==0||file_get_contents($path)!==$before)throw new RuntimeException('Повторный patch должен оставить байты без изменений.');
$custom=$actual;$custom['utm']['cons']=['weights'=>['1'=>20,'3'=>80],'default'=>'3'];
file_put_contents($path,json_encode($custom,JSON_THROW_ON_ERROR));$before=file_get_contents($path);
exec(escapeshellarg(PHP_BINARY).' '.escapeshellarg($script).' '.escapeshellarg($path),$output,$code);
if($code!==0||file_get_contents($path)!==$before)throw new RuntimeException('Следующий deploy не должен сбросить настроенный владельцем cons.');
foreach([[],true,1.5,'2147483648','0']as$invalid){
$broken=$custom;$broken['utm']['cons']['default']=$invalid;
file_put_contents($path,json_encode($broken,JSON_THROW_ON_ERROR));$before=file_get_contents($path);
exec(escapeshellarg(PHP_BINARY).' '.escapeshellarg($script).' '.escapeshellarg($path).' 2>/dev/null',$output,$code);
if($code!==2||file_get_contents($path)!==$before)throw new RuntimeException('Некорректный существующий cons должен остановить deploy без перезаписи config.');
}
$objectFixture='{"host":"keep","extra":{},"map":{"0":"a","1":"b"},"list":[],"utm":{"chb":{"default":"a"},"other":{"weights":{}}}}';
file_put_contents($path,$objectFixture);exec(escapeshellarg(PHP_BINARY).' '.escapeshellarg($script).' '.escapeshellarg($path),$output,$code);
$beforeObject=json_decode($objectFixture,false,512,JSON_THROW_ON_ERROR);$afterObject=json_decode(file_get_contents($path),false,512,JSON_THROW_ON_ERROR);
unset($beforeObject->utm->chb,$afterObject->utm->cons);
if($code!==10||json_encode($beforeObject)!==json_encode($afterObject))throw new RuntimeException('Patch должен сохранять типы соседних пустых и numeric-key JSON объектов.');
echo "PASS: cons config migration preserves other fields and JSON types, idempotent\n";
}finally{@unlink($path);}
+10
View File
@@ -0,0 +1,10 @@
<?php
/** preland#17: consent-конфиг мигрируется после совместимого кода, без замены runtime-снимком. */
declare(strict_types=1);
$root=dirname(__DIR__);$workflow=file_get_contents($root.'/.gitea/workflows/deploy.yml');
$upload=strpos($workflow,'- name: Upload over SFTP');$patch=strpos($workflow,'- name: Migrate consent UTM in preserved host config');
if($upload===false||$patch===false||$patch<$upload)throw new RuntimeException('Consent config patch должен выполняться после совместимого кода.');
$section=substr($workflow,$patch,strpos($workflow,'- name: Check the page',$patch)-$patch);
foreach(['get -p $REMOTE_DIR/config.json','ensure-cons-config.php','config.json.upload-$revision','rename $REMOTE_DIR/config.json.upload-$revision']as$expected){if(!str_contains($section,$expected))throw new RuntimeException('Отсутствует безопасный этап: '.$expected);}
if(str_contains($section,'put -p config.json '))throw new RuntimeException('Снимок Git не должен заменять runtime config.');
echo "PASS: consent config patch follows compatible code and uses atomic targeted replacement\n";
+32
View File
@@ -0,0 +1,32 @@
<?php
/** preland#17: concrete cons независим от random-весов, старый chb не управляет переходом. */
declare(strict_types=1);
$file = dirname(__DIR__) . '/components/cons-utm.php';
if (!is_file($file)) {
fwrite(STDERR, "FAIL: нормализация cons ещё не реализована.\n");
exit(1);
}
require $file;
$checks = 0;
$expect = static function (bool $ok, string $message) use (&$checks): void {
if (!$ok) throw new RuntimeException($message);
$checks++;
};
$fixture = ['host'=>'api.example.test','utm'=>['theme'=>['default'=>'dark','weights'=>['dark'=>3]],'chb'=>['default'=>'a','weights'=>['a'=>1]]]];
$normalized = prelandNormalizeConsConfig($fixture);
$expect(!isset($normalized['utm']['chb']), 'Старый chb должен быть удалён из runtime-копии конфига.');
$expect($normalized['host'] === $fixture['host'] && $normalized['utm']['theme'] === $fixture['utm']['theme'], 'Соседние поля должны сохраняться.');
$expect($fixture['utm']['chb']['default'] === 'a', 'Нормализация не должна менять исходный массив.');
$settings = $normalized['utm']['cons'];
$expect(prelandResolveCons(null, $settings) === '2', 'Отсутствующее значение должно получить default2.');
foreach (['1','2','3','99'] as $value) $expect(prelandResolveCons($value, $settings) === $value, 'Concrete cons должен приниматься независимо от весов: '.$value);
foreach (['0','-1','a','1x','',[],['1']] as $value) $expect(prelandResolveCons($value, $settings) === '2', 'Неверное значение должно получить default2.');
for ($i=0; $i<1000; $i++) $expect(prelandResolveCons('rnd', $settings) === '1', 'rnd обязан выбрать только группу1.');
$settings = ['default'=>'2','weights'=>['2'=>0,'1'=>100]];
for ($i=0; $i<1000; $i++) $expect(prelandResolveCons('rnd', $settings) === '1', 'Нулевой вес не должен случайно попасть в выборку.');
$query = prelandNormalizeConsQuery(['chb'=>'a','cons'=>'3','utm_source'=>'ad','subid'=>'42'], $settings);
$expect($query === ['cons'=>'3','utm_source'=>'ad','subid'=>'42'], 'Query должен удалить только chb и нормализовать cons.');
$query = prelandNormalizeConsQuery(['chb'=>['b'],'utm_source'=>'ad'], $settings);
$expect(!isset($query['chb']) && $query['cons'] === '2' && $query['utm_source'] === 'ad', 'Array legacy chb не должен влиять на fallback.');
echo "PASS: cons resolver {$checks} checks\n";
+1 -1
View File
@@ -37,7 +37,7 @@ $expected = [
'degr' => '1',
'registration' => 'after',
'bchoice' => 'a',
'chb' => 'b',
'cons' => '2',
'tp' => $config['utm']['tp']['default'] ?? null,
'focus' => 'face',
'utm_source' => 'stage-ad',
+1 -1
View File
@@ -27,7 +27,7 @@ if (!is_array($config)) {
$failures[] = 'Рабочий config.json должен фиксировать явно согласованный focus=face.';
}
foreach (['indpic', 'theme', 'degr', 'registration', 'bchoice', 'chb', 'tp'] as $key) {
foreach (['indpic', 'theme', 'degr', 'registration', 'bchoice', 'cons', 'tp'] as $key) {
$setting = $config['utm'][$key] ?? null;
if (!is_array($setting) || !is_array($setting['weights'] ?? null) || !is_string($setting['default'] ?? null)) {
$failures[] = "Рабочий config.json должен хранить валидный stage-снимок UTM {$key}.";