Files
Gnuboard7/tests/Unit/Extension/CoreVersionCheckerEnvPriorityTest.php
T
HeuJung ffab451b4a fix(core,installer): dev vendor 설치본의 코어 업데이트 중단 수정 — stale 패키지 매니페스트 3계층
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.
2026-09-08 09:46:57 +09:00

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');
}
}