7.0.11 인스톨러·코어 업데이트 변경(e60a82d73)에 대해 과거 회귀 22건을 부류별로 대조한
결과, 신규 노출면 1건과 테스트 위생 1건이 나와 인터뷰 결정대로 조치했다.
1. argv 채널의 SAPI 게이트 — CGI/FPM 은 register_argc_argv=On 이면 $_SERVER['argv'] 를
쿼리스트링을 '+' 로 쪼개 채우므로(`GET /?x+core:update` → argv[1]==='core:update', php-cgi
실측) 비인증 웹 요청이 업데이트 트리로 판정되어 bootstrap/app.php 자가 치유가 요청마다
패키지 매니페스트를 지우고 다시 만들었다. CoreUpdateContext 와 bootstrap/app.php 복제본
모두 argv 를 cli·phpdbg 에서만 읽는다. env 플래그 채널은 웹에서 주입할 수 없으므로 그대로
두어 웹 요청 안에서 시작하는 업데이트 흐름(7.1.0)에 영향이 없다. 동형성 테스트에 SAPI 축을
더했다.
2. 매니페스트 삭제 실패 기록 — PackageManifestCacheHelper::clear 가 지우지 못한 파일의
경로를 돌려주고, spawn 직전 호출부가 업그레이드 로그·콘솔에 경고로 남긴다. 권한·소유권
불일치면 자식의 자가 치유도 같은 이유로 실패해 증상은 제보와 같은 「Class not found」 인데,
이 경고가 원인이 권한이라는 유일한 흔적이다.
3. 테스트 격리 — tests/bootstrap.php 가 APP_PACKAGES_CACHE/APP_SERVICES_CACHE 를 테스트
전용 경로로 돌린다. proc_open 으로 자식을 띄우는 기존 테스트 2종의 자식이 개발 클론의
실제 bootstrap/cache 매니페스트를 지우고 다시 쓰던 것(stat 실측)을 부모·자식 함께 막는다.
관리자 [시스템 최적화] 경로(withPreservedContainer 파사드 복원의 미실측 형제 호출처)는
임시 설치본에서 API 로 실측했다 — 200, 설정·라우트 캐시 재생성, 후속 요청 200, 로그 오류 0.
4. 트러블슈팅 사례 ↔ 회귀 테스트 앵커 계약 — 신규 사례는 헤딩에 <!-- case:{영역}-{번호} -->
앵커를 달고 같은 문자열을 그 사례를 잠그는 회귀 테스트에도 남겨야 한다. 사례 번호는
문서마다 1부터 재시작하고 병합으로 중복되므로(이번 리베이스에서도 우리 사례가 develop 과
같은 29 였다가 31 로 밀렸다), 개수만 대조하면 다른 사례를 덮는 테스트도 초록이 된다.
그런데 판정기 check-troubleshooting-test-coverage.cjs 를 부르는 지점이 저장소에 하나도
없었다 — 스크립트 자체 주석에만 실행법이 적혀 있어 아무도 부르지 않으면 영원히 돌지
않았고, 그 사이 위반이 9건 쌓였다(backend 26~31, cache 17~19). 돌지 않는 대조는 아무것도
잠그지 못하므로 위반 해소와 실행 지점 부여를 함께 한다.
9건 전부에 앵커를 부착하고(각 사례가 선언한 회귀 테스트 중 가장 구체적인 파일에 배치,
한 파일이 두 사례에 선언된 경우는 갈라 배치), stop-guard 7.2 에 앵커 계약 + 미커버
baseline ratchet 두 축으로 등록했다. 트러블슈팅 사례 추가 프로토콜에 6단계를
더하고 coverage 에 troubleshooting-case-anchor-contract(manual-only, 전용 판정기 위임)를
등재했다. 판정기 종료코드 1 → 0, 미커버 건수는 전 문서 baseline 그대로다.
280 lines
11 KiB
PHP
280 lines
11 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} 는 업데이트 트리로 판정한다");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 웹 SAPI 에서는 argv 를 인정하지 않는다.
|
|
*
|
|
* CGI/FPM 은 `register_argc_argv=On` 이면 `$_SERVER['argv']` 를 쿼리스트링을 `+` 로 쪼갠 값으로
|
|
* 채운다 — `GET /?x+core:update` 가 `argv[1] === 'core:update'` 를 만든다(2026-09-08 php-cgi 실측).
|
|
* 그 SAPI 에서 argv 를 믿으면 비인증 요청이 업데이트 트리로 판정되어 `bootstrap/app.php` 의
|
|
* 자가 치유가 요청마다 매니페스트를 지운다.
|
|
*
|
|
* @effects CoreUpdateContext_ignores_argv_outside_console_sapi
|
|
*/
|
|
#[Test]
|
|
public function 웹_sapi_에서는_위조_가능한_argv_를_인정하지_않는다(): void
|
|
{
|
|
// register_argc_argv=On 인 FPM 이 `?x+core:update` 로 만드는 형태.
|
|
$_SERVER['argv'] = ['x', 'core:update'];
|
|
|
|
foreach (['fpm-fcgi', 'cgi-fcgi', 'apache2handler', 'litespeed', 'cli-server'] as $sapi) {
|
|
$this->assertFalse(CoreUpdateContext::isInProgress($sapi), "{$sapi} 에서는 argv 로 트리 안이 되면 안 된다");
|
|
}
|
|
|
|
foreach (['cli', 'phpdbg'] as $sapi) {
|
|
$this->assertTrue(CoreUpdateContext::isInProgress($sapi), "{$sapi} 에서는 argv 보조 판정이 유효하다");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* env 플래그 채널은 SAPI 와 무관하다 — 웹 요청으로는 주입할 수 없고, 웹 요청 안에서 시작하는
|
|
* 업데이트 흐름이 프로세스 안에서 세우는 채널이다.
|
|
*
|
|
* @effects CoreUpdateContext_env_flag_is_honored_regardless_of_sapi
|
|
*/
|
|
#[Test]
|
|
public function env_플래그는_웹_sapi_에서도_인정한다(): void
|
|
{
|
|
$_ENV['G7_UPDATE_IN_PROGRESS'] = '1';
|
|
$_SERVER['argv'] = ['index.php'];
|
|
|
|
foreach (['fpm-fcgi', 'cgi-fcgi', 'cli'] as $sapi) {
|
|
$this->assertTrue(CoreUpdateContext::isInProgress($sapi), "{$sapi} 에서도 env 플래그는 트리 안이다");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* `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 로 인정하는 커맨드 목록 · argv 를 신뢰하는 SAPI 목록(뒤의 둘은 리플렉션으로 이
|
|
* 클래스에서 파생 — 손으로 적으면 항목이 하나 늘어도 통과한다).
|
|
*/
|
|
#[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}` 가 없다 — 그 커맨드로 도는 자식은 방어를 받지 못한다"
|
|
);
|
|
}
|
|
|
|
// ⑤ argv 를 신뢰하는 SAPI — 목록은 이 클래스에서 파생한다. 게이트가 빠지면 register_argc_argv=On 인
|
|
// CGI/FPM 에서 `?x+core:update` 한 번에 자가 치유가 켜진다.
|
|
$sapis = $reflection->getConstant('CONSOLE_SAPIS');
|
|
|
|
$this->assertIsArray($sapis);
|
|
$this->assertNotEmpty($sapis, 'SAPI 목록이 비면 이 대조는 공허하게 통과한다');
|
|
$this->assertStringContainsString(
|
|
'PHP_SAPI',
|
|
$block,
|
|
'자가 치유 블록의 argv 판정에 SAPI 게이트가 없다 — 웹 요청이 쿼리스트링으로 매니페스트 삭제를 켤 수 있다'
|
|
);
|
|
|
|
foreach ($sapis as $sapi) {
|
|
$this->assertStringContainsString(
|
|
"'{$sapi}'",
|
|
$block,
|
|
"자가 치유 블록의 SAPI 게이트에 `{$sapi}` 가 없다 — 그 SAPI 로 도는 명령줄 자식은 argv 방어를 받지 못한다"
|
|
);
|
|
}
|
|
|
|
foreach (['fpm-fcgi', 'cgi-fcgi', 'apache2handler'] as $webSapi) {
|
|
$this->assertStringNotContainsString(
|
|
"'{$webSapi}'",
|
|
$block,
|
|
"자가 치유 블록이 웹 SAPI `{$webSapi}` 의 argv 를 신뢰한다"
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 세 채널($_ENV/$_SERVER/putenv)에서 플래그를 제거합니다.
|
|
*/
|
|
private function clearFlag(): void
|
|
{
|
|
unset($_ENV['G7_UPDATE_IN_PROGRESS'], $_SERVER['G7_UPDATE_IN_PROGRESS']);
|
|
putenv('G7_UPDATE_IN_PROGRESS');
|
|
}
|
|
}
|