Laravel PackageManifest 는 bootstrap/cache/packages.php 가 있으면 stale 여부를 검사하지 않고 그대로 읽어 등재된 provider 를 new 한다. 코어 업데이트는 Step 6/8 에서 vendor 를 --no-dev 로 교체하지만 그 파일은 Step 11 까지 이전 설치본의 것이 남으므로, 옵션 없는 `composer install` 로 깔린 사이트에서는 Step 10 spawn 자식이 새 vendor 에 없는 provider 를 찾다 부팅 단계에서 죽는다. 부팅 전이라 앱 로그에 흔적이 없고 부모에게는 자식의 비정상 종료로만 보여, 운영자에게는 「Class ... not found」 와 수동 재개 안내만 남는다. (sir.kr 커뮤니티 제보, 7.0.9 → 7.0.10) 3계층으로 막는다. 1. 부모 — spawn 직전 PackageManifestCacheHelper::clear 2. 자식 — bootstrap/app.php 가 G7_UPDATE_IN_PROGRESS 를 보고 스스로 정리한다. 이미 배포된 7.0.9·7.0.10 부모는 고칠 수 없으므로 그 아래에서 도는 신버전 자식의 유일한 방어다. App\ 클래스를 참조하지 않고 실패는 무시한다. 3. 범위 — CoreVersionChecker 의 env APP_VERSION 우선을 CoreUpdateContext 트리 안으로 축소한다. 업데이트 전에 뜬 artisan serve·큐 워커가 옛 값을 물고 확장을 incompatible_core 로 끄던 경로를 닫는다(관리자 템플릿이 대상이면 복구 UI 자체에 도달할 수 없다). 업데이트 트리 판정은 App\Support\CoreUpdateContext 가 단독 소유하고 CoreServiceProvider::isCoreUpdateInProgress 는 위임으로 남는다. bootstrap/app.php 의 복제본은 부팅 전이라 불가피한 예외이며, 두 조건의 동형성을 테스트가 단언한다. 실측 중 드러난 결함 2건을 함께 고쳤다. - ConfigCacheHelper::withPreservedContainer 가 파사드 애플리케이션을 되돌리지 않아 Step 11 이 `Target class [command.tinker] does not exist` 로 실패·롤백했다. - updateVersionInEnv 가 프로세스 환경을 갱신하지 않아 config 캐시에 이전 버전이 구워졌다 (Laravel env 저장소가 불변이라 재부팅으로도 덮이지 않는다). 인스톨러는 재사용 vendor 의 개발용 패키지를 installed.json 으로 감지해 설치 환경 확인 카드·설치 로그로 알리되 설치를 차단하지 않고( 결정 D1), 재사용 경로에서도 컴파일 캐시를 정리한다. 실행되는 명령만이 아니라 실패 시 안내하는 수동 명령까지 --no-dev 로 맞췄다. 코어 업데이트 완료·핸드오프·단독 재개 사후 단계에서 queue:restart 신호를 보낸다(D3). 코어 7.0.10 → 7.0.11.
196 lines
7.5 KiB
PHP
196 lines
7.5 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\Extension;
|
|
|
|
use App\Extension\CoreVersionChecker;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* CoreVersionChecker::getCoreVersion() 의 env 우선 판독 **범위** 회귀 테스트.
|
|
*
|
|
* 두 방향의 회귀를 함께 잠근다.
|
|
*
|
|
* 1. 업데이트 트리 **안**: spawn 프로세스(proc_open)가 `APP_VERSION={toVersion}` env 를 전달하지만
|
|
* `bootstrap/cache/config.php` 가 있으면 `LoadConfiguration` 이 캐시된 리터럴을 써서 env
|
|
* 오버라이드가 반영되지 않는다. 여기서는 env 가 우선해야 확장이 자동 비활성화되지 않는다.
|
|
* 2. 업데이트 트리 **밖**: `php artisan serve` · 큐 워커처럼 오래 사는 프로세스는 기동 시점의
|
|
* `APP_VERSION` 을 물고 있다. 업데이트가 끝나도 그 프로세스만 옛 버전으로 판정해, 새 코어를
|
|
* 요구하는 확장을 `incompatible_core` 로 꺼 버린다(관리자 템플릿이 꺼지면 복구 UI 도 사라진다 —
|
|
* 2026-09-07 실측). 여기서는 config 가 유일한 근거여야 한다.
|
|
*/
|
|
class CoreVersionCheckerEnvPriorityTest extends TestCase
|
|
{
|
|
private ?string $originalEnvVersion = null;
|
|
|
|
private ?string $originalUpdateFlag = null;
|
|
|
|
/** @var array<int, string> */
|
|
private array $originalArgv = [];
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->originalEnvVersion = $_ENV['APP_VERSION'] ?? null;
|
|
|
|
$flag = $_ENV['G7_UPDATE_IN_PROGRESS'] ?? $_SERVER['G7_UPDATE_IN_PROGRESS'] ?? getenv('G7_UPDATE_IN_PROGRESS');
|
|
$this->originalUpdateFlag = ($flag === false || $flag === null) ? null : (string) $flag;
|
|
|
|
$this->originalArgv = $_SERVER['argv'] ?? [];
|
|
|
|
// 각 테스트가 트리 안/밖을 명시적으로 세우도록 기본은 "트리 밖" 으로 둔다.
|
|
$this->setUpdateFlag(false);
|
|
$_SERVER['argv'] = ['artisan', 'inspire'];
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
unset($_ENV['APP_VERSION'], $_SERVER['APP_VERSION']);
|
|
if ($this->originalEnvVersion !== null) {
|
|
$_ENV['APP_VERSION'] = $this->originalEnvVersion;
|
|
$_SERVER['APP_VERSION'] = $this->originalEnvVersion;
|
|
putenv('APP_VERSION='.$this->originalEnvVersion);
|
|
} else {
|
|
putenv('APP_VERSION');
|
|
}
|
|
|
|
if ($this->originalUpdateFlag === null) {
|
|
$this->setUpdateFlag(false);
|
|
} else {
|
|
$_ENV['G7_UPDATE_IN_PROGRESS'] = $this->originalUpdateFlag;
|
|
$_SERVER['G7_UPDATE_IN_PROGRESS'] = $this->originalUpdateFlag;
|
|
putenv('G7_UPDATE_IN_PROGRESS='.$this->originalUpdateFlag);
|
|
}
|
|
|
|
$_SERVER['argv'] = $this->originalArgv;
|
|
|
|
parent::tearDown();
|
|
}
|
|
|
|
/**
|
|
* 업데이트 트리 안에서는 env APP_VERSION 이 config 보다 우선한다.
|
|
*
|
|
* @effects getCoreVersion_prefers_env_APP_VERSION_only_inside_update_tree
|
|
*/
|
|
#[Test]
|
|
public function test_env_app_version_overrides_config(): void
|
|
{
|
|
$this->setUpdateFlag(true);
|
|
config()->set('app.version', '7.0.0-beta.1'); // 캐시된 stale 값 시뮬레이션
|
|
$_ENV['APP_VERSION'] = '7.0.0-beta.2'; // spawn 이 전달한 toVersion
|
|
|
|
$this->assertSame(
|
|
'7.0.0-beta.2',
|
|
CoreVersionChecker::getCoreVersion(),
|
|
'업데이트 트리 안에서는 env APP_VERSION 이 config 보다 우선해야 합니다.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 업데이트 트리 안이어도 env 가 없으면 config 로 내려간다.
|
|
*
|
|
* @effects getCoreVersion_falls_back_to_config_when_env_absent_inside_update_tree
|
|
*/
|
|
#[Test]
|
|
public function test_falls_back_to_config_when_env_missing(): void
|
|
{
|
|
$this->setUpdateFlag(true);
|
|
unset($_ENV['APP_VERSION'], $_SERVER['APP_VERSION']);
|
|
putenv('APP_VERSION');
|
|
config()->set('app.version', '7.0.0-beta.1');
|
|
|
|
$this->assertSame(
|
|
'7.0.0-beta.1',
|
|
CoreVersionChecker::getCoreVersion(),
|
|
'env 가 없으면 config 값을 반환해야 합니다.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* spawn 자식 시나리오: 캐시된 config 는 fromVersion, env 는 toVersion → 확장이 살아남는다.
|
|
*/
|
|
#[Test]
|
|
public function test_spawn_scenario_cached_config_with_env_override(): void
|
|
{
|
|
// spawn 환경 시뮬레이션: config 는 fromVersion 으로 캐시, env 는 toVersion
|
|
$this->setUpdateFlag(true);
|
|
config()->set('app.version', '7.0.0-beta.1');
|
|
$_ENV['APP_VERSION'] = '7.0.0-beta.2';
|
|
|
|
// 확장이 >=7.0.0-beta.2 요구 — env 기반 판정이어야 호환성 통과
|
|
$this->assertTrue(
|
|
CoreVersionChecker::isCompatible('>=7.0.0-beta.2'),
|
|
'spawn 에서 env 로 전달된 toVersion 기준으로 호환성이 평가되어 확장이 자동 비활성화되지 않아야 합니다.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 업데이트 트리 **밖**에서는 프로세스 env 의 옛 APP_VERSION 을 무시하고 config 를 쓴다.
|
|
*
|
|
* 업데이트 전에 띄운 `php artisan serve` 가 `7.0.9` 를 물고 있는 상황을 시뮬레이션한다.
|
|
* env 를 그대로 믿으면 `>=7.0.10` 을 요구하는 관리자 템플릿이 비활성화되어 화면이 통째로
|
|
* 사라진다 — 그 상태에서는 복구 UI 에도 도달할 수 없다.
|
|
*
|
|
* @effects getCoreVersion_ignores_env_APP_VERSION_outside_update_tree
|
|
*/
|
|
#[Test]
|
|
public function test_env_app_version_is_ignored_outside_update_tree(): void
|
|
{
|
|
$this->setUpdateFlag(false);
|
|
config()->set('app.version', '7.0.10'); // 업데이트가 끝나 .env·config 는 새 버전
|
|
$_ENV['APP_VERSION'] = '7.0.9'; // 기동 시점 env 를 물고 있는 상주 프로세스
|
|
$_SERVER['APP_VERSION'] = '7.0.9';
|
|
putenv('APP_VERSION=7.0.9');
|
|
|
|
$this->assertSame(
|
|
'7.0.10',
|
|
CoreVersionChecker::getCoreVersion(),
|
|
'업데이트 트리 밖에서는 config 버전이 근거여야 합니다.'
|
|
);
|
|
|
|
$this->assertTrue(
|
|
CoreVersionChecker::isCompatible('>=7.0.10'),
|
|
'상주 프로세스의 옛 env 때문에 새 코어를 요구하는 확장이 비활성화되면 안 됩니다.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* env 플래그가 전파되지 않아도 argv 가 업데이트 커맨드면 env 우선이 켜진다.
|
|
*/
|
|
#[Test]
|
|
public function test_argv_update_command_enables_env_priority(): void
|
|
{
|
|
$this->setUpdateFlag(false);
|
|
$_SERVER['argv'] = ['artisan', 'core:execute-upgrade-steps', '--from=7.0.9', '--to=7.0.10'];
|
|
|
|
config()->set('app.version', '7.0.9');
|
|
$_ENV['APP_VERSION'] = '7.0.10';
|
|
|
|
$this->assertSame(
|
|
'7.0.10',
|
|
CoreVersionChecker::getCoreVersion(),
|
|
'argv 보조 판정으로도 업데이트 트리로 인식해 env 를 우선해야 합니다.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 업데이트 트리 플래그를 세 채널($_ENV/$_SERVER/putenv)에 세우거나 지웁니다.
|
|
*
|
|
* @param bool $enabled true 면 플래그 설정, false 면 세 채널 모두 제거
|
|
*/
|
|
private function setUpdateFlag(bool $enabled): void
|
|
{
|
|
if (! $enabled) {
|
|
unset($_ENV['G7_UPDATE_IN_PROGRESS'], $_SERVER['G7_UPDATE_IN_PROGRESS']);
|
|
putenv('G7_UPDATE_IN_PROGRESS');
|
|
|
|
return;
|
|
}
|
|
|
|
$_ENV['G7_UPDATE_IN_PROGRESS'] = '1';
|
|
$_SERVER['G7_UPDATE_IN_PROGRESS'] = '1';
|
|
putenv('G7_UPDATE_IN_PROGRESS=1');
|
|
}
|
|
}
|