Files
Gnuboard7/tests/Unit/Support/CoreUpdateContextTest.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

210 lines
8.0 KiB
PHP

<?php
namespace Tests\Unit\Support;
use App\Support\CoreUpdateContext;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
/**
* 코어 업데이트 트리 판정 단일 SSoT 테스트 (dev-g7 #658).
*
* 이 판정 하나가 세 가지를 게이트한다 — 확장 자동 비활성화 스킵, 코어 버전의 env 우선 판독,
* `bootstrap/app.php` 의 패키지 매니페스트 자가 치유. 판정이 어긋나면 그중 한 경로만 조용히
* 다르게 동작하므로 세 채널($_ENV/$_SERVER/putenv)과 argv 를 각각 잠근다.
*/
class CoreUpdateContextTest extends TestCase
{
/** @var string|null 원래 env 값 (3채널 복원용) */
private ?string $originalFlag = null;
/** @var array<int, string> 원래 argv */
private array $originalArgv = [];
protected function setUp(): void
{
parent::setUp();
$current = $_ENV['G7_UPDATE_IN_PROGRESS'] ?? $_SERVER['G7_UPDATE_IN_PROGRESS'] ?? getenv('G7_UPDATE_IN_PROGRESS');
$this->originalFlag = ($current === false || $current === null) ? null : (string) $current;
$this->originalArgv = $_SERVER['argv'] ?? [];
$this->clearFlag();
$_SERVER['argv'] = ['artisan', 'inspire'];
}
protected function tearDown(): void
{
if ($this->originalFlag === null) {
$this->clearFlag();
} else {
$_ENV['G7_UPDATE_IN_PROGRESS'] = $this->originalFlag;
$_SERVER['G7_UPDATE_IN_PROGRESS'] = $this->originalFlag;
putenv('G7_UPDATE_IN_PROGRESS='.$this->originalFlag);
}
$_SERVER['argv'] = $this->originalArgv;
parent::tearDown();
}
/**
* 플래그도 업데이트 argv 도 없으면 트리 밖이다.
*
* @effects CoreUpdateContext_isInProgress_detects_env_flag_or_update_argv
*/
#[Test]
public function 플래그와_argv_가_모두_없으면_트리_밖이다(): void
{
$this->assertFalse(CoreUpdateContext::isInProgress());
$this->assertFalse(CoreUpdateContext::hasEnvFlag());
}
/**
* `$_ENV` 채널만으로도 감지한다.
*/
#[Test]
public function env_배열_채널의_플래그를_감지한다(): void
{
$_ENV['G7_UPDATE_IN_PROGRESS'] = '1';
$this->assertTrue(CoreUpdateContext::isInProgress());
$this->assertTrue(CoreUpdateContext::hasEnvFlag());
}
/**
* `$_SERVER` 채널만으로도 감지한다 (`variables_order` 에 `E` 가 없으면 `$_ENV` 가 빈다).
*/
#[Test]
public function server_배열_채널의_플래그를_감지한다(): void
{
$_SERVER['G7_UPDATE_IN_PROGRESS'] = '1';
$this->assertTrue(CoreUpdateContext::isInProgress());
$this->assertTrue(CoreUpdateContext::hasEnvFlag());
}
/**
* putenv 채널만으로도 감지한다 (proc_open 이 자식에 넘긴 프로세스 환경 테이블).
*/
#[Test]
public function putenv_채널의_플래그를_감지한다(): void
{
putenv('G7_UPDATE_IN_PROGRESS=1');
$this->assertTrue(CoreUpdateContext::isInProgress());
$this->assertTrue(CoreUpdateContext::hasEnvFlag());
}
/**
* '1' 이 아닌 값은 플래그가 아니다.
*/
#[Test]
public function 플래그_값이_1_이_아니면_트리_밖이다(): void
{
$_ENV['G7_UPDATE_IN_PROGRESS'] = '0';
$this->assertFalse(CoreUpdateContext::isInProgress());
$this->assertFalse(CoreUpdateContext::hasEnvFlag());
}
/**
* 플래그가 전파되지 않은 극단 상황 대비 — argv 로도 판정한다.
*/
#[Test]
public function 업데이트_커맨드_argv_로도_트리_안으로_판정한다(): void
{
foreach (['core:update', 'core:execute-upgrade-steps'] as $command) {
$_SERVER['argv'] = ['artisan', $command, '--force'];
$this->assertTrue(CoreUpdateContext::isInProgress(), "argv {$command} 는 업데이트 트리로 판정한다");
}
}
/**
* `hasEnvFlag()` 는 argv 보조 판정을 섞지 않는다 — 섞으면 단독 실행이 spawn 자식으로
* 오판되어 사전·사후 단계를 통째로 건너뛴다.
*/
#[Test]
public function has_env_flag_는_argv_만으로는_참이_되지_않는다(): void
{
$_SERVER['argv'] = ['artisan', 'core:execute-upgrade-steps', '--force'];
$this->assertTrue(CoreUpdateContext::isInProgress(), 'isInProgress 는 argv 로도 참');
$this->assertFalse(CoreUpdateContext::hasEnvFlag(), 'hasEnvFlag 는 env 플래그만 본다');
}
/**
* `bootstrap/app.php` 자가 치유 블록의 판정 조건이 이 클래스와 동형이다.
*
* 그 블록은 부팅 전이라 `App\` 클래스를 참조할 수 없어 같은 판정을 순수 PHP 로 **복제**한다.
* 복제본은 어긋나도 예외를 내지 않는다 — 옛 부모(7.0.9·7.0.10) 아래에서 도는 신버전 자식의
* 유일한 방어가 조용히 사라지고, 증상은 그 조합에서만 나타나므로 개발 저장소에서는 영영
* 드러나지 않는다. 그래서 두 조건이 같은지를 여기서 잠근다 — 종전에는 양쪽 주석의 상호
* 참조가 유일한 방어였다.
*
* 대조 항목은 판정을 이루는 네 축 전부다: 환경변수 이름 · 읽는 채널 3종 · 참으로 받는 값
* 3종 · argv 로 인정하는 커맨드 목록(리플렉션으로 이 클래스에서 파생 — 손으로 적으면
* 커맨드가 하나 늘어도 통과한다).
*/
#[Test]
public function bootstrap_자가치유_조건이_이_판정기와_동형이다(): void
{
$bootstrap = (string) file_get_contents(base_path('bootstrap/app.php'));
$start = strpos($bootstrap, 'Core Update: Stale Package Manifest Self-Heal');
$this->assertNotFalse($start, '자가 치유 블록의 주석 마커를 찾지 못했다 — 마커가 바뀌었다면 이 검사부터 고친다');
$end = strpos($bootstrap, 'return $app;', $start);
$this->assertNotFalse($end, '자가 치유 블록의 끝(return $app;)을 찾지 못했다');
$block = substr($bootstrap, $start, $end - $start);
// ① 환경변수 이름 · ② 읽는 채널 3종 — CoreUpdateContext::hasEnvFlag() 와 같아야 한다.
foreach ([
"\$_ENV['G7_UPDATE_IN_PROGRESS']",
"\$_SERVER['G7_UPDATE_IN_PROGRESS']",
"getenv('G7_UPDATE_IN_PROGRESS')",
] as $channel) {
$this->assertStringContainsString(
$channel,
$block,
"자가 치유 블록이 판정 채널 {$channel} 을 읽지 않는다 — 그 채널만 살아 있는 호스팅에서 방어가 빠진다"
);
}
// ③ 참으로 받는 값 3종.
foreach (["=== '1'", '=== 1', '=== true'] as $accepted) {
$this->assertStringContainsString(
$accepted,
$block,
"자가 치유 블록이 플래그 값 비교 `{$accepted}` 를 갖지 않는다"
);
}
// ④ argv 로 인정하는 커맨드 — 목록은 이 클래스에서 파생한다.
$reflection = new \ReflectionClass(CoreUpdateContext::class);
$commands = $reflection->getConstant('UPDATE_COMMANDS');
$this->assertIsArray($commands);
$this->assertNotEmpty($commands, '커맨드 목록이 비면 이 대조는 공허하게 통과한다');
foreach ($commands as $command) {
$this->assertStringContainsString(
"'{$command}'",
$block,
"자가 치유 블록의 argv 판정에 `{$command}` 가 없다 — 그 커맨드로 도는 자식은 방어를 받지 못한다"
);
}
}
/**
* 세 채널($_ENV/$_SERVER/putenv)에서 플래그를 제거합니다.
*/
private function clearFlag(): void
{
unset($_ENV['G7_UPDATE_IN_PROGRESS'], $_SERVER['G7_UPDATE_IN_PROGRESS']);
putenv('G7_UPDATE_IN_PROGRESS');
}
}